结构化输出:JSON Schema、Pydantic、Zod 与受限解码


文档摘要

结构化输出:JSON Schema、Pydantic、Zod 与受限解码 本节摘要:「友好地请模型返回 JSON」在前沿模型上仍有 5%15% 的失败率。结构化输出用受限解码(Constrained Decoding)弥合这道鸿沟:模型被字面意义上禁止产出会违反 schema 的 token。OpenAI 的 strict 模式、Anthropic 的 schema 类型化工具调用、Gemini 的 、Pydantic AI 的 、Zod 的 ,是同一思想的五种表面形态。本节造出你将用于每条生产抽取管线的 schema 校验器与 strict 模式契约,并把失败坍缩成唯一一种类型化结果:refusal。

结构化输出:JSON Schema、Pydantic、Zod 与受限解码

本节摘要:「友好地请模型返回 JSON」在前沿模型上仍有 5%~15% 的失败率。结构化输出用受限解码(Constrained Decoding)弥合这道鸿沟:模型被字面意义上禁止产出会违反 schema 的 token。OpenAI 的 strict 模式、Anthropic 的 schema 类型化工具调用、Gemini 的 responseSchema、Pydantic AI 的 output_type、Zod 的 .parse,是同一思想的五种表面形态。本节造出你将用于每条生产抽取管线的 schema 校验器与 strict 模式契约,并把失败坍缩成唯一一种类型化结果:refusal。

学习目标

阅读完本节,你应当能够:

  1. 为抽取目标写一份 JSON Schema 2020-12,正确使用约束(enum、min/max、required、pattern)。
  2. 解释为什么 strict 模式与受限解码给出的保证,不同于「生成后再校验」。
  3. 区分三种失败模式:解析错误、schema 违反、模型拒绝
  4. 上线一条带类型化修复类型化 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 年的前沿厂商都出货了某种形式的路线三:

  • OpenAI:response_format:{type:"json_schema", strict:true},模型拒绝时响应里带 refusal
  • Anthropic:在 tool_use 输入上强制 schema;没有 stop_reason:"refusal",但「end_turn 且无工具调用」就是信号。
  • Gemini:请求级 responseSchema;2026 年 Gemini 对部分类型出货了 token 级文法约束。
  • Pydantic AI:output_type=InvoiceModel,产出类型化为 InvoiceModel 的结构化 RunResult
  • Zod(TypeScript):运行时解析器,按 Zod schema 校验厂商输出;配合 OpenAI 的 beta.chat.completions.parse

共同主线:声明一次 schema,端到端强制

二、从零实现

JSON Schema 2020-12——通用语

每家厂商都接受 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——Python 绑定

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——TypeScript 绑定

Zod(z.object({customer: z.string(), ...}))是 TS 的等价物。OpenAI 的 Node SDK 暴露 zodResponseFormat(Invoice),翻译成 API 的 JSON Schema payload。

Refusal(拒绝)

strict 模式无法强迫模型作答。当输入塞不进 schema(「这封邮件是首诗,不是发票」),模型产出一个含原因的 refusal 字段。你的代码必须把它当作一等结果处理,而非失败。refusal 也是有用的安全信号:让模型从受保护内容邮件里抽取信用卡号,会返回带安全原因的 refusal。

开源侧的受限解码

开源权重的实现用三种技术:

  1. 基于文法的解码(outlinesguidancelm-format-enforcer):从 schema 构造一个确定性有限状态机(FSM);每一步把会违反 FSM 的 token 的 logit 掩掉。
  2. 配 JSON 解析器的 logit 掩码:让一个流式 JSON 解析器与模型同步跑;每步算出合法下一 token 集合。
  3. 带验证器的投机解码:便宜的草稿模型提议 token,验证器强制 schema。

商业厂商在幕后选其一。2026 年的现状是:对短结构化输出比裸生成更快,对长的则速度相当。

三种失败模式

  1. 解析错误:输出不是合法 JSON。strict 模式下不可能;非 strict 厂商上仍可能。
  2. schema 违反:能解析但违反 schema。strict 模式下不可能;否则常见。
  3. refusal:模型拒绝。必须作为类型化结果处理。

重试策略

当你在 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 三条路径。生产时把假输出换成任意厂商的真实响应即可。

五、练习

  1. 跑校验器:运行 code/main.py,加第四个测试用例,让 total_usd 为负数,确认校验器按 minimum 约束路径拒绝。

  2. 支持判别 oneOf:扩展校验器支持带判别器的 oneOf。常见场景:line_item 是产品或服务,用 kind 打标签。strict 模式在这里有微妙规则,查 OpenAI 结构化输出指南。

  3. Pydantic 对照:把同一个 Invoice 写成 Pydantic BaseModel,把 model_json_schema() 输出与手写 schema 对照,找出 Pydantic 默认会设、手写版却漏掉的那个字段。

  4. 测 refusal 率:构造 10 个本不该能抽取的输入(歌词、数学证明、空邮件),用真实厂商的 strict 模式跑,数 refusal vs 幻觉输出——这是你 refusal 感知重试的真值。

  5. 读禁用构造:从头到尾读 OpenAI 结构化输出指南,找出它明确禁止、而普通 JSON Schema 允许的那个构造。设计一个非必要地使用该禁用构造的 schema,再重构为 strict 兼容。

本节要点回顾

  1. 「提示要 JSON」不可靠:前沿模型仍有 5%~15% 失败,六种失败形态。
  2. 受限解码是正解:解码期掩掉非法 token,输出保证可解析且合规,失败坍缩为唯一模式 refusal。
  3. JSON Schema 2020-12 是通用语:每家都接受;OpenAI strict 额外要求全 required、全 additionalProperties:false、无 $ref
  4. Pydantic/Zod 是绑定:Pydantic model_json_schema()、Zod zodResponseFormat,在边缘翻译成各家格式。
  5. 三种失败模式:解析错误(strict 下不可能)、schema 违反(strict 下不可能)、refusal(必须类型化处理)。
  6. 重试上限 3 次:超过 3 次说明 schema 本身有问题;Anthropic/非 strict 走「生成→校验→注入错误重试」。
  7. 与模型规模解耦:3B + 文法强制胜过 70B + 裸提示,这是结构化输出对生产的关键价值。

下一节,我们聚焦工具模式设计——命名、描述、参数约束,把模型选错工具的概率压到最低。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U