当前位置: 首页 > news >正文

Structured Outputs 实战:让大模型稳定输出 JSON 的三种方案对比

Structured Outputs 实战:让大模型稳定输出 JSON 的三种方案对比

你调大模型 API 的时候,有没有遇到过这种噩梦:明明让它输出 JSON,结果返回的是 Markdown 代码块包裹的 JSON,或者字段名拼错了,或者数组变成了字符串?这篇文章对比三种方案,彻底解决大模型 JSON 输出不稳定的问题。


问题根源:模型生成的是文本,不是数据

大模型本质上是个文本生成器,它不"理解"JSON 格式,只是模仿训练数据里见过的 JSON 样式。这意味着它随时可能:

  • 在 JSON 前后加上```json代码块标记
  • 输出注释(// 这里是姓名),导致 JSON 解析失败
  • 字段名用中文而你期望英文
  • 嵌套层级不对
  • 用单引号代替双引号
  • 数字字段输出成字符串
# 你发送的 prompt"提取以下文章的标题、作者、发布日期,以 JSON 格式返回"# 模型可能返回的各种"惊喜"""" 当然!以下是提取的信息: ```json{'title':'深度学习入门',//单引号!'author':'张三','date':'2025年1月1日'//末尾没逗号}

“”"

上面这段内容,`json.loads()` 直接报错。 接下来我们看三种解决方案,从简单到可靠依次展开。 --- ## 方案一:Prompt 工程(不推荐用于生产) 最直觉的做法:在 prompt 里要求模型输出 JSON。 ```python import openai import json client = openai.OpenAI( api_key="your-api-key", base_url="https://api.deepseek.com" ) def extract_with_prompt(text: str) -> dict: response = client.chat.completions.create( model="deepseek-chat", messages=[ { "role": "system", "content": ( "你是一个信息提取助手。" "请严格以 JSON 格式返回结果,不要包含任何其他文字,不要使用 Markdown 代码块。" "必须使用双引号,不能有注释,不能有尾随逗号。" ) }, { "role": "user", "content": f"从以下文本中提取:标题(title)、作者(author)、日期(date)\n\n{text}" } ] ) content = response.choices[0].message.content.strip() # 还得加一堆防御性清理 if content.startswith("```"): content = content.split("```")[1] if content.startswith("json"): content = content[4:] content = content.strip() return json.loads(content)

问题:即使加了再多限制,模型偶尔还是会"创意发挥"。线上跑一段时间,总会遇到解析失败的情况,而且失败率随着 prompt 复杂度上升。这种方案在测试环境看起来不错,但在生产环境会让你持续救火。


方案二:json_object模式(基本可靠)

几乎所有主流大模型都支持response_format: { type: "json_object" }参数,开启后模型保证输出合法的 JSON 对象。

defextract_json_object(text:str)->dict:response=client.chat.completions.create(model="deepseek-chat",messages=[{"role":"system","content":"你是信息提取助手,以 JSON 格式返回提取结果。"},{"role":"user","content":f"提取 title、author、date 字段:\n\n{text}"}],response_format={"type":"json_object"}# 关键参数)content=response.choices[0].message.contentreturnjson.loads(content)# 这次可以直接 parse,不会抛异常

优点:JSON 格式本身有保证,json.loads()不会失败。
缺点:字段名、字段类型、是否必填——这些都没有约束。模型可能返回{"标题": "...", "作者": "..."}而不是{"title": "...", "author": "..."},或者多出来几个你不需要的字段。


方案三:json_schema模式(最可靠,推荐)

这是目前最强的方案:你提供一个 JSON Schema,模型的输出会严格符合这个 schema——包括字段名、类型、是否必填、枚举值等。

defextract_with_schema(text:str)->dict:schema={"type":"object","properties":{"title":{"type":"string","description":"文章标题"},"author":{"type":"string","description":"作者姓名"},"date":{"type":"string","description":"发布日期,格式 YYYY-MM-DD"},"summary":{"type":"string","description":"文章摘要,100字以内"},"tags":{"type":"array","items":{"type":"string"},"description":"文章标签列表"}},"required":["title","author","date"],"additionalProperties":False# 不允许多余字段}response=client.chat.completions.create(model="deepseek-chat",messages=[{"role":"system","content":"你是信息提取助手。"},{"role":"user","content":f"提取文章信息:\n\n{text}"}],response_format={"type":"json_schema","json_schema":{"name":"article_info","strict":True,"schema":schema}})returnjson.loads(response.choices[0].message.content)

输出结构完全可预期,字段名、类型都精确匹配 schema,后续代码直接用,不需要做任何防御性处理。


国产模型支持情况

模型json_objectjson_schema备注
DeepSeek-V3 / R1支持支持strict 模式可用
Qwen-Max / Plus支持支持通过response_format参数
Qwen-Turbo支持部分支持schema 复杂时效果不稳定
GLM-4支持支持需要较新版本 API
Moonshot (Kimi)支持开发中目前建议用 json_object
Yi-Large支持部分支持

各模型对json_schema的支持程度参差不齐,在多模型场景下需要维护一份兼容表格。笔者在 TheRouter 中做了统一处理:网关层会自动检测目标模型的能力,将json_schema请求适配成对应模型支持的格式,调用方只需传一份 schema,跨模型兼容由网关透明处理。

实践建议

  • 字段结构简单(3–5个字段)→json_object足够
  • 有嵌套结构、枚举值、数组 → 一定用json_schema
  • 确认目标模型支持json_schema再用,否则 fallback 到json_object

实战场景一:商品信息提取

frompydanticimportBaseModel,FieldfromtypingimportOptional,Listimportjson# 用 Pydantic 定义数据模型classProductInfo(BaseModel):name:str=Field(description="商品名称")price:float=Field(description="价格,单位元")category:str=Field(description="商品分类")brand:Optional[str]=Field(default=None,description="品牌名称")features:List[str]=Field(description="核心功能特点列表")in_stock:bool=Field(description="是否有货")# Pydantic 模型自动转换为 JSON Schemadefpydantic_to_schema(model_class)->dict:schema=model_class.model_json_schema()# Pydantic 生成的 schema 里 $defs 需要做内联处理(简单场景不需要)returnschema product_schema=pydantic_to_schema(ProductInfo)defextract_product(description:str)->ProductInfo:response=client.chat.completions.create(model="deepseek-chat",messages=[{"role":"system","content":"从商品描述中提取结构化信息。"},{"role":"user","content":description}],response_format={"type":"json_schema","json_schema":{"name":"product_info","strict":True,"schema":product_schema}})data=json.loads(response.choices[0].message.content)returnProductInfo(**data)# 测试desc=""" 小米14 Ultra 全面发布!搭载骁龙8 Gen3处理器, 6.73寸2K AMOLED屏幕,徕卡影像系统, 5000mAh电池支持90W快充。 官方定价5999元,现货发售中。 """product=extract_product(desc)print(f"商品名:{product.name}")print(f"价格: ¥{product.price}")print(f"分类:{product.category}")print(f"特点:{', '.join(product.features)}")print(f"有货:{'是'ifproduct.in_stockelse'否'}")

实战场景二:文章摘要结构化

classArticleSummary(BaseModel):title:strone_sentence_summary:str=Field(description="一句话总结,不超过50字")key_points:List[str]=Field(description="3-5个核心要点")sentiment:str=Field(description="情感倾向",pattern="^(positive|negative|neutral)$")difficulty_level:int=Field(description="技术难度 1-5分",ge=1,le=5)defsummarize_article(article_text:str)->ArticleSummary:response=client.chat.completions.create(model="deepseek-chat",messages=[{"role":"system","content":"你是专业的文章分析助手,提供客观准确的摘要分析。"},{"role":"user","content":f"分析以下文章:\n\n{article_text}"}],response_format={"type":"json_schema","json_schema":{"name":"article_summary","strict":True,"schema":ArticleSummary.model_json_schema()}})data=json.loads(response.choices[0].message.content)returnArticleSummary(**data)

注意sentiment字段用了pattern约束,只允许三种值。json_schema模式会强制模型在这三个值中选一个,不会输出其他内容。


错误处理与 Fallback 策略

即使用了json_schema,生产环境也要做防御性处理:

importloggingfromtypingimportTypeVar,Type T=TypeVar("T",bound=BaseModel)defsafe_structured_extract(client:openai.OpenAI,model:str,messages:list,output_model:Type[T],fallback_value:T=None,max_retries:int=2)->T:""" 带重试和 fallback 的结构化提取 """schema=output_model.model_json_schema()forattemptinrange(max_retries+1):try:response=client.chat.completions.create(model=model,messages=messages,response_format={"type":"json_schema","json_schema":{"name":output_model.__name__.lower(),"strict":True,"schema":schema}})content=response.choices[0].message.content# 检查是否因 token 截断导致输出不完整finish_reason=response.choices[0].finish_reasoniffinish_reason=="length":raiseValueError("输出被截断,JSON 不完整,请增大 max_tokens")data=json.loads(content)returnoutput_model(**data)exceptjson.JSONDecodeErrorase:logging.warning(f"JSON 解析失败 (attempt{attempt+1}):{e}")ifattempt==max_retries:iffallback_valueisnotNone:returnfallback_valueraiseexceptExceptionase:logging.error(f"结构化提取失败 (attempt{attempt+1}):{e}")ifattempt==max_retries:iffallback_valueisnotNone:returnfallback_valueraisereturnfallback_value# 理论上不会到这里

几个重要的 fallback 场景:

  1. finish_reason == "length":输出被max_tokens截断,JSON 不完整——这种情况要加大max_tokens或截短输入
  2. 模型不支持json_schema:捕获 API 错误,降级到json_object模式
  3. Pydantic 验证失败:数据解析成功但不符合业务约束,触发重试或返回默认值

Pydantic → JSON Schema 的进阶技巧

Pydantic v2 的model_json_schema()生成的 schema 和 OpenAIstrict模式要求的有些细微差异,需要做预处理:

defprepare_schema_for_strict_mode(schema:dict)->dict:""" 将 Pydantic 生成的 schema 处理成兼容 strict 模式的格式 """importcopy schema=copy.deepcopy(schema)defprocess_node(node:dict):# strict 模式要求所有 object 都声明 additionalProperties: falseifnode.get("type")=="object":node["additionalProperties"]=False# 确保 properties 里的每个字段都在 required 里if"properties"innodeand"required"notinnode:node["required"]=list(node["properties"].keys())# 递归处理嵌套结构forkeyin["properties","items","anyOf","allOf"]:ifkeyinnode:ifisinstance(node[key],dict):forvinnode[key].values():ifisinstance(v,dict):process_node(v)elifisinstance(node[key],list):foriteminnode[key]:ifisinstance(item,dict):process_node(item)process_node(schema)returnschema# 使用raw_schema=MyModel.model_json_schema()strict_schema=prepare_schema_for_strict_mode(raw_schema)

三种方案总结对比

方案可靠性字段约束实现复杂度适用场景
Prompt 工程低(80-90%)原型验证、非关键路径
json_object高(>99%)简单结构、快速开发
json_schema极高(>99.9%)生产环境、关键业务

最终建议

  • 生产环境一律使用json_schema+ Pydantic 组合
  • 用 Pydantic 定义数据模型,自动生成 schema,顺便得到类型检查和验证
  • 加重试逻辑和 fallback,不要假设大模型永远正常
  • finish_reason == "length"是最容易被忽视的坑,一定要检查

结构化输出一旦做对了,大模型就从一个"可能返回 JSON"的黑盒,变成了一个真正可靠的数据处理组件。


作者:TheRouter 开发者,专注 AI 模型路由网关。项目主页:therouter.ai

http://www.jsqmd.com/news/550710/

相关文章:

  • Kerberos并发认证难题:解析kinit缓存冲突与KRB5CCNAME的实战应用
  • 使用VSCode调试PDF-Parser-1.0模型的完整指南
  • 3分钟快速上手:零基础用AI自动生成高质量解说视频的完整指南
  • APK-Installer终极指南:在Windows电脑上快速安装安卓应用的完整教程
  • 告别驱动烦恼:在Windows上用Electron + noble-winrt搞定蓝牙通信(保姆级避坑指南)
  • Mist工具:如何一站式解决macOS固件和安装程序管理难题?
  • Windows 7/Vista终极救星:如何在老旧系统上安装最新Python 3.14?
  • 探索LSPosed:Android Hook技术实战指南
  • 决策树算法对比:ID3 vs C4.5 从原理到实战(含数据集和Python示例)
  • 一文讲透|盘点2026年实力封神的的降AIGC工具
  • 手把手教你用频谱仪测试电磁屏蔽效果(附Python数据分析代码)
  • 3种方法让Mac鼠标秒变生产力神器:Mac Mouse Fix终极安装指南
  • 解决RColorBrewer颜色不够用的问题:手把手教你扩展配色方案
  • Kali Linux下快速安装蚁剑的3种方法(附常见错误解决方案)
  • java怎样使用泛型提高代码安全性
  • 5分钟搞定Qwen2.5-3B-Instruct-GGUF本地部署(附OpenAI API调用指南)
  • TFT实战指南:从理论到代码,构建可解释的多步时间序列预测系统
  • 告别Xcode臃肿,用Trae IDE在Mac上搭建轻量级C++开发环境(附完整配置文件)
  • AI专著写作新突破!高效工具助力,轻松打造百万字专著
  • 多维时序预测应用 Transformer-BILSTM
  • 跨平台方案:Windows与Mac共享百川2-13B-4bits模型服务
  • 三步解锁多平台资源下载工具:轻松解决视频、音乐保存难题
  • foobox-cn:重构foobar2000体验的DUI配置方案
  • TMS320F280049系列文章之第二章 工程搭建实战:从零配置到路径设置的避坑指南
  • Python数据分组聚合:从入门到进阶的实战指南
  • 从轮询到DMA:STM32 ADC注入组+PWM触发的相电流采样方案全解析
  • 好用还专业!2026年实力出众的专业降AIGC平台
  • 5步掌握SillyTavern:打造你的专属AI角色聊天室终极指南
  • 解决Ubuntu18.04网络共享中的常见问题:从Permission denied到外网访问失败
  • 带隙基准,二阶温度补偿电路 [1]带启动电路,无版图,提供的工艺smic180nm [2]输入...