Prhub

#44448 [Frontend][Metrics] Add `vllm:tool_call_parser_invocations_total` Prometheus metric

原始 PR 作者 yzong-rh 合并时间 2026-06-10 22:29 文件变更 3 提交数 8 评论 22 代码增减 +167 / -20

执行摘要

新增 tool_call_parser_invocations_total Prometheus 指标

PR 正文指出:添加该指标以便操作员观察解析器运行频率及是否产生了工具调用,从而在模型上线或运行时变更中更容易发现工具调用回归。

值得阅读 review 中关于指标设计、延迟初始化和低基数标签的讨论,有助于理解 vLLM 中 Prometheus 指标注册的约束与模式。

讨论亮点
  • 环境变量控制是否必要:robertgshaw2-redhat 认为应默认开启,作者测试后确认无性能影响,最终移除环境变量。
  • try-finally 重复:chaunceyjiang 建议使用装饰器,作者将所有提取路径合并到两个方法,减少重复。
  • 多进程模式行为:chaunceyjiang 询问 --api-server-count>1 时指标表现,markmc 解释了 Prometheus 多进程模式下 Counter 自动聚合。
  • 延迟初始化原因:markmc 质疑为何不直接模块级创建,作者说明 PrometheusStatLogger 会取消注册 vllm::* 前缀指标,因此必须在其后初始化。
  • 标签基数考虑:markmc 强调低基数重要性,建议添加 model_name,作者采纳并确认标签集有限。

实现拆解

  1. 创建 vllm/parser/metrics.py,定义 ToolCallOutcomeRequestType 枚举,以及 init_parser_metricsrecord_tool_parser_invocation 函数。
  2. vllm/entrypoints/openai/api_server.pyinit_app_state 中,当 args.tool_call_parser 非 None 时调用 init_parser_metrics,传入模型名称。
  3. 修改 vllm/parser/abstract_parser.py 中的 DelegatingParser 类,在 extract_tool_callsextract_tool_calls_streaming 方法中添加 record_tool_parser_invocation 调用,使用 try-finally 确保异常时也记录(当前异常视为 no tool call)。同时调整方法签名支持 ChatCompletionRequest | ResponsesRequest
  4. 根据 review 建议移除了最初引入的环境变量开关,性能测试未观察到影响。
  5. 增加了 model_name 标签,便于跨实例聚合。
文件 模块 状态 重要度
vllm/parser/metrics.py 解析器 added 8.6
vllm/parser/abstract_parser.py 解析器 modified 7.01
vllm/entrypoints/openai/api_server.py API 服务 modified 6.26

关键符号

init_parser_metrics record_tool_parser_invocation DelegatingParser.extract_tool_calls DelegatingParser.extract_tool_calls_streaming

关键源码片段

vllm/parser/abstract_parser.py dependency-wiring

DelegatingParser 是工具解析器的调度入口,在此注入指标记录逻辑,覆盖所有工具调用路径。

# vllm/parser/abstract_parser.py (DelegatingParser 相关方法 )
​
    def extract_tool_calls(
        self,
        model_output: str,
        request: ChatCompletionRequest | ResponsesRequest,
    ) -> ExtractedToolCallInformation:
        if self._tool_parser is None:
            return ExtractedToolCallInformation(
                tools_called=False, tool_calls=[], content=model_output
            )
        result = None
        is_tool_called: bool | Exception = False
        try:
            result = self._tool_parser.extract_tool_calls(
                model_output,
                request=request, # type: ignore[arg-type]
            )
            is_tool_called = bool(result.tools_called)
        except Exception as e:
            # 记录异常标记,后续统计为 no_tool_call,但继续抛出异常
            is_tool_called = e
            raise
        finally:
            # 无论是否异常,都记录调用;异常时 is_tool_called 为异常对象,
            # record_tool_parser_invocation 内部会转为 NO_TOOL_CALL
            record_tool_parser_invocation(
                is_tool_called=is_tool_called,
                is_streaming=False,
                request=request,
            )
        return result
​
    def extract_tool_calls_streaming(
        self,
        previous_text: str,
        current_text: str,
        delta_text: str,
        previous_token_ids: Sequence[int],
        current_token_ids: Sequence[int],
        delta_token_ids: Sequence[int],
        request: ChatCompletionRequest | ResponsesRequest,
    ) -> DeltaMessage | None:
        if self._tool_parser is None:
            return None
        result = None
        is_tool_called: bool | Exception = False
        try:
            result = self._tool_parser.extract_tool_calls_streaming(
                previous_text,
                current_text,
                delta_text,
                previous_token_ids,
                current_token_ids,
                delta_token_ids,
                request=request, # type: ignore[arg-type]
            )
            is_tool_called = bool(
                result.tool_calls is not None and len(result.tool_calls) > 0
            )
        except Exception as e:
            is_tool_called = e
            raise
        finally:
            record_tool_parser_invocation(
                is_tool_called=is_tool_called,
                is_streaming=True,
                request=request,
            )
        return result
vllm/entrypoints/openai/api_server.py entrypoint

在 API server 初始化阶段注册指标,确保指标在使用前已创建。

# vllm/entrypoints/openai/api_server.py (init_app_state 片段 )
​
    # 当启用了 tool_call_parser 时,初始化 parser 相关的 Prometheus 指标
    if args.tool_call_parser is not None:
        from vllm.parser.metrics import init_parser_metrics
​
        init_parser_metrics(
            model_name=cast(str, vllm_config.model_config.served_model_name)
        )
​
    if supported_tasks is None:
        warnings.warn(...)

评论区精华

环境变量控制是否必要 设计

robertgshaw2-redhat 认为不需要环境变量,应默认开启。作者担心性能,但测试后确认无影响。

结论:移除环境变量,默认开启。 · 已解决

使用装饰器简化 try-finally style

chaunceyjiang 建议使用装饰器避免重复。作者将所有提取路径合并到两个方法,精简为两处 try-finally。

结论:采用两处 try-finally 方案。 · 已解决

多进程模式下指标行为 question

chaunceyjiang 询问 --api-server-count>1 时的表现。markmc 解释了 Prometheus 多进程模式。

结论:多进程模式下计数器正常聚合。 · 已解决

延迟初始化原因 设计

markmc 质疑为什么需要延迟初始化。作者解释 PrometheusStatLogger 会取消注册 vllm::* 前缀指标,因此必须在其后创建。

结论:保留延迟初始化。 · 已解决

标签基数讨论与 model_name 添加 设计

markmc 强调低基数标签的重要性,建议添加 model_name。作者最终添加 model_name 并确认标签集有限:mode(2), outcome(2), request_type(3), model_name( 少量 )。

结论:最终保留 mode, outcome, request_type, model_name 四个低基数标签。 · 已解决

风险与影响

该变更新增 Prometheus 计数器,无 API/配置 breaking change。性能测试表明开销可忽略。主要风险:parser 内部异常被捕获时,当前视为 no tool call,无法区分真实错误。已在 docstring 说明,未来可通过 propagating exception 改进。多进程模式下 Prometheus 原生支持,已验证。

用户:获得新指标 vllm:tool_call_parser_invocations_total,改进可观测性。系统:极低性能开销。团队:便于监控 tool-calling 回归,但该指标仅覆盖非 harmony 路径,harmony 路径尚未接入 DelegatingParser。

新监控指标无兼容性问题 多进程支持已验证 异常场景统计可能不准 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论