Prhub

#38973 [ToolParser][Bugfix] Re-land: Fix anyOf/oneOf/$ref type resolution in Qwen3CoderToolParser (#37831)

原始 PR 作者 AAISSJ 合并时间 2026-05-21 12:24 文件变更 2 提交数 13 评论 8 代码增减 +193 / -108

执行摘要

修复 Qwen3CoderToolParser 对 anyOf/oneOf/$ref 类型解析,适配 Pydantic v2

Pydantic v2 为 Optional[T] 字段生成 anyOf schema(详见 PR body 与关联 Issue #37831),导致 Qwen3CoderToolParser 将可空整数/字符串等路由到 json.loads 或直接返回原始字符串,破坏参数类型转换正确性。同时,原始修复 #37831 因与工具解析器重构 PR #38189 的合并顺序冲突被回滚(参见 PR body 说明)。

值得精读:此 PR 体现了将重复的内联类型解析逻辑提取为共享工具的设计思路,适合想了解 vllm 工具调用系统内部及 Pydantic v2 schema 处理的工程师。
关注点extract_types_from_schemacoerce_to_schema_type 的定义(位于 vllm/tool_parsers/utils.py)是理解本修复的关键。

讨论亮点
  1. anyOf 内的 $ref 处理(reviewer gemini-code-assist):指出当 anyOf 变体包含 $ref(如 {"anyOf": [{"$ref": "..."}, {"type": "null"}]})时,原实现会因缺少 type 字段而回退为 "string"。最终方案通过共享 extract_types_from_schema 已内置此逻辑,无需在解析器内单独处理。
  2. 范围控制(approver sfeng33):最终提交中将 XML 解析器的修改剥离,仅保留 Coder 解析器修复,以“减少爆炸半径”。这一决策在 approval 评语中明确说明。

实现拆解

  1. 简化 _convert_param_value:将原先长达 100+ 行的内联类型逻辑(手动处理 anyOf、type-as-array、$ref、类型字符串匹配等)整体替换为 param_schema = param_config.get(param_name, {}) + extract_types_from_schema(param_schema) + coerce_to_schema_type(param_value, param_types) 三行核心调用,将类型解析职责委托给 vllm 共享工具函数(定义在 vllm/tool_parsers/utils.py)。
  2. 调整导入:新增 coerce_to_schema_typeextract_types_from_schema 导入,移除了无用的 import ast
  3. 补充测试:在 tests/tool_parsers/test_qwen3coder_tool_parser.py 中新增两个端到端测试函数:
    • test_extract_tool_calls_anyof_type_conversion:覆盖 anyOf[integer|null]、anyOf[string|null]、anyOf[array|null]、anyOf[object|null]、type-as-array、多非空类型 anyOf、$ref 共 7 种模式,验证非流式场景下的类型转换。
    • test_extract_tool_calls_anyof_type_conversion_streaming:完整流式管线(分词 → 增量解码 → extract_tool_calls_streaming)下验证 anyOf 类型解析,确保整型、字符串、布尔、$ref 等正确输出。
  4. 范围收窄:根据审核人 sfeng33 的建议,移除了对 Qwen3XMLToolParser 的修改,仅聚焦 Coder 解析器以减少影响面。
文件 模块 状态 重要度
vllm/tool_parsers/qwen3coder_tool_parser.py 工具解析器 modified 7.48
tests/tool_parsers/test_qwen3coder_tool_parser.py 测试 modified 6.97

关键符号

_convert_param_value test_extract_tool_calls_anyof_type_conversion test_extract_tool_calls_anyof_type_conversion_streaming

关键源码片段

vllm/tool_parsers/qwen3coder_tool_parser.py core-logic

核心源码文件:重写 `_convert_param_value` 方法,消除内联类型解析,使用共享工具函数处理 anyOf/oneOf/$ref 等复杂 schema。同时调整导入,移除无用 import。

    def _convert_param_value(
        self, param_value: str, param_name: str, param_config: dict, func_name: str
    ) -> Any:
        """Convert parameter value based on its type in the schema."""
        # 如果已经是非字符串值(如中间处理结果),直接返回
        if not isinstance(param_value, str):
            return param_value
        # 从参数配置中取出对应参数的定义(可能为 anyOf/oneOf/$ref 等)
        param_schema = param_config.get(param_name, {})
        # 使用共享工具提取所有可能的类型(处理 anyOf/oneOf、$ref、type-as-array 等)
        param_types = extract_types_from_schema(param_schema)
        # 使用共享工具将字符串值强制转换为第一个非 null 类型
        return coerce_to_schema_type(param_value, param_types)

(注意:原内联 100+ 行的逻辑被以上三行核心调用取代,清晰且复用性强。)

评论区精华

$ref 在 anyOf 变体中的处理 正确性

gemini-code-assist 指出:当 anyOf 变体使用 $ref 代替 type 字段时(如 `{"anyOf": [{"$ref": "#/$defs/Model"}, {"type": "null"}]}` ),解析器会回退为字符串。建议在变体循环中检查 "$ref" 并返回 "object"。

结论:最终实现通过共享工具 `extract_types_from_schema` 自动处理了 $ref,无需在解析器内单独判断。审核人未再要求修改。 · 已解决

修改范围的收缩 设计

sfeng33 在最终批准评论中表示:“I made some changes to use the shared utils and reduce the scope to just the coder parser to reduce the blast radius.” 即移除了对 Qwen3XMLToolParser 的改动,仅保留 Coder 修复。

结论:接受范围收缩,XML 解析器的修复推迟至后续 PR。 · 已解决

风险与影响

核心路径变更_convert_param_value 是工具调用参数类型转换的核心方法,改用共享工具可能带来行为差异。但由于共享工具 extract_types_from_schema 已用于其他解析器(如 DeepSeek),并经过测试验证,风险可控。
回归风险:原内联逻辑中的一些边缘情况(如 param_value.lower() == "null" 返回 None)现在通过共享工具的 coerce_to_schema_type 处理,若共享工具不处理空值则可能遗漏。但测试覆盖了 null 情况,且共享工具应已考虑。
兼容性:仅改动 Qwen3CoderToolParser,其他模型不受影响。

用户影响:使用 Qwen3-Coder 模型并依赖工具调用且包含可选参数的用户,参数类型将按预期转换(如 "42" 转为 42"true" 转为 True),避免因类型错误导致的工具调用失败。
系统影响:代码行数净减少 99 行(+9/-108),测试增加 184 行,整体可维护性提升。
团队影响:为后续工具解析器复用共享工具提供了范例。

核心路径变更 共享工具依赖 测试覆盖有限(仅 Coder 解析器)

关联 Issue

#37831 [Bugfix] Fix Qwen3CoderToolParser anyOf/oneOf type resolution for nullable params
#38189 [Tool Parser][2/3] Use self.tools instead of request.tools in tool parsers

完整报告

参与讨论