这篇学完你能回答什么
- 约束解码和「在提示词里要求返回 JSON」有什么本质区别?
- 有了结构化输出,还需要写校验吗?为什么?
- schema 能表达什么、不能表达什么?表达不了的那部分该放哪儿?
从一个真实故障讲起
一个订单改期与退款的 Agent,模型返回 JSON 驱动下游 API。它的事故史刚好分成三个阶段,正好是这一篇的主线。
第一阶段:模型偶尔在 JSON 前面加一句「好的,这是你要的结果:」,json.loads 直接抛异常。团队上了约束解码,这类问题清零。所有人都松了口气。
第二阶段:两周后出现更棘手的——JSON 完全合法、字段齐全、类型正确,但 new_date 是 "2026-02-30"。语法无懈可击,日期不存在。
第三阶段:refund_amount: 99999。schema 里没写上限,模型「合理地」填了一个数,下游照单执行,钱打出去了。
第一类问题约束解码解决了;后两类,它在结构上不可能解决。搞不清这条边界的团队,会在上了结构化输出之后放松警惕,然后在更贵的地方摔一跤。
JSON 前多了句寒暄
「好的,这是你要的结果:」,
json.loads抛异常上约束解码
这类语法问题清零,所有人松了口气
两周后:日期不存在断点
JSON 合法、字段齐、类型对,
2026-02-30金额填了 99999
schema 里没写上限,下游照单执行,钱打出去了
json.loads 抛异常约束解码之后清零2026-02-30 这一天不存在99999 是合法整数钱照单打了出去核心概念:先打比方,再给定义
Structured outputs(结构化输出)。让模型的输出严格符合你给定的 JSON Schema。
Constrained decoding(约束解码)。它的实现机制:把你的 schema 编译成一份语法,在采样阶段就把所有会违反结构的候选 token 屏蔽掉。
比喻:不是叮嘱司机「尽量别开出这条路」,而是把所有岔路口都用护栏封死。模型不是「更听话了」,是物理上吐不出违反结构的 token。
两个不同的东西,别混:
| 名称 | 约束对象 | 用在哪 |
|---|---|---|
| JSON 输出 | 模型的回复本身 | 你要一个结构化的答案 |
| 严格工具调用(strict tool use) | 模型生成的工具调用参数 | 你要它调用函数时参数一定合规 |
JSON mode(旧的、弱的那一档)。只保证输出是合法 JSON,不保证符合你的 schema——字段可以缺、类型可以错。看到有人把这两个混为一谈时要能分辨。
路线大概率没错,几乎零成本
全靠他自觉 —— 偶尔还是会拐出去,你事后才知道
「请只返回 JSON,不要有其他文字」
模型「尽量」照做;JSON mode 保证能 parse,{"随便什么": 1} 也算合法
物理上开不出去,不靠自觉
护栏只管路面,管不了你开到的那个地方对不对
schema 编译成语法,采样阶段封死
违反结构的候选 token 概率直接置零 —— 模型不是更听话了,是吐不出违规的 token
原理拆解
"2026-02-30" 死在这一层 —— minimum / maximum 严格 schema 根本不支持refund_amount: 99999 死在这一层;高危动作的判断权必须收回代码语法表达不了的,就一定管不住。截至 2026-08 官方明确列出的不支持项:递归 schema、外部 $ref、minimum / maximum、minLength / maxLength、其他数组长度约束;additionalProperties 只能是 false。
复杂度也有上限:所有严格 schema 合起来算,超了直接报编译错误。官方给的修法顺序是 —— 只给关键工具加 strict → 把可选参数改成必填 → 拍平嵌套 → 拆到多个请求。每个可选参数都是状态空间大户。
路线一:提示词请求
"请只返回 JSON,不要有其他文字"
→ 模型「尽量」照做 → 大概率对,偶尔加寒暄 / 缺字段 / 类型飘
路线二:JSON mode
→ 保证能 parse,但 {"随便什么": 1} 也算合法
路线三:约束解码(当前默认选择)
你的 JSON Schema
│ 编译
▼
语法(grammar)
│
▼
每生成一步:把不符合语法的 token 概率置零 → 只在合法候选里采样
→ 结构上不可能违规# 约束解码在做什么(示意)
for step in range(max_tokens):
logits = model.next_token_logits(state)
allowed = grammar.allowed_tokens(parse_state) # 语法告诉你此刻哪些 token 合法
logits[~allowed] = -inf # 其余全部封死
tok = sample(logits)
parse_state = grammar.advance(parse_state, tok)关键限制(这是本篇最值钱的一段)
既然 schema 要被编译成语法,那语法表达不了的约束,就一定管不住。截至 2026-08,官方文档明确列出的不支持项包括:递归 schema、外部 $ref、minimum / maximum、minLength / maxLength、其他数组长度约束;additionalProperties 只能是 false。此外还有整体复杂度上限——所有严格 schema 合起来算,超了会直接报编译错误,官方给的修法顺序是:只给关键工具加 strict → 把可选参数改成必填 → 拍平嵌套 → 拆到多个请求。(每个可选参数都会显著放大语法的状态空间,是复杂度大户。)编译产物会缓存 24 小时,所以首次调用有额外延迟,高频调用可以忽略。
把这些串起来就是本篇的结论:
数值范围、字符串长度、日期是否真实存在、金额是否在权限内、引用是否与来源一致——这些全部属于语义,语法层根本表达不了。它们必须活在你的代码里。
回头看开头的故障:"2026-02-30" 是合法字符串;99999 是合法整数。约束解码从来没打算管这两件事,是团队误以为它管。
工程实践(截至 2026-08)
表 1:四条路线选型
| 路线 | 保证什么 | 什么时候用 |
|---|---|---|
| 提示词里要求返回 JSON | 什么都不保证 | 模型或端点不支持约束解码时的兜底 |
assistant 预填 { |
曾经的经典技巧 | 已失效:截至 2026-08,Claude Opus 4.7 及以后返回 400,官方要求改用结构化输出 |
| JSON mode | 语法合法,schema 不保证 | 老模型 |
| 约束解码(JSON 输出 / strict 工具) | 符合 schema 的结构 | 默认选择 |
表 2:三层校验(可复用骨架)
第 1 层 · 语法层 约束解码 → 能 parse、字段齐、类型对 第 2 层 · 值域层 代码校验 → 枚举是否越界、数值范围、日期是否真实存在、格式正则 第 3 层 · 业务层 代码校验 → 是否在该用户权限内、是否与来源一致、是否超过审批阈值
一句话:能用 schema 表达的交给 schema,不能表达的必须写代码,绝不写进提示词指望模型自觉。
这条和 Agent 章的判断顺序是同一个逻辑:能代码判的用代码判 → 能变可逆的变可逆 → 剩下的才人工确认(Q3-19)。退款金额是否合理,属于第一类,根本轮不到模型来判断。
避坑清单
- 别把 schema 当校验器。 它是格式契约,不是业务规则引擎。
- 可选字段要克制。 每个可选参数都在放大语法状态空间;能必填就必填,用一个显式的「未知」枚举值代替「字段缺失」。
- 枚举优先于自由文本。 这是唯一一类「语法层能顺便管住语义」的字段——能枚举就绝不让模型自由发挥。
- 别设计一个模型填不满的 schema。 强行要求一堆它无从得知的必填字段,会诱导它编造,因为语法不允许它空着。
- 结构化输出不降低幻觉率,只是把幻觉变成格式合法的幻觉。副作用是:parse 不再报错,你反而失去了一道天然告警,第 2、3 层校验因此更重要,不是更不重要。
- 注意档位交互:过高的 effort 在结构化输出这类任务上可能过度思考,收益递减(T0-5)。
| 路线 | 保证什么 | 什么时候用 |
|---|---|---|
| 提示词里要求返回 JSON | 什么都不保证 | 模型或端点不支持约束解码时的兜底 |
assistant 预填 { | 曾经的经典技巧 | 已失效:截至 2026-08,Claude Opus 4.7 及以后返回 400,官方要求改用结构化输出 |
| JSON mode | 语法合法,schema 不保证 | 老模型 |
| 约束解码(JSON 输出 / strict 工具)默认选择 | 符合 schema 的结构 | 结构上不可能违规 —— 当前的默认选择 |
- 三层骨架
- 语法层交给约束解码;值域层与业务层一律代码校验,绝不写进提示词指望模型自觉
- 可选字段要克制
- 每个可选参数都在放大语法状态空间;能必填就必填,用一个显式的「未知」枚举值代替字段缺失
- 枚举优先
- 唯一一类「语法层能顺便管住语义」的字段 —— 能枚举就绝不让模型自由发挥
- 别设计填不满的
- 强行要求一堆它无从得知的必填字段,会诱导它编造,因为语法不允许它空着
- 判断顺序
- 能代码判的用代码判 → 能变可逆的变可逆 → 剩下的才人工确认(Q3-19)
- 档位交互
- 过高的 effort 在结构化输出这类任务上可能过度思考,收益递减(T0-5)
面试视角
- 先讲机制差异请求 vs 封路,把「约束解码」这个词说准
- 明确它保证的是语法不是语义
- 主动给不支持清单数值范围、字符串长度、日期真实性
- 把高危判断收回代码给三层校验骨架,退款金额不归模型判
- 以为「开了结构化输出就不用校验了」—— 最贵的一条误解
- 用
maximum写在 schema 里,就以为管住了金额 - 分不清 JSON mode 和 schema 约束,把两者当同一件事
- 还在推荐 assistant 预填
{,不知道它已经返 400
- 知道 schema 只支持子集,说得出至少两条不支持项
- 把高危动作的判断,彻底移出模型的输出链路
- 主动指出「结构化输出让幻觉变得格式合法,反而更难发现」
- schema 复杂到编译报错时,先改可选参数而不是先拆模型
面试官怎么问。 Q1-10 在工程向一二面几乎必问,平台组尤其爱问。它的第一层很浅(怎么让它返回 JSON),第二层就见功力:「有了这个还要不要校验」。
答题结构建议
- 先讲机制差异(请求 vs 封路),把「约束解码」这个词说准;
- 明确它保证的是语法;
- 主动给出表达不了的清单(数值范围、长度、日期真实性);
- 给出三层校验骨架,并把高危字段的判断权收回代码。
分水岭信号
- 只读过:以为「开了结构化输出就不用校验了」;分不清 JSON mode 和 schema 约束;还在推荐预填
{这个技巧;用maximum写在 schema 里就以为管住了金额。 - 真做过:知道 schema 只支持子集,说得出至少两条不支持项;能主动指出「结构化输出让幻觉变得格式合法,反而更难发现」;schema 复杂到编译报错时知道该先改可选参数而不是先拆模型;把高危动作的判断彻底移出模型输出链路。
小结与延伸
继续深入
本篇归属第 1 章「LLM 与 Prompt 基础」,去做这一章的题。