模型的输出是逐 token 采样出来的,下游程序需要的是确定的结构。这条鸿沟放在哪一层堵,决定了你是「经常重试」还是「结构上不会错」——以及要为此付什么代价。
这篇记的是:四种失败为什么不是一种、三层约束各自的代价、重试到底值不值,以及哪些误解会让人以为「用了 JSON Schema 就万无一失」。
1. 前提:为什么”求它输出 JSON”天然不可靠#
1.1 生成是逐 token 采样【事实】#
模型在每个位置做的是:从整个词表(几万到十几万 token)里,按概率分布挑一个。它不是在”写一个 JSON 对象”,是在一个位置一个位置地预测下一个 token。
【推导】这一条直接推出三件事:
- 合法结构不是天然保证,是概率上更可能。模型见过海量 JSON,所以”下一个 token 是
"” 的概率很高——但”很高”不是 1。 - 越到后面越容易崩。输出越长,前面每一步都不出错的条件越多;长 JSON 的尾部(闭合括号、最后一个字段)是失败高发区。
- 提示只能降低概率,不能消除概率。你在 prompt 里写十遍”必须返回合法 JSON”,改的是分布,不是采样规则。
那三个采样旋钮呢?它们也只改分布,不改规则【事实】:
| 参数 | 它做什么 | 常见误区 |
|---|---|---|
| Temperature | 把概率分布拉平(更随机)或拉尖(更集中) | 设成 0 也不承诺全链路确定 |
| Top-p | 只在累积概率达到 p 的候选集合里采样 | 不该与高 Temperature 盲目叠加调参 |
| max_tokens / stop | 控制终止与输出预算 | 设太短会把 JSON 截断,以 stop_reason: "max_tokens" 结束——这种输出会绕过 schema 校验 |
【推导】为什么 Temperature = 0 不等于”输出确定”:它只是把”随机挑一个”换成”挑概率最高的那个”。但”概率最高的那个”本身就不是常量——
- 模型版本:快照一变,权重就变,同一个位置的最大概率 token 可能就换了
- 数值层面:同一份输入在不同硬件、不同批处理并行下,浮点求和顺序不同,两个候选的分数可能极其接近甚至翻转
- 上下文:哪怕差一个空格,分布就不同
【推论】所以确定性要从系统设计里要,不能从采样参数里要:结构化输出把语法封死(见第 3 节)、固定模型版本、确定性的业务校验、缓存、以及评测兜底。 “我把 temperature 调成 0 了所以结果稳定”这句话站不住——正确的说法是”我把不确定性挡在了系统边界外”。
1.2 失败不是一种,是四种【事实】#
说”加个 try-catch 就行”的人,通常只想到第一种:
| 失败形态 | 例子 | 谁能在生成时防住 |
|---|---|---|
| 语法错 | 少一个逗号、字符串没闭合 | 解码约束 ✅ |
| 缺字段 | 没有 reason 字段 | 解码约束 ✅ |
| 类型不对 | "amount": "18.5"(字符串而非数字) | 解码约束 ✅ |
| 枚举越界 / 语义错 | "status": "pendng";或者 user_id: 99999 这个人根本不存在 | 解码约束只防前者,后者谁都防不住 |
这一栏的分界线就是本节的主线:左边三种是语法问题,可以靠约束解码从根上消灭;右边两种是语义问题,只能在业务层拦。
2. 候选方案:三层约束(先各自介绍,不比较)#
2.1 提示约束【事实】#
在 prompt 里给出 schema 描述、字段含义、少量示例,要求模型按格式返回。这是最古老、零成本、也最不可靠的一层。
2.2 解码约束(constrained decoding)【事实】#
Claude 平台提供两个互补的能力:
- JSON outputs(
output_config.format):约束回复正文的格式,保证是符合你 schema 的 JSON - Strict tool use(
strict: true):约束工具名与工具参数,用 grammar-constrained sampling 保证符合 schema
原理是在采样那一刻把不合法 token 的概率压成 0——模型不是”尽量写对”,是语法上不可能写错。
2.3 事后校验 + 有限修复【事实】#
拿到输出后:parse → schema 校验 → 失败则把精简后的错误反馈给模型重试(通常 1–2 次)→ 连续失败则换模型 / 简化任务 / 转人工。每次失败都要进 Trace,不能静默吞掉。
3. 逐个淘汰(统一指标:一次请求的”结构可用率”,以及失败时的代价)#
3.1 为什么只靠提示不够【推导】#
提示改的是概率分布,不改采样规则。所以:
- 首次成功率永远小于 1,且随输出长度下降(见 1.1 第 2 条)
- 失败率不能靠”把 prompt 写得更好”压到 0,只能压低
【事实】而且”多给示例”这条路正在变窄:Anthropic 明确说,对新一代模型,给工具使用示例反而会把模型限制在一个探索空间里,“number one rule for tool usage was to give examples” 已经是过时做法——更好的做法是设计接口本身(比如把 status 做成 pending / in_progress / completed 的枚举,光是列出来就在提示怎么用)。
3.2 为什么只靠事后重试不够【推导】#
重试能救成功率,但要在两个维度上付账:token 成本和尾延迟。
重试要把输入原样重发一遍 → 失败的那一次,输入和输出都白付
而且延迟是累加的:第 2 次尝试的延迟 = 2 × 单次延迟text【取舍】所以重试的正确用法是兜底,不是主路径。把它当主路径,等于承认”我每次都在赌”。
3.3 解码约束的代价:不是免费的【事实】#
把语法错误消灭在采样时是有账单的,而且账单随 schema 复杂度指数增长:
| 限制 | 值 | 含义 |
|---|---|---|
| 每请求 strict 工具数 | 20 | 只有标了 strict: true 的才算,非 strict 不计 |
| 可选参数总数 | 24 | 所有 strict schema 里”不在 required 里”的参数合计 |
| union 类型参数 | 16 | 用 anyOf 或类型数组的参数;它们尤其贵,因为编译成本是指数级的 |
【事实】超出后 API 返回 400 “Schema is too complex for compilation”;即使没超表里的限制,还可能在 180 秒编译超时上挂掉。原因是 schema 复杂度不是一个维度:可选参数、union 类型、嵌套深度、工具数量会互相作用,编译出来的 grammar 可能大得不成比例。
【事实】几个容易忽略的细节:
- 编译结果缓存 24 小时;改
name或description不会失效,改 schema 结构会 - 改
output_config.format会让该会话线程的 prompt cache 失效——所以别在中途反复改 schema - 有
stop_reason会绕过 schema:refusal(安全拒答,拒答消息优先于 schema 约束,输出可能不匹配)和max_tokens(被截断)
3.4 胜出方案:三层叠加,各管一段【取舍】#
提示约束 → 降低出错概率(免费,但不保证)
解码约束 → 消灭语法错误(保证,但有编译成本与上限)
事后校验 → 拦住语义错误(必须,因为上面两层都管不了)text【推论】这三层不是三选一,因为它们管的根本不是同一件事。 能说清”哪一层管哪一类错”,比记住任何一层的 API 都有价值。
自检:为什么”用了 JSON Schema 就万无一失”是错的?(语法保证 ≠ 语义保证,而且 refusal / max_tokens 会绕过)
4. 量化验证:重试到底值不值#
统一场景:一次结构化抽取,输入 3K token、输出 500 token,模型 Opus 5(输入 25/MTok)。
单次成本 = 3K × $5/1M + 500 × $25/1M
= $0.015 + $0.0125 = $0.0275text重试策略:最多尝试 3 次(首次 + 重试 2 次),三次都失败转人工。
期望尝试次数 = 1 + p + p² (p = 单次失败率)
转人工比例 = p³text| 单次失败率 p | 期望尝试次数 | 成本放大 | 转人工比例 |
|---|---|---|---|
| 10%(解码约束 + 简单 schema) | 1 + 0.1 + 0.01 = 1.11 | +11% | 0.1% |
| 40%(纯提示约束 + 长输出) | 1 + 0.4 + 0.16 = 1.56 | +56% | 6.4% |
【推导】注意最后两列的关系:失败率从 40% 降到 10%,重试次数只降了 29%,但转人工率降了 64 倍(6.4% → 0.1%)。原因是转人工率是 p 的三次方——尾部风险对 p 极其敏感。
【取舍】这解释了为什么解码约束值得付 grammar 编译的固定成本:它买的不是”少重试几次”,是把 p 压到接近 0,从而把 p³ 压到可以忽略。 而如果你的失败率本来就在 40%,重试策略救不了你——你只是把 6.4% 的请求推给人工。
自检:为什么”失败就重试”在高失败率下会失效?
5. 边界与常见误解#
| 常见说法 | 修正 |
|---|---|
| ”用了 JSON Schema,下游可以直接信任结果” | 只保证了结构与类型。枚举值是否存在、ID 是否有效、用户是否有权限、状态前置条件是否满足——schema 全都管不了 |
| ”解析失败就重试,重试能兜住” | 重试拿延迟买成功率:P99 延迟翻倍,且失败率高时转人工率按 p³ 放大。它是兜底不是主路径 |
”加了解码约束,stop_reason 就不用管了” | refusal 和 max_tokens 都会绕过 schema。前者是拒答消息优先于约束,后者是被截断 |
| ”枚举值写了就一定按写的来” | 【事实】模型可能在大小写上偏离(期望 conversation topic 3,得到 Conversation Topic 3),不报错、无特殊 stop_reason、正常结束。要大小写不敏感比较,且避免只靠大小写区分的枚举值 |
| ”schema 越详细越好” | 每个可选参数大致让一部分 grammar 状态空间翻倍;union 类型是指数级成本。“把可选改成必填”比”少写字段”更有效 |
| ”工具都给 strict 最稳” | 每请求最多 20 个 strict 工具。只把”违反了会真出事”的工具标 strict,其余靠模型自然遵守 + 事后校验 |
| ”给模型几个示例,输出就稳了” | 【事实】对新一代模型,示例反而会限制探索空间。优先改接口设计(枚举、参数命名、默认值),而不是堆示例 |
【取舍】读成一句话:结构正确是”约束”出来的,语义正确只能”校验”出来——两者不能互相替代。