本系列前几篇分别讲了为什么 Agent 需要 JSON(《Tool Calling 到 MCP 的数据流》)、JSON Schema 为何成为基础设施(《Schema、Function Calling 与 MCP 演进》),以及 Gemini 一家的 Structured Output 配置(《Gemini API 教程》)。
本文把视角拉高:不论你用的是哪家模型 API,怎样从「请在回复里输出 JSON」走到「输出必定符合这份 JSON Schema」。这是 2024–2026 年各厂商 converged 的能力,名字都叫 Structured Outputs / JSON Schema mode,但配置字段、支持子集、与 Tool Calling 的边界略有差异。
四个层级:一层比一层硬
团队里常见四种「让模型出 JSON」的做法,可靠性差一个数量级:
| 层级 | 做法 | 你控制什么 | 典型失败 |
|---|---|---|---|
| L0 | 只在 Prompt 写「请输出 JSON」 | 软约束 | ```json 围栏、解释文字、单引号、尾逗号 |
| L1 | Prompt + 示例(few-shot JSON) | 形状有样例,无硬约束 | 字段名拼写漂移、缺字段、类型混用 |
| L2 | JSON Mode(response_format: json_object 等) | 输出必须是合法 JSON | 能 parse,但 price 可能是字符串 |
| L3 | Structured Output + JSON Schema | 字段、类型、enum、必填 | 语义幻觉、截断、个别关键字被忽略 |
生产抽取、分类标签、填表入库,至少做到 L3。L0–L1 适合探索;L2 适合形状多变、只需保证能 JSON.parse 的场景。L3 才是「程序可以直接消费」的契约。
JSON Schema 管什么、不管什么
JSON Schema 是一份描述 JSON 文档结构的元数据:有哪些字段、各是什么类型、哪些必填、枚举取值范围、数组元素形状等。各厂商 Structured Output 本质上都是把这份 Schema 编译进生成过程,而不是只贴在 Prompt 里当说明。
Schema 能管:语法形状(object / array / string / integer)、required、enum、minimum / maximum、additionalProperties: false(禁止未声明字段)、嵌套对象与数组。
Schema 管不了:业务正确性。例如「total_cents 必须等于各行 qty × unit_price 之和」——这类不变量要在 Schema 校验通过后再用代码断言。也不要指望 Schema 替你做事实核查:类型对的幻觉值(编造的发票号)仍可能出现。
和 Tool Calling 里工具的 inputSchema 是同一套语言;区别只是 Structured Output 约束的是最终答复,Tool Calling 约束的是工具参数。详见《数据流解析》。
约束解码:为什么 Schema 比 Prompt 硬
Prompt 只能提高「模型愿意遵守」的概率。Structured Output 走的是约束解码(constrained decoding):在生成每个 token 时,解码器根据当前已输出片段和 Schema,把会导致 JSON 语法错误或 Schema 违规的 token 概率压到接近零。
因此你拿到的文本通常已经是一份可解析、形状正确的 JSON,而不必先写正则剥 markdown 围栏。各厂商实现细节不同(有限状态机、grammar、logit mask 等),但对开发者暴露的接口一致:把 Schema 交给 API,而不是只写在 Prompt 里。
注意:约束解码保证的是结构,不是语义。上线后仍应用同一份 Schema 做二次校验,并加上业务规则层。
OpenAI、Gemini、Anthropic 对照
概念相同,字段名各有一套。下面以「抽取一张发票对象」为例:
| 厂商 | JSON Mode | Structured Output / Schema | 备注 |
|---|---|---|---|
| OpenAI | response_format: { type: "json_object" } | response_format: { type: "json_schema", json_schema: { name, schema, strict: true } } | strict: true 时尽量拒绝 Schema 外字段;配合 Pydantic model_json_schema() |
| Google Gemini | responseMimeType: "application/json" | 同上 + responseJsonSchema 或 SDK response_schema | 详见《Gemini 教程》 |
| Anthropic | 依赖 Prompt + 解析 | output_format(Claude 结构化输出)或 Messages API 中的 schema 约束 | 字段随 SDK 版本更新;Schema 建议保持扁平 |
跨厂商迁移时,Schema 本体尽量用标准 JSON Schema(type、properties、required、enum),各 SDK 只负责包一层请求体。不要把 OpenAPI 3.0 大写类型(OBJECT、STRING)和 JSON Schema 小写(object、string)交叉粘贴。
写好一份 Schema:从 Pydantic 到线上
推荐工作流:先在代码里用 Pydantic / Zod 定义类型 → 导出 JSON Schema → 微调后送进 API。字段说明写进 description:它会进入模型上下文,决定「qty 是件数还是箱数」这类语义,光靠 type: integer 挡不住。
from pydantic import BaseModel, Field
class LineItem(BaseModel):
name: str = Field(description="商品名称")
qty: int = Field(description="数量,正整数", ge=1)
unit_price_cents: int = Field(description="单价,分", ge=0)
class Invoice(BaseModel):
vendor: str
currency: str = Field(description="ISO 4217,如 CNY")
items: list[LineItem]
total_cents: int
schema = Invoice.model_json_schema()
# 生产建议补上 additionalProperties: false
实用原则:
- 根类型优先用 object,少用根级 array;部分 API 对
{ "items": [...] }更稳。 - 先上 type / properties / required / enum,再加
additionalProperties、min/max;别把完整 Draft 2020-12 一股脑塞进去,部分关键字会被忽略。 - 嵌套不要太深;循环引用会被拒绝,应把 Schema 写扁。
- Schema 与 Prompt 分工:Schema 管形状;Prompt 管语义(「从下面文本抽取发票……」)。
OpenAI Structured Outputs 示例
OpenAI Chat Completions 在 2024 年后支持 json_schema 响应格式。strict: true 时模型应只输出 Schema 内字段:
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class Invoice(BaseModel):
vendor: str
total_cents: int
response = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "user", "content": "从文本抽取发票:Acme 卖了 2 个键盘,共 398 元。"}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": True,
"schema": Invoice.model_json_schema(),
},
},
)
data = response.choices[0].message.content # JSON 字符串
import json
invoice = json.loads(data)
Gemini 侧对应 response_mime_type + response_json_schema,完整示例见 Gemini 专题文。Anthropic 请查阅当前 SDK 的 structured output 文档——概念一致,字段名以官方为准。
落地流水线:生成 → 解析 → 校验 → 重试
Structured Output 不是「调一次 API 就结束」。建议固定四步:
- 生成:带 Schema 调模型 API;记录 prompt、Schema 版本、原始
content。 - 解析:
JSON.parse(或 SDK 的parsed);失败则整段重试,不要半解析。 - Schema 校验:用同一份 JSON Schema 跑 AJV /
jsonschema/ Pydantic;失败则重试或降级。 - 业务校验:自定义断言(金额合计、外键存在等);失败则人工或规则引擎。
开发阶段把 Schema 与 2~3 组正/反例存进仓库,用 JSON 工具箱在浏览器本地看结构、做 Diff——这和测 REST 契约同一思路,只是消费者换成了 LLM。
常见坑:开了 JSON Mode 后仍要求「先解释再输出 JSON」;超长输出被截断(提高 max tokens 或拆任务);API Key 进前端演示仓库;Schema 版本与 Prompt 不同步导致 silently ignore 新字段。
和 Tool Calling 怎么分工
| Structured Output | Tool Calling / MCP | |
|---|---|---|
| 约束对象 | 模型对用户的最终 JSON | 工具参数 JSON(inputSchema) |
| 谁执行副作用 | 无;只是数据 | 宿主 / MCP Server |
| 典型场景 | 抽取、分类、填表、Agent 间传递 | 查库存、写文件、调外部 API |
| 失败回流 | 校验失败 → 重试或人工 | 错误写入 tool 消息 → 再问模型 |
一条完整 Agent 链路常常是:Structured Output 抽出结构化意图 → Tool Calling 执行动作 → Structured Output 或自然语言总结给用户。不要用 Structured Output 假装「已经调过支付 API」——模型没有真的调。
常见问题 FAQ
只在 Prompt 里写「请输出 JSON」够吗?
不够。提示词只能提高模型遵守的概率,仍可能出现 markdown 围栏、尾逗号、字段名漂移。生产环境应至少开启 JSON Mode,最好把 JSON Schema 交给 API 的 Structured Output 通道,让解码阶段就排除非法 token。
JSON Mode 和 Structured Output 有什么区别?
JSON Mode 只保证输出是合法 JSON 文本,不约束字段名、类型和必填。Structured Output 在 JSON Mode 基础上附带 JSON Schema,生成每个 token 时按 Schema 过滤,形状才稳定,才能直接入库或传给下一跳程序。
OpenAI、Gemini、Anthropic 的配置字段一样吗?
概念相同、字段名不同。OpenAI 用 response_format: { type: json_schema, json_schema: { schema, strict } };Gemini 用 responseMimeType + responseJsonSchema;Anthropic 用 output_format 或 tools 里的 structured output。Schema 本体尽量用标准 JSON Schema,各 SDK 再包一层。
Structured Output 能替代 Tool Calling 吗?
不能。Structured Output 约束的是模型对用户的最终 JSON 答复;Tool Calling 约束的是工具参数 JSON,且需要宿主真正执行工具。抽取、分类、填表用前者;查库存、写文件、调 MCP 用后者。完整 Agent 链路常常两者都用。
模型输出还需要再校验吗?
需要。约束解码能大幅降低语法错误和类型漂移,但不能保证语义正确(字段类型对、值是编的)。生产环境应把同一份 JSON Schema 再跑一遍校验器,失败则重试、降级或人工审核。
如何在本地验证 Schema 与样例输出?
把 JSON Schema 和两三组模型输出样例存成 JSON 文件,用 JSON 工具箱在浏览器本地做语法校验与结构对照,数据不上传服务器。
总结
让 AI 生成符合 JSON Schema 的 JSON,正确顺序是:先定 Schema,再开 JSON Mode / Structured Output,最后才写 Prompt。Prompt 负责语义;Schema 负责形状;Pydantic / Zod 是作者友好的前端,线上走各厂商的 Schema 通道。
建议你用一张真实发票或一段客服对话跑通:写出 Schema → 调一次 API → 把输出贴进校验器对照。对得上再接数据库或下一跳 Agent。工具参数那一侧仍然走 Tool Calling / MCP,不要混成一种 API。