结构化生成与约束解码#

这章解决什么问题#

这一章解决的是“模型输出怎样被约束成目标格式”。如果不单独讲这一层,读者很容易把结构化生成误读成“API 层的附加能力”,而忽略它其实直接插在 generation path 里,和 sampling、logits 选择、tool parser 一起工作。

从官方文档看,SGLang 把 structured outputs 定义得很直接:你可以为请求指定 json_schemaregexebnf,并且这三种约束参数是互斥的,只能选一种。这不是外围包装,而是 generation 过程本身的一部分。

为什么它属于执行链,而不是纯 API 功能#

python/sglang/srt/sampling/sampling_params.py 里,SamplingParams 直接包含 json_schemaregexebnfstructural_tag 字段,并在 verify(...) 里明确检查 “Only one of regex, json_schema, or ebnf can be set.”。这说明约束解码不是协议层的装饰,而是采样参数对象的一部分。

这也是本章和上一章衔接的原因。执行模型章节已经解释了 sampling 参数怎样参与 token 选择;这里进一步说明,当这些参数变成 grammar constraint 时,输出就不再只是“按概率采样”,而是“在满足约束的前提下继续生成”。从系统设计上看,这比单纯在最终文本上做后处理要更稳,因为约束是在生成过程中被满足,而不是生成后再去修正。

这里最适合补图,因为“约束到底插在哪里”很难靠一两段话稳稳说清。下面这张图回答的是:Frontend / HTTP 两侧传入的 json_schemaregex、tool parser 配置,最终怎样汇入 sampling / generation path。

flowchart TB
    A["Frontend gen(...)\nregex / json_schema"] --> C["SamplingParams"]
    B["HTTP / OpenAI-compatible request\nresponse_format / extra body"] --> C
    C --> D["Grammar backend\nXGrammar / Outlines / llguidance"]
    C --> E["tool parser / function calling parser"]
    D --> F["generation path\nconstrained token selection"]
    E --> F
    F --> G["structured output / tool call payload"]

相对于纯文字,这张图多解释了“参数对象是汇合点”这一层。调用方可能从不同表面进入系统,但只要最后落到 SamplingParams 和对应 parser / grammar backend,结构化生成就不是外围技巧,而是 runtime 能力。

支持哪些约束形式#

官方 docs/advanced_features/structured_outputs.ipynb 当前明确列出了三类约束:JSON Schema、regular expression 和 EBNF。文档还指出,不同 grammar backend 对这三类约束的支持范围不一样,默认 backend 是 XGrammar,而 Outlines 与 llguidance 也可以通过 server 参数切换。

这里最值得读者抓住的,不是“有哪些后端名字”,而是设计方向:SGLang 把结构化生成看作一层 grammar backend abstraction。调用方通过 json_schema / regex / ebnf 描述约束,运行时通过 grammar backend 把这些约束落实到 generation path 上。这样做的好处,是约束形式可以演进,而上层调用接口保持稳定。

tool parser 和 function calling 在哪里接进来#

docs/advanced_features/tool_parser.ipynb 展示了另一条重要能力线:不同模型可以通过不同 parser 解释 function calling 结果,例如 deepseekv3glmqwenpythonic 等 parser。这里的重点不是模型名单,而是它暴露出一个事实:结构化输出不仅可以约束成 JSON 或 regex,还可以约束成工具调用语义。

这条线和纯 grammar constraint 的关系是:前者更偏“把输出解释成可执行结构”,后者更偏“在 token 生成时限制格式空间”。两者放在同一章,是因为它们都在回答“模型怎样稳定地产生可消费结构”,但仍然需要分开理解,避免把 parser 逻辑误写成 grammar backend 逻辑。

一个更像“书中例子”的最小约束片段#

如果只说 json_schemaregex,读者容易觉得这只是参数名。更直观的最小例子可以写成下面这样:

{
  "temperature": 0,
  "json_schema": "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}"
}

这个片段的重点不在 JSON 本身,而在于它提醒你:结构化生成不是“模型先自由输出,再拿 schema 去检查”,而是 schema 从一开始就进入 generation path。只要带着这个视角再回头读 SamplingParams,你就更容易理解为什么这些字段要和 sampling 参数放在同一个对象里。

如果把这个例子放回更完整的调用上下文#

在 frontend language 一侧,你可以把它想象成这样一种使用姿势:prompt program 仍然用自然语言组织上下文,但 gen(...) 最后一步不再只接 max_tokenstemperature,而是同时接入结构约束。这意味着结构化生成不是在“另一个系统”里完成的,而是在同一段生成逻辑里发生的。

在 HTTP / OpenAI-compatible 一侧,调用形式会不同,但阅读方式应该一样:不要先盯请求 JSON 长什么样,而要先问“这个约束最后是不是进入了 generation path”。只要这个问题能回答清楚,frontend 和 HTTP 两侧在理解上就会重新汇合。

Frontend 与 HTTP 两侧是怎么使用这些约束的#

在 frontend language 一侧,python/sglang/lang/api.py 里的 gen(...) 已经直接接受 regexjson_schema 参数。这说明约束生成不需要一定经过 HTTP server 才能使用,它本来就是语言层 API 的一部分。对使用者来说,这条路径更像“在 prompt program 里直接声明结构”。

在 HTTP / OpenAI-compatible 一侧,官方 structured outputs 文档展示了通过 response_format 或额外 body 参数传入 schema / regex 的方式。两侧的共同点是:最终都会落到 sampling 参数和 grammar backend。也就是说,接口表面可以不同,但内部真正消费约束的地方仍然在 generation path。

失败模式与边界条件#

结构化生成最容易被误解成“只要传了 schema 就一定等于高质量输出”。这其实不是一回事。约束能保证的是结构空间,而不是语义正确性。一个 JSON 结构完全合法,但字段值仍然可能不符合业务预期;同样,一个 tool call 形状正确,也不代表工具选择本身就是最优的。

因此,本章真正强调的是:结构化生成解决的是“输出可消费”,不是“输出永远正确”。这也是为什么在工程实践里,结构化生成通常要和后续校验、业务语义检查或工具结果回填一起看,而不是把它当成单独万能层。

另一个边界条件是:不同 grammar backend 的支持范围并不完全相同。官方文档已经明确区分了 XGrammar、Outlines 和 llguidance 的能力覆盖面。也就是说,当你观察到“同一份约束,在不同 backend 上行为差异明显”时,不要第一时间把它归因于模型输出不稳定,更稳的做法是先回头确认 grammar backend 与约束类型是否匹配。

调试时最容易忽略的两件事#

第一,先确认约束有没有真的进入 generation path,而不是只停留在外层请求体里。也就是说,排障时应当先回到 SamplingParams、frontend gen(...) 或 OpenAI-compatible structured output 输入层,看 json_schema / regex 是否已经被正确传入。

第二,不要把 parser 行为问题误判成 grammar backend 问题。前者更像“输出结构怎样被解释”,后者更像“输出空间怎样被限制”。这两层虽然都属于结构化生成,但排障入口完全不同。

为什么优秀系统会把“约束”提前,而不是事后修补#

从工程直觉上说,很多人会想:先让模型自由生成,再写一个后处理脚本去清洗结构,不也可以吗?对很简单的输出,这种办法当然有时能工作;但一旦输出要被下游程序、工具调用或 schema 校验消费,事后修补的稳定性通常会快速下降。

SGLang 在这件事上的设计选择,是把约束前移到 generation path。这让系统在“生成中”就尽量待在可接受空间里,而不是先跑出一段难以处理的文本,再试图把它改造成结构化输出。也正因为这样,json_schemaregex、tool parser 才值得被看成 runtime 能力,而不是外围清洗技巧。

本章对应哪些代码路径#

这一章最重要的文件与文档锚点包括 python/sglang/srt/sampling/sampling_params.pypython/sglang/lang/api.pydocs/advanced_features/structured_outputs.ipynbdocs/advanced_features/tool_parser.ipynbdocs/basic_usage/sampling_params.md

要继续深挖,推荐顺序是:先看 SamplingParams 里的约束字段和校验逻辑,再看 lang/api.py 里 frontend gen(...) 怎样暴露这些参数,然后回到 structured outputs / tool parser 文档看调用样式。这样能把“参数对象”“语言入口”“文档示例”三者连成一条线。

小结#

结构化生成这一层的关键,不是“格式更漂亮”,而是把输出空间主动收窄到目标结构。SGLang 之所以把它设计成 generation path 的一部分,而不是事后修补文本,是因为只有这样,json_schemaregex、EBNF 和 tool parser 才能真正成为 runtime 能力,而不是外围技巧。