结构化抽取与模式约束解码

Constrained Decoding / Structured Output

模式约束解码是强制 LLM 输出严格符合 JSON Schema / XML DTD / 正则 / 函数调用参数等预定义格式的技术,保证下游系统可直接解析而无需写正则兜底。

详细解释

Constrained Decoding(约束解码 / 模式强制解码)Structured Output(结构化输出) 是 2024 年下半年开始成为 LLM 生产化基石的能力:“你告诉模型一个格式模板(比如 JSON Schema、一个 TypeScript interface、一个正则表达式),模型输出的每一个 Token 都会在”被允许的 Token 集合”里选,保证最后 100% 能 parse 成功,不会再出现”模型多写了一句人话就导致 JSON.parse 报错”这种经典事故。”

没有约束解码之前,90% 的工程师在做 LLM 落地时的日常工作就是:写 100 行正则,去从模型输出里”尽量 extract 我想要的字段”,经常有 1% 至 2% 的失败 case 要兜底。约束解码把这部分失败率从 1% 降到 0.00%,节省了半个工程师的日常修 bug。

主流约束解码实现(四代技术)

技术时代代表实现原理约束表达力对模型本身要求
第一代:Prompting + 后处理(2023 及之前)手工写 “你必须只返回 JSON,json ... ,不要任何其他文字” + try-catch 重试 3 次纯靠模型听话,失败率约 3% 到 10%JSON / XML 纯字符串不需要
第二代:Grammar/Regex 运行时过滤(2024 初)llama.cpp Grammar / Outlines / LM Format Enforcer / Guidance在每一步生成 next_token_logits 之后,用一个状态机(CFG / Regex / JSON Schema state)把”不合法的 token” 概率掩码成 -∞,强迫模型只选合法 token。极强:任意 CFG 文法、任意 JSON Schema、任意正则任何模型都能用(vLLM / SGLang / llama.cpp 已原生集成)
第三代:模型原生 Structured Output(2024 中至今)OpenAI response_format=json_schema / Anthropic tool_use strict / 唯元智创 /chat/completions strict=true在 SFT 阶段专门训练了”遵守格式”的数据,让模型骨子里就会严格按 Schema 输出,不用运行时再做 logits 掩码。失败率 < 0.4%。JSON Schema / Tools parameters 严格校验模型必须训过”格式遵守”(SFT 数据里混入格式样本)
第四代:端到端 Speculative Constrained(2024 下半年至今)SGLang + Grammar Speculative DecodingGrammar 状态机和 Speculative Decoding 合起来,生成长 JSON 时吞吐比纯 logits mask 快 2x。同第二代任何模型都能用

现在 2025 年的最佳实践是:第三代(原生 strict=true 模式)+ 第二代(Grammar 运行时掩码)做双层保险。比如唯元智创兼容 API:当你请求里传的 response_format 对象包含 type、json_schema、strict 三个字段,strict 设置为 true、json_schema 字段填你自己的 Schema 定义时,内部先过一遍 SFT 原生结构化生成,再用 Outlines 的 Grammar 掩码作为第二层兜底,保证返回的 100% 可解析。

真实落地的 4 个典型结构化场景 + 推荐 Schema 写法

场景输入示例期望输出结构约束精度推荐方式
客户工单意图识别(5 分类 + 3 个实体)用户聊天记录原文一段包含意图分类字段 intent(枚举 refund / exchange 等 5 类) + 可选实体字段 order_id / product_sku / customer_phone 三个字符串字段分类字段 Enum 100% 命中,实体字段遗漏率 < 1%strict=true JSON Schema
简历结构化(50 字段)求职者简历 PDF 图片顶层 name / phone / email 基础字段 + education 教育经历数组 + work_experience 工作经历数组 + skills 技能数组;education 数组每个元素内嵌 school / major / degree / start / end 五个子字段字段缺省率 < 3%JSON Schema required:[] + additionalProperties:false
SQL 生成(只允许 SELECT,不能 DROP)自然语言问题 + 表结构描述SELECT ... FROM ... WHERE ...;必须是单条 SELECT,禁止分号后第二句;禁止 DELETE/DROP/UPDATE100% 语法可执行Regex 约束 + 白名单关键字过滤
工具调用多参数校验(FC 严格模式)用户问”订明天飞北京的机票两张,经济舱,预算 2000”顶层 tool 字段固定为 book_flight;内嵌 args 对象包含 from_city / to_city / date(格式 YYYY-MM-DD) / pax(整数) / cabin(枚举 Y/F/C 舱等) / budget_max(数字)6 个带类型的参数必填字段不缺 < 0.5%FC strict=true + Grammar 第二层

写好 JSON Schema for LLM 的 5 条黄金法则

Schema 写得差,模型即使有约束解码也会瞎填字段——失败的不是”格式解析”,而是”字段值语义错”。五条法则收藏级:

  1. 每个字段必须写 description(一句话告诉模型这个字段啥意思、用什么格式、从哪里取),别只写 type。对 date 字段写 description: ISO 8601 YYYY-MM-DD,只允许今天之后 180 天内,模型就不会给你填”2025年8月”这种。
  2. Enum 能写就写,不要让模型自由字符串:城市字段如果你的后端只支持 31 个省辖市,Enum 31 个全部列出来。
  3. required 字段必须真实准确:后端实际 order_id 是必填,Schema 里没写 required,模型 20% 会漏;后端实际不支持附加字段,Schema 写 additionalProperties: false,模型就不会乱塞 comment 字段。
  4. 嵌套数组给 example:对 education[] 数组,在 description 里写 example 给出一个真实中文案例数组项,包含学校北京大学、专业计算机科学与技术、学历本科、起止时间 2018-09 至 2022-06 五个字段的具体取值,模型一眼学会格式,生成稳定 +20%。
  5. 字段类型能 number 就别 string:金额、数量、分数、年龄,直接 type:number,约束解码会强制它输出数字而不是”约 2 千块”这种字符串,你后续计算省一大半转类型的代码。

常见问题

约束解码会不会把模型逼成”格式对了但内容瞎编”?
会,但这是”字段值质量问题”不是”约束解码本身的问题”,你不做约束解码它也瞎编,只不过以前是”格式也错内容也错”,现在是”至少格式对了,你能只判内容”。解决方式:(1) Schema description 里写清楚”只从上下文中原文拷贝,不要编造,未知填 null”;(2) 对关键数字字段再加一层业务后校验(如金额不能负数、年龄不能 300);(3) 对复杂 50 字段场景,开 strict=true + 原生 Structured Output + 后处理业务校验,校验失败把具体错误以 role=system error message 格式回塞让模型改,2 轮内 99.9% 能改对。
Streaming(流式输出)下能支持约束解码吗?
完全可以,SGLang / Outlines / llama.cpp Grammar 这一代实现,状态机是每生成一个 token 就推进一个状态,天然就是逐 token 的,所以流式输出没有任何额外阻碍。OpenAI 官方的 stream=true + response_format=json_schema 现在也是逐 chunk 吐,返回的最后一个 chunk 里还会带一个 finish_reason=stop 和 usage。唯元智创 兼容端点的 stream 结构化输出同样支持 strict=true,实测首字延迟只比无约束多 50ms(因为 Grammar 状态机推进开销极小),几乎不影响用户体验。
Function Calling 和 Structured Output 我该选哪个?
一句话:需要”先调用外部函数,拿到结果再继续生成最终答案” → 用 Tool Use / Function Calling;只需要”模型最终直接吐出结构化 JSON 给我,没有后续第二轮” → 用 Structured Output response_format=json_schema。两者底层能力是同一套(都是模型对格式的严格遵守),只是协议不同。你也可以混用:对”结构化 JSON 结果 + 返回后异步调我们内部订单 API”这种场景,先 Structured Output 拿到 JSON,自己再调内部 API——比走 Function Calling 少一轮”模型生成 tool_calls 再回塞”的来回,更省 Token 也更快。