教程

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 那篇。