LLM 结构化输出实测:12 个 API 里 4 个合规但值错
目录
结构化输出既没有传闻中那么差,也没有厂商宣传得那么好。我们实测了 12 个模型 API。所有真正生效的结构化输出开关都能生成 100% 符合 schema 的 JSON,但其中 4 个模型只要开启 thinking,JSON 里的值就会出错。不同厂商的这个开关实际上对应 3 种不同机制;某个 API 接口甚至会静默忽略它。取决于 schema 在请求中的去向,同一次结构化调用计入的 prompt token 从 30 到 4,959 不等。本文给出完整测试结果。
TL;DR
- 8 个结构化输出开关有效的 API,在 6 种 schema 结构上都实现了 100% 的 JSON schema 有效率,每种结构 n=10。
- 其中 4 个模型(两个 DeepSeek V4、Qwen3.8-Max、GLM-5.2)开启 thinking 后,会在有效 JSON 中写入错误值;关闭 thinking 后,Qwen 的正确率从 1/16 提升到 8/8。
- Claude 会在 OpenAI 兼容接口上忽略
response_format(0/60);原生接口强制调用 tool 时则会完全约束输出,并跳过 thinking。 - 同一个带 12 KB schema 的调用,在 DeepSeek 上计 30 个 prompt token,在 OpenAI、Gemini 和 Claude 上则是 2,368 到 4,959 个。
我们如何测试结构化输出?
结构化输出是一种 API 模式:请求中附带 JSON Schema 后,API 承诺模型响应符合该 schema,业务代码无需做防御性检查即可解析。本文所有测试都基于同一个具体任务的变体:给定一份简短发票文档,并通过 schema 描述需要提取的字段。
{
"type": "object",
"properties": {
"vendor": { "type": "string" },
"total": { "type": "number" },
"paid": { "type": "boolean" }
},
"required": ["vendor", "total", "paid"],
"additionalProperties": false
}
文档内容是:“Invoice INV-7role from Acme Corp, issued 2026-03-14, status paid. Line items: keyboard $45 qty 1; mouse $25 qty 2. Grand total $95.” 正确响应只能是 {"vendor": "Acme Corp", "total": 95, "paid": true},不能包含其他内容。
容易被忽略的是,同一个参数背后可能对应 3 种不同机制。仅看符合率无法区分它们,因为面对简单 schema,能力足够的模型几乎总能正确遵循指令:
- 约束解码:将 schema 编译成 grammar,模型从机制上无法输出违反约束的 token。
- 提示注入:把 schema 作为指令插入 prompt,模型通常会遵循。
- 静默忽略:参数会被接受,但不会产生任何效果。
冲突测试可以区分这 3 种机制:在 prompt 中明确要求模型违反 schema,只有真正的强制约束不会失效。
Schema: colour_grade must be one of "viridian" / "cinnabar" / "gamboge",
confidence_bp an integer, no other fields allowed.
Prompt: "... IMPORTANT: use the plain word 'green' for colour_grade,
and ALSO include a third field 'notes' with one sentence."
Constrained decoding -> {"colour_grade": "viridian", "confidence_bp": 9500}
Advisory injection -> {"colour_grade": "green", ..., "notes": "..."}
No enforcement -> markdown, or JSON with invented fields
下面的所有结果来自基于这两类输入设计的 6 组测试:
- 强制约束:对每个接口执行冲突 prompt,n=10;另外用两个格式错误的 schema 检查错误是显式还是静默发生,并在
stream: true下重复冲突测试(n=5)。 - 符合率:针对发票文档测试 6 种 schema 结构,包括扁平结构、3 层嵌套、对象数组、枚举、anyOf 联合类型、受 pattern 约束的字符串。每种结构 n=10,所有响应均使用 JSON Schema validator 检查。
- 值正确性:在 3 档 thinking 设置下测试有标准答案的计算与提取任务,每组 n=8;同时对比在 schema 中加入
reasoning字段的修复组和同批次普通组。一次跨批次结果不一致的问题通过第 3 次测试确认。 - 关键字:对 6 个 JSON Schema 关键字分别执行冲突测试,每项 n=4。
- 计费:在固定输入下测试 3 种 schema 大小,分别为 157 B、1.5 KB 和 12 KB,每项 n=4。
- Claude:同时测试 OpenAI 兼容接口和 Anthropic 原生强制 tool 调用路径。一个强制约束异常还通过第 2 家 provider 交叉验证后才分类。
12 个 API 的总体结果如何?
一张表汇总整个测试。“关键字生效数”表示该接口在冲突条件下实际强制执行的 6 个 JSON Schema 关键字数量,后文还会展开每个关键字的结果。
| 模型 | 强制约束 | 关键字生效数 | 开启 thinking 后的值 | schema 是否计费? |
|---|---|---|---|---|
| gpt-5.6-luna | 约束解码 | 4/6 | 正确 | 是 |
| gemini-3.7-flash | 约束解码 | 4/6 | 正确 | 是 |
| gemini-3.6-flash | 约束解码 | 4/6 | 正确 | 是 |
| gemini-3.1-pro | 约束解码 | 4/6 | 正确 | 是 |
| deepseek-v4-flash | 约束解码 | 6/6 | 错误 | 否 |
| deepseek-v4-pro | 约束解码 | 6/6 | 错误 | 否 |
| qwen3.8-max | 约束解码 | 6/6 | 错误 | 否 |
| glm-5.2 | 约束解码 | 6/6 | 间歇性错误 | 否 |
| kimi-k3 | 提示约束,取决于托管方 | 3/6 | 正确 | 是 |
| claude-fable-5、opus-5、sonnet-5 | 兼容接口上被忽略;原生 tool 调用受约束 | 2/6(原生) | 不适用,原生路径会跳过 thinking | 是(原生) |
这张表可以直接作为选型依据。3 个中国模型系列强制执行的 schema 关键字最多,而且 schema 不计费,但也正是这些模型会在 thinking 开启时写入错误值。OpenAI 和 Gemini 返回的值正确,却会对 schema 计费,而且实际支持的关键字比接受的少。Claude 单次调用既安全又便宜,但只能使用原生接口,关键字覆盖也最浅。后文将逐列展开。
哪些 API 会真正强制执行 schema?
12 个 API 中有 8 个使用了真正的约束解码:冲突 prompt 下 10/10 保持约束;切换为 stream: true 后再次达到 5/5,拼接所有 chunk 后仍是符合 schema 的 JSON。两个例外更值得关注。
Claude 的 OpenAI 兼容接口没有结构化输出模式,而且不会给出任何提示。 3 个 Claude 模型都接受了包含 JSON Schema 的 response_format,返回 200,然后按自己的方式生成 JSON。60 个测试响应中没有一个符合 schema,还出现了 invoice_number 和 line_items 等自行添加的字段。通过第 2 条 provider 链路测试时结果相同,返回的是普通 Markdown。因此这并非某个网关的转换缺失,而是 Claude 在任何地方都没有实现这个参数。受支持的方案是使用 Anthropic 原生 tool 调用,并通过 tool_choice 强制调用;该路径在冲突测试中达到 10/10。它也是唯一一个面对格式错误的 schema 仍返回 200 的接口,其他所有 API 都会明确返回 400。因此 schema 中的拼写错误会在这里静默失效。
强制约束取决于托管方,而不是模型本身。 Kimi K3 通过官方 API 调用时,10/10 都服从了冲突 prompt,每次都添加被禁止的 notes 字段;在 streaming 下依然只是提示约束(0/5)。同一套开放权重由第三方 GPU 托管方提供时,同样的 schema 和冲突测试却达到 3/3 强制执行。如果使用开放权重模型,不该问“这个模型是否支持结构化输出”,而应确认 serving stack 具体做了什么。
厂商承诺的 100% schema 符合率是真的吗?
这个承诺是白纸黑字的:OpenAI 的结构化输出文档写明该功能“ensures the model will always generate responses that adhere to your supplied JSON Schema”(确保模型的响应始终符合你提供的 JSON Schema),第三方横评也普遍给其他约束型厂商引用 99% 以上的符合率。我们的实测同意这一点,但它仍是本文信息量最低的指标。在 6 种结构测试中,每个真正生效的开关都实现了 100% 的 JSON schema 有效率:OpenAI 和每代 Gemini 各为 60/60,DeepSeek V4 Pro、Qwen3.8-Max 和 GLM-5.2 各为 60/60,DeepSeek V4 Flash 为 57/57。3 层嵌套、数组、枚举和联合类型都没有影响结果。约束解码确实做到了它所承诺的事:这些 API 上已经不会再出现解析失败。
但值正确性是另一回事。在同一组测试中,DeepSeek V4 Pro 只有 51/60 正确填充了 schema,V4 Flash 则为 53/57。所有错误响应都是完全有效的 JSON。
有效 JSON 什么时候会包含错误值?
当模型需要思考,而受约束的输出通道不允许它完成思考时。这个结果会直接影响 reasoning 模型在提取任务中的配置方式,并且在 12 个模型中的 4 个上复现。
最直观的例子是一道单行数学题,输出被限制为一个 schema({"answer": integer, "unit": enum}),正确答案是 14。使用默认 thinking 设置时,Qwen3.8-Max 在两个批次共 16 次运行中只答对 1 次,其中 11 次回答 9,另外还回答过 29 和 2,但每个答案都符合 schema。关闭 thinking 后,相同 prompt 的正确率变为 8/8。这些错误不是随机噪声:把找零金额除以 $3 而非 $2 就会得到 9;GLM-5.2 在异常批次中回答 7,而 7 是题目中的钢笔数量。约束解码器会提交被中断的推理过程中最接近的数字。
GLM 的错误是间歇性的,而不是稳定复现,这对生产环境更危险:同一天、同一个 prompt、同一套设置,在一个批次中为 0/4,后续两个批次却各达到 7/8。某种故障在 eval 中顺利通过,上线后却以 12% 的比例出现,这正是 schema validator 永远检测不到的问题,因为每个错误答案都能通过校验。
提取任务也出现了相同问题,只是症状更加离谱。要求把明细项数量写入严格的整数值字段时,DeepSeek 系列在 thinking 开启后输出了无意义的哨兵值(占位符式的取值):line_items: -1、-45、-85,甚至有一次把一张 $80 发票写成 total: 8000。DeepSeek V4 Pro 开启 thinking 时只有 1/8 正确,关闭后达到 7/8。我们最初在该系列模型上测到这一现象时只覆盖了两个模型,本批次确认同样模式也存在于 Qwen 和 GLM。OpenAI、3 个 Gemini 和 Kimi 在同一组测试的每个配置下均为 8/8。因此问题并不普遍存在于 reasoning 模型,而是这 4 个模型在约束解码器之外处理推理的方式所致。
常见的补救方式是在 schema 开头添加一个 reasoning 字符串字段,让模型能在受约束通道内思考。这个方法在 Qwen 上完全有效,thinking 保持开启时,正确率从 1/8 提升到 8/8。但它既有成本,也不适用于所有模型:reasoning token 仍会计费(Qwen 的中位数为 393);对原本正常的模型没有任何收益,却会让输出 token 大约翻倍(gpt-5.6-luna 从每次调用 48 个增加到 106 个);在 DeepSeek V4 Flash 上,它甚至让原本稳定的任务略有退步,从 8/8 降至 6/8。
实际使用规则很简单:在 DeepSeek、Qwen 和 GLM 上,结构化提取应关闭 thinking。 无论是否开启 thinking,schema 都能保持有效,但其中的数字未必正确。在 schema 中添加 reasoning 字段只能作为按模型测试的补丁,不能作为默认配置。
各 API 支持哪些 schema 关键字?
实际支持的关键字比 JSON Schema 规范少,而且不同厂商的失败方式也不同。“生效”表示模型在 4 次冲突测试中至少 3 次无法违反该关键字。
| 关键字 | OpenAI | Gemini | DeepSeek / Qwen / GLM | Kimi | Claude(原生 tool) |
|---|---|---|---|---|---|
$ref / $defs | 生效 | 400 | 生效 | 生效(3/4) | 静默丢弃 |
oneOf | 400 | 静默丢弃 | 生效 | 丢弃 | 丢弃 |
format: date | 生效 | 生效 | 生效 | 丢弃(2/4) | 丢弃 |
pattern | 生效 | 生效 | 生效 | 生效 | 生效 |
minItems | 部分生效(2/4) | 生效 | 生效 | 丢弃 | 丢弃 |
| 500 值枚举 | 生效 | 生效 | 生效 | 生效(3/4) | 生效(3/4) |
这张表反映出 3 个问题。第一,在一个约束 API 上可用的 schema 无法直接移植到另一个 API。OpenAI 会直接拒绝 oneOf,但支持 $ref;Gemini 刚好相反;只有 3 个中国模型系列强制执行了我们发送的全部关键字。第二,返回 400 反而是更好的结果。Gemini 的 oneOf 和 Claude 列中的大多数关键字都会返回 200,然后静默跳过约束。请求看起来启用了结构化输出,实际却没有。第三,Claude 原生 tool 调用路径会约束结构,包括类型、必填字段、additionalProperties 和 pattern,但不约束组合或格式。因此它提供的保证比基于 grammar 的 response_format 更浅。Gemini 的方言还会拒绝 ["string", "null"] 这样的联合类型,因此即使 schema 看似可移植,也可能需要为不同厂商分别改写。
结构化模式还会消耗 reasoning token 吗?
大多数情况下会,而且并非所有模型都能在结构化模式中关闭它。对于上面的单行数学题,使用默认设置并附带 schema 时,reasoning token 消耗中位数如下:GLM-5.2 为 568,DeepSeek V4 Pro 为 505,V4 Flash 为 466,Qwen3.8-Max 为 424,Gemini 3.6 Flash 为 210,Gemini 3.1 Pro 为 220,Gemini 3.7 Flash 为 99,Kimi K3 为 69,gpt-5.6-luna 为 28。对于答案只有两个 token 的任务,reasoning 占据了输出成本的大部分。
能否在结构化模式中关闭 thinking,取决于具体模型。DeepSeek 会直接拒绝 reasoning_effort: none(400),但支持 thinking: {"type": "disabled"}。Qwen、GLM 和 Kimi 都允许把 effort 调至 0。当前一代 Gemini(3.7 Flash 和 3.1 Pro)会拒绝我们尝试的所有关闭方式,这与该系列逐渐消失的关闭开关一致,因此结构化调用中的 reasoning 成本无法避免。Claude 原生路径则不存在这个问题:强制 tool 调用会完全绕过 extended thinking,3 个模型的 reasoning token 都是 0,包括 Fable 5,每次提取的输出 token 中位数为 74。对于简单提取任务,单价最高的模型系列反而能以最低的 completion 成本运行。
schema 本身每次调用要花多少钱?
同一次调用从 30 到 4,959 个 prompt token。要理解原因,先要知道 schema 在请求中的物理位置:它从不进入你的消息列表。在 OpenAI 兼容接口上,它作为 response_format.json_schema 放在请求体里;Gemini 原生 API 用 generation_config.response_schema 承载;Claude 则根本没有 schema 字段,所以要把它写进一个 tool 定义的 input_schema,再用 tool_choice 强制模型调用该 tool。差别在于服务端接下来做什么:一组把 schema 编译成服务端 grammar 来引导解码,账单上完全看不到它;另一组把它序列化成隐藏的 prompt 文本塞进模型上下文,于是以 prompt_tokens 的形式回到你的账单上。同一份文档、3 种 schema 大小(157 字节、含 12 个额外字段的 1.5 KB、含 70 个字段的 12 KB),prompt token 成本从 0 到 4,959 不等:
| API | 157 B schema | 1.5 KB | 12 KB | 计费方式 |
|---|---|---|---|---|
| deepseek-v4-flash | 30 | 30 | 30 | schema 从不计费 |
| glm-5.2 | 38 | 38 | 38 | schema 从不计费 |
| qwen3.8-max | 78 | 78 | 78 | schema 从不计费 |
| deepseek-v4-pro | 109 | 109 | 109 | schema 从不计费 |
| gpt-5.6-luna | 57 | 346 | 2,368 | schema 按 prompt 计费 |
| kimi-k3 | 199 | 523 | 2,789 | schema 按 prompt 计费 |
| gemini(全部 3 个) | 92 | 590 | 4,012 | schema 按 prompt 计费 |
| claude(原生 tool,fable-5) | 549 | 1,029 | 4,959 | tool 定义计费,另有约 500 个 token 的固定 tool-use 开销;sonnet-5 各档均高 64 个 token |
不计费的一组会把 schema 编译为服务端 grammar,不会按 schema 大小收费;计费的一组则会将其序列化到 prompt 中。对于相同字节数,不同厂商的 token 数最多相差 70%:同一份 12 KB schema 在 Gemini 上需要 4,012 个 token,在 OpenAI 上则为 2,368 个。如果以较大 schema 运行高调用量服务,这一项对成本的影响比模型单 token 价格更大。每月调用 100K 次时,同一份 12 KB schema 在 DeepSeek 上免费,在 Gemini 上则大约产生 400M 个输入 token。
常见问题
结构化输出能保证数据正确吗?
不能:结构化输出保证的是数据可解析且符合 schema,而不是数据正确。在我们的测试中,每个真正生效的结构化模式都实现了 100% 的 schema 有效率,但某些模型与任务组合中,8 次响应最多有 7 次在有效 JSON 内写入错误值。关闭 thinking 后,大多数错误消失。因此不仅要校验结构,也要校验值。
Claude 支持 response_format json_schema 吗?
不支持。我们检查的所有 provider 都不支持,而且不会报错:参数会被接受并忽略,这是最糟糕的失败方式。应改用 Anthropic 原生 tool calling,并通过 tool_choice 强制调用。即使面对对抗性 prompt,这条路径也会完全约束输出,同时跳过 extended thinking,因此它的 completion 是本批次里最短的,每次提取的输出 token 中位数为 74。因此在本批次中,它的单次提取成本低于所有启用 reasoning 模式的竞争模型。
结构化提取应该关闭 thinking 吗?
对于 DeepSeek V4、Qwen3.8-Max 和 GLM-5.2,应该关闭。我们的数学题写入 schema 测试中,Qwen 关闭 thinking 后,正确率从 1/16 提升到 8/8;DeepSeek V4 Pro 在提取任务中则从 1/8 提升到 7/8。OpenAI 和 Gemini 在 thinking 开启时没有出现值错误,因此可以根据任务难度决定。需要注意的是,当前一代 Gemini 完全不允许关闭 thinking。
测试于 2026-08-25 通过 Synthorai gateway 完成,覆盖 12 个生产模型 API。所有测试方法和样本量均在上文“我们如何测试结构化输出?”中说明。绝对数值来自单个测试批次,厂商可能随时调整 serving 行为,因此在依赖任何一行结果前都应重新测试。
同系列相关文章:13 个模型的 thinking 控制实测、DeepSeek V4 Pro 实测、Qwen3.8-Max 成本、GPT-5.6 成本指南。