Prhub

#52222 [Bugfix][GPT-OSS] Fix strict tool-call grammar to accept Harmony renders

原始 PR 作者 yzong-rh 合并时间 2026-08-27 09:05 文件变更 2 提交数 2 评论 2 代码增减 +111 / -81

执行摘要

修复 Harmony 严格工具调用语法,兼容真实渲染输出

PR body 明确指出这是对 #51020 的重做,目的是修复在维持 BFCL 性能时被发现的严格工具调用语法 bug,并通过放宽约束来提升解析鲁棒性;同时将测试更新为使用真实 Harmony 渲染输出,而非手写样本,以减少测试与实际行为之间的偏差。

值得精读,尤其是 vllm/parser/harmony.py 中如何通过枚举约束变体与 OrFormat 组合来对齐模型真实渲染,以及测试中如何用 render_conversation 生成真实输出 token 来做回归验证。对工具调用解析相关维护者具有参考价值。

讨论亮点

本次 PR 的 review 讨论很少:唯一一条有效评论是维护者 bbrowning 的 Looks good - thanks!,并直接批准合并;claude[bot] 提示由于 PR 来自 fork,自动审查被禁用,可由维护者手动执行 @claude review。没有遗留的代码评审意见或争议点。

实现拆解

  1. 统一约束写法:在 vllm/parser/harmony.py 中新增常量 _JSON_CONSTRAINS = [" json", " <|constrain|>json"],取代原先硬编码的两种写法,作为后续标签生成的唯一约束来源。
  2. 重写函数调用模板:将 _FUNCTION_CALL_BEGINS 精简为两条带前导空格的通用模板 " to=functions.{name}{channel}{constrain}<|message|>" 与 "{channel} to=functions.{name}{constrain}<|message|>",随后在 get_harmony_structural_tag 中通过三层循环(工具 × 模板 × 约束 × channel)生成全部 TagFormat,确保解析器同时接受 Harmony 渲染产生的两种格式。
  3. 放宽 final 通道与结构化输出:get_harmony_structural_tag 中 tool_choice == "auto" 分支改为对 _JSON_CONSTRAINS + [""] 全部生成标签;_adjust_output_format 中当 final_content 为 JSONSchemaFormat 时,用 OrFormat 包装两种约束写法,使结构化输出也能被两种渲染风格接受。
  4. 测试改造:tests/parser/test_harmony.py 中 assistant() 增加 content_type 参数,get_model_output_tokens() 改为接受只包含 response_messages 的调用,并在内部固定使用 Message.from_role_and_content(Role.USER, "x") 作为 prompt,再通过 render_conversation_for_completion 与 render_conversation 生成真实模型输出 token;新增 get_model_output_str() 辅助获取渲染字符串;各测试用例从手写 prompt 迁移到真实渲染输出。
  5. 验证配套:运行 pytest tests/parser/test_harmony.py 得到 64 passed;BFCL 实测准确率 multi_turn_base 54.50%、multi_turn_long_context 33.50%、multi_turn_miss_func 44.50%、multi_turn_miss_param 45.00%。
文件 模块 状态 重要度
vllm/parser/harmony.py 解析器 modified 6.81
tests/parser/test_harmony.py 测试 modified 7.12

关键符号

get_harmony_structural_tag _adjust_output_format _assemble_tag assistant get_model_output_tokens get_model_output_str

关键源码片段

vllm/parser/harmony.py core-logic

核心解析器逻辑修改:统一 JSON 约束写法、放宽函数调用与 final 通道的语法标签,使 Harmony 解析器能够接受 GPT-OSS 真实渲染输出,是本次修复的关键。

# vllm/parser/harmony.py
# 统一两种 JSON 约束写法:Harmony 渲染可能输出 " json",也可能输出 " <|constrain|>json"
_JSON_CONSTRAINS = [" json", " <|constrain|>json"]
​
# Harmony 的 stop token(<|return|>、<|call|>)在 xgrammar 约束下被表示为 "",
# 因为 xgrammar 不允许在约束中使用 stop token,否则会导致生成错误或无限循环
_END_TAG = ["<|end|>", ""]
_FINAL_BEGIN = "<|channel|>final{constrain}<|message|>"
​
# 工具调用可以出现在 commentary / analysis / final 三个 channel 中
_TOOL_CALL_CHANNELS = [
    "<|channel|>commentary",
    "<|channel|>analysis",
    "<|channel|>final",
]
​
# 模板带前导空格,以匹配 Harmony 渲染的 " to=functions..." 风格
_FUNCTION_CALL_BEGINS = [
    " to=functions.{name}{channel}{constrain}<|message|>",
    "{channel} to=functions.{name}{constrain}<|message|>",
]
​
​
@register_vllm_structural_tag("harmony")
def get_harmony_structural_tag(tools, builtin_tools, tool_choice, reasoning):
    # Harmony 始终启用 reasoning,忽略该参数
    del reasoning
​
    if builtin_tools:
        # 内置工具走宽松 fallback:任意文本直到 <|start|> 或 <|end|>
        tags = [
            TagFormat(
                begin="to=",
                content=AnyTextFormat(excludes=["<|start|>"]),
                end=_END_TAG,
            )
        ]
    else:
        # 普通函数:对每个工具、每种 channel、每种约束写法都生成一个 TagFormat,
        # 让解析器同时接受两种渲染风格,修复之前严格语法拒绝真实输出的问题
        tags = [
            TagFormat(
                begin=pattern.format(
                    name=tool.function.name,
                    channel=channel,
                    constrain=constrain,
                ),
                content=JSONSchemaFormat(
                    json_schema=get_function_parameters(tool.function)
                ),
                end=_END_TAG,
            )
            for tool in tools
            for pattern in _FUNCTION_CALL_BEGINS
            for constrain in _JSON_CONSTRAINS
            for channel in _TOOL_CALL_CHANNELS
        ]
​
    if tool_choice == "auto":
        # auto 模式允许 final 通道在两种约束或完全不带约束之间选择
        tags.extend(
            TagFormat(
                begin=_FINAL_BEGIN.format(constrain=constrain),
                content=_ANY_CONTENT,
                end=_END_TAG,
            )
            for constrain in _JSON_CONSTRAINS + [""]
        )
​
    return _assemble_tag(
        allow_analysis=True,
        allow_commentary=True,
        content=OrFormat(elements=tags),
    )

评论区精华

整体审查与审批 other

维护者 bbrowning 对 PR 给出 `Looks good - thanks!` 并直接批准;claude[bot] 提示来自 fork 的 PR 自动审查被禁用,可由维护者手动执行 `@claude review`。没有内联评论或未解决讨论。

结论:PR 被批准并合并,无遗留问题。 · 已解决

风险与影响

  1. 解析约束放宽:vllm/parser/harmony.py 中函数调用与 final 通道的语法从固定单一写法扩展为两种约束写法,理论上可能让部分不符合预期的渲染被解析为合法工具调用,存在误解析风险;但 xgrammar 仍会对 JSON 内容本身做结构校验,风险可控。
  2. 测试覆盖局限:测试仅覆盖 tests/parser/test_harmony.py 的单元场景,未包含端到端工具调用集成测试,放宽后的语法对完整推理链路的影响尚未被充分验证。
  3. 模型范围风险:改动基于 GPT-OSS 的 Harmony 渲染验证,其他使用 Harmony 格式的模型是否同样受益或出现回归,需要额外评估。

影响范围集中在使用 Harmony 渲染格式的模型(如 GPT-OSS)的工具调用解析路径。修复后严格工具调用语法不再拒绝真实渲染输出,可提升 BFCL 等 benchmark 的准确率,并避免因解析失败导致的会话中断;测试基础设施的改进(真实渲染驱动测试)也为后续模型解析器提供更可靠的回归保障。

解析约束放宽 缺少端到端集成测试 仅 GPT-OSS 验证

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论