状态:草稿 (2026-10-27),实测。 测试于 2026-10-27,环境为单张 8GB 级笔记本 GPU(Ada 架构),vLLM 0.31.0(xgrammar 0.2.7,llguidance 1.7.6),Qwen3-Reranker-0.6B。官方文档基于 0.32.0 版本确认,结构化输出文档与 0.31.0 相同。 2026-10-27 新增。 在第 3.1 节中补充了使用对话版 Qwen3-0.6B·Qwen3-4B(AWQ) 和 vLLM 0.32.0(xgrammar 0.2.8) 对带标准答案的提取任务进行测试的结果。0.31.0 的数值保持不变。 编写规则:以实测数据为基础,未能测出的标记为「未实测」,未确认的内容标记为「未确认」。
目录
- 1. 原理:从下一个 Token 候选集中清除语法之外的内容
- 2. 使用方法与名称变更
- 3. 亲自测一测
- 3-1. 2026-10-27 重新测定:带标准答案的提取任务
- 4. 生产环境的取舍
- 5. 未确认的事项
- 参考
1. 原理:从下一个 Token 候选集中清除语法之外的内容
模型在每一步都会对整个词表(该模型为 151,669 个)中的每个词进行打分,并挑选其中一个。结构化输出会在挑选前插入一个步骤:将目前已生成的文本输入语法引擎(有限状态自动机或下推自动机)以获取「当前可接受的 Token」集合,并将其余 Token 的分数设为负无穷。该集合会被转换为位图(Bitmask),并一次性应用到 GPU 的分数张量上。
지금까지: {"priority": "
문법상 다음에 올 수 있는 것: l(ow), m(edium), h(igh) 로 시작하는 토큰만
나머지 151,000여 토큰 → 점수 -inf
因此,无论模型多么不靠谱,结果都会遵守语法。但这也带来了两项代价。
- 生成语法的代价:即把 JSON Schema 或正则表达式转换为 Token 级引擎的工作。由于词表大小达 15 万,预先计算「此 Token 在此状态下是否被允许」的开销很大。相同的 Schema 会被缓存。
- 每步构建掩码的代价:每次请求都需要推进一步状态、构建掩码并发送给 GPU。
XGrammar 论文(Dong 等, MLSys 2025)指出,通过预先计算与上下文无关的判定 Token,并使语法计算与 GPU 执行重叠,其速度比传统方法快最多 100 倍,且与推理引擎配合使用时几乎没有额外开销(基于摘要,具体条件摘要中未提及)。vLLM 同时使用了 xgrammar 和 llguidance(guidance 后端),默认值 auto 会根据请求进行选择。
2. 使用方法与名称变更
这里有两种写法:OpenAI 格式的 response_format 和 vLLM 专用的 structured_outputs 。
| 所需功能 | 请求中的参数 |
|---|---|
| JSON Schema | response_format: {"type": "json_schema", "json_schema": {...}} 或 structured_outputs: {"json": 스키마} |
| 任意 JSON 对象 | response_format: {"type": "json_object"} |
| 选项之一 | structured_outputs: {"choice": ["low", "medium", "high"]} |
| 正则表达式 | structured_outputs: {"regex": "[A-Z]{3}-[0-9]{4}"} |
| 上下文无关文法 (EBNF) | structured_outputs: {"grammar": "..."} |
| 仅标签内为 JSON | structured_outputs: {"structural_tag": ...}(第 5 篇中的 strict 工具调用即为此方式) |
随着版本迭代,名称也发生了变化。旧的 guided_json, guided_regex, guided_choice, guided_grammar 和每个请求的 guided_decoding_backend 已在 0.12.0 中移除,现在合并为 structured_outputs 一个。后端由服务器通过 --structured-outputs-config.backend 指定,V1 版本已取消按请求选择后端的功能。正则表达式语法因后端而异:xgrammar 和 guidance 使用 Rust 风格,lm-format-enforcer 使用 Python re 风格。从 0.32.0 开始,嵌套深度超过 64 层的 JSON Schema 将会返回 400 错误(基于发布说明,未实测)。
3.亲自测一测
方法
- 环境:单张 8GB 级笔记本 GPU(Ada 架构),vLLM 0.31.0,Qwen3-Reranker-0.6B (bf16)。该模型如果通过对话提问,几乎总是回答「No.」。因此, 有无强制约束的差异表现得非常明显。
- 请求:通过聊天 API 提问「请仅用包含 name、priority (low/medium/high)、tokens (整数)、tags (字符串列表) 四个键的 JSON 回答一个任务」,并设置了
temperature 0.7,max_tokens 160,每次请求给予不同的随机种子(seed)。 - 变体:无约束、
json_schema(必须包含四个键、禁止附加键、字符串长度上限)、json_object,choice,regex. - 服务器:分别将后端设置为
auto(默认)、guidance,xgrammar启动了三次。 - 重复:并发 1 测试五批(首批预热),并发 16 测试四批(首批预热)。「正确性」的判定标准为:能够将响应解析为 JSON,且包含准确的四个键且值类型正确。
结果 1:强制约束下出错的仅有被截断的情况
并发 16 下,三次测量批次(48 个请求)的总计。
| 变体 | auto | guidance | xgrammar | 响应示例 |
|---|---|---|---|---|
| 无强制约束 | 0/48 | 0/48 | 0/48 | No. |
| json_object | 0/48 | 0/48 | 0/48 | {"status": "no", "data": []} |
| json_schema | 47/48 | 46/48 | 46/48 | {"name": "", "priority": "high", "tokens": 0, "tags": []} |
| choice | 48/48 | 48/48 | 48/48 | low |
| regex | 48/48 | 48/48 | 48/48 | IOW-1139 |
- json_schema 中出错的响应全部都是
finish_reason因length导致的。 在 guidance 和 xgrammar 中各有两次触及 160 个 Token 的限制,在 JSON 闭合前结束(auto 服务器在记录此项前已重启,故未记录原因)。语法只能约束「下一个 Token」,并不能保证「在适时的时候结束」。必须为max_tokens设置比 Schema 最大长度更充裕的余量,且客户端必须务必检查finish_reason。 - json_object 仅遵守 JSON 语法。 样本虽然是语法正确的 JSON,但完全没有包含要求的键。如果键名很重要,请使用 json_schema。
- 语法正确不等于有用。 符合 Schema 的响应大多是空名字、0 或空列表。语法无法替代模型不会做的事。
- auto 服务器的 json_schema 响应输出 Token 数与 xgrammar 固定服务器在每个批次中完全一致(并发 1 下为 28、35、32、27)。推测此请求中 auto 选择了 xgrammar(日志中无选择记录,故未确认)。
结果 2:代价:首次请求,以及每个 Token 的微小开销
| 变体 | 后端 | 新 Schema 首次请求 TTFT(并发 1,预热批次) | 后续 TTFT 中位数 | Token 间隔中位数(并发 1) | Token 间隔中位数(并发 16) |
|---|---|---|---|---|---|
| 无强制约束 | xgrammar 服务器 | 43.6ms | 14.1ms | 5.9ms | 6.3ms |
| json_schema | xgrammar | 445.7ms | 14.2ms | 6.3ms | 7.4ms |
| json_schema | guidance | 974.6ms | 14.3ms | 6.0ms | 7.3ms |
| json_schema | auto | 508.0ms | 14.3ms | 6.2ms | 7.5ms |
| regex | xgrammar | 15.0ms | 13.7ms | 6.5ms | 7.8ms |
| choice | xgrammar | 22.3ms | 14.2ms | 6.4ms | 7.7ms |
- 新 Schema 的首次请求开销最大。 相同 Schema 的第二个请求开始,TTFT 就与无约束时相同了(约 14ms)。首次请求的值中不仅包含生成语法的开销,还夹杂了服务器首次运行内核的准备时间,因此两者无法区分。
- 单并发时的 Token 间隔增加了约 7%(5.9 → 6.3ms),16 并发时增加了约 17%(6.3 → 7.4ms)。 不过,由于无约束响应只有「No.」三个 Token,对比基准较短。16 并发时的增幅更大,这 vermutlich 是因为需要为 16 个请求在每一步都构建掩码。
- 由于 regex 和 choice 的语法较小,首次请求的开销几乎可以忽略不计。
遇到的问题:首个采样批次的几秒钟延迟
测试过程中,服务器日志中打印了这样的警告:
WARNING [jit_monitor.py:140] Triton kernel JIT compilation during inference: _topk_topp_kernel.
This causes a latency spike; consider extending warmup to cover this shape/config.
当新服务器首次接收到温度采样(top-k·top-p)请求批次时,它会当场编译该内核,导致该批次的 TTFT 从 3.9 秒(guidance 服务器,并发 1)增加到 11 秒(guidance 服务器,并发 16)。在使用贪婪解码期间未出现此警告。编译过程并非只发生一次。在另一台服务器上依次以并发 1、8、32 发送温度 1.0 的请求时,每次首次改变并发度都会导致该批次再次变慢(生成 256 个 Token 时,并发 1 从 5.4 秒降至 1.5 秒,并发 8 从 7.4 秒降至 1.9 秒,并发 32 从 8.4 秒降至 3.2 秒)。这表明每个批次大小都会重新编译内核。同时还打印了模型的 generation_config.json 覆盖了默认采样值(temperature 0.6, top_k 20, top_p 0.95)的警告。在生产环境中,启动服务器后,应先使用实际采样设置的请求进行一次预热,然后再接收流量。0.32.0 的发布说明中包含了一项关于减少采样器预热编译的内容(未实测)。
3-1. 2026-10-27 重新测定:带标准答案的提取任务
前面的测试仅验证了「是否为正确的 JSON」。这次我们同时验证了数值是否正确。
- 环境:vLLM 0.32.0 官方容器。模型包括对话版 Qwen3-0.6B (bf16)、其 FP8 在线量化版、GPTQ 8 位版、Qwen3-4B AWQ 4 位版。已关闭思考过程。
- 任务:同时发送六个类似「The job called backup-db has high priority, needs 1200 tokens and should be tagged nightly and storage. Reply with JSON only.」的句子(作业名称、优先级、Token 数量、标签各不相同),共测试三批(18 个请求)。首批作为预热剔除。
- 统计:「直接读取」指响应能否直接作为
json.loads解析,「数值正确」指剥离代码块后四个值是否全部正确。
| 模型 | 变体 | 直接读取 | 符合 Schema | 数值正确 | 响应时间中位数 |
|---|---|---|---|---|---|
| Qwen3-0.6B | 无强制约束 | 0/18 | 0/18 | 18/18 | 254.9ms |
| Qwen3-0.6B | json_object | 18/18 | 18/18 | 18/18 | 248.0ms |
| Qwen3-0.6B | json_schema | 18/18 | 18/18 | 18/18 | 273.5ms |
| Qwen3-0.6B FP8 | 无强制约束 | 6/18 | 6/18 | 18/18 | 187.0ms |
| Qwen3-0.6B GPTQ 8-bit | 无强制 | 0/18 | 0/18 | 18/18 | 185.6ms |
| 上述三个模型 | json_object·json_schema | 54/54 | 54/54 | 54/54 | |
| Qwen3-4B AWQ 4-bit | 无强制 | 18/18 | 18/18 | 18/18 | 476.0ms |
| Qwen3-4B AWQ 4-bit | json_schema | 18/18 | 18/18 | 18/18 | 507.5ms |
- 小型对话模型答对了值,但格式不对。 在没有强制要求时,Qwen3-0.6B 总是像
json …那样用 Markdown 代码块包裹输出。人类阅读起来没问题,但程序无法直接读取。使用 json_object 后,这个问题便迎刃而解了。 - 即使是同一个模型,其习惯也会随量化方式而改变。 FP8 版本在 18 次中由 6 次没有使用代码块直接回答。这意味着依赖模型习惯的解析器在更换模型或量化方式时可能会失效。
- 大模型则直接回答,没有使用代码块。 4B 模型即使在没有强制要求的情况下,18 次也都能被直接读取。
- 结构化输出并未改变数值。 所有三个变体的值准确率都是 18/18。部分原因也是因为这个任务比较简单。
- 成本方面的趋势与 0.31.0 时相同。在 0.6B 中,json_schema 的响应比无强制时约长 19ms(7%),对于新 schema 的首次请求,在预热批次中的 TTFT 为 195ms。
- 使用重排序模型在 0.32.0 中重新运行上述测试的结果(并发 16,json_schema 47/48,错误的一个是长度超限)也与 0.31.0 相同。首个采样批次的内核编译延迟在 0.32.0 中依然存在(重排序模型 5.3 秒,Qwen3-0.6B 6.2 秒)。
4. 运维经验总结
- 提前预热 schema。 如果服务使用的 schema 数量是固定的,在服务器启动后立即发送一次,就能避免首位用户多等待数百毫秒。对于 schema 随请求而变的服务(例如用户发送 schema 的 API),则需要持续承担此开销。
- 用户发送的 schema 是不可信的输入。 构建语法规则的开销随 schema 的大小和复杂度而增加,因此如果从外部接收 schema,应为其大小和深度设置上限。0.32.0 开始拒绝深度超过 64 的 schema 估计也是出于同样的原因。
finish_reason进行了确认。 结构化输出的“始终保证正确的 JSON”是指完整生成结束时的情况。- 如果需要键名,请使用 json_schema 而不是 json_object。
- 后端可以默认设为 auto,但如果首次请求延迟是个问题,可以像本次测量一样,使用相同的 schema 对两个后端进行重新测试并选择。在本次 schema 测试中,xgrammar 的首次请求延迟是 guidance 的一半,之后的数值则相差无几。
5. 未能确认的事项
- 大型 schema(深度嵌套的对象、长 enum)、上下文无关文法(EBNF)以及结构标签的开销尚未进行实测。
- 在首次请求的开销中,无法区分语法构建和内核准备各自占多少时间。auto 和 guidance 服务器在并发 16 下的首个 json_schema 批次耗时超过 1.5 秒,而 xgrammar 服务器仅为 30ms 的原因也尚未明确。
- 与推理模型(优先输出思考过程的模型)配合使用时的行为(
--structured-outputs-config.enable_in_reasoning)尚未实测。
参考资料
- vLLM 文档,Structured Outputs(0.31.0·0.32.0 相同):https://docs.vllm.ai/en/latest/features/structured_outputs.html
- Dong 等,“XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models”,MLSys 2025. https://arxiv.org/abs/2411.15100
- llguidance: https://github.com/guidance-ai/llguidance
- vLLM 0.32.0 发布说明(限制 schema 深度、采样器预热):https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 本系列第 5 篇 工具调用