Prhub

#41154 [Model] Add Apertus Tool Parser

原始 PR 作者 blancsw 合并时间 2026-05-18 23:20 文件变更 5 提交数 12 评论 12 代码增减 +1385 / -0

执行摘要

为 Apertus 模型添加工具调用解析器

重新开启旧PR #26307,基于新版vLLM的工具解析器接口,使Apertus模型(如swiss-ai/Apertus-70B-Instruct-2509)能够支持函数调用。PR body明确要求兼容两种API格式和多轮工具调用,并提供了测试验证。

建议认可该PR的设计和测试覆盖,作为未来新增工具解析器的模板。建议后续改进异常处理,将通用捕获改为具体异常。

讨论亮点

Review中gemini-code-assist两次提出应将except Exception替换为具体的JSONDecodeError等异常,以避免掩盖错误(文件内评论)。该建议未获得作者回应或代码变更。另外,bbrowning发现docstring中多余空格导致文档构建失败,提供了修复diff,由作者应用到最终版本。

实现拆解

  1. 创建解析器类:在vllm/tool_parsers/apertus_tool_parser.py中定义ApertusToolParser,继承ToolParser。核心使用正则表达式(<|tools_prefix|>)(.*?)(<|tools_suffix|>|$)提取工具调用JSON数组。非流式方法extract_tool_calls将完整输出中的工具块解析为ToolCall列表;流式方法extract_tool_calls_streaming通过_extract_streaming_buffer_delta_text处理增量token,应对特殊标记被分块的情况。

  2. 注册解析器:在vllm/tool_parsers/__init__.py_TOOL_PARSERS_TO_REGISTER中添加'apertus'键,指向新模块。

  3. 提供聊天模板examples/tool_chat_template_apertus.jinja针对Apertus的对话格式设计,兼容两种API的工具调用格式,支持多轮历史和并行调用。

  4. 编写测试tests/tool_parsers/test_apertus_tool_parser.py覆盖非流式(无工具、单工具、多参数、嵌套参数、多个工具调用、不完整调用)和流式场景(工具块不同步、前缀缓冲区、后缀缓冲区、特殊标记分块等),共30余个测试用例。

  5. 更新文档:在docs/features/tool_calling.md中添加Apertus章节,列出支持模型、启动参数和模板路径。

文件 模块 状态 重要度
vllm/tool_parsers/apertus_tool_parser.py 工具解析器 added 8.98
tests/tool_parsers/test_apertus_tool_parser.py 测试套件 added 7.48
examples/tool_chat_template_apertus.jinja 聊天模板 added 5.06
vllm/tool_parsers/__init__.py 注册中心 modified 4.67
docs/features/tool_calling.md 用户文档 modified 2.77

关键符号

ApertusToolParser.__init__ ApertusToolParser._reset_streaming_state ApertusToolParser.adjust_request ApertusToolParser._buffer_delta_text ApertusToolParser.extract_tool_calls ApertusToolParser.extract_tool_calls_streaming ApertusToolParser._extract_streaming

关键源码片段

vllm/tool_parsers/apertus_tool_parser.py core-logic

核心实现,定义了 ApertusToolParser 类及其非流式 / 流式工具调用提取方法

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Tool call parser for Apertus models.Extracts tool calls from the format:
<|tools_prefix|>[{"function_name": {"arg1": "value1", ...}}, ...]<|tools_suffix|>Used when --enable-auto-tool-choice --tool-call-parser apertus are set.
"""import json
from collections.abc import Sequenceimport regex as re
from partial_json_parser.core.options import Allowfrom vllm.entrypoints.chat_utils import make_tool_call_id
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.engine.protocol import (
    DeltaFunctionCall, DeltaMessage, DeltaToolCall,
    ExtractedToolCallInformation, FunctionCall, ToolCall,
)
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.logger import init_logger
from vllm.tokenizers import TokenizerLike
from vllm.tool_parsers.abstract_tool_parser import Tool, ToolParser
from vllm.tool_parsers.utils import find_common_prefix, partial_json_loadslogger = init_logger(__name__)# Apertus special tokens for tool calls
TOOL_CALLS_PREFIX = "<|tools_prefix|>"
TOOL_CALLS_SUFFIX = "<|tools_suffix|>"
​
​
class ApertusToolParser(ToolParser):
    """
    Tool call parser for Apertus models.
    Handles extraction from both non-streaming and streaming environments.
    Format: `<|tools_prefix|>[{"func": {...}}, ...]<|tools_suffix|>`
    """
​
    def __init__(self, tokenizer: TokenizerLike, tools: list[Tool] | None = None):
        super().__init__(tokenizer, tools)
​
        if not self.model_tokenizer:
            raise ValueError(
                "The model tokenizer must be passed to the ToolParser "
                "constructor during construction."
            )
        # Regex to extract tool calls block (suffix is optional for incomplete outputs)
        self.tool_call_regex = re.compile(
            rf"{re.escape(TOOL_CALLS_PREFIX)}"
            rf"(.*?)"
            rf"(?:{re.escape(TOOL_CALLS_SUFFIX)}|$)",
            re.DOTALL,
        )
​
        self._reset_streaming_state()
​
    def _reset_streaming_state(self) -> None:
        """Resets all streaming state variables for a new completion request."""
        self.buffered_delta_text = ""
        self.current_tool_id = -1
        self.current_tool_name_sent = False
        self.streamed_args_for_tool: list[str] = []
​
    def adjust_request(
        self, request: ChatCompletionRequest | ResponsesRequest
    ) -> ChatCompletionRequest | ResponsesRequest:
        """Forces `skip_special_tokens=False` so that tool tokens are surfaced to the engine."""
        request = super().adjust_request(request)
        if request.tools and request.tool_choice != "none":
            request.skip_special_tokens = False
        return request
​
    # ... additional methods like _buffer_delta_text, extract_tool_calls,
    # extract_tool_calls_streaming, _extract_streaming are defined below.

评论区精华

通用异常捕获应替换为具体异常 正确性

gemini-code-assist 指出代码中使用通用 except Exception 会掩盖 JSON 和正则解析错误,建议捕获具体异常如 json.JSONDecodeError 和 regex 错误。

结论:未在 PR 中看到修改;作者未回应,最终合并时未变更。 · unresolved

文档构建失败:空格问题 documentation

bbrowning 指出 docstring 中的 whitespace 问题导致文档构建失败,并提供了 diff 修复。

结论:作者随后应用了修复,文档构建通过。 · 已解决

风险与影响

主要风险来自流式解析器对特殊标记分块的处理:若token边界切在<|tools_prefix|>中间,缓冲逻辑可能出错。当前测试未覆盖超长文本或高并发场景。异常处理使用宽泛的except Exception可能隐藏JSON格式错误或正则匹配失败,增加调试难度。但这些风险对已有功能无影响,仅影响新解析器本身。

对用户:Apertus模型现已支持工具调用,用户可通过命令行启用,体验与其他模型一致。对系统:独立的解析器模块,无侵入性,不会影响其他工具解析器。对团队:增加了需维护的组件,但聊天模板和解析逻辑较通用,维护成本可控。

异常处理不够具体 流式解析边界条件 新模块需社区验证

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论