约束解码与语法引导生成
大语言模型(LLM)自由生成时,输出格式不可控——可能多一句寒暄、少一个引号、字段拼错。约束解码(Constrained Decoding) 在解码阶段用语法规则(正则、JSON Schema、上下文无关文法)强制 LLM 只能生成符合格式的 token,从根本上保证输出 100% 合法。它是结构化输出的底层实现手段,也是 LLM 从”聊天工具”变成”可靠 API 组件”的关键技术。
一句话定位:Prompt 只是”请求”模型输出某种格式,约束解码则是”强制”模型输出某种格式——前者靠概率,后者靠数学。
约束解码 = 给 LLM 的嘴装一个语法过滤器。
普通 LLM 说话像自由演讲——想到什么词吐什么词。约束解码像一个同声传译戴着耳麦,每说一个字都先在脑子里过一遍语法规则:不符合的词,直接咽回去,永远不会说出口。
- JSON Mode = 最粗的过滤器:厂商 API(如 OpenAI、Anthropic)在服务端开启,模型只能输出合法 JSON,但不保证内部字段结构。
- 正则约束 = 精确到字符的过滤器:用正则表达式定义输出格式(如电话号码、日期),模型逐字生成时只能走正则允许的路径。
- 语法引导(Grammar-guided)= 最灵活的过滤器:用上下文无关文法(CFG)定义完整的语言规则,可以约束任意结构化输出——SQL、代码、自定义 DSL(Domain-Specific Language,领域专用语言)。
- logits processor = 过滤器的物理实现:每一步生成时,在模型输出概率分布上把非法 token 的分数设为负无穷,强制采样只在合法 token 中进行。
为什么不能只靠 Prompt?
Section titled “为什么不能只靠 Prompt?”一个常见的误解是:“我在 prompt 里写清楚要输出 JSON,再加个 few-shot 示例就够了。“实际生产中这远远不够:
- 长文本退化:当输出变长时,模型的注意力会分散,容易在中间漏掉括号或引号。
- 边缘情况:模型遇到训练数据中少见的字段组合时(如嵌套数组里套空对象),格式错误率飙升。
- 累积可靠性问题:单次 95% 的成功率听起来不错,但在一条调用 1000 次的流水线里,整体成功率只剩 。约束解码把每一步都拉到 100%。
logits processor:约束的底层机制
Section titled “logits processor:约束的底层机制”要理解约束解码,先要理解 LLM 生成 token 的完整管线:
- 前向计算(forward pass):模型根据当前上下文(prompt + 已生成内容)计算出一个 logits 向量。这里”logits”是模型对词表中每个候选 token 输出的原始分数(未归一化的实数,可以是负数),向量的长度等于词表大小(通常 3 万到 15 万)。
- 这里的”token”是 LLM 处理文本的最小单位——模型不直接处理字符或词,而是处理 token。一个 token 可能是一个完整单词(如
hello)、一个子词(如un+believ+able)或标点符号。词表与 token 的映射由 tokenizer 定义。
- 这里的”token”是 LLM 处理文本的最小单位——模型不直接处理字符或词,而是处理 token。一个 token 可能是一个完整单词(如
- softmax 归一化:经过 softmax 函数,将 logits 转为概率分布(所有值在 0-1 之间且和为 1)。
- 采样(sampling):根据概率分布(配合 temperature、top-p 等策略)选出下一个 token。
约束解码的切入点在第 2 步之前——插入一个 logits processor:将所有非法 token 的 logits 设为负无穷(即 -inf),这样经过 softmax 后概率为零,无论怎么采样都只会选到合法 token。
原始流程: logits → softmax → 采样约束流程: logits → [屏蔽非法 token 为 -inf] → softmax → 采样这个”屏蔽”操作本质是一个 mask(掩码),对每个 token 在词表中的位置打一个二值标记:允许(保留原 logits)或禁止(设为 -inf)。每一步生成的核心问题就变成了:如何高效计算这个 mask?
正则约束:有限状态机
Section titled “正则约束:有限状态机”正则表达式可以编译成一个有限状态机(FSM, Finite State Machine)——一种由有限个”状态”和状态之间的”转移规则”组成的计算模型。每生成一个 token 后,FSM 转移到新状态,根据当前状态确定下一步哪些字符(从而哪些 token)合法。
Outlines(最流行的开源约束解码库)的算法分两步预编译:
- 正则 → FSM:用标准算法(Thompson 构造法 + 子集构造法)把正则编译成状态转移图。
- FSM → token 掩码表:对每个状态,遍历词表中所有 token,检查该 token 是否能从当前状态走一条合法路径。如果能,这个 token 在该状态下合法。预计算成一张
状态 → 合法 token 集合的查找表。
例如正则 \d{3}-\d{4} 约束电话号码格式(3 位数字 - 4 位数字):
- 状态 0(开头):只允许数字 0-9。
- 状态 1-2(第 2-3 位):只允许数字 0-9。
- 状态 3:只允许连字符
-。 - 状态 4-7(后四位):只允许数字 0-9。
- 状态 8(接受状态):只允许结束符(EOS)。
模型在任何状态下都不可能生成非法字符——不是”大概率不生成”,而是”数学上不可能”。
关键细节——token 与字符的错位:词表中的 token 可能包含多个字符(如
"42"可能是一个 token,也可能是"4"+"2")。约束解码器必须在 token 层面而非字符层面做匹配,这是约束解码实现中最棘手的部分。Outlines 的核心贡献之一就是高效解决了这个 token 对齐问题:它会尝试每个 token 的”前缀”能否从当前 FSM 状态继续走,以此判断该 token 是否合法。
JSON Schema 约束
Section titled “JSON Schema 约束”JSON Schema 是描述 JSON 数据结构的规范——它定义了字段的类型(string / number / array / object)、必填项、枚举值、字符串格式(如 "format": "email")等约束。约束解码器会将 Schema 转为对应的文法规则(通常编译成 FSM 或 CFG),确保输出的 JSON 在结构层面完全合法:
- 括号匹配、引号闭合(JSON 语法正确)
- 字段名与 Schema 定义一致(不会凭空多字段或漏字段)
- 值的类型正确(
"age"字段输出整数而非字符串) - 枚举值约束(
"status"只能是"active"或"inactive") - 嵌套结构正确(
"address"是对象而非字符串)
相比 JSON Mode(只保证 JSON 合法但不保证字段结构),Schema 约束能精确控制内部结构。2024 年 8 月 OpenAI 推出的 Structured Outputs API 就是把 JSON Schema 约束做进服务端的代表——用户传入一个 JSON Schema,API 保证输出严格匹配该 Schema。
语法正确 ≠ 语义正确:约束解码能保证 JSON 格式正确、字段类型匹配,但不能保证”age 字段填了一个合理的人类年龄”(模型可能输出 999)。语义层面的合理性仍需模型自身能力或后处理校验。这是约束解码的根本边界:它管”形状”,不管”内容”。
上下文无关文法(CFG)
Section titled “上下文无关文法(CFG)”对于更复杂的结构(如 SQL、编程语言子集),可以用上下文无关文法(CFG, Context-Free Grammar) 定义语法规则。CFG 比 FSM(正则)更强——它可以表达递归嵌套结构(如括号嵌套、if-else 嵌套),而 FSM 不行。
CFG 由一组”产生式规则”组成,例如定义简单算术表达式的文法:
expr := term (("+" | "-") term)*term := factor (("*" | "/") factor)*factor := number | "(" expr ")"number := [0-9]+llama.cpp 的 GBNF 格式(GGML BNF)、XGrammar 等工具支持用 CFG 约束解码,让 LLM 输出严格符合语法的代码或查询语句。XGrammar(2024 年发布的高性能文法约束引擎)引入了”上下文级跳过”等优化,大幅降低了复杂文法下的解码开销,已被 vLLM 和 TensorRT-LLM 集成。
性能挑战:CFG 的约束计算比正则复杂得多——正则只需查 FSM 状态表,CFG 需要维护一个解析栈(类似 LR 解析器),每一步都要更新栈状态。XGrammar 的核心创新在于:将文法分解为可独立处理的小块,预先计算转移表,把运行时开销压到接近正则的水平。
约束对生成质量的影响
Section titled “约束对生成质量的影响”约束解码不是”免费的”——强行屏蔽 token 会改变模型本来的概率分布,可能带来副作用:
- 强制路径偏移:模型原本最想说的 token 被屏蔽,被迫走另一条路径,后续内容可能不太连贯。
- 重复或退化:在约束很死的空间里(如枚举值太少),模型可能在合法空间内打转。
- “token healing”问题:约束边界附近(如 JSON 的引号、括号),模型可能产出语义上不自然的内容。
实践中,Schema 约束通常影响较小(合法空间够大),而过于严苛的正则或枚举约束需要谨慎设计。一个好经验是:约束”形状”而非”内容”——用 Schema 保证结构,把内容选择权留给模型。
约束解码工作流程
Section titled “约束解码工作流程”KV Cache 是 LLM 推理加速的关键优化:每生成一个 token,模型需要重新计算注意力,但之前 token 的 Key/Value 向量可以缓存复用,避免重复计算。约束解码器插在 logits 之后,不影响 KV Cache 的使用。
从左到右:约束能力递增,实现复杂度递增,可靠性递增。选择哪一层取决于你的需求——大多数结构化输出场景,Schema 约束(第三层)已经是性价比最高的选择。
正则编译为 FSM 的过程
Section titled “正则编译为 FSM 的过程”预编译只在模型启动时做一次,之后每次解码只需查表——这就是为什么现代约束解码库的开销可以压到 10% 以下。
示例 1:Outlines(正则与 JSON Schema 约束)
Section titled “示例 1:Outlines(正则与 JSON Schema 约束)”import outlinesimport json
# 加载本地开源模型(这里用 Qwen2.5-1.5B-Instruct 作为示例)model = outlines.models.transformers("Qwen/Qwen2.5-1.5B-Instruct")
# 一、正则约束:强制输出电话号码格式phone = outlines.generate.regex(model, r"\d{3}-\d{8}")print("电话:", phone("请给我一个联系电话")) # 如 138-12345678
# 二、JSON Schema 约束:保证字段类型与结构完全正确schema = { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0, "maximum": 150}, "role": {"type": "string", "enum": ["engineer", "designer", "manager"]} }, "required": ["name", "age", "role"]}person = outlines.generate.json(model, schema)result = person("描述一个虚构人物")print(json.dumps(result, ensure_ascii=False))# 如 {"name": "李明", "age": 28, "role": "engineer"}——age 一定是合法整数,role 一定是枚举值之一示例 2:Pydantic 模型约束(更 Pythonic)
Section titled “示例 2:Pydantic 模型约束(更 Pythonic)”from pydantic import BaseModel, Fieldimport outlines
model = outlines.models.transformers("Qwen/Qwen2.5-1.5B-Instruct")
# 用 Pydantic 定义数据模型(Pydantic 会自动转为 JSON Schema)class BookReview(BaseModel): title: str = Field(description="书名") rating: int = Field(ge=1, le=5, description="评分 1-5") summary: str = Field(description="一句话书评") tags: list[str] = Field(description="标签列表")
# Outlines 直接接受 Pydantic 模型reviewer = outlines.generate.json(model, BookReview)review = reviewer("为《三体》写一条短评")print(review.title) # "三体"print(review.rating) # 一定是 1-5 的整数print(review.tags) # list[str],每个元素都是字符串Pydantic 是 Python 最流行的数据验证库,用类型注解定义数据模型。Outlines 和 vLLM 都支持直接传 Pydantic 模型,内部自动转为 JSON Schema 再转为约束规则——让你用熟悉的 Python 类型系统驱动约束解码。
示例 3:vLLM 服务端约束解码
Section titled “示例 3:vLLM 服务端约束解码”如果你用 vLLM 部署模型,可以直接在 API 请求中指定约束,无需客户端处理 logits:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
# 方式一:正则约束response = client.completions.create( model="Qwen/Qwen2.5-7B-Instruct", prompt="我的订单号是", extra_body={"guided_regex": r"[A-Z]{2}\d{8}"} # 如 AB12345678)print(response.choices[0].text)
# 方式二:JSON Schema 约束response = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": "提取用户信息"}], extra_body={ "guided_json": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"} }, "required": ["name", "email"] } })print(response.choices[0].message.content) # 100% 匹配 Schema 的 JSON示例 4:llama.cpp GBNF 文法约束
Section titled “示例 4:llama.cpp GBNF 文法约束”对于需要约束 SQL 或自定义语法的场景,llama.cpp 的 GBNF 格式非常轻量:
# simple_sql.gbnf — 简化版 SQL SELECT 文法root ::= select_stmtselect_stmt ::= "SELECT " column_list " FROM " table_name (" WHERE " condition)?column_list ::= column_name (", " column_name)*column_name ::= [a-zA-Z_]+table_name ::= [a-zA-Z_]+condition ::= column_name (" = " | " > " | " < ") valuevalue ::= [0-9]+ | "'" [a-zA-Z0-9 ]* "'"# llama.cpp 启动时加载文法./llama-server -m qwen2.5-7b.gguf --grammar-file simple_sql.gbnf模型只会输出符合该文法的 SQL,如 SELECT name, age FROM users WHERE age > 25,绝不会出现语法错误。
- 约束解码 vs Prompt 提示:Prompt 提示靠模型自觉,可靠性约 80-95%;约束解码是数学保证,100% 合法。生产环境中涉及解析的关键场景(下游代码直接
json.loads()或反序列化为对象)必须用约束解码。 - 选哪一层约束?
- 只需要”是合法 JSON” → 厂商 JSON Mode 足够
- 需要固定字段和类型 → JSON Schema 约束(最常用)
- 需要精确到字符的格式(电话、日期、ID) → 正则约束
- 需要约束非 JSON 的语言(SQL、代码、自定义格式) → CFG 文法
- 预编译是关键:Outlines、XGrammar 等工具在首次调用时编译约束(正则 → FSM、Schema → 文法),之后缓存的编译结果可复用。同一个 Schema 调用多次时,编译开销摊薄到几乎为零。
- 解码开销通常低于 10-30%:现代工具预编译后,每步只需查表应用掩码,远小于一次重试(重新跑一次完整前向计算)的成本。XGrammar 在复杂文法上的优化进一步把这个数字压低。
- 首 token 延迟影响小:约束计算插在 logits 之后,不影响 prefill(首 token 生成的预填充阶段)——而 prefill 通常是延迟的大头。
- 批处理兼容性:在 vLLM 等批处理引擎中,不同请求可以使用不同的约束,引擎会在 batch 内逐请求应用各自的 logits mask。XGrammar 专门优化了批处理场景。
- JSON Mode 的局限:厂商 JSON Mode 只保证输出是合法 JSON,不保证字段名、类型、结构正确。如果下游代码直接反序列化为强类型对象(如 Python 的 Pydantic 或 Go 的 struct),字段不匹配会导致运行时错误。需要精确控制字段时,必须用 Schema 约束。
- 开源 vs 闭源模型:完整的约束解码(正则 / 文法)需要访问 logits 层,闭源 API(如 OpenAI、Anthropic)通常只提供 JSON Mode 或 Structured Outputs,不支持自定义正则或 CFG。要使用完整约束,需用开源模型(如 Qwen、LLaMA)配合 vLLM、llama.cpp、Outlines 等。
- 避免过度约束:约束太死(如枚举值只有两三个选项)可能导致模型”无路可走”——在合法空间里找不到有意义的 token 序列,输出垃圾内容。一个经验法则是:合法 token 数量在每一步都保持 > 5,给模型留出表达空间。
- tokenizer 依赖:约束掩码是针对特定 tokenizer 预计算的,换模型(换 tokenizer)必须重新编译。Outlines 会按
(tokenizer, 约束)缓存编译结果。 - 长文本退化:约束解码解决了格式问题,但如果输出内容很长,模型仍可能在合法格式内产出重复或低质量内容。结合 解码策略(如重复惩罚 repetition penalty)可以缓解。
- 先在无约束模式下跑一遍,看模型”自然想说什么”,再据此设计约束。
- 如果约束后输出质量下降,检查是不是约束太死——尝试放宽 Schema(如减少
required字段、扩大枚举范围)。 - Outlines 提供
outlines.generate.text作为无约束基线,对比有约束时的输出差异。
- 结构化 API 输出:让 LLM 输出严格匹配后端数据结构的 JSON,直接反序列化为对象,免去正则提取和容错处理。这是约束解码最常见的场景,也是 RAG 系统中”抽取 / 摘要”环节的标配。详见结构化输出。
- 信息抽取流水线:从合同、简历、医疗报告中抽取字段,用 Schema 约束保证抽取结果可直接入库(写入数据库或搜索引擎索引)。
- 工具调用参数生成:在 MCP 协议与工具调用 和 Agents 场景中,用约束解码保证 LLM 生成的函数参数严格匹配参数签名(参数名、类型、必填项),避免 function calling 时参数格式错误导致工具调用失败。
- 代码与查询生成:用 CFG 约束 LLM 输出合法 SQL 或特定 DSL(如 GraphQL、PromQL),避免语法错误。对 Text-to-SQL 场景尤其有价值——约束保证 SQL 语法正确,剩下只需关注语义正确性。
- 数据合成与增强:批量生成符合特定 Schema 的合成数据,用于训练数据增强、测试用例生成、数据脱敏替代等。
- Guardrails 与安全护栏:约束模型输出只能是安全合规的内容格式(如限定回复必须包含免责声明),作为输出侧的安全控制手段。
- 多步推理的结构化中间结果:在思维链(Chain-of-Thought)推理中,每一步的中间结果用 Schema 约束,保证后续步骤能正确解析前一步输出。详见推理技术。
典型类库与工具
Section titled “典型类库与工具”| 类库 / 工具 | 语言 | 说明 |
|---|---|---|
| Outlines | Python | 最流行的开源约束解码库,支持正则、JSON Schema、Pydantic 模型、CFG,兼容 Transformers / vLLM / llama.cpp 等多种后端。预编译 FSM 后开销极小。 |
| Guidance | Python | 微软出品,用模板语法混合约束与生成(类似 Jinja2 模板里嵌入约束块),支持正则、JSON、选择列表。语法直观,适合复杂 prompt 结构。 |
| XGrammar | Python / C++ | 高性能文法约束引擎(2024 年发布),支持复杂 CFG,引入”上下文级跳过”等优化,已集成至 vLLM 和 TensorRT-LLM。在复杂文法下性能领先。 |
| llama.cpp GBNF | C++ | llama.cpp 内置的文法约束格式(GGML BNF),轻量高效,适合本地部署和边缘设备。 |
| vLLM Guided Decoding | Python | vLLM 推理引擎内置的约束解码功能,支持正则、JSON Schema、Choice(枚举),底层可切换 Outlines 或 XGrammar 引擎。 |
| OpenAI Structured Outputs | API | OpenAI 2024 年 8 月推出的服务端 JSON Schema 约束,通过 response_format 参数传入 Schema,保证输出严格匹配。闭源 API 中的 Schema 约束标杆。 |
| LlamaIndex / Instructor | Python | 上层框架,封装约束解码为简洁的 Pythonic 接口(Pydantic 模型直接驱动),底层对接 Outlines / vLLM / OpenAI API。 |
| TensorRT-LLM | C++ / Python | NVIDIA 的高性能推理引擎,集成 XGrammar 做文法约束,面向生产部署优化。 |
| 术语 | 英文 | 解释 |
|---|---|---|
| 约束解码 | Constrained Decoding | 在解码阶段用规则约束 token 选择,保证输出格式 100% 合法 |
| logits 处理器 | Logits Processor | 插在 logits 与采样之间的模块,将非法 token 的 logits 设为 -inf,使其概率为零 |
| logits | Logits | 模型对词表中每个候选 token 输出的原始分数(未归一化实数) |
| token | Token | LLM 处理文本的最小单位,由 tokenizer 从文本切分而来,一个 token 可能是词、子词或标点 |
| 有限状态机 | FSM, Finite State Machine | 正则表达式编译后的状态转移图,用于追踪当前在合法路径上的哪个位置 |
| 上下文无关文法 | CFG, Context-Free Grammar | 用产生式规则描述递归结构的语法形式化工具,可约束 SQL、代码等复杂嵌套输出 |
| 语法引导生成 | Grammar-guided Generation | 用文法(CFG)定义输出语言规则,引导模型合法生成 |
| 正则约束 | Regex Constraint | 用正则表达式限定输出格式,如日期、电话、邮箱,底层编译为 FSM |
| JSON Schema | JSON Schema | 描述 JSON 数据结构的标准规范,定义字段类型、必填项、枚举等约束 |
| JSON 模式 | JSON Mode | 厂商 API 提供的服务端约束,保证输出为合法 JSON(但不保证字段结构) |
| 词表 | Vocabulary | 模型能输出的所有 token 的集合,约束掩码的长度等于词表大小 |
| 结构化输出 | Structured Output | 让 LLM 按预定义格式(JSON、表格等)输出的技术总称,约束解码是其底层实现 |
- Willard & Louf,「Efficient Guided Generation for Large Language Models」(2023):Outlines 核心论文,提出将正则编译为 FSM 并预计算 token 掩码,实现高效约束解码——本领域的奠基性工作。
- XGrammar 论文与开源项目 (2024):高性能文法约束引擎,引入上下文级跳过和并行化优化,在复杂 CFG 场景下大幅降低开销,已集成至 vLLM 和 TensorRT-LLM。
- OpenAI Structured Outputs 文档 (2024):OpenAI 官方对服务端 JSON Schema 约束的技术说明,介绍了
response_format参数的用法和限制。 - Microsoft Guidance 项目文档:微软的约束生成框架,用模板语言混合自然语言与结构约束,文档中有丰富的交互式示例。
- llama.cpp GBNF 文档:llama.cpp 的文法约束格式说明,包含 SQL、JSON、代码等多种文法示例,轻量实用。
- 本站相关页面:
- 结构化输出:从应用视角讲解结构化输出的完整技术栈。
- 解码策略:temperature、top-p、top-k 等采样策略——约束解码屏蔽非法 token 后,采样策略在合法 token 中做最终选择。
- MCP 协议与工具调用:约束解码保证 function calling 的参数格式正确。
- Tokenizers:理解 token 与词表是理解约束解码为什么需要”token 级掩码”的基础。
- LLM 推理优化:约束解码与 KV Cache、连续批处理等推理优化技术如何协同工作。