结构化输出:JSON Schema、Pydantic、Zod 与受限解码 本节摘要:「友好地请模型返回 JSON」在前沿模型上仍有 5%15% 的失败率。结构化输出用受限解码(Constrained Decoding)弥合这道鸿沟:模型被字面意义上禁止产出会违反 schema 的 token。OpenAI 的 strict 模式、Anthropic 的 schema 类型化工具调用、Gemini 的 、Pydantic AI 的 、Zod 的 ,是同一思想的五种表面形态。本节造出你将用于每条生产抽取管线的 schema 校验器与 strict 模式契约,并把失败坍缩成唯一一种类型化结果:refusal。
本节摘要:「友好地请模型返回 JSON」在前沿模型上仍有 5%~15% 的失败率。结构化输出用受限解码(Constrained Decoding)弥合这道鸿沟:模型被字面意义上禁止产出会违反 schema 的 token。OpenAI 的 strict 模式、Anthropic 的 schema 类型化工具调用、Gemini 的
responseSchema、Pydantic AI 的output_type、Zod 的.parse,是同一思想的五种表面形态。本节造出你将用于每条生产抽取管线的 schema 校验器与 strict 模式契约,并把失败坍缩成唯一一种类型化结果:refusal。
阅读完本节,你应当能够:
一个读取采购订单邮件的 Agent,要把自由文本变成 {customer, line_items, total_usd}。三条路:
路线一:提示要 JSON——「用 JSON 回复,字段含 customer、line_items、total_usd」。前沿模型上 85%~95% 成功,失败有六种:缺括号、尾逗号、类型错、幻觉字段、token 上限截断、泄漏散文(如「Here is your JSON:」)。
路线二:生成后校验——自由生成、解析、按 schema 校验、失败重试。可靠但昂贵:每次重试都要付费,截断 bug 每次多花一轮。
路线三:受限解码——厂商在解码期强制 schema,非法 token 被从采样分布里掩掉。输出保证能解析、保证合规。失败坍缩成唯一模式:refusal(模型判定输入不符合 schema)。
每家 2026 年的前沿厂商都出货了某种形式的路线三:
response_format:{type:"json_schema", strict:true},模型拒绝时响应里带 refusal。tool_use 输入上强制 schema;没有 stop_reason:"refusal",但「end_turn 且无工具调用」就是信号。responseSchema;2026 年 Gemini 对部分类型出货了 token 级文法约束。output_type=InvoiceModel,产出类型化为 InvoiceModel 的结构化 RunResult。beta.chat.completions.parse。共同主线:声明一次 schema,端到端强制。
每家厂商都接受 JSON Schema 2020-12。最常用的构造:
type:object / array / string / number / integer / boolean / null 之一。properties:字段名到子 schema 的映射。required:必须出现的字段名列表。enum:允许值的封闭集合。minimum / maximum(数字),minLength / maxLength / pattern(字符串)。items:应用于每个数组元素的子 schema。additionalProperties:false 禁止额外字段(默认值因模式而异)。INVOICE_SCHEMA = { "type": "object", "properties": { "customer": {"type": "string", "minLength": 1}, "line_items": { "type": "array", "items": {"type": "object", "properties": { "name": {"type": "string"}, "qty": {"type": "integer", "minimum": 1}}, "required": ["name", "qty"]}, }, "total_usd": {"type": "number", "minimum": 0}, }, "required": ["customer", "line_items", "total_usd"], "additionalProperties": False, # strict 模式必需 }
OpenAI strict 模式额外要求三条:每个属性都必须列在
required里、处处additionalProperties:false、不允许未解析的$ref。违反这些,API 在请求期就返回 400。
Pydantic v2 通过 model_json_schema() 把 dataclass 形态的模型生成 JSON Schema。Pydantic AI 把它包起来,你只需写:
from pydantic import BaseModel from decimal import Decimal class LineItem(BaseModel): name: str qty: int # 自动加 minimum:1? 不会,需显式 Field class Invoice(BaseModel): customer: str line_items: list[LineItem] total_usd: Decimal
Agent 框架在边缘把 schema 翻译成 OpenAI strict 模式、Anthropic input_schema 或 Gemini responseSchema。模型输出以类型化的 Invoice 实例返回;校验错误抛 ValidationError,带类型化的错误路径。
Zod(z.object({customer: z.string(), ...}))是 TS 的等价物。OpenAI 的 Node SDK 暴露 zodResponseFormat(Invoice),翻译成 API 的 JSON Schema payload。
strict 模式无法强迫模型作答。当输入塞不进 schema(「这封邮件是首诗,不是发票」),模型产出一个含原因的 refusal 字段。你的代码必须把它当作一等结果处理,而非失败。refusal 也是有用的安全信号:让模型从受保护内容邮件里抽取信用卡号,会返回带安全原因的 refusal。
开源权重的实现用三种技术:
outlines、guidance、lm-format-enforcer):从 schema 构造一个确定性有限状态机(FSM);每一步把会违反 FSM 的 token 的 logit 掩掉。商业厂商在幕后选其一。2026 年的现状是:对短结构化输出比裸生成更快,对长的则速度相当。
当你在 strict 模式之外(Anthropic 工具调用、非 strict OpenAI、老 Gemini)时,恢复模式是:
generate -> parse -> validate -> 失败则注入错误重试,最多 3 次
一次重试通常够;三次能抓住弱模型的偶发失败。超过三次往往说明 schema 有问题:模型对某些输入无法满足,该改的是提示或 schema。
受限解码对小模型有效。一个 3B 参数的开源模型配上文法强制,在结构化任务上胜过一个 70B 模型配裸提示。这正是结构化输出对生产如此重要的主因:它把可靠性与模型规模解耦。
| 维度 | 提示要 JSON | 生成后校验 | 受限解码(strict) |
|---|---|---|---|
| 合法 JSON 率 | 85%~95% | 接近 100%(靠重试) | 100%(解码期保证) |
| 成本 | 低 | 高(重试付费) | 中 |
| 失败模式 | 6 种,需各防 | 重试/截断 | 唯一:refusal |
| 小模型适用 | 差 | 中 | 好(与规模解耦) |
💡 选型心法:生产抽取管线一律上 strict 模式;若 schema 复杂到 strict 不支持(如带判别器的
oneOf),退到「生成后校验 + 最多 3 次重试」,并把 refusal 当一等结果。
本节产出 outputs/skill-structured-output-designer.md——给定一个自由文本抽取目标(发票、工单、简历等),它产出一份 strict 模式兼容的 JSON Schema 2020-12,以及镜像它的 Pydantic 模型,refusal 与重试处理都已类型化占位。
code/main.py 用标准库造了一个最小 JSON Schema 2020-12 校验器(支持 type、required、enum、min/max、pattern、items、additionalProperties),包住一份 Invoice schema,把假 LLM 输出跑过校验器,演示解析错误、schema 违反、refusal 三条路径。生产时把假输出换成任意厂商的真实响应即可。
跑校验器:运行 code/main.py,加第四个测试用例,让 total_usd 为负数,确认校验器按 minimum 约束路径拒绝。
支持判别 oneOf:扩展校验器支持带判别器的 oneOf。常见场景:line_item 是产品或服务,用 kind 打标签。strict 模式在这里有微妙规则,查 OpenAI 结构化输出指南。
Pydantic 对照:把同一个 Invoice 写成 Pydantic BaseModel,把 model_json_schema() 输出与手写 schema 对照,找出 Pydantic 默认会设、手写版却漏掉的那个字段。
测 refusal 率:构造 10 个本不该能抽取的输入(歌词、数学证明、空邮件),用真实厂商的 strict 模式跑,数 refusal vs 幻觉输出——这是你 refusal 感知重试的真值。
读禁用构造:从头到尾读 OpenAI 结构化输出指南,找出它明确禁止、而普通 JSON Schema 允许的那个构造。设计一个非必要地使用该禁用构造的 schema,再重构为 strict 兼容。
additionalProperties:false、无 $ref。model_json_schema()、Zod zodResponseFormat,在边缘翻译成各家格式。下一节,我们聚焦工具模式设计——命名、描述、参数约束,把模型选错工具的概率压到最低。