结构化输出与约束解码
约束解码把 JSON Schema 编译成语法,在采样时屏蔽不合法 token,保证的是语法而不是语义。语法表达不了的约束(数值范围、跨字段一致性、业务规则)必须靠校验层;四条路线(提示词要求 / 预填 / 函数调用 / 原生结构化输出)保证的东西不同,选型看端点支持与失败代价。
也叫:结构化输出 · structured outputs · 约束解码 · constrained decoding · JSON Schema · JSON 模式
原理拆解出自 T1-2
"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 是合法整数。约束解码从来没打算管这两件事,是团队误以为它管。
以上节选自T1-2 结构化输出:约束解码保证语法,保证不了语义,读全文能看到前后语境。