Skip to content

约束解码与语法引导生成

大语言模型(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 里写清楚要输出 JSON,再加个 few-shot 示例就够了。“实际生产中这远远不够:

  1. 长文本退化:当输出变长时,模型的注意力会分散,容易在中间漏掉括号或引号。
  2. 边缘情况:模型遇到训练数据中少见的字段组合时(如嵌套数组里套空对象),格式错误率飙升。
  3. 累积可靠性问题:单次 95% 的成功率听起来不错,但在一条调用 1000 次的流水线里,整体成功率只剩 0.951000≈00.95^{1000} \approx 0。约束解码把每一步都拉到 100%。

要理解约束解码,先要理解 LLM 生成 token 的完整管线:

  1. 前向计算(forward pass):模型根据当前上下文(prompt + 已生成内容)计算出一个 logits 向量。这里”logits”是模型对词表中每个候选 token 输出的原始分数(未归一化的实数,可以是负数),向量的长度等于词表大小(通常 3 万到 15 万)。
    • 这里的”token”是 LLM 处理文本的最小单位——模型不直接处理字符或词,而是处理 token。一个 token 可能是一个完整单词(如 hello)、一个子词(如 un + believ + able)或标点符号。词表与 token 的映射由 tokenizer 定义。
  2. softmax 归一化:经过 softmax 函数,将 logits 转为概率分布(所有值在 0-1 之间且和为 1)。
  3. 采样(sampling):根据概率分布(配合 temperature、top-p 等策略)选出下一个 token。

约束解码的切入点在第 2 步之前——插入一个 logits processor:将所有非法 token 的 logits 设为负无穷(即 -inf),这样经过 softmax 后概率为零,无论怎么采样都只会选到合法 token。

原始流程: logits → softmax → 采样
约束流程: logits → [屏蔽非法 token 为 -inf] → softmax → 采样

这个”屏蔽”操作本质是一个 mask(掩码),对每个 token 在词表中的位置打一个二值标记:允许(保留原 logits)或禁止(设为 -inf)。每一步生成的核心问题就变成了:如何高效计算这个 mask?

正则表达式可以编译成一个有限状态机(FSM, Finite State Machine)——一种由有限个”状态”和状态之间的”转移规则”组成的计算模型。每生成一个 token 后,FSM 转移到新状态,根据当前状态确定下一步哪些字符(从而哪些 token)合法。

Outlines(最流行的开源约束解码库)的算法分两步预编译:

  1. 正则 → FSM:用标准算法(Thompson 构造法 + 子集构造法)把正则编译成状态转移图。
  2. 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 是描述 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)。语义层面的合理性仍需模型自身能力或后处理校验。这是约束解码的根本边界:它管”形状”,不管”内容”。

对于更复杂的结构(如 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 的核心创新在于:将文法分解为可独立处理的小块,预先计算转移表,把运行时开销压到接近正则的水平。

约束解码不是”免费的”——强行屏蔽 token 会改变模型本来的概率分布,可能带来副作用:

  1. 强制路径偏移:模型原本最想说的 token 被屏蔽,被迫走另一条路径,后续内容可能不太连贯。
  2. 重复或退化:在约束很死的空间里(如枚举值太少),模型可能在合法空间内打转。
  3. “token healing”问题:约束边界附近(如 JSON 的引号、括号),模型可能产出语义上不自然的内容。

实践中,Schema 约束通常影响较小(合法空间够大),而过于严苛的正则或枚举约束需要谨慎设计。一个好经验是:约束”形状”而非”内容”——用 Schema 保证结构,把内容选择权留给模型。

KV Cache 是 LLM 推理加速的关键优化:每生成一个 token,模型需要重新计算注意力,但之前 token 的 Key/Value 向量可以缓存复用,避免重复计算。约束解码器插在 logits 之后,不影响 KV Cache 的使用。

从左到右:约束能力递增,实现复杂度递增,可靠性递增。选择哪一层取决于你的需求——大多数结构化输出场景,Schema 约束(第三层)已经是性价比最高的选择。

预编译只在模型启动时做一次,之后每次解码只需查表——这就是为什么现代约束解码库的开销可以压到 10% 以下。

示例 1:Outlines(正则与 JSON Schema 约束)

Section titled “示例 1:Outlines(正则与 JSON Schema 约束)”
import outlines
import 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, Field
import 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 类型系统驱动约束解码。

如果你用 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

对于需要约束 SQL 或自定义语法的场景,llama.cpp 的 GBNF 格式非常轻量:

# simple_sql.gbnf — 简化版 SQL SELECT 文法
root ::= select_stmt
select_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 (" = " | " > " | " < ") value
value ::= [0-9]+ | "'" [a-zA-Z0-9 ]* "'"
Terminal window
# 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 约束,保证后续步骤能正确解析前一步输出。详见推理技术。
类库 / 工具语言说明
OutlinesPython最流行的开源约束解码库,支持正则、JSON Schema、Pydantic 模型、CFG,兼容 Transformers / vLLM / llama.cpp 等多种后端。预编译 FSM 后开销极小。
GuidancePython微软出品,用模板语法混合约束与生成(类似 Jinja2 模板里嵌入约束块),支持正则、JSON、选择列表。语法直观,适合复杂 prompt 结构。
XGrammarPython / C++高性能文法约束引擎(2024 年发布),支持复杂 CFG,引入”上下文级跳过”等优化,已集成至 vLLM 和 TensorRT-LLM。在复杂文法下性能领先。
llama.cpp GBNFC++llama.cpp 内置的文法约束格式(GGML BNF),轻量高效,适合本地部署和边缘设备。
vLLM Guided DecodingPythonvLLM 推理引擎内置的约束解码功能,支持正则、JSON Schema、Choice(枚举),底层可切换 Outlines 或 XGrammar 引擎。
OpenAI Structured OutputsAPIOpenAI 2024 年 8 月推出的服务端 JSON Schema 约束,通过 response_format 参数传入 Schema,保证输出严格匹配。闭源 API 中的 Schema 约束标杆。
LlamaIndex / InstructorPython上层框架,封装约束解码为简洁的 Pythonic 接口(Pydantic 模型直接驱动),底层对接 Outlines / vLLM / OpenAI API。
TensorRT-LLMC++ / PythonNVIDIA 的高性能推理引擎,集成 XGrammar 做文法约束,面向生产部署优化。
术语英文解释
约束解码Constrained Decoding在解码阶段用规则约束 token 选择,保证输出格式 100% 合法
logits 处理器Logits Processor插在 logits 与采样之间的模块,将非法 token 的 logits 设为 -inf,使其概率为零
logitsLogits模型对词表中每个候选 token 输出的原始分数(未归一化实数)
tokenTokenLLM 处理文本的最小单位,由 tokenizer 从文本切分而来,一个 token 可能是词、子词或标点
有限状态机FSM, Finite State Machine正则表达式编译后的状态转移图,用于追踪当前在合法路径上的哪个位置
上下文无关文法CFG, Context-Free Grammar用产生式规则描述递归结构的语法形式化工具,可约束 SQL、代码等复杂嵌套输出
语法引导生成Grammar-guided Generation用文法(CFG)定义输出语言规则,引导模型合法生成
正则约束Regex Constraint用正则表达式限定输出格式,如日期、电话、邮箱,底层编译为 FSM
JSON SchemaJSON 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、连续批处理等推理优化技术如何协同工作。