Prhub

#39870 [BugFix] Support custom tool parsers when tool_choice is `required` and named function

原始 PR 作者 JaredforReal 合并时间 2026-04-18 00:38 文件变更 5 提交数 10 评论 15 代码增减 +100 / -12

执行摘要

修复 GLM 等模型在 tool_choice='required' 或命名函数时因 XML 工具调用输出导致的解析失败。

根据PR body,GLM-5.1等模型使用XML工具调用语法定义在聊天模板中,而引导解码强制JSON输出,导致解析失败。讨论中sfeng33指出'required/named tool choice generally don't work well with models that generate xml style tool call',因此需要修复以支持自定义工具解析器处理这类场景。

该PR值得精读,展示了通过类级标志扩展工具解析器支持不同输出格式的设计模式,关注supports_required_and_named的引入和请求调整逻辑,以学习如何优雅地处理模型特定行为,同时注意风险点如响应格式清理和测试覆盖。

讨论亮点

Copilot指出在adjust_request中可能未清理response_format,存在引导解码冲突风险;gemini-code-assist提示流式生成器中tc.function访问可能类型不安全;sfeng33确认supports_required_and_named标志默认应为True,GLM解析器设为False,并建议支持ResponsesRequest;JaredforReal相应修复;chaunceyjiang询问是否通用功能,结论是临时修复而非通用特性。

实现拆解

  1. 添加类级标志:在vllm/tool_parsers/abstract_tool_parser.py的ToolParser类中添加supports_required_and_named: bool = True属性,作为子类覆盖的开关,默认使用标准JSON解析。
  2. GLM解析器设置标志并调整请求:在vllm/tool_parsers/glm4_moe_tool_parser.py和glm47_moe_tool_parser.py中设置supports_required_and_named = False,并重写adjust_request方法,当tool_choice为'required'或命名函数时跳过super().adjust_request()以避免设置structured_outputs,从而允许XML输出。
  3. 非流式请求处理:在vllm/entrypoints/openai/engine/serving.py的_parse_tool_calls_from_content函数中,检查tool_parser_cls.supports_required_and_named标志,如果为False,则将required/named路由到工具解析器的extract_tool_calls方法;否则保持标准JSON解析逻辑。
  4. 流式请求处理:在vllm/entrypoints/openai/chat_completion/serving.py的chat_completion_stream_generator中,添加tool_choice_uses_parser逻辑,当标志为False时,将required/named视为自动工具选择路径,使用工具解析器的extract_tool_calls_streaming方法处理流式输出。
  5. 测试配套:PR body提到手动测试GLM-4.7、GLM-5、GLM-5.1和Kimi-K2.5模型,但代码变更未包含测试文件,需注意测试覆盖不足的风险。
文件 模块 状态 重要度
vllm/entrypoints/openai/engine/serving.py 请求处理 modified 6.78
vllm/tool_parsers/abstract_tool_parser.py 抽象解析器 modified 5.77
vllm/tool_parsers/glm4_moe_tool_parser.py GLM 解析器 modified 6.18
vllm/entrypoints/openai/chat_completion/serving.py 流式生成 modified 6.7
vllm/tool_parsers/glm47_moe_tool_parser.py GLM47 解析器 modified 4.89

关键符号

_parse_tool_calls_from_content adjust_request chat_completion_stream_generator

关键源码片段

vllm/entrypoints/openai/engine/serving.py core-logic

处理非流式请求的工具调用解析,是核心路由逻辑的关键文件。

def _parse_tool_calls_from_content(
    request: ChatCompletionRequest,
    tokenizer: TokenizerLike | None,
    enable_auto_tools: bool,
    tool_parser_cls: type[ToolParser] | None,
    content: str | None = None,
) -> tuple[list[FunctionCall] | None, str | None]:
    # 当工具解析器类存在且不支持 required/named 时,回退到工具解析器路径
    if tool_parser_cls and (
        enable_auto_tools
        and (
            request.tool_choice == "auto"
            or request.tool_choice is None
            or (
                not tool_parser_cls.supports_required_and_named # 关键检查:如果为 False,则路由到解析器
                and request.tools
                and (
                    request.tool_choice == "required"
                    or isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam)
                )
            )
        )
    ):
        # 自动工具调用解析(也作为 required/named 的回退路径)
        try:
            tool_parser = tool_parser_cls(tokenizer, request.tools)
        except RuntimeError as e:
            logger.exception("Error in tool parser creation.")
            raise e
        tool_call_info = tool_parser.extract_tool_calls(
            content if content is not None else "",
            request=request,
        )
        if tool_call_info is not None and tool_call_info.tools_called:
            function_calls = [
                FunctionCall(
                    id=tool_call.id,
                    name=tool_call.function.name,
                    arguments=tool_call.function.arguments,
                )
                for tool_call in tool_call_info.tool_calls
            ]
            content = tool_call_info.content
            return function_calls, content
    # 其他情况保持原有逻辑,如标准 JSON 解析或强制函数调用
    # ...
vllm/tool_parsers/abstract_tool_parser.py core-logic

定义工具解析器基类,新增 supports_required_and_named 标志,是所有解析器的行为开关。

class ToolParser:
    """
    抽象ToolParser类,不应直接使用。提供的属性和方法应在派生类中使用。
    """
​
    # 当为 True(默认)时,服务层对 tool_choice="required" 和命名函数工具选择使用标准 JSON 解析,
    # 适用于引导解码产生良好 JSON 输出的模型(如 Hermes)。
    # 子类设为 False 当标准解析不适用于其模型输出格式时(如使用 XML 的 GLM 模型)。
    # 当为 False 时,服务层回退到工具解析器的 extract_tool_calls / extract_tool_calls_streaming 方法,
    # 将 required/named 视为与 "auto" 相同。
    supports_required_and_named: bool = True
​
    def __init__(
        self,
        tokenizer: TokenizerLike,
        tools: list[Tool] | None = None,
    ):
        self.prev_tool_call_arr: list[dict] = []
        self.current_tool_id: int = -1
        self.current_tool_name_sent: bool = False
        self.streamed_args_for_tool: list[str] = []
        self.model_tokenizer = tokenizer
        if tools:
            self.tools: list[ChatCompletionToolsParam | FunctionTool] = [
                tool
                for tool in tools
                if isinstance(tool, (ChatCompletionToolsParam, FunctionTool))
            ]
        else:
            self.tools = []
    # ... 其他方法
vllm/tool_parsers/glm4_moe_tool_parser.py core-logic

GLM 模型的具体工具解析器,设置 supports_required_and_named 为 False 并重写 adjust_request 以支持 XML 输出。

class Glm4MoeModelToolParser(ToolParser):
    """GLM-4模型的工具解析器,支持增量字符串流式处理。"""
​
    supports_required_and_named = False # 关键设置:禁用标准 JSON 解析
​
    def adjust_request(
        self, request: ChatCompletionRequest | ResponsesRequest
    ) -> ChatCompletionRequest | ResponsesRequest:
        """调整请求参数以处理工具调用令牌。
​
        对于required/named tool_choice,跳过设置structured_outputs,
        因为GLM模型以XML格式输出工具调用(根据聊天模板)。
        引导解码会强制JSON输出,与XML格式冲突导致解析失败。
        """
        if request.tools:
            tc = request.tool_choice
            if tc == "required" or isinstance(tc, ChatCompletionNamedToolChoiceParam):
                # 不要为 required/named 调用 super().adjust_request(),
                # 因为它会设置 structured_outputs 并通过引导解码强制 JSON 输出。
                # GLM 模型使用 XML 工具调用语法,必须跳过引导解码以让模型自由输出 XML。
                # 工具解析器从 XML 输出中提取工具调用。
                if request.tool_choice != "none":
                    request.skip_special_tokens = False
                return request
        request = super().adjust_request(request)
        if request.tools and request.tool_choice != "none":
            request.skip_special_tokens = False
        return request
    # ... 其他方法

评论区精华

supports_required_and_named 默认值 设计

sfeng33 建议标志默认应为 True,GLM 解析器设为 False,以确保其他解析器默认使用标准 JSON 解析。

结论:已采纳,在 abstract_tool_parser.py 中设置 supports_required_and_named: bool = True,GLM 解析器设为 False。 · 已解决

响应格式清理风险 正确性

Copilot 指出在 adjust_request 中可能未清理 response_format 和 structured_outputs,可能导致引导解码冲突。

结论:未明确解决,代码中仅跳过 super().adjust_request(),但未显式清理这些字段,存在潜在风险。 · 待处理

流式生成器类型安全 正确性

gemini-code-assist 提示流式生成器中访问 tc.function 作为字典可能导致类型错误,代码已包含检查但可能不充分。

结论:代码通过 isinstance 检查处理,但风险仍存,需注意未来维护。 · partially_resolved

功能通用性 question

chaunceyjiang 询问这是通用功能还是 GLM 定制,sfeng33 回应是临时修复而非通用特性。

结论:确认是针对 GLM 模型的临时修复,但设计上通过标志允许其他解析器扩展。 · 已解决

风险与影响

技术风险包括:

  1. 如果response_format未清理,引导解码可能仍被启用,导致XML输出解析失败(Copilot评论)。
  2. 流式生成器中tc.function的字典访问可能导致运行时类型错误(gemini-code-assist评论)。
  3. 新增的opt-in路径缺少集成测试,可能引入回归。
  4. 默认行为改变可能影响其他自定义工具解析器,若未设置标志可能意外使用JSON解析。

对用户:GLM模型用户现在可以在tool_choice='required'或命名函数时正常使用工具调用,提升体验和功能完整性。对系统:增加了工具解析器的灵活性和可扩展性,支持更多模型输出格式,但代码复杂性增加。对团队:需要理解新标志的逻辑,并在未来开发中注意类似模型特定行为的处理方式。

响应格式未清理 类型安全风险 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论