Meter 指标采集¶
为什么需要指标¶
Agent 的一次运行涉及多种资源消耗:LLM 调用消耗 Token(直接对应费用),工具执行消耗时间和外部配额,底层硬件消耗算力和内存。这些消耗在开发阶段容易被忽视,但在生产环境中是运营决策的关键依据——成本分摊、容量规划、异常告警都离不开量化指标。
TFRobot 集成 OpenTelemetry Metrics 作为指标采集的基础设施。框架提供统一的 Meter 获取方式和装饰器模式,使得任何需要量化观测的环节都可以接入指标体系。当前已落地的指标聚焦于 LLM Token 消耗——这是 Agent 运行中成本最高、最需要实时观测的资源。未来,工具调用耗时、硬件资源占用等指标也会按相同模式逐步纳入。
LLM Token 指标(当前实现)¶
三个 Counter 指标¶
tfrobot/telemetry/meter.py 定义了三个 Counter 类型指标:
| 指标名称 | 说明 | 标签 |
|---|---|---|
llm.tokens.total |
单次调用的总 Token 数 | model_name |
llm.tokens.prompt |
Prompt 部分的 Token 数 | model_name |
llm.tokens.completion |
Completion 部分的 Token 数 | model_name |
选择 Counter(只增不减的累加器)是因为 Token 消耗是一个天然的单调递增量——你不会「退还」已经消耗的 Token。
@report_llm_metrics 装饰器¶
指标采集通过装饰器自动完成,不需要在每个 LLM 子类中手动编写上报逻辑:
# tfrobot/telemetry/meter.py(简化)
from opentelemetry import metrics
meter = metrics.get_meter(__name__)
METER_LLM_TOTAL_TOKENS = "llm.tokens.total"
METER_LLM_PROMPT_TOKENS = "llm.tokens.prompt"
METER_LLM_COMPLETION_TOKENS = "llm.tokens.completion"
def report_llm_metrics(complete):
@wraps(complete)
async def wrapper(self, *args, **kwargs):
result = await complete(self, *args, **kwargs)
try:
labels = {"model_name": self.name or "unknown"}
meter.create_counter(METER_LLM_TOTAL_TOKENS).add(result.usage["total_tokens"], labels)
meter.create_counter(METER_LLM_PROMPT_TOKENS).add(result.usage["prompt_tokens"], labels)
meter.create_counter(METER_LLM_COMPLETION_TOKENS).add(result.usage["completion_tokens"], labels)
except Exception as e:
warnings.warn(f"上报LLM指标时出现异常: {e}")
return result
return wrapper
关键设计点:
- 异常不传播:指标上报失败只
warn,不影响 LLM 调用本身 - 同时支持同步和异步:装饰器内部检测被装饰函数是否为协程,分别生成对应的 wrapper
- 标签维度:当前以
model_name区分,可按模型粒度查看消耗
在 LLM 中的应用¶
装饰器应用于所有 LLM 子类的 complete 和 async_complete 方法。以 SglangDesk 为例:
# tfrobot/brain/chain/llms/generation_llms/desk_llm/sglang_desk.py
class SglangDesk(BaseDeskLLM):
@report_llm_metrics # ← 自动上报 Token 指标
@retry(stop=stop_after_attempt(3), ...)
def complete(self, msg, **kwargs):
...
目前已覆盖的 LLM 提供商包括:OpenAI、Anthropic、DeepSeek、ZhipuAI、Google Gemini、Ollama、DashScope、OpenRouter,以及所有 DeskLLM 变体,共计 17 个实现类。
MeterProvider 设计策略¶
上面的 LLM Token 指标是当前落地的第一个场景,但 Meter 的基础设施是面向整个框架的。无论未来新增工具调用耗时(Histogram)、并发 Chain 数量(UpDownCounter)还是硬件资源快照(ObservableGauge),都通过同一个全局 MeterProvider 汇聚和导出。
TFRobot 不在框架内部初始化 MeterProvider,而是使用 OpenTelemetry 的全局默认实例:
# tfrobot/telemetry/meter.py
meter = metrics.get_meter(__name__) # 使用全局 MeterProvider
这是一个有意的设计决策:
graph LR
A["调用方<br/>TFRobotServer / Celery"] -->|"set_meter_provider()"| B["全局 MeterProvider"]
C["tfrobot 框架<br/>get_meter()"] -->|获取| B
B -->|导出| E["Collector / Prometheus"]
| 角色 | 职责 |
|---|---|
| TFRobot 框架 | 只负责使用 Meter 上报指标 |
| 外部调用方 | 负责配置 MeterProvider(选择导出器、设置采集间隔等) |
这样做的好处是:
- 关注点分离:框架不需要知道指标最终发往 Prometheus 还是 OTLP Collector
- 安全降级:如果外部没有配置 MeterProvider,OpenTelemetry 默认使用 NoOp 实现,指标调用不会报错,只是不会导出数据
- 灵活组合:不同部署环境(本地开发、CI、生产)可以配置不同的导出策略
外部配置示例¶
在 TFRobotServer 或 Celery Worker 的启动代码中配置 MeterProvider:
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
resource = Resource.create({"service.name": "tfrobot-prod"})
exporter = OTLPMetricExporter(endpoint="otel-collector:4317")
provider = MeterProvider(
resource=resource,
metric_readers=[PeriodicExportingMetricReader(exporter)],
)
metrics.set_meter_provider(provider)
# 之后 TFRobot 框架中的 meter.create_counter(...).add(...) 就会将数据导出到 Collector
故障排查¶
当指标未出现在监控系统中时,按以下顺序检查:
-
MeterProvider 是否已设置
from opentelemetry.metrics import get_meter_provider print(type(get_meter_provider())) # 期望: <class 'opentelemetry.sdk.metrics.MeterProvider'> # 若为 NoOpMeterProvider 则说明未配置 -
LLM 返回值是否包含 usage:
@report_llm_metrics从result.usage读取 Token 数。如果 LLM 提供商未返回 usage 字段,装饰器会 warn 但不会上报数据。 -
Collector 是否在线:检查 OTLP Collector 的连接状态和日志。
规划中的指标¶
以下指标尚未实现,但已纳入设计规划。它们将沿用与 LLM Token 指标相同的模式——装饰器自动采集、全局 MeterProvider 导出。
| 指标名称 | 类型 | 标签 | 说明 |
|---|---|---|---|
tool.invocation.count |
Counter | tool_name, scene, chain_id |
统计各工具被调用的次数 |
chain.failure.count |
Counter | failure_reason, scene, chain_id |
统计 Chain 执行失败次数,按原因分类 |