response format 与 grammar constraints#
到了这一章,问题已经不再是“请求怎样走”或者“执行层怎样选 token”,而是“这些 token 在生成时怎样被限制在某种结构空间里”。这就是 response format 和 grammar constraints 进入执行链的地方。
这一节解决什么问题#
这一节主要回答三件事:
response_format为什么最后会落到采样参数里;- grammar constraint 为什么不是后处理,而是生成期约束;
- 为什么结构化约束必须和执行层一起理解,而不能只当作 API 表面功能。
response_format 真正落在哪#
从 ChatCompletionRequest.to_sampling_params
可以直接看出,response_format 并不会停留在 OpenAI-compatible 请求表面,而是会被转换成:
json_schemastructural_tag- 或其他结构化约束字段
也就是说,对 runtime 来说,它最后看到的不是 “response_format 是什么对象”,而是“这一轮 token selection 受什么结构约束”。
如果把这条链先压成图,会更容易看清:
flowchart LR
A["response_format"] --> B["to_sampling_params(...)"]
B --> C["json_schema / structural_tag / regex / ebnf"]
C --> D["SamplingParams"]
D --> E["token selection"]图里最重要的一点是:response_format 的终点不是“另一个响应对象”,而是执行层里的约束输入。
为什么 grammar constraint 不是后处理#
如果结构化输出只是后处理,系统应该先自由生成,再在结果出来后检查格式。但当前设计不是这样:约束在 to_sampling_params(...) 阶段就进入了执行层的参数集合。
这意味着:
- 约束不是“结果出来后再修补”
- 而是“一开始就影响 token 可选空间”
这件事在代码里也很直接:
if self.response_format and self.response_format.type == "json_schema":
sampling_params["json_schema"] = convert_json_schema_to_str(...)
elif self.response_format and self.response_format.type == "json_object":
sampling_params["json_schema"] = '{"type": "object"}'所以结构化约束并不是执行之后的格式层,而是在执行之前就已经进入了采样参数集合。
这也是结构化生成和采样必须放在同一部分里的原因。
结构化约束和普通采样参数为什么必须一起看#
从执行层视角看:
temperaturetop_pjson_schemaregexebnf
都属于”这一轮选 token 时要一起看的条件”。这不是说它们语义相同,而是说它们会在同一个执行阶段共同作用。
所以如果把 grammar constraint 看成”另一个系统”,就很难理解为什么它最终还是要通过 SamplingParams 进入执行链。
grammar constraint 的机制:token mask 是怎样构造出来的#
知道”grammar constraint 进入了执行层”还不够。这一层更关键的问题是:约束是怎样真正影响 token selection 的?
答案是 token mask:在每个 decode 步执行之前,grammar 引擎计算出当前状态下”哪些 token 合法”,生成一个与词表等大的二进制掩码,然后把不合法 token 的 logit 设为 -inf,让它们的采样概率归零。
整个过程如下:
grammar (EBNF / JSON schema / regex)
↓ 编译(一次性,启动时或首次使用时)
自动机状态(FSM for regex, PDA for EBNF)
↓ 每个 decode 步前推进状态
当前状态下的合法 token 集合
↓ 转成词表大小的 bitmask
token mask: [1, 0, 1, 1, 0, ...] (vocab_size 维)
↓ 与 logits 相乘(等价于不合法 token logit → -inf)
masked logits → sampling编译阶段(一次性开销)
Grammar 引擎(SGLang 使用 XGrammar)在请求进入执行链之前,先把 grammar 描述编译成自动机:
regex编译为 DFA(确定性有限自动机),状态转移确定,掩码查找 O(1);json_schema编译为特化的 pushdown automaton,带有 JSON 对象 / 数组 / 字符串等内置处理;ebnf编译为通用 pushdown automaton,支持任意上下文无关文法,编译开销最大。
编译结果会被缓存:相同 schema 字符串的请求可以复用同一份自动机,不必每次重新编译。
掩码计算阶段(每个 decode 步)
自动机在每步前向之后:
- 接受上一步生成的 token,推进当前状态;
- 从新状态出发,枚举所有可以出现在下一位置的合法 token;
- 生成词表大小的 bitmask(通常是 bitset,按 64 位块存储);
- 把 bitmask 转为 float mask,让不合法位置的 logit 加上
-1e9(等价于-inf)。
计算代价取决于自动机类型和词表大小。对 DFA(regex)来说,每步是 O(vocab_size);对 PDA(json_schema / ebnf)来说,需要递归展开,开销更高。
为什么 grammar 约束会影响吞吐
每个 decode 步都要算一次 token mask,而模型前向本身已经在 GPU 上。掩码计算通常在 CPU 上做(XGrammar 对此做了批量优化),但如果 grammar 复杂或词表大(32K+),这个 CPU 侧的掩码计算会成为瓶颈。
实际测量中,复杂 JSON schema 约束比无约束生成的 decode 吞吐降低 10%–40%,具体取决于 schema 深度和词表大小。regex 的开销通常小于 JSON schema,EBNF 最重。
这也是为什么 grammar constraint 是”执行期”约束而不是”后处理”:它在每个 token 选择之前都要运行,不是一个可以延后的可选步骤。
调试这层问题时先看哪里#
如果你看到的现象是:
- response format 看起来传进去了,但输出没有被约束住;
- 同一份 schema 在不同场景下行为不同;
- regex / ebnf 看起来存在,但生成仍然跑偏;
更稳的顺序通常是:
- 先确认
response_format是否已经被翻译成具体结构化参数; - 再确认这些参数是否真的进入了执行层;
- 最后再区分是 grammar 约束失效,还是 parser / 输出解释出了问题。
小结#
这一节真正要稳定下来的,是一个非常简单的判断:
response_format不会停留在协议层;- grammar constraint 不是后处理;
- 结构化输出从一开始就是执行期约束。
理解了这点,后面再读 tool parser 和 Responses API 时,就不会把它们误看成“只是在外层包装不同”。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。