成本监控:给每个 Agent 加“水表”,用 Prometheus + Grafana 盯住 LLM Token 消耗
1. 多 Agent 跑起来之后钱到底花在哪了如果你正在自建 Agent 服务大概率遇到过这个场景三个 Agent 各司其职一个负责抓取、一个负责总结、一个负责写日报跑了一周感觉挺顺月底一看账单傻眼。不是付不起而是完全说不清钱花在哪——哪个 Agent 是耗 Token 大户哪次调用重复了Pro 模型是不是被用在了本该 Flash 干的活上这就是 LLM 成本的黑盒问题。传统后端服务有 CPU、内存、QPS 指标但 LLM 调用天然是「按量计费 外部依赖」如果不主动埋点你手里只有一张月底账单没有任何中间过程。没有计量的系统就像没有水表的出租屋房东说多少就是多少。这篇要做的就是给每个 Agent 装一块「水表」每次 LLM 调用自动算成本指标汇总到 PrometheusGrafana 出图超阈值告警。同时用 TaoToken 作为统一的 Key/API 通道让所有 Agent 走同一个入口Token 计数上报是否生效一眼可验。适合已经有一个能跑的 Agent 服务、想把它从「能跑」推进到「可观测」的开发者。2. 前置准备统一通道 指标栈在埋点之前先把两件事定下来否则后面指标口径会乱。第一件是调用通道。多 Agent 各自持有不同的 Key会导致成本归属混乱——你没法判断这笔消耗属于哪个 Agent。我的做法是让所有 Agent 统一走 TaoToken 的 API 通道用同一个 Key 出口Agent 身份通过请求头或业务标签区分。这样 Prometheus 里的agentlabel 才有意义。TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的/v1/chat/completions现有 SDK 基本不用改。第二件是指标栈。Prometheus 负责采集和存储Grafana 负责可视化Agent 服务里用prometheus/client_golang暴露/metrics。三者关系很简单Agent 进程暴露指标 → Prometheus 定时抓取 → Grafana 查询展示。注意Prometheus 是拉模型pull所以你的 Agent 服务必须有一个 HTTP 端口能被 Prometheus 访问到。本地开发用:9090生产环境记得和业务端口分开避免暴露内部指标。先把依赖装上go get github.com/prometheus/client_golang/prometheus go get github.com/prometheus/client_golang/prometheus/promhttp如果你用的是 Python Agent对应换成prometheus_client思路完全一致后面指标定义可以照搬。3. 可复制配置从成本计算到指标暴露3.1 先定义价格表别让成本算错成本监控最容易翻车的地方不是埋点是价格算错。把定价写成代码集中维护// internal/llm/cost.go package llm type ModelPricing struct { InputPrice float64 // 元/百万 Token OutputPrice float64 } var ModelPrices map[string]ModelPricing{ deepseek-v4-pro: {InputPrice: 2.0, OutputPrice: 8.0}, deepseek-v4-flash: {InputPrice: 0.5, OutputPrice: 2.0}, qwen2.5-coder:7b: {InputPrice: 0, OutputPrice: 0}, // 本地模型免费 } func CalculateCost(model string, inputTokens, outputTokens int) float64 { pricing, ok : ModelPrices[model] if !ok { // 未知模型按最贵的估宁可高估不可漏算 pricing ModelPrices[deepseek-v4-pro] } return float64(inputTokens)/1_000_000*pricing.InputPrice float64(outputTokens)/1_000_000*pricing.OutputPrice }一次典型调用1000 输入 500 输出按 Pro 算就是(1000/1M×2)(500/1M×8)¥0.006不到一分钱。单次看着小但 Agent 是高频调用量级一上来差距就出来了。3.2 指标定义四个核心指标就够不要一上来定义十几个指标先覆盖「次数、Token、成本、耗时」四类// internal/middleware/metrics.go package middleware import ( context net/http time github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/promhttp ) var ( callsTotal prometheus.NewCounterVec( prometheus.CounterOpts{ Name: agent_llm_calls_total, Help: LLM 调用总次数, }, []string{agent, model, status}, ) tokensConsumed prometheus.NewCounterVec( prometheus.CounterOpts{ Name: agent_llm_tokens_total, Help: LLM Token 消耗总量, }, []string{agent, model, type}, // type input / output ) callCost prometheus.NewCounterVec( prometheus.CounterOpts{ Name: agent_llm_cost_yuan_total, Help: LLM 调用总费用元, }, []string{agent, model}, ) callDuration prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: agent_llm_duration_seconds, Help: LLM 调用耗时分布, Buckets: []float64{0.1, 0.5, 1, 2, 5, 10, 30, 60}, }, []string{agent, model}, ) ) func init() { prometheus.MustRegister(callsTotal, tokensConsumed, callCost, callDuration) }关键点agent这个 label 是整篇的核心。没有它你只能看到「总消耗」看不到「哪个 Agent 消耗」。多 Agent 场景下这个 label 就是水表的户号。3.3 中间件把埋点塞进调用链用装饰器模式包一层业务代码零改动type MetricsMiddleware struct { next agent.Client agent string } func Metrics(next agent.Client, agentName string) agent.Client { return MetricsMiddleware{next: next, agent: agentName} } func (mm *MetricsMiddleware) Chat( ctx context.Context, messages []agent.Message, ) (*agent.Response, error) { start : time.Now() resp, err : mm.next.Chat(ctx, messages) elapsed : time.Since(start).Seconds() status : success if err ! nil { status error } model : unknown if resp ! nil resp.Model ! { model resp.Model } callsTotal.WithLabelValues(mm.agent, model, status).Inc() callDuration.WithLabelValues(mm.agent, model).Observe(elapsed) if err nil resp ! nil { tokensConsumed.WithLabelValues(mm.agent, model, input). Add(float64(resp.Usage.InputTokens)) tokensConsumed.WithLabelValues(mm.agent, model, output). Add(float64(resp.Usage.OutputTokens)) cost : llm.CalculateCost(model, resp.Usage.InputTokens, resp.Usage.OutputTokens) callCost.WithLabelValues(mm.agent, model).Add(cost) } return resp, err }组装的时候每个 Agent 传自己的名字// 抓取 Agent fetcherClient : middleware.Metrics(baseClient, fetcher) // 总结 Agent summarizerClient : middleware.Metrics(baseClient, summarizer) // 日报 Agent reporterClient : middleware.Metrics(baseClient, reporter) middleware.StartMetricsServer(:9090)StartMetricsServer就是标准的promhttp.Handler()挂到/metrics这里不重复贴了。3.4 Prometheus 抓取配置在prometheus.yml里加一个 jobscrape_configs: - job_name: agent-llm scrape_interval: 15s static_configs: - targets: [host.docker.internal:9090] labels: env: dev如果你用 Docker Compose 跑 Prometheushost.docker.internal指向宿主机K8s 里换成 Service 名即可。抓取间隔 15s 是成本和精度的平衡点再密意义不大因为 LLM 调用本身是秒级。4. 验证请求确认 Token 计数真的上报了配置写完不代表生效必须做一次端到端验证。分三步。第一步手动打一次请求触发指标变化curl -s http://localhost:9090/metrics | grep agent_llm_cost_yuan_total如果返回空说明指标没注册或服务没起来。正常应该看到类似agent_llm_cost_yuan_total{agentreporter,modeldeepseek-v4-flash} 0.0006第二步连续调用几次观察计数是否累加。这里有个坑Counter 类型只增不减如果你重启了 Agent 进程计数会归零。这是正常的Prometheus 用rate()处理重启导致的跳变不要手动去「修正」它。第三步在 Prometheus 的 Graph 页面执行查询确认能查到数据sum(rate(agent_llm_cost_yuan_total[5m])) by (agent)这条查询会按 Agent 分组返回每个 Agent 每分钟的成本速率。如果某个 Agent 没出现检查它的中间件是否真的被包进了调用链——我踩过的坑就是只包了主流程异步任务里的调用漏了导致那部分消耗完全没上报。Grafana 面板骨架三个最关键的图{ panels: [ { title: 每小时成本按 Agent, type: timeseries, targets: [ { expr: sum(rate(agent_llm_cost_yuan_total[1h])) by (agent) * 3600, legendFormat: {{agent}} } ] }, { title: Token 消耗趋势按模型, type: timeseries, targets: [ { expr: sum(rate(agent_llm_tokens_total[5m])) by (model, type), legendFormat: {{model}}-{{type}} } ] }, { title: 调用成功率, type: stat, targets: [ { expr: sum(rate(agent_llm_calls_total{status\success\}[5m])) / sum(rate(agent_llm_calls_total[5m])) } ] } ] }告警规则建议先加一条成本突增groups: - name: agent-cost rules: - alert: AgentCostSpike expr: sum(rate(agent_llm_cost_yuan_total[10m])) by (agent) * 3600 5 for: 5m labels: severity: warning annotations: summary: Agent {{ $labels.agent }} 每小时成本超过 5 元这条规则的意思是某个 Agent 的实时成本速率折算成小时超过 5 元持续 5 分钟就告警。阈值按你的业务量调重点是「实时发现异常」而不是等月底。5. 本篇常见错排查指标查不到/metrics返回 404。检查StartMetricsServer是否在main里被调用以及端口是否被占用。另一个常见原因是把promhttp.Handler()挂到了业务路由上被鉴权中间件拦了。成本算出来是 0。大概率是resp.Usage为空。有些兼容接口在流式模式下不返回 usage需要显式请求stream_options: {include_usage: true}或者在流结束后手动累加。走 TaoToken 通道时非流式请求默认带 usage流式需要确认一下。Grafana 里曲线是断的。检查 Prometheus 的scrape_interval和 Grafana 查询的[5m]窗口是否匹配。如果抓取间隔是 15s查询窗口至少给 1m否则rate()会因为样本不足返回空。重启后成本归零累计值不对。这是 Counter 的正常行为。要算「历史总成本」用sum(increase(agent_llm_cost_yuan_total[30d]))不要直接读 Counter 的瞬时值。多个 Agent 的 label 混在一起。检查每个 Agent 的中间件是否传了不同的agentName。如果都传了默认值Prometheus 里就只有一个时间序列分组失去意义。6. 把水表读起来再谈优化指标跑通之后你会发现很多之前靠感觉的判断都变得可验证。比如我实测下来日报 Agent 里真正贵的是总结环节而不是抓取环节因为总结用的是 Pro 模型而抓取用 Flash 就够。把总结拆成「Flash 初筛 Pro 精修」之后同样的任务成本降了大约四成。下一步可以做的给每个 Agent 设独立的成本预算超了自动降级到便宜模型把agent_llm_cost_yuan_total接到告警异常突增时第一时间收到通知再往后用 OpenTelemetry 把每次调用的 trace 串起来定位到具体是哪一步 Prompt 写胖了。如果你还没统一调用通道建议先把所有 Agent 的出口收敛到 TaoToken 的 API 通道Key 管理、用量统计、模型切换都在一个地方埋点口径也统一。接入文档在https://taotoken.net/api对应的文档页API Keys 在控制台的api-keys页面生成。指标跑起来之后模型对话和 Coding Plan 的用量也能对照着看成本这件事就从「月底惊吓」变成了「实时读数」。