教程
AI Structured Output 是什么?JSON Schema 如何让大模型输出可靠 JSON
提示词里写「只输出 JSON」只能降低概率。Structured Output 把形状钉在解码阶段,JSON Schema 再把字段、类型和枚举写成契约。
把大模型接到流水线里,最怕的不是文笔差,而是返回值没法被代码吃掉:少一个逗号、字段改了名字、数字变成字符串。Structured Output 要解决的就是这件事——让模型在生成 token 时就按你声明的形状说话,而不是先写一段散文再指望你用正则抠出 JSON。JSON Schema 则是这份形状最常见的书面形式。读完你可以判断自己卡在「语法」「形状」还是「业务对错」,并知道 Schema 之后仍然要本地校验。
提示词里的 JSON 为什么不可靠
「请只返回 JSON,不要解释」是最常见的权宜之计。它有时有效,因为它把偏好写进了上下文。它不可靠,因为偏好不是约束:模型仍可能在对象外包一层 markdown 代码块,漏掉必填键,或把priority写成更「通顺」的urgency。下游一旦JSON.parse,失败就不是文案问题,而是整条链路中断。
更隐蔽的失败是「能 parse,但形状不对」。你期望items是数组,模型给了对象;你期望分数是数字,模型给了"0.91"。代码读到undefined或字符串拼接,问题会拖到很后面才爆。提示词解决不了这类漂移,因为它没有在解码阶段否决非法 token。
所以不要把 Structured Output 理解成「一种更严厉的提示词」。它是生成机制的一部分:服务端用 Schema 编译出允许的下一 token 集合,模型无法吐出括号不配、键名不在名单里的片段。提示词仍然负责任务说明,形状交给 Schema。
Structured Output 约束哪一层
可以把它想成三层,混用其中两层就会觉得「模型胡来」。第一层是语法:输出必须是能 parse 的 JSON。第二层是形状:键名、类型、必填、枚举必须符合 Schema。第三层是语义:分类是否选对、金额是否真实。Structured Output 覆盖前两层,第三层永远是你的责任。
| 档位 | 你配置什么 | 实际保证 |
|---|---|---|
| 提示词约定 | 「只输出 JSON」 | 偏好,不是契约 |
| JSON 模式 | MIME 或json_object |
大概率是合法 JSON,字段仍由模型决定 |
| Schema 模式 | JSON Schema + 严格开关 | 形状、类型、必填与枚举按 Schema 约束 |
OpenAI 把 Structured Outputs 写成 JSON mode 的下一步:两者都能产出合法 JSON,只有前者保证贴合你提供的 Schema。官方对照见Structured model outputs。Gemini 一侧同样区分「只要 JSON」和「按 Schema 吐字段」,具体开关见站内的Gemini API JSON 输出教程,本文不再重复 SDK 细节。
还有一个容易忽略的边界:安全拒答、截断、工具调用失败,不一定还能塞进你的成功对象。有的接口会另给refusal或空内容。Schema 约束的是「一旦开始按格式说话」的那一段,不是「这次调用一定成功」。
JSON Schema 如何变成契约
JSON Schema 本来是描述 JSON 文档的词汇表:类型、必填、枚举、数值范围、数组元素。写 Schema 的标准讲法见Understanding JSON Schema。接到大模型之后,同一份词汇表多了一层用途:它不再只用于事后校验,而是在生成时收窄搜索空间。
对工程来说,这意味着 Schema 是编译期和运行期共用的契约。Pydantic、Zod、Swift 的可生成类型,最后往往都要落到一份语言无关的 JSON Schema,才能交给云端模型、写进日志、给测试夹具回放。字段表只维护一份,避免 App 说totalCents、API 说amount、提示词里又写「金额」。
契约要写「你真正会读的对象」,不要写「完整的世界模型」。每多一个可选字段,模型就多一次填错或填空的机会。必填进required,封闭集合进enum,数字范围用minimum / maximum。属性上的description往往比在 prompt 里再解释一遍更稳,因为约束和解码绑在一起。
严格模式还有一条常见规则:对象必须additionalProperties: false,且声明过的字段都进required。真正可选的值不要「不写 required」,而要允许null。否则模型可能省略键,你的代码还在obj.field上假设键一定存在。
各家 API 怎么接 Schema
各家产品名都叫 Structured Output,包装字段并不相同。选型时先问两句:这份 Schema 是约束最终答复,还是约束工具参数?当前模型快照是否真的支持严格模式?不要假设文档里的关键词在所有端点行为一致。
OpenAI:json_schema 加 strict
Chat Completions 里把response_format设为json_schema,并打开strict: true。Responses API 把同一件事写在文本格式字段下,Schema 规则相同,只是外壳不同。下面这份请求抽取一张工单:类别只能是枚举,账号可空。
{
"model": "gpt-4o-2024-08-06",
"messages": [
{ "role": "system", "content": "Extract the ticket into the schema." },
{ "role": "user", "content": "Checkout 500s on a saved card. Account acct_8842." }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "other"]
},
"severity": { "type": "integer" },
"account_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }]
}
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
接到响应后按「整段都是 JSON」处理,不要先用正则抠代码块。SDK 的 parse 助手可以直接反序列化成类型对象;同时要处理拒答分支,安全策略触发时内容可能对不上成功 Schema。
Gemini 与其他栈
Gemini 用 MIME 类型声明「这是 JSON」,再用responseSchema或responseJsonSchema钉字段。后者更接近标准 JSON Schema,适合anyOf、$ref和数值范围。细节、Python / JS 示例见Gemini Structured Output 教程。
Apple 的 on-device 模型走的是@Generable一类编译期形状,不是你手写一份 JSON Schema 文件;但跨出进程、打 HTTP、写日志时,仍然要有一份可序列化的 JSON。三条调用链怎么分,见Apple AI Agent 与 JSON。外部 Agent 走 MCP 或 REST 时,载荷几乎一定是 JSON,Schema 仍然是适配层该对齐的那张表。
一份能落地的 Schema
下面这份 Schema 模拟工单分类:类别封闭、优先级是整数、摘要是字符串。这就是 Schema 该管的事——形状,而不是「这张工单在业务上该不该标紧急」。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
数组用items描述元素。元组感更强的固定长度列表,在 JSON Schema 路径下看prefixItems。嵌套对象先内联;只有第三次出现相同结构,或树节点引用自身时,才值得上$defs + $ref。过早抽象会让报错更难读:服务端拒绝 Schema 时,你要对着一份展开后的大对象排查。
Prompt 和 Schema 不要各写一遍结构。重复描述会让模型在两种说法之间摇摆,也浪费输入 token。任务说明留在提示词,键名和类型只出现在 Schema。本地类型改了字段,请求里的 Schema 必须一起改,否则线上会静默出现新键或缺键。
约束之后仍要本地校验
Structured Output 让解析层安静很多,它不保证分类正确,也不保证数字符合你的业务上限。枚举能限制类别集合,不能阻止模型选错类别。优先级写成 5 合法,写成「其实该是 2」也合法。关键路径要抽检或上规则。
落地时把模型返回值当成一份普通 JSON 来看。先JSON 格式化看清嵌套,再用JSON 校验确认能 parse,然后把同一份对象丢进JSON Schema做本地二次校验。和黄金集对比时用JSON Diff,一眼看出键顺序或可空字段是否漂移。
建议存三份夹具:ticket.valid.json、ticket.missing-field.json、ticket.wrong-enum.json。第一份确认 happy path,后两份确认你的本地校验真的会拒绝。模型侧已经约束过的错误,本地仍应能复现——否则哪天换模型或关掉严格模式,流水线会在没有网的时候才发现。
常见问题 FAQ
Structured Output 和 JSON mode 有什么差别?
JSON mode 保证输出是能 parse 的 JSON。Structured Output 在此之上保证贴合你提供的 JSON Schema:键名、类型、必填和枚举都按契约来。代码要读具名字段时,用后者。
有了 Schema 还要不要写「只输出 JSON」?
可以留一句短说明,但不要再把字段表抄进提示词。形状以 Schema 为准。重复两套描述会拉低质量,也浪费配额。
严格模式里怎样表示可选字段?
多数严格实现要求对象关闭额外属性,并且列出的字段都必填。可选值写成可空类型,例如字符串或 null 的联合,而不是从 required 里删掉键。
Schema 通过了,业务还可能错吗?
会。Schema 管形状,不管真伪。分类选错、金额幻觉、该拒答却硬填,都可能是合法 JSON。生产里仍然要规则、抽检或人工确认。
总结与下一步
Structured Output 不是文案技巧,而是把 JSON 形状从「希望」改成「解码约束」。JSON Schema 是这份约束最通用的写法:必填、枚举、范围和禁止多余键,决定了下游能不能稳定JSON.parse并读到预期字段。各家 API 的开关名字不同,分层是一样的——语法、形状、语义,不要混为一谈。
下一步先写一份你真正会读的小 Schema,用严格模式跑通一条抽取或分类,再在浏览器里对着返回值做本地校验。需要 Gemini 请求示例时打开上一篇教程;需要看 Agent 参数如何落到 JSON 时打开 Apple 那篇。