Prhub

#7044 [rollout] fix: handle malformed Qwen3 XML tool calls

原始 PR 作者 zzzzzzzxh 合并时间 2026-07-17 17:00 文件变更 2 提交数 1 评论 2 代码增减 +47 / -4

执行摘要

修复 Qwen3 XML 工具解析器对畸形结构的崩溃

Qwen3 模型在 agent 循环中有时会生成残缺的 XML 工具调用(如 <parameter=truncated 缺少 > 分隔符),原有代码使用 str.index(">") 会直接抛出 ValueError: substring not found,并被外层 except Exception 捕获,导致整个工具调用提取结果被丢弃,即使同一调用中仍有合法参数。该问题在 vLLM issue #39771 中被报告,社区已在 vLLM PR #39772 中修复,但 VERL 维护独立的解析器,故需自行修补。

值得快速合并的 bugfix,设计决策清晰——采用与 vLLM 社区相同的修复方案。建议阅读 _parse_xml_function_call 函数中 .find().index() 的替换模式,以及 None 守卫的处理方式,可作为类似解析器防御性编程的参考。

讨论亮点

review 过程中,gemini-code-assist[bot] 指出一个高优先级问题:若 tools 参数为 Noneextract_tool_calls 的默认值),在 get_arguments_config 中迭代 tools 会引发 TypeError: 'NoneType' object is not iterable。建议在 _parse_xml_function_call 入口处添加 tools = tools or []。该建议被采纳,且已在最终代码中实现。除此之外,PR 作者 wuxibin89 已批准合并。无未解决疑虑。

实现拆解

变更涉及两个文件的局部修改:

  1. verl/experimental/agent_loop/tool_parser.py

    • _parse_xml_function_call 的返回类型从 FunctionCall 改为 Optional[FunctionCall],允许返回 None
    • 在方法开头添加 tools = tools or [],将 None 归一化为空列表,防止 get_arguments_config 中迭代 None 引发 TypeError
    • 函数名提取:将 str.index(">") 替换为 str.find(">"),若返回 -1(未找到 >),记录 warning 并返回 None
    • 参数循环:同样将 str.index(">") 替换为 str.find(">"),若返回 -1,记录 warning 并通过 continue 跳过该畸形参数,保留已解析的有效参数。
    • extract_tool_calls 中,添加一行过滤 None 结果的列表推导:tool_calls = [t for t in tool_calls if t is not None]
  2. tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py

    • 新增三个测试函数:test_malformed_parameter_is_skipped_but_valid_parameter_is_preserved(验证畸形参数被跳过,合法参数保留)、test_malformed_function_header_returns_none(验证畸形函数头返回 None 且不抛异常)、test_missing_tools_defaults_to_empty_list(验证 tools=None 时能正常解析)。
文件 模块 状态 重要度
verl/experimental/agent_loop/tool_parser.py 工具解析器 modified 6.74
tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py 测试 modified 5.87

关键符号

_parse_xml_function_call extract_tool_calls

关键源码片段

verl/experimental/agent_loop/tool_parser.py core-logic

核心修复所在,修改了 `_parse_xml_function_call` 的返回值类型、添加 `None` 守卫、替换 `.index()` 为 `.find()` 并跳过畸形结构。

def _parse_xml_function_call(
    self, function_call_str: str, tools: Optional[list[OpenAIFunctionToolSchema]]
) -> Optional[FunctionCall]:
    # 将 None 归一化为空列表,防止后续迭代 tools 时 TypeError
    tools = tools or []
​
    # ... 内部函数定义 ...
​
    # 尝试提取函数名。使用 find 而非 index,这样找不到 > 时不会崩溃
    end_index = function_call_str.find(">")
    if end_index == -1:
        # 函数头不完整,无法恢复,跳过并记录警告
        logger.warning(f"Skipping malformed function call without '>' separator: {function_call_str!r}")
        return None
​
    function_name = function_call_str[:end_index]
    param_config = get_arguments_config(function_name)
    parameters = function_call_str[end_index + 1 :]
    param_dict = {}
    for match in self.tool_call_parameter_regex.findall(parameters):
        match_text = match[0] if match[0] else match[1]
        # 同样使用 find,若找不到 > 则跳过该畸形参数,但保留已解析的有效参数
        idx = match_text.find(">")
        if idx == -1:
            logger.warning(
                f"Skipping malformed parameter without '>' separator in tool call for function "
                f"'{function_name}': {match_text!r}"
            )
            continue
        param_name = match_text[:idx]
        param_value = str(match_text[idx + 1 :])
        # ... 值转换逻辑 ...
        param_dict[param_name] = convert_param_value(param_value, param_name, param_config, function_name)
​
    return FunctionCall(name=function_name, arguments=json.dumps(param_dict))

评论区精华

tools=None 导致 TypeError 正确性

gemini-code-assist[bot] 指出,若 `_parse_xml_function_call` 的 `tools` 参数为 `None`(`extract_tool_calls` 的默认值),在 `get_arguments_config` 中迭代 `tools` 会引发 `TypeError: 'NoneType' object is not iterable`,建议在方法开头添加 `tools = tools or []`。

结论:已采纳,在 `_parse_xml_function_call` 第一行添加 `tools = tools or []`。 · 已解决

风险与影响

变更范围极小,仅涉及工具解析器的两处字符串查找逻辑和一处 None 守卫,无性能或安全风险。由于模型输出天然不可预测,跳过畸形参数并保留合法参数是合理的降级策略,不会引入回归。测试文件仅新增对 CPU 的单元测试,不影响 GPU 或 NPU 环境。

影响范围仅限于使用 Qwen3 或 Qwen3.5 模型且启用 agent_loop 工具调用的场景。修复后,模型输出的畸形 XML 工具调用不会导致整个提取失败,合法参数仍能被成功解析并传递给工具执行。对不使用工具调用的用户无影响。对系统整体稳定性有正面提升。

核心路径变更

关联 Issue

#39771 [Bug]: Qwen3Coder tool parser crashes on malformed <parameter=...> (ValueError: substring not found)

完整报告

参与讨论