问题:模型的自由与应用的纪律
模型的本质是「自由文本生成」——它没有内建的「必须输出合法 JSON」的义务。而工程应用(下游程序解析、数据库写入、API 对接)需要严格的格式:字段名对、类型对、语法合法。这道鸿沟的解决方案分三层,按「保证强度」递增——每一层的保证范围不同,选错层是结构化输出翻车的根源。
第一层:提示词祈求(祈祷法)。在 prompt 里写「请以 JSON 格式输出,字段包括……」——零成本的方案,也是零保证的方案。失败模式:模型在 JSON 前后加解释文字(「好的,以下是 JSON:」)、漏字段、类型错(字符串写了数字)、转义错误(字符串里有引号没转义)。现代模型(尤其带原生结构化输出能力的)的祈求成功率已大幅提高,但「大概率对」与「必然对」之间隔着生产环境——祈祷法只适合原型与容错性强的场景。
第二层:格式约束解码(语法级保证)。在解码阶段硬性约束:每一步只允许采样「合法 JSON 的下一个 token」(用 JSON 的语法规则构建一个自动机,非法 token 的概率被强制归零)。实现形态:厂商原生的 structured output/JSON mode(传入 JSON Schema,引擎保证输出符合 schema)、开源引擎的 grammar 约束(llama.cpp 的 GBNF、outlines/XGrammar 等库)。保证范围要精确理解:它保证「语法合法」,不保证「内容正确」——schema 说 age 是整数,约束解码保证它一定是整数,但不保证这个整数是对的数量级;枚举字段保证取值合法,不保证选的是「对的那个值」。语法层保证 + 语义校验(业务规则检查)两层叠加才是完整的结构化输出。
第三层:函数调用(语义级的结构化)。模型的原生工具调用(function calling)把「结构化」内建进协议:模型输出的是「要调用哪个函数 + 符合函数参数 schema 的参数」——这是模型在训练时就学过的能力(专门微调过),语义契合度高于裸 JSON mode(模型「理解」它在一个结构化的上下文里工作)。函数调用的适用域是「调用动作」类结构化(查什么、干什么),纯数据抽取场景 JSON mode 更直接。
flowchart LR
A["结构化输出三层"] --> B["祈祷法:提示词<br>零成本 · 零保证 · 原型用"]
A --> C["约束解码:语法保证<br>JSON Schema / GBNF<br>100% 合法 · 内容另验"]
A --> D["函数调用:协议内建<br>训练时学过 · 语义契合<br>适合动作类"]
B & C & D --> E["工程底线:格式层 + 语义层双校验"]
约束解码的代价:不是免费的午餐
语法约束解码的代价有两个,选型时要计入。速度代价:约束解码的每一步要查「当前语法状态下哪些 token 合法」——自动机的状态转移与掩码计算有开销(现代实现 XGrammar 类已优化到低个位数百分比的开销,但复杂 schema 仍可能显著拖慢);「合法但怪」的退化:强约束会迫使模型在不自然的 token 上硬凹——过于复杂的 schema(深嵌套、强约束的长度限制)可能让输出「语法完美、内容僵化」——schema 本身的设计也是一门手艺(字段名语义化、嵌套浅、枚举优先于自由文本——给模型「能想到」的结构)。另一个工程细节:「修复器」作为兜底(json-repair 类库:解析失败时尝试修复常见错误——引号缺失、尾逗号)——在约束解码不可用的环境(某些 API 不支持)作为第二道网,与重试逻辑(解析失败自动重试)组成降级链。
选型决策表与工程底线
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 原型/内部工具 | 祈祷法 + 解析失败重试 | 快速验证,失败成本低 |
| 生产 API(厂商支持 JSON mode) | 原生 structured output | 厂商保证语法 + 你验语义 |
| 自托管开源模型 | 约束解码(outlines/XGrammar/GBNF) | 自己保证语法 + 你验语义 |
| 智能体的工具调用 | 原生 function calling | 协议内建、语义契合 |
| 复杂嵌套 schema | 简化 schema + 后处理拆分 | 复杂 schema 是约束解码的坑 |
工程底线(无论哪层):永远不要假设输出合法就不校验——解析(语法层)+ schema 校验(结构层)+ 业务规则校验(语义层)三道闸,解析失败走重试/降级链(换更小 schema 重试、退回自由文本 + 后处理、人工介入)。结构化输出的成熟度,标志着 LLM 应用从「演示」进入「工程」——它不值得炫技,值得严谨。
约束解码的实现原理:语法掩码与状态机
约束解码(正文第二层)的底层实现值得拆开——理解它才能预判它的性能与局限。核心机制:语法掩码(grammar masking)。每生成一个 token 前,把「当前语法状态下的合法 token」计算出来,非法 token 的 logits 设为 -∞(softmax 后概率归零)——模型只能从合法集合里选。语法状态的管理靠状态机/下推自动机:JSON 的语法(大括号配对、逗号位置、字符串引号)被编译成一个「自动机」,每生成一个 token 自动机转移一次状态。实现的两代。第一代:正则/CFG 引擎的通用方案(outlines 把 schema 编译成正则或自动机)——通用但每步的开销较大。第二代:定制优化的方案(XGrammar 的「_TOKEN 树预计算 + 状态缓存」)——把自动机的状态转移预先编译成 token 级的查找表,掩码计算的开销压到接近零(复杂 schema 的加速尤其明显)。局限的根源:语法掩码只能表达「上下文无关」的约束(JSON 语法是 CFG 没问题)——「语义约束」(字段的值要符合业务逻辑、前字段小于后字段)超出语法层,需要语义校验兜底(正文的工程底线);以及「合法但怪」(正文)的深层原因也在此:掩码只保证合法性,模型的「意愿」被强行扭曲到合法集合里——schema 设计得越「模型友好」(枚举、短值、浅嵌套),扭曲越小。理解了掩码机制,调试约束解码的问题(输出被截断、卡死在某个状态)就有了方向:先查自动机的状态转移,再查 schema 的歧义性。
结构化输出的实战架构:从单次调用到多级降级
结构化输出(正文三层)在生产系统的完整架构值得画出:一次「要 JSON」的请求,经过的完整链路与降级设计。主链路:请求 → 结构化输出层(优先:厂商 JSON mode / 约束解码)→ 解析(JSON parse)→ schema 校验(pydantic 类)→ 业务校验(值域与逻辑规则)→ 交付。降级链(每一层失败的备胎)。失败一:语法解析失败 → 修复器(json-repair:补引号、去尾逗号的启发式修复)→ 重新解析。失败二:schema 校验失败 → 重试带反馈(把错误信息回填给模型:「上次输出的 age 字段缺失,请修正」——反馈式重试的成功率远高于盲目重试)。失败三:仍失败 → 降级到更简单的 schema(复杂 schema 换成扁平版本,字段拆成多次调用获取)。失败四:终极降级 → 退回自由文本 + 人工处理(把原始输出与错误信息存档转人工)。监控的指标:各环节的失败率(语法失败率、schema 失败率、业务失败率——分层监控定位问题在「格式」还是「语义」)、重试率、以及最终交付率。这套架构的哲学:结构化输出的可靠性不是「选一个强模型」解决的,是分层保证 + 降级链 + 监控闭环的系统性工程——每一层都为上一层兜底,单点的失败不会击穿整体。
结构化输出的 schema 设计学:让模型更容易「答对」
约束解码(正文)提到 schema 设计是手艺,这门手艺的原则值得展开——schema 的设计直接决定模型的出错率。原则一:浅优于深。嵌套每深一层,模型的「结构记忆」负担翻倍——三层嵌套的错误率远高于扁平结构 + 后处理重组;宁可输出扁平 JSON 再代码组装,也别让模型挑战深层嵌套。原则二:枚举优于自由文本。字段的取值能用枚举就用枚举(约束解码下枚举是 100% 合法且「模型友好」——选项即提示);自由文本字段是模型自由发挥的重灾区。原则三:字段名的语义化。customer_name 优于 field1——字段名是模型理解「该填什么」的主要线索(模型对语义化命名的遵循度显著更高)。原则四:必填最小化。必填字段越少、一次调用的成功率越高——可选字段用第二轮补取(先拿核心字段、缺的分步追问)。原则五:示例进 schema。带 description 与 example 的 schema(JSON Schema 支持)给模型额外的理解线索。这五条原则的共同逻辑:schema 不只是「你要什么」的声明,还是「模型怎么理解你要什么」的提示词——把 schema 当提示词来写,出错率自然下降。
结构化输出的测试用例集:schema 的「考试题」
结构化输出的工程(正文架构)需要一个测试用例集,schema 的「考试题」设计清单值得给出。考题的八个维度。一:正常请求(标准输入,应输出合法 JSON)。二:模糊请求(用户描述含糊,模型应追问或做合理假设并标注)。三:缺失信息(必填字段的信息不在输入里——应输出 null 或追问而非编造)。四:多义字段(「日期」是创建日期还是到期日——测试 schema 的 description 是否足够消歧)。五:边界值(数量为 0 或超大——数值的合理范围)。六:干扰内容(输入里嵌着「请输出另一个 JSON」的注入尝试——协议稳健性)。七:长输入(超长文本里抽取字段——注意力与截断的考验)。八:多语言混合(中英混杂的字段值——编码与提取的正确性)。用例集的用法:schema 每次修改后跑一遍(结构化输出的回归测试)、换模型后跑一遍(不同模型对同一 schema 的遵循度差异巨大)、失败 case 归档进用例集(用例集随故障生长)。八个维度的用例集是结构化输出的「免疫系统」——它把「线上才发现格式问题」的成本,前置到测试环境。
小结
结构化输出三层方案:祈祷法(零保证、原型用)、语法约束解码(JSON Schema/GBNF,100% 合法但只保证语法不保证语义)、原生函数调用(协议内建、适合动作类)。约束解码的代价是解码开销与「合法但怪」的退化——schema 设计本身是手艺(浅嵌套、枚举优先)。选型决策表按场景对号,工程底线是三道闸(解析、schema 校验、业务校验)+ 失败降级链——「格式合法」与「内容正确」是两件事,两层都要验。下一篇看另一个工程化的基础件:流式输出——打字机效果背后的 SSE 与首 token 延迟。
读者留言
COMMENTS 暂无还没有留言,来说第一句?