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

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_keyyour-api-key, base_urlhttps://api.deepseek.com ) def extract_with_prompt(text: str) - dict: response client.chat.completions.create( modeldeepseek-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:responseclient.chat.completions.create(modeldeepseek-chat,messages[{role:system,content:你是信息提取助手以 JSON 格式返回提取结果。},{role:user,content:f提取 title、author、date 字段\n\n{text}}],response_format{type:json_object}# 关键参数)contentresponse.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# 不允许多余字段}responseclient.chat.completions.create(modeldeepseek-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支持支持需要较新版本 APIMoonshot (Kimi)支持开发中目前建议用 json_objectYi-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:strField(description商品名称)price:floatField(description价格单位元)category:strField(description商品分类)brand:Optional[str]Field(defaultNone,description品牌名称)features:List[str]Field(description核心功能特点列表)in_stock:boolField(description是否有货)# Pydantic 模型自动转换为 JSON Schemadefpydantic_to_schema(model_class)-dict:schemamodel_class.model_json_schema()# Pydantic 生成的 schema 里 $defs 需要做内联处理简单场景不需要returnschema product_schemapydantic_to_schema(ProductInfo)defextract_product(description:str)-ProductInfo:responseclient.chat.completions.create(modeldeepseek-chat,messages[{role:system,content:从商品描述中提取结构化信息。},{role:user,content:description}],response_format{type:json_schema,json_schema:{name:product_info,strict:True,schema:product_schema}})datajson.loads(response.choices[0].message.content)returnProductInfo(**data)# 测试desc 小米14 Ultra 全面发布搭载骁龙8 Gen3处理器 6.73寸2K AMOLED屏幕徕卡影像系统 5000mAh电池支持90W快充。 官方定价5999元现货发售中。 productextract_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:strField(description一句话总结不超过50字)key_points:List[str]Field(description3-5个核心要点)sentiment:strField(description情感倾向,pattern^(positive|negative|neutral)$)difficulty_level:intField(description技术难度 1-5分,ge1,le5)defsummarize_article(article_text:str)-ArticleSummary:responseclient.chat.completions.create(modeldeepseek-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()}})datajson.loads(response.choices[0].message.content)returnArticleSummary(**data)注意sentiment字段用了pattern约束只允许三种值。json_schema模式会强制模型在这三个值中选一个不会输出其他内容。错误处理与 Fallback 策略即使用了json_schema生产环境也要做防御性处理importloggingfromtypingimportTypeVar,Type TTypeVar(T,boundBaseModel)defsafe_structured_extract(client:openai.OpenAI,model:str,messages:list,output_model:Type[T],fallback_value:TNone,max_retries:int2)-T: 带重试和 fallback 的结构化提取 schemaoutput_model.model_json_schema()forattemptinrange(max_retries1):try:responseclient.chat.completions.create(modelmodel,messagesmessages,response_format{type:json_schema,json_schema:{name:output_model.__name__.lower(),strict:True,schema:schema}})contentresponse.choices[0].message.content# 检查是否因 token 截断导致输出不完整finish_reasonresponse.choices[0].finish_reasoniffinish_reasonlength:raiseValueError(输出被截断JSON 不完整请增大 max_tokens)datajson.loads(content)returnoutput_model(**data)exceptjson.JSONDecodeErrorase:logging.warning(fJSON 解析失败 (attempt{attempt1}):{e})ifattemptmax_retries:iffallback_valueisnotNone:returnfallback_valueraiseexceptExceptionase:logging.error(f结构化提取失败 (attempt{attempt1}):{e})ifattemptmax_retries:iffallback_valueisnotNone:returnfallback_valueraisereturnfallback_value# 理论上不会到这里几个重要的 fallback 场景finish_reason length输出被max_tokens截断JSON 不完整——这种情况要加大max_tokens或截短输入模型不支持json_schema捕获 API 错误降级到json_object模式Pydantic 验证失败数据解析成功但不符合业务约束触发重试或返回默认值Pydantic → JSON Schema 的进阶技巧Pydantic v2 的model_json_schema()生成的 schema 和 OpenAIstrict模式要求的有些细微差异需要做预处理defprepare_schema_for_strict_mode(schema:dict)-dict: 将 Pydantic 生成的 schema 处理成兼容 strict 模式的格式 importcopy schemacopy.deepcopy(schema)defprocess_node(node:dict):# strict 模式要求所有 object 都声明 additionalProperties: falseifnode.get(type)object:node[additionalProperties]False# 确保 properties 里的每个字段都在 required 里ifpropertiesinnodeandrequirednotinnode: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_schemaMyModel.model_json_schema()strict_schemaprepare_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