Prhub

#45560 [GPT-OSS] Strict tool call and constrained decoding for Harmony

原始 PR 作者 yzong-rh 合并时间 2026-08-02 00:10 文件变更 10 提交数 3 评论 30 代码增减 +632 / -511

执行摘要

Harmony 严格工具调用与全生成期约束解码

PR body 指出:'vLLM's constrained decoding for Harmony delays enforcing response_format constraints until <|channel|>final...<|message|> is detected. This works correctly for JSON formats but creates a mismatch for structural-tag tool calling, since those constraints need to apply during the commentary/tool-call phase rather than only in the final-message channel.' 即现有实现只在 final 通道触发约束,工具调用阶段的 commentary/analysis 通道不受约束,导致 GPT-OSS 小模型生成格式漂移。作者在 issue 评论中补充:'The structural tags help gpt-oss-20b, which has trouble following the Harmony format, a lot. 20% uplift in BFCL multiturn-base if we decide to enable it by default',说明 20b 模型是本次严格约束的主要受益者。

值得精读。该 PR 展示了三个可借鉴的设计决策:一是用 vLLM 自建 structural tag 而非 patch 上游 xgrammar 内置实现,降低对上游 bug 的耦合;二是把用户各类 response_format 统一规范化为单个 structural_tag 通道,用 grammar admission 测试(xgrammar.testing._is_grammar_accept_string)从正反样本锁定接受集合;三是把约束起点从 final 通道前移到全生成期并用 reasoning-aware 包装避免行为回退。阅读时建议重点对照 vllm/parser/harmony.py 的 _assemble_tag/get_harmony_structural_tag 与 tests/parser/test_harmony.py 的 TestAdjustRequest,理解两处前端接线(online_renderer.py、responses/serving.py)的同步关系。

讨论亮点

核心讨论集中在 4 个点上:

  1. 自建 tag 还是 patch xgrammar:sfeng33 在 structural_tag_registry.py 的 _patch_harmony 处评论 'We can define the tags directly in HarmonyParser',chaunceyjiang 也建议 'implementing the complete structural tag directly in vLLM rather than relying on this kind of patch-based solution'。结论:yzong-rh 改为在 HarmonyParser 内通过 @register_vllm_structural_tag('harmony') 自建完整 tag,绕开 xgrammar 内置实现的 bug。
  2. 无条件 is_reasoning_end=True 的行为影响:bbrowning 警告 'if a client passes structured_outputs directly the new unconditional is_reasoning_end of True will cause that to start enforcing output immediately ... I suspect we'll break some in the wild usage'。结论:yzong-rh 追加 commit 把用户传入的 request.structured_outputs 也转换为 reasoning-aware structural_tag,避免用户可见行为变化。
  3. 控制 token 泄漏:ankrovv 实测发现 gpt-oss-120b 的响应中 mcp_call 的 name/server_label 变成 '<|constrain|>json',即 harmony 控制 token 泄漏进 recipient。结论:yzong-rh 确认是 openai_harmony StreamableParser 的处理问题,由 #45657 与 #47185 修复,并额外禁止 <|channel|>final json<|message|> 形态。
  4. 死代码:bbrowning 在 approval 中指出本 PR 删除了 prepare_structured_tag 的唯一真实实现,剩余调用点变成 no-op,但该方法是 ReasoningParser 接口的一部分,删除需要更广泛考虑,同意推迟。

实现拆解

实现按以下 5 步展开:

  1. 在 vllm/parser/harmony.py 定义 Harmony 专用 structural tag:新增 _END_TAG、_FINAL_BEGIN、_TOOL_CALL_CHANNELS、_FUNCTION_CALL_BEGINS 等常量;_assemble_tag 把 analysis/commentary 可选通道(内部放行任意文本)与核心 content 拼接为完整 SequenceFormat;通过 @register_vllm_structural_tag('harmony') 注册 get_harmony_structural_tag,其中无内置工具时按工具 x 前缀 x 通道的笛卡尔积生成严格 JSONSchemaFormat 约束,有内置工具时退化为仅保证调用某个工具的 fallback 路径,tool_choice == 'auto' 时额外放行两种 final 通道写法。
  2. 新增请求规范化入口:_params_to_final_content 把 StructuredOutputsParams(json_object、json、regex、choice、grammar、structural_tag,含 LegacyStructuralTagResponseFormat 与 StructuralTagResponseFormat 两种 tag 格式)映射为 xgrammar Format;_adjust_output_format 将请求改写为只保留 structured_outputs.structural_tag 的 reasoning-aware tag,其余约束字段清空;HarmonyParser.adjust_request 先执行 _adjust_output_format 再走父类逻辑。
  3. 简化 GptOssReasoningParser:删除约 130 行,包括 no_func_reasoning_tag、from_builtin_tool_to_tag、tag_with_builtin_funcs、prepare_structured_tag 以及与 <|channel|>final 前缀扫描相关的 token 级推理结束检测;is_reasoning_end 与 is_reasoning_end_streaming 直接返回 True,理由是由 structural tag 全程接管约束、不再需要边界切换。
  4. 接入两处前端调用点:vllm/renderers/online_renderer.py 的 render_chat 与 vllm/entrypoints/openai/responses/serving.py 的 _make_request_with_harmony 在进入 Harmony 渲染前调用 parser.adjust_request;同时删除 serving 中 tool_choice 仅支持 auto/none 的 NotImplementedError 守卫,使 required/named 也可用(strict 逻辑本身已支持),并同步删除集成测试里“required 必抛 InternalServerError”的旧断言。
  5. 配套调整与测试:vllm/tool_parsers/structural_tag_registry.py 把 _get_function_parameters 公开重命名为 get_function_parameters 供 harmony.py 使用,并从 XGRAMMAR_BUILTIN_STRUCTURAL_TAG_MODELS 移除 harmony;vllm/tool_parsers/gptoss_tool_parser.py 声明 structural_tag_model = 'harmony'。测试方面,tests/parser/test_harmony.py 新增 TestAdjustRequest,用 xgrammar.testing._is_grammar_accept_string 对 COMMENTARY、TOOL_CALL_1/2、FINAL_JSON_SCHEMA 等样本断言 grammar 接受/拒绝集合;tests/reasoning/test_gptoss_reasoning_parser.py 从 351 行缩减为对恒 True 边界的断言;Responses 集成测试对 tool_choice 做 auto/required 参数化。
文件 模块 状态 重要度
vllm/parser/harmony.py 解析器 modified 8.82
vllm/reasoning/gptoss_reasoning_parser.py 推理解析 modified 7.92
tests/parser/test_harmony.py 解析测试 modified 7.52
vllm/tool_parsers/structural_tag_registry.py 工具解析 modified 5.94
vllm/entrypoints/openai/responses/serving.py 响应服务 modified 5.76
vllm/renderers/online_renderer.py 渲染器 modified 5.48
vllm/tool_parsers/gptoss_tool_parser.py 工具解析 modified 4.49
tests/reasoning/test_gptoss_reasoning_parser.py 推理测试 modified 6.92
tests/entrypoints/openai/responses/test_harmony.py 集成测试 modified 6.32

关键符号

adjust_request _assemble_tag get_harmony_structural_tag _params_to_final_content _adjust_output_format get_function_parameters is_reasoning_end is_reasoning_end_streaming

分析完成后,这里会展示 LLM 生成的相对完整源码片段和详细注释。

评论区精华

自建 harmony structural tag 还是 patch xgrammar 内置实现 设计

sfeng33 在 structural_tag_registry.py 的 _patch_harmony 处评论 'We can define the tags directly in HarmonyParser';chaunceyjiang 也建议 'implementing the complete structural tag directly in vLLM rather than relying on this kind of patch-based solution'。

结论:yzong-rh 改为在 HarmonyParser 内通过 @register_vllm_structural_tag('harmony') 自建完整 tag,绕开 xgrammar 内置实现的 bug,且不占用 registry 的 patch 路径。 · 已解决

无条件 is_reasoning_end=True 对 structured_outputs 存量用户的影响 正确性

bbrowning 警告 'If a client passes structured_outputs directly the new unconditional is_reasoning_end of True will cause that to start enforcing output immediately ... I suspect we'll break some in the wild usage'。

结论:yzong-rh 追加 commit 将用户传入的 request.structured_outputs 也转换为 reasoning-aware structural_tag,避免用户可见行为变化。 · 已解决

<|constrain|>json 控制 token 泄漏导致 MCP 调用 正确性

ankrovv 实测 gpt-oss-120b 时发现 mcp_call 的 name/server_label 变成 '<|constrain|>json',即 harmony 控制 token 泄漏进 recipient;yzong-rh 确认是 openai_harmony StreamableParser 把 <|constrain|>json 当 recipient 处理,由 #45657 与 #47185 修复,并额外禁止 <|channel|>final json<|message|> 形态。

结论:依赖上游修复,本 PR 通过 _normalize_recipient 与禁止危险前缀规避。 · 已解决

reasoning 参数是否应传给 structural tag builder 设计

sfeng33 认为不需要为 harmony 修改 abstract_parser 的 reasoning 传递;chaunceyjiang 建议 'keeping reasoning=False for now'。

结论:保持 reasoning=False,未改动 abstract_parser 行为。 · 已解决

prepare_structured_tag 成为死代码 other

bbrowning 在 approval 中指出本 PR 删除了 prepare_structured_tag 的唯一真实实现,剩余调用点是 no-op,但它是 ReasoningParser 接口的一部分,删除需更广泛考虑。

结论:推迟清理,保留接口,后续单独处理。 · 待处理

Responses API 放开 tool_choice 限制 设计

yzong-rh 说明删除了 tool_choice 仅限 auto/none 的守卫以支持 required/named,并邀请 ankrovv 对比验证与 #44664 prefill 方案的效果。

结论:已放开限制,集成测试按 auto/required 参数化覆盖。 · 已解决

风险与影响

  1. 核心生成路径行为变更:is_reasoning_end 从 token 扫描改为恒 True,所有依赖该边界判断的逻辑(推理分段、约束施加时机)都会改变;bbrowning 已提示这可能破坏线上直传 structured_outputs 的存量用户,虽然本 PR 做了转换兜底,仍需关注边缘路径。
  2. 特殊 token 泄漏:yzong-rh 自测发现模型可能以子词方式逐步生成特殊 token(如 '< | e n d | >')而非单个 <|end|>,导致 tag 边界泄漏进输出;这不是 Harmony 特有,但在全生成期约束下更易暴露,属于未解决的已知问题。
  3. xgrammar API 依赖:自建 tag 大量依赖 xgrammar 的 OptionalFormat、SequenceFormat、TriggeredTagsFormat 等 API,且 _END_TAG 里的 '' 是绕开默认停止符的 workaround,xgrammar 版本升级可能引入兼容性风险。
  4. 前端双调用点:online_renderer.py 与 responses/serving.py 各自独立调用 adjust_request 且都带 TODO 注明未来统一,后续重构时容易遗漏一侧。
  5. 遗留死代码:ReasoningParser 抽象上的 prepare_structured_tag 已无实现,后续调用者可能误用。

用户影响:GPT-OSS/Harmony 系列(20b/120b)的工具调用行为显著变化——Responses API 的 tool_choice=required/named 从 NotImplementedError 变为可用;gpt-oss-20b 的 BFCL multi_turn 四类指标从 baseline 的 23%-35.5% 提升到 guided 的 32.5%-58%;120b 变化不大(部分指标小幅波动)。系统影响:约束解码从 xgrammar 内置 harmony tag 切换为 vLLM 自建 tag,benchmark 显示 guided 与 baseline 吞吐几乎一致(78.55 vs 80.37 req/s,输出 token 吞吐 7563.75 vs 7773.67 tok/s),无明显性能回退。团队影响:parser、reasoning、tool_parsers、frontend 多模块联动,get_function_parameters 公开与 structural_tag_model 声明为后续其他模型复用 vLLM 自有 tag 提供了模式;测试报告 429 passed、6 skipped、1 xfailed、1 xpassed。

核心解析路径行为变更 structured_outputs 语义变化 特殊 token 泄漏风险 两处前端调用点需同步维护 prepare_structured_tag 遗留死代码

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论