面向 LLM 结构化输出的 JSON Schema
JSON Schema结构化输出解析:OpenAI、Gemini和Claude如何强制有效模式输出,并附设置要点与常见陷阱。
结构化输出(Structured outputs)会将 LLM 的响应约束在你提供的 JSON Schema 之内,因此模型返回的是与你定义的字段、类型和枚举相匹配的机器可读 JSON,而不是需要你手动解析的自由文本。
任何上线过 LLM 功能的人都清楚另一种做法是什么样:用正则去掉多余的 markdown 代码围栏,在 JSON.parse 外面套一层 try/catch,再加一个触发频率高得让人不安的重试循环。这套方案平时还能撑住,直到某个早晨它悄无声息地失效。
服务方会在生成过程中强制执行 schema,这就把“希望模型返回合法 JSON”变成了一份契约。这一能力如今在 OpenAI、Google Gemini 和 Anthropic Claude 上都已可用。三家都接受 JSON Schema,所以同一份 schema 是可移植的,你只需改动请求的接线方式,而不是契约本身。本文将介绍结构化输出是什么、JSON Schema 如何驱动它、强制执行在底层是如何实现的、如何按各服务方接入,以及有哪些值得防范的失效模式。
关键要点
- JSON 模式只保证语法合法的 JSON;严格结构化输出保证 JSON 与你的具体 schema 相匹配:这是“能解析”和“具备你需要的字段”之间的区别。
- 强制执行通过受约束解码(constrained decoding)实现:在每一个 token 处,模型只能生成那些让输出对你的 schema 保持合法的后续内容,因此一致性是在生成过程中被强制的,而不是事后校验的。
- OpenAI、Gemini 和 Claude 都能理解 JSON Schema,所以一份 schema 可跨服务方复用;Pydantic 和 Zod 会编译成 JSON Schema,这也是大多数团队实际采用的工作流。
- 即使开启了严格模式,拒答(refusal)或因长度被截断的响应仍会以成功状态返回,但并不是符合 schema 的合法 JSON,所以在信任解析结果之前要先校验对象。
- 每家服务方都只支持 JSON Schema 的一个子集,因此像
minimum、pattern或深层递归这类关键字可能被丢弃或拒绝。请对照各服务方的“支持子集”文档进行核实。
从自由文本到受 schema 约束的 JSON
放任不管时,LLM 会输出让解析器崩溃的自由文本:在 JSON 周围添加散文、漏掉引号,或者凭空发明字段。结构化输出通过把响应约束到一份 JSON Schema 来解决这个问题,使输出可被机器读取并可靠地解析。
这是对旧版 JSON 模式的升级。OpenAI 在 2023 年引入了 JSON 模式,作为强制输出合法 JSON 的手段,但它只承诺输出可被解析,并不保证遵循你定义的任何 schema。严格结构化输出通过强制执行 schema 本身填补了这一空缺。在 OpenAI 自己针对复杂 schema 遵循能力的评测中,启用 Structured Outputs 的 gpt-4o-2024-08-06 得分为 100%,而较早的 gpt-4-0613 不到 40%。这是一项特定模型的基准测试,并非普适保证,但它清楚地体现了从“通常能解析”到“与 schema 相匹配”的转变。
JSON Schema 在其中扮演什么角色?
Discover how at OpenReplay.com.
JSON Schema 是针对你的数据的声明式契约:它声明类型、必填字段、枚举和取值约束。你随请求一起传入它,服务方就会约束生成过程以匹配它。一份用于联系人抽取的紧凑 schema 如下所示:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"plan_interest": {
"type": "string",
"enum": ["starter", "pro", "enterprise"]
}
},
"required": ["name", "email", "plan_interest"],
"additionalProperties": false
}
在生产环境中很少有人手写这些。常见的工作流是用 Pydantic(Python)或 Zod(TypeScript)定义数据结构,然后让 SDK 生成 JSON Schema。OpenAI 的 SDK 直接支持这一点:把一个 Pydantic 或 Zod 对象交给它们,它们会生成对应的 JSON schema,把响应还原成你的类型化对象,并为你暴露拒答信息。Gemini 也加入了同样的便利能力,将 JSON Schema 支持扩展到所有仍在支持期内的 Gemini 模型,因此 Pydantic 和 Zod 的 schema 无需转换步骤即可使用。
如果你不想手动拼装 schema 或各服务方所需的包装结构,OpenReplay 的 JSON Schema builder 可以在浏览器里完成这两件事。你可以添加带类型、描述、枚举和嵌套的字段,或粘贴一段示例 JSON 来推断出起始结构,然后从导出面板复制结果,导出格式包括 JSON Schema、OpenAI response_format、OpenAI function tool、Anthropic、Gemini、Zod 和 Pydantic。你输入的任何内容都不会离开该页面。
schema 强制执行是如何工作的?
结构化输出之所以有效,是因为服务方对解码过程施加了约束:在每个生成步骤,模型只能产生那些让输出对你的 schema 保持合法的 token。schema 被编译成一套语法(或一个有限状态机),会违反它的 token 在采样之前就被屏蔽掉。Anthropic 自己的文档描述了相同的机制:你的 schema 被编译成语法,受约束采样使生成过程始终处于该语法之内,因此模型根本无法输出会破坏 schema 的 token。
同样的思路可以推广到 JSON 之外。对于本地和自托管模型,llama.cpp 使用 GBNF 语法文件,Outlines 施加基于正则和语法的约束,两者都依靠同一套 token 屏蔽原理来强制执行任意格式(SQL、自定义 DSL 或 JSON)。
各服务方的结构化输出
三家主要服务方都能理解 JSON Schema,因此这一模式可以移植。差异在于请求的接线方式和失败信号。
| 服务方 | schema 传入位置 | Schema 方言 | 拒答 / 不完整信号 |
|---|---|---|---|
| OpenAI | response_format(Chat Completions)或 text.format(Responses API),并设置 strict: true | JSON Schema 子集 | refusal 字段;finish_reason: "length" |
| Gemini | generationConfig 中的 responseFormat.text(mimeType + schema) | JSON Schema 子集(含 anyOf、$ref) | 被截断的 candidate;对过于复杂的 schema 予以拒绝 |
| Claude | output_config.format,或在某个工具的 input_schema 上设置 strict: true | JSON Schema 子集 | stop_reason: "refusal" / "max_tokens" |
OpenAI。 设置 strict: true 并传入 schema。OpenAI 的指南建议新项目从其当前模型开始,并指出 Responses API 更换了该参数的位置:在 Chat Completions 上使用 response_format: { type: "json_schema", strict: true },在 Responses API 上使用 text: { format: { type: "json_schema", strict: true } }。
Gemini。 通过 generationConfig 提供 schema:
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Extract the contact from this email...",
config={
"response_format": {
"text": {
"mime_type": "application/json",
"schema": person_schema,
}
},
},
)
Claude。 Anthropic 现已提供原生结构化输出,不再需要借助工具调用(tool call)绕行。这里有两个互补特性:通过 output_config.format 控制响应体的 JSON 输出,以及通过 strict: true 控制工具输入的严格工具调用,二者可独立使用或组合使用。严格工具调用保证调用的参数与其 input_schema 相匹配,因为该 schema 会被编译成一套约束采样的语法,与 OpenAI 和 Gemini 所用的技术属于同一类。请注意 API 接口在正式发布(GA)时发生了变化:output_format 参数已迁移为 output_config.format,并且不再需要 beta 请求头。
陷阱与最佳实践
严格模式并不保证输出可解析,schema 支持也不是全量的。请对以下情况加以防范。
保持 schema 扁平。 深层嵌套或递归结构是“schema 过于复杂”错误以及推理质量下降的最常见原因。Claude 会直接暴露这一点,当编译后的语法过大时返回 400;而 Gemini 的文档警告,非常大或深层嵌套的 schema 可能被拒绝。冗长的属性名、大数组、取值众多的枚举,以及塞满可选属性的对象,都会增加开销。请把大规模抽取拆分为更小、更扁平的 schema。
处理拒答与截断。 即使开启严格模式,拒答或因长度被截断的响应也会返回成功状态,但并不是符合 schema 的合法 JSON。OpenAI 为此新增了专门的信号:响应上的 refusal 字段告诉你模型选择了拒答,而不是返回了与你 schema 匹配的内容。输出 token 上限并不统一。各模型的上限各不相同,任何在对象中途触及上限的响应都会产生非法 JSON,所以要按最坏情况设置 max_tokens,并核对你所用模型在文档中标明的上限。
核实受支持的子集。 在严格模式下,每家服务方只支持 JSON Schema 的一部分。OpenAI 的指南明确说明,规范中相当一部分得到了覆盖,但出于性能或技术原因,也有一些部分被排除在外。像 minimum、pattern 或默认值这类关键字可能被丢弃或拒绝,所以不要假定支持完整规范,而应查阅服务方的“支持子集”文档。
无论如何都要校验。 由于拒答和截断会产生“状态成功但 JSON 非法”的响应,即使设置了 strict: true,也要在信任对象之前依据你的 schema 对其进行解析和校验。
先推理,再输出。 约束输出可能会在某些任务上降低推理质量。一份关于 Claude 结构化输出的实践指南把这视为与扩展思考(extended thinking)之间的真实取舍:如果某项任务从模型推理中获得的收益大于从保证 schema 合规中获得的收益,就让思考部分不受约束。一条务实的中间路线是让模型在思考阶段自由推理,只对最终的 JSON 施加约束。
结构化输出把 LLM 的响应变成了你可以像类型化 API 一样对待的东西,而且由于 OpenAI、Gemini 和 Claude 都接受 JSON Schema,这一模式可以顺畅地在它们之间迁移。先用 Pydantic 或 Zod 定义你的数据结构,在服务方上启用严格模式,保持 schema 扁平,并用能处理拒答和截断的校验逻辑把解析包裹起来,然后把同一份 schema 接入你实际部署所依赖的任何服务方。
常见问题
JSON 模式与严格结构化输出有什么区别?
JSON 模式只保证模型返回语法合法、能无错解析的 JSON,但不保证输出与任何特定 schema 相匹配。严格结构化输出会在生成过程中强制执行你的具体 JSON Schema,因此返回的对象具备你定义的字段、类型和枚举。二者的区别在于“能解析”与“具备你需要的字段”。OpenAI 在 2023 年引入了 JSON 模式,后来又加入了受 schema 强制的结构化输出以填补这一空缺。
启用严格模式能保证总是得到合法、可解析的 JSON 吗?
不能。严格模式会在正常完成过程中把 token 生成约束到你的 schema 上,但安全拒答或因长度被截断的响应仍会返回成功状态,同时产出并不符合 schema 的非法 JSON。OpenAI 暴露了专门的 refusal 字段以及取值为 length 的 finish_reason;Claude 则通过取值为 refusal 或 max_tokens 的 stop_reason 来发出信号。由于这些响应会返回 200 并计费,你应当在信任对象之前依据你的 schema 对其进行解析和校验。
我能在 OpenAI、Gemini 和 Claude 之间复用同一份 JSON Schema 吗?
基本可以。OpenAI、Google Gemini 和 Anthropic Claude 都接受 JSON Schema,因此同一份 schema 是可移植的,你只需改动请求的接线方式,而不是契约本身。不同之处在于 schema 传入的位置:OpenAI 使用 response_format 或 text.format,并设置 strict 为 true;Gemini 把 schema 嵌套在 generationConfig 的 responseFormat.text 之下;Claude 使用 output_config.format 或严格工具调用。每家服务方只支持 JSON Schema 的一个子集,所以在假定完全可移植之前,请对照各服务方的“支持子集”文档核实不受支持的关键字。
为什么我的 schema 会因过于复杂而被拒绝,该如何解决?
复杂度限制来自深层嵌套或递归结构、冗长的属性名、较大的数组上限、取值众多的枚举,或含有大量可选属性的对象。当编译后的语法过大时,Claude 会返回 400;Gemini 也可能拒绝非常大或深层嵌套的 schema。解决办法是保持 schema 扁平,并把大规模抽取拆分为更小、更扁平的 schema。深层嵌套结构也是推理质量下降的常见原因,因此扁平化既能提高被接受的概率,也能改善输出质量。