让Agent变得可行,大模型结构化输出与受限解码技术
博客列表 主页

让Agent变得可行,大模型结构化输出与受限解码技术

大语言模型(Large Language Models, LLMs)在充当智能体(Agent)的大脑或系统控制中枢时,必须与传统的确定性软件系统进行交互。这种交互通常要求数据以极其严格的结构化格式(如复杂的 JSON Schema、XML 或特定领域语言的抽象语法树 AST)进行传递。然而,自回归生成机制与绝对可靠的结构化数据输出之间,存在着根本性的数学与架构冲突。

要求大模型在推理侧原生、绝对可靠地输出复杂结构化数据,不可避免地会遭遇注意力漂移(Attention Drift)与统计学概率衰减的阻碍。当模型在一个长上下文中被迫同时兼顾深度的语义逻辑推理(Semantics)与极其繁琐的格式闭合规则(Syntax)时,其 Transformer 架构中的自注意力机制(Self-Attention Mechanism)会被严重透支。导致模型性能的严重下降。

如何让语言模型生成可靠的结构化格式,是全面迈向AI Agent的必经之路。而非常多的Agent开发者完全不了解相关研究,而是仍然使用最为传统的提示词约束和重试机制,完全不了解目前的结构化输出技术已经深入到模型推理侧,并且具备相当高的可用性,简单的重试机制不仅会带来更多的API消耗,也可能导致模型陷入无休止的循环。

Prompt Engineering (2023年及以前)

在大模型应用落地的极早期阶段,由于缺乏对推理引擎底层的干预手段,业界几乎完全依赖提示词工程(Prompt Engineering)来引导模型生成结构化输出。这一阶段的技术以启发式(Heuristic)规则为主,主要利用大模型的上下文内学习(In-Context Learning)能力。

基于上下文分布诱导

早期引导格式的技术主要包括零样本指令(Zero-shot Instructions)、少样本示例(Few-shot Examples)以及基于模板的输出引导(Template-based Prompting)。开发者通常会在 System Prompt 中写入极其详尽的格式指令,例如:“你必须严格按照以下 JSON 格式输出,不要输出任何额外的自然语言解释,必须包含键值 ‘data’、’confidence’ 和 ‘reasoning’”。随后,通过提供数个完美的 JSON 问答对(Few-shot),试图在上下文窗口中构建出一种强烈的模式匹配惯性 。

从数学和统计机制来看,这种做法的本质是利用 Transformer 的自注意力机制,对条件概率分布进行偏移。标准自回归生成的概率模型为:

\[P(Y \mid X) = \prod_{t=1}^{N} P(y_t \mid y_{<t}, X, \theta)\]

其中 $X$ 是包含 Prompt 和 Few-shot 的上下文,$y_t$ 是生成的 Token。通过在 $X$ 中高频注入特定格式的 Token(如 {, “, :),自注意力机制在计算 Query 和 Key 的点积时,会赋予这些格式 Token 更高的权重,从而使得模型在预测 $y_t$ 时,输出期望语法符号的概率局部逼近于 1。

统计学和注意力机制的限制

尽管 Prompt Engineering 在简单任务中表现尚可(实际上由于目前模型推理能力的进步和位置编码技术带来的上下文扩增,他们在目前的复杂任务上依旧相对可靠),但在长上下文或复杂推理(如嵌套的 JSON Schema 或长篇代码生成)中,这种方法存在数学意义上的必然失败率 。其局限性主要体现在以下两个核心层面:

大模型的输出本质上是概率采样过程。假设模型在充分 Prompt 引导下,生成每一个正确格式 Token 的边缘概率极高,例如 $P(y_t \text{ is valid syntax}) = 0.999$。然而,一个完整的复杂 JSON 对象可能包含数百甚至上千个 Token。由于联合概率的乘法性质,生成一个完全合法的长度为 $N$ 的结构化数据的总概率将呈现指数级衰减。

Transformer 的核心是注意力机制。当模型被要求执行复杂的逻辑链(如数学推导或多步工具调用)时,其注意力质量(Attention Mass)必须高度集中在问题实体和逻辑推断上 。如果在 Prompt 中强加了冗长繁琐的 JSON Schema 约束和 Few-shot 示例,这些约束 Token 会疯狂稀释注意力矩阵的权重分布,导致其自身的语义的推断和理解能力下降。

与此同时,JSON Schema的约束在Token Efficiency上很差,会导致很多Token被浪费在JSON格式上,所以学术圈曾经有过一轮创造新格式的热潮,但是基本上不了了之,JSON和YAML作为经典的格式,方便不同开发者之间交流,可能只有企业内部的全新项目才可能可以推动相关的新格式作为Agent框架的一等公民。这还没有考虑推动新格式缺少预训练的影响。

JSON Mode 与针对性微调(2023-2024)

意识到 Prompt 工程无法打破概率诅咒后,研究界和工业界开始转向在模型权重层级和 API 系统层级进行干预。相关研究过渡到了模型训练与启发式后处理相结合的阶段。其产生原因很大程度上是学术界关于Tool Calling的大量研究开始出现,

例如,Toolformer 的核心思想是让语言模型通过自监督的方式学会使用工具,其训练语料中穿插了大量严格按照 [API_NAME(args)] 格式构建的结构化调用。Gorilla 7B 模型则更进一步,专门针对庞大的 API 集合进行了微调,并在微调过程中引入了检索感知(Retriever-aware)的训练方法,迫使模型将生成的输出与特定的 API 参数文档对齐。

然而,微调方案在工程实践中暴露出了严重的泛化性和成本缺陷。基于微调的模型极其容易对训练集中出现的特定 Schema 过拟合,一旦在推理时遇到全新定义的、未见过的嵌套 JSON Schema,模型的性能会急剧下降。为业务系统中成百上千个不断迭代的 API 接口持续进行 SFT 训练,其算力成本和数据维护成本也是极度高昂的 。

与此同时,以 OpenAI 为代表的头部厂商在 API 层面推出了原生的 JSON Mode。 其大致的底层逻辑基于高度优化的系统级微调,配合极其简单的启发式输出过滤(Heuristic Filtering):它仅仅保证模型最终输出的字符串可以被 json.loads() 成功解析,即保证了基础语法层面上的括号闭合、逗号分隔和字符串转义是合法的。但它的底层只认识JSON这种通用数据交换格式的宽泛语法树,而不了解开发者具体的业务逻辑,无法处理特定的Schema。

Python 级别的受限解码与库(2023-2024)

和JSON Mode一起发展的还有受限解码技术。这一阶段以 Python 级别的干预库(如早期的 Outlines、LMQL、Guidance)为代表,首次将离散数学中的自动机理论(Automata Theory)与 LLM 的概率采样过程结合,真正实现了向数学约束的跨越。

实现逻辑

受限解码的本质是拒绝让 LLM 在庞大且无边界的词表中自由采样,而是在每一步生成前,通过一个外置的有限状态机(Finite-State Machine, FSM)计算出当前上下文中“绝对合法”的 Token 集合,并强制掩码(Mask)掉所有非法的 Token 。

以 Outlines 为例,其基础算法首先将开发者定义的 JSON Schema 转换为一个庞大的正则表达式(Regex),随后将其编译为FSM,映射得到绝对合法的集合,从而得到一个掩码向量$m(S_{t-1}, Regex) \in {0, 1}^{\mid V \mid}$。模型前向传播计算出下一个 Token 的原始对数几率向量(Logits)$\alpha \in \mathbb{R}^{\mid V \mid}$ 。

在 Softmax 之前,执行 Logit Masking 的数学表达为:\(\tilde{\alpha} = m \odot \alpha\) 严格的实现中,掩码逻辑表现为将非法 Token 的 Logit 设为负无穷大:\(\tilde{\alpha}_i = \begin{cases} \alpha_i & \text{if } m_i = 1 \\ -\infty & \text{if } m_i = 0 \end{cases}\)最后,通过 Softmax 函数进行采样:\(\tilde{s}_t \sim \text{Categorical}(\text{Softmax}(\tilde{\alpha}))\)由于 $e^{-\infty} = 0$,非法 Token 被采样的概率在数学上被绝对清零,从而在解码期实现了 100% 的 Schema 服从率。

性能瓶颈

尽管 Outlines 的 FSM 掩码在数学上是完美的,但其早期纯 Python 层的实现引发了灾难性的性能瓶颈,导致许多底层推理引擎(如 vLLM)难以高效集成 。无论是CPU-GPU 同步与 GIL 锁开销,还是与高性能批处理推理本身的不兼容,以及FSM 无法处理嵌套递归结构的问题都导致相关技术无法被广泛应用。相较于JSON约束这个很少人在意的小问题,推理速度的重要性不言而喻

高性能底层受限解码引擎(2024 - 2026)

为了打破 Phase 3 中 Python 层面的性能瓶颈,系统和编译领域的研究者在 2024 至 2026 年间展开了激烈的角逐。当前的技术核心(State-of-the-Art)是将受限解码的逻辑彻底下沉到 C++/Rust/CUDA 等底层编译与执行环境中,并在算法上用下推自动机(Pushdown Automaton, PDA)和预编译语法树取代简单的 FSM。

从 FSM 到下推自动机(PDA)

为了完美解析 JSON 等具有嵌套结构的语法,现代底层引擎将 Schema 转换为上下文无关文法(Context-Free Grammar, CFG)。执行 CFG 的核心数据结构是下推自动机(PDA)。

PDA 相当于在传统的有限状态机上增加了一个“内存栈(Stack)” 。当遇到左括号 { 时,PDA 将一个状态压入栈中;遇到右括号 } 时,则进行弹栈操作。这种栈结构的引入,使得 PDA 能够无限层级地追踪 JSON 的嵌套深度 。

然而,PDA 的状态是由“FSM 状态 + 栈内容”共同决定的,由于栈的深度未知,PDA 的状态空间理论上是无限的,这意味着无法像 Phase 3 那样完全在离线阶段预计算出一张静态的 Mask 映射表 。主流 SOTA 方案均围绕“如何在不降低吞吐量的前提下,极其廉价地进行 PDA 的 Token 筛选”这一难题给出了不同的架构解法。

XGrammar:预编译和缓存命中

由 CMU/MLC 团队主导开发的 XGrammar 是目前 vLLM 等框架默认集成的结构化引擎 。XGrammar 的天才设计在于:虽然 PDA 的状态是无限的,但在实际解码的任何时刻,掩码中 $99\%$ 以上的 Token 其合法性并不依赖于当前栈的深度 。

XGrammar 在 C++ 层严格将词表分为两大类:

上下文独立 Token(Context-independent tokens):这些 Token 的合法性仅由当前 PDA 所处的顶层 FSM 状态决定,不需要查栈(例如在生成字符串内容 “value” 时,普通的英文字母都是独立合法的) 。

上下文依赖 Token(Context-dependent tokens):必须检查整个栈才能确定合法性的 Token(例如闭合括号 },只有当栈中存在对应的打开对象时才合法)。

XGrammar 在预编译阶段生成上下文独立 Token 的静态掩码。在解码运行期(Runtime),其 C++ 引擎维护一个自适应 Token 掩码缓存(Adaptive Token Mask Cache) 。对于绝大多数生成步骤,XGrammar 直接以 $O(1)$ 复杂度命中独立 Token 缓存,跳过了海量的 PDA 查栈计算,将每 Token 的 CPU 开销压缩至仅 $30-80 \mu s$ 。

SGLang (Compressed FSM):基于压缩状态机的向前跳跃解码

SGLang 从有限状态机的拓扑结构出发,提出了一项革命性的优化——压缩有限状态机(Compressed FSM)与向前跳跃解码(Jump-Forward Decoding)。

在解析 JSON Schema 时,存在大量必然的、确定的格式字符串。例如,当生成到某一步时,接下来的字符必须是 {“status”: “。在传统受限解码中,即便是合法的,LLM 也要白白浪费十几个前向传播(Forward Pass)周期,逐个预测出这些毫无信息熵的字符 。

技术拆解:SGLang 的压缩算法在离线阶段分析 FSM 的拓扑图,寻找奇异转移边(Singular Transition Edge):即某个状态有且仅有一个合法的后继状态的路径 。SGLang 将这些连续的单一路径压缩合并成一条粗粒度的多字符边(Compressed Edge)。

在解码运行期,一旦引擎到达压缩边的起点,系统直接跳过大模型的前向传播,将这一整串确定性字符串在 CPU 层进行 Tokenize,然后利用底层接口,直接 Append 写入到 GPU 的 KV Cache 显存树中 。

与底层内存管理机制的结合

底层受限解码的性能腾飞,离不开与现代显存分页管理机制的深度协同。

传统的 VLLM 采用 PagedAttention,将 KV Cache 切分为固定大小(如 16 Tokens)的内存块(Blocks),这极大地缓解了显存碎片化 。然而,PagedAttention 在处理包含大量相同 System Prompt 或 Schema 模板的结构化并发请求时,其前缀匹配和重用效率并非最优 。

SGLang 引入了 RadixAttention(基数树注意力机制),它是一种专门针对 KV Cache 复用(Reuse)而设计的结构 。在 RadixAttention 中,所有请求的 KV Cache 被组织成一棵巨大的字典树(Radix Tree / Prefix Tree)。当多个 Agent 同时请求遵循同一个复杂的 JSON Schema 时,它们的前置指令和 Schema 定义的 Token 序列是完全一致的。 RadixAttention 以 $O(\text{prefix_length})$ 的复杂度瞬间匹配出这些请求的共享前缀,并在 GPU 显存中实现细粒度(Fine-grained)的无拷贝共享 。

总结

回顾结构化输出技术从 2023 年至 2026 年的演进史,我们清晰地看到了一条“系统复杂度重心转移”的主线。在 Phase 1 和 Phase 2 中,生成语法的重担全部压在大模型脆弱的自注意力机制和概率权重矩阵上,导致极高的算力损耗和必然的失败率 。在 Phase 3 中,负担转移到了 Python 解释器和同步掩码计算上,导致了严重的 IO 阻塞 。而到了当前的 Phase 4,复杂语法树的解析、跳转与剪枝被彻底编译器化,固化到了 C++/Rust 的高性能底层甚至与 KV Cache 的内存树管理深度融合,大模型得以完全卸下语法包袱,全身心投入纯粹的语义逻辑推断 。

对于当前正在自研复杂 Agent 框架(如多智能体协作、长上下文 Function Calling、代码生成)的底层开发者而言,建议在系统架构中采取以下核心策略: 彻底摒弃提示词控制格式的旧范式:在设计 Agent System Prompt 时,应剥离所有关于格式符号的具体指令,将上下文窗口的宝贵 Attention 容量全部留给任务拆解、角色定义和背景知识库的检索结果 。格式约束必须通过向底层服务引擎传递强类型 Schema 来强制执行。还是有部分厂商不支持这样的受限解码,如Deepseek,但Gemini,OpenAI已经原生提供该服务,如果确实不支持,则需要回退到旧范式。

根据动态性选取最优底层解码引擎:如果业务需要大规模并发、Schema 相对固定的场景(如标准化的话单抽取、固定的日志 JSON 结构),则强烈建议使用 XGrammar 配合 vLLM。预编译阶段极高的上下文无关缓存命中率,能在大并发下提供极佳的吞吐量 。反之可以考虑SGLang等推理引擎。

要认识到,JSON Output已经不是靠提示词约束重试抽奖的游戏了,现有的编译优化允许我们高度自定义输出格式,推理引擎也会在推理的时候考虑这一点。

一些代码示例

vLLM 默认集成了 XGrammar、Outlines 和 LM-Format-Enforcer 等底层受限解码后端。通过 OpenAI 兼容服务(OpenAI-Compatible Server),在请求体中传入 JSON Schema。

from openai import OpenAI
from pydantic import BaseModel

class Sentiment(BaseModel):
    sentiment: str
    confidence: float

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

completion = client.chat.completions.create(
    model="Qwen/Qwen2.5-3B-Instruct",
    messages=[{"role": "user", "content": "Classify this sentiment: vLLM is wonderful!"}],
    # 启用受限解码,后端自动生成基于 XGrammar 或 Outlines 的 FSM
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "sentiment_response",
            "schema": Sentiment.model_json_schema()
        }
    }
)
print(completion.choices.message.content)

嵌套 JSON 格式化输出示例 对于复杂的嵌套结构(如思维链的逐步推导),同样依赖 Pydantic 构建 Schema。vLLM 的底层 PDA(下推自动机)引擎能够完美解析递归与嵌套关系。

from openai import OpenAI
from pydantic import BaseModel

# 定义嵌套的底层模型
class Step(BaseModel):
    explanation: str
    output: str

# 定义顶层模型
class MathResponse(BaseModel):
    steps: list[Step]
    final_answer: str

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
completion = client.chat.completions.create(
    model="your-model-name",
    messages=,
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "math_reasoning",
            "strict": True, 
            "schema": MathResponse.model_json_schema()
        }
    }
)

SGLang 同样提供了 OpenAI 兼容的 API 服务,支持 {"structured_outputs": {"json":...}} 语法。但其最强大的特性在于使用自带的 SGLang 前端语言(SGLang Frontend Language),它能与底层的压缩 FSM 和向前跳跃解码(Jump-Forward Decoding)深度融合,实现零开销并行生成。

简单的 SGLang 原生正则约束示例

import sglang as sgl

@sgl.function
def extract_info(s, text):
    s += f"Extract the name from this text: {text}\n"
    s += "Name: "
    # 使用正则表达式强制约束输出格式,底层会自动跳过确定性前缀
    s += sgl.gen("name", regex=r"[A-Z][a-z]+ [A-Z][a-z]+")

state = extract_info.run(text="The user is John Doe.")
print(state["name"])

嵌套 JSON 约束(基于 Jump-Forward 优化) 在生成嵌套 JSON 时,SGLang 可以通过传入 Regex 或直接传入 JSON Schema 来启用结构化输出。当底层检测到 Schema 中存在的硬编码字符串(如嵌套大括号和静态字段名 "wand": { "wood": )时,引擎将跳过前向计算直接写入 KV Cache。

import sglang as sgl

# 可以使用符合 JSON 结构的嵌套 Regex
nested_json_regex = r"""\{
  "name": "[\w\s]+",
  "house": "(Gryffindor|Slytherin|Ravenclaw|Hufflepuff)",
  "wand": \{
    "wood": "[\w\s]+",
    "core": "[\w\s]+",
    "length": \d+(\.\d+)?
  \}
\}"""

@sgl.function
def generate_character(s):
    s += "Generate a Harry Potter character profile in JSON:\n"
    s += sgl.gen("character", regex=nested_json_regex)

# SGLang 会使用压缩 FSM 加速这段深层嵌套的生成
state = generate_character.run()
print(state["character"])

如果直接购买API服务,则需要参考官方相关文档,Gemini,OpenAI都提供了相关API,基本都基于Pydantic的Schema来约束输出格式。但每家的参数名都有不同之处。