脱敏说明:本文为大模型 API 教学项目的知识点提炼,所有示例采用 OpenAI 兼容的通用接口写法,不绑定任何模型厂商。
一、为什么"让模型输出 JSON"是个真问题
业务需要的从来不是一段散文,而是程序可以直接消费的结构:提取出的销售线索(姓名、意向度、预算)、切片清单(编号、起止时间、标题、分数)、考核评分(题号、分数、评语)。直接在提示词里写"请输出 JSON"会遇到三类失败:JSON 外裹 markdown 代码块、字段缺失或类型错误、遇到复杂 schema 干脆输出散文。
本文从最原始的"提示词约定"讲到厂商原生结构化输出,并给出生产环境的解析兜底模板。
二、姿势一:提示词约定 + 手工解析
prompt = <span>"请严格输出 JSON 数组,每项包含 name、intent、budget 三个字段,不要输出其他内容"</span>
content = llm.call(prompt)
data = json.loads(content) <span># 现实中会在这里不断抛异常</span>
能用,但脆弱。只适合原型验证。
三、姿势二:Pydantic 定义 Schema
用数据模型表达"我要什么",字段描述同时写给模型和程序看:
<span>from</span> pydantic <span>import</span> BaseModel, Field
<span>from</span> typing <span>import</span> <span>Literal</span>
<span>class</span> <span>SalesLead</span>(<span>BaseModel</span>):
name: <span>str</span> = Field(description=<span>"客户称呼,未提及时为空字符串"</span>)
intent: <span>Literal</span>[<span>"high"</span>, <span>"medium"</span>, <span>"low"</span>] = Field(description=<span>"意向程度"</span>)
budget: <span>int</span> | <span>None</span> = Field(description=<span>"预算,单位元,未提及为 null"</span>)
<span>class</span> <span>LeadList</span>(<span>BaseModel</span>):
leads: <span>list</span>[SalesLead]
Pydantic 的价值有两层:运行时校验(类型不符直接报错并可触发重试)、自动生成 JSON Schema 供工具调用或原生结构化输出使用。所有需要结构化结果的地方都应该先定义 Pydantic 模型——它是模型契约和程序契约的同一份真源。
四、姿势三:Function Calling / Tool Calling 间接结构化
很多模型早期没有原生 JSON 模式,工程上的通用技巧是把目标 schema 伪造成一个工具:
tools = [{
<span>"type"</span>: <span>"function"</span>,
<span>"function"</span>: {
<span>"name"</span>: <span>"submit_leads"</span>,
<span>"description"</span>: <span>"提交提取到的销售线索"</span>,
<span>"parameters"</span>: LeadList.model_json_schema(),
},
}]
模型为了"调用工具"会严格按 schema 产出参数,再从工具调用参数里取出对象。教学项目里的"结构化输出"在很长一段时间都是这个套路,优点是兼容所有支持工具调用的模型。
五、姿势四:厂商原生结构化输出
主流 API 已普遍支持 response_format 直接约束 schema,框架层负责把 Pydantic 模型传下去并自动解析:
<span># 通用写法示意:模型保证返回符合 schema 的对象</span>
result = llm.with_structured_output(LeadList).invoke(messages)
<span># result 直接是 LeadList 实例</span>
这是目前最推荐的常规姿势:开发体验好、稳定性高。代价是与厂商实现质量绑定,个别小模型对复杂嵌套 schema 遵从度一般。
六、姿势五:Agent 主循环内集成(2025 年后的新形态)
LangChain 1.0 把结构化输出直接集成进 Agent 主循环:模型在一次调用中同时完成"回答/调工具/产出结构",消除了过去"先工具调用再二次解析"的额外模型往返,延迟和成本双降。可以精细选择生成策略:优先用厂商原生结构化,模型不支持时回退工具调用。
七、生产环境的解析兜底模板
无论用哪种姿势,真实项目里解析函数都要像下面这样设防(切片项目的真实做法):
<span>def</span> <span>parse_json_array</span>(<span>content: <span>str</span></span>):
<span># 1. 剥掉 markdown 代码块围栏</span>
<span>if</span> <span>"```"</span> <span>in</span> content:
content = content.strip().strip(<span>"`"</span>)
content = content.split(<span>"\n"</span>, <span>1</span>)[-<span>1</span>] <span>if</span> content.startswith(<span>"json"</span>) <span>else</span> content
<span># 2. 直接解析</span>
<span>try</span>:
data = json.loads(content)
<span>if</span> <span>isinstance</span>(data, <span>list</span>):
<span>return</span> data
<span>except</span> json.JSONDecodeError:
<span>pass</span>
<span># 3. 正则兜底:提取第一个数组片段</span>
<span>match</span> = re.search(<span>r"\[.*\]"</span>, content, re.DOTALL)
<span>if</span> <span>match</span>:
<span>try</span>:
<span>return</span> json.loads(<span>match</span>.group(<span>0</span>))
<span>except</span> json.JSONDecodeError:
<span>pass</span>
<span># 4. 全部失败:返回安全空值/触发重试,而不是抛给用户</span>
<span>return</span> []
配合两条纪律:单批最多重试 3 次(避免坏请求无限烧钱)、解析失败降级而不是崩溃(例如该批片段跳过、整体流程继续)。
八、温度控制:不同任务用不同创造性
同一个模型在结构化任务里的参数应当随任务性质切换,教学项目给出了清晰对照:
| 任务 | 温度 | 理由 |
|---|---|---|
| 出题/文案创作 | 0.7 | 需要多样性 |
| 内容打分、评分评判 | 0.3 | 需要稳定一致 |
| 结构化抽取 | 0~0.2 | 要的是确定性 |
九、技术演进与最新差异(2025—2026)
- 原生结构化输出全面普及。 主流国产与海外模型均支持 JSON schema 约束,"伪工具调用"技巧主要用于兼容旧模型,新项目优先原生接口。
- 标准内容块。 LangChain 1.0 的 content blocks 把各家模型的私有返回字段统一为跨厂商抽象,结构化输出、推理痕迹、引用都有标准访问方式,迁移模型时改动面显著缩小。
- 2026 年 MCP 新规范采用完整 JSON Schema 2020-12。 工具参数定义能力增强(条件 schema、更丰富的校验关键字),工具的"输入契约"表达力更强,工具侧的结构化校验与本文的输出侧校验正在共用同一套 schema 体系。
十、小结
结构化输出的方法论:用 Pydantic 写一份同时给模型和程序看的契约,优先走原生结构化输出,兼容场景用工具调用伪装,永远保留剥围栏、正则提取、重试与降级四道防线。再配合"抽取低温、创作高温"的参数纪律,JSON 解析失败就从日常 bug 变成了可观测的小概率事件。
把结构化输出从提示词玄学变成可落地工程范式,五种姿势的取舍与兜底代码都很实用,适合做 LLM 应用、数据抽取与 Agent 开发的同学照着搭骨架。