Agent模型账单失控?用洞察台拆解Token消耗,定位最费钱的任务类型

发布时间:2026/10/8 4:35:23
Agent模型账单失控?用洞察台拆解Token消耗,定位最费钱的任务类型
1. 从一张说不清的账单说起上个月有个做 Agent 产品的朋友找我说他们团队这个月的模型账单比上个月多了将近一倍但谁也说不清钱花在哪了。产品说是研发在疯狂调试研发说是线上流量涨了运维说调用量曲线看着挺平稳。三方各执一词最后只能拍脑袋决定下个月省着点用。这个场景我太熟了。只要你的系统里跑着 Agent只要它背后挂着大模型 API账单失控几乎是迟早的事。因为 Agent 和传统的接口调用完全不是一个物种——一次用户请求可能触发十几轮模型调用每一轮带着不同的上下文长度中间还夹着工具调用、重试、反思循环。你看到的一次对话在账单上可能是几十次 Token 消耗。所以问题从来不是账单为什么涨而是账单涨在了哪一类活上。这两者的区别就像你只知道这个月伙食费超支了和你知道超支是因为天天点外卖还是因为买了太多零食——后者才能指导你行动。这篇东西就是围绕这个痛点展开的。我会讲清楚怎么用一套叫洞察台内部代号 Caravel的可观测方案把 Agent 的模型消耗拆解到哪类任务最费 Token这个粒度。适合正在做 Agent 开发、被账单困扰、又不想盲目砍功能的同学。全文基于我自己的实操经验涉及参数和配置的地方我会把计算过程写清楚你可以直接抄。2. 为什么 Agent 的账单天生就难算2.1 Agent 和普通调用的本质差异普通的大模型调用很好算账一次请求输入多少 Token输出多少 Token乘上单价就完事。你甚至能在网关层做个简单的计数就搞定。Agent 不行。一个典型的 Agent 执行链路是这样的用户提问 → 规划一次模型调用→ 决定调用工具 → 工具返回结果 → 把结果塞回上下文再问模型 → 模型可能觉得信息不够再调一次工具 → 最后生成回答。这中间每一次问模型都是一次独立的计费事件而且每次的上下文都在膨胀因为历史消息、工具返回、中间推理都要带上。我实测过一个简单的查天气并推荐穿搭的 Agent用户就问了一句话背后跑了 7 次模型调用Token 消耗是单次问答的 11 倍。如果这个 Agent 还带反思机制让模型检查自己的答案轻松翻到 20 倍。2.2 账单失真的三个典型来源第一个来源是上下文重复计费。Agent 每一轮都要把完整历史重新发一遍这意味着同一段对话内容会被反复计费。一个 10 轮的 Agent 任务第一轮的内容可能被计费了 10 次。很多人第一次看到这个真相都会愣一下。第二个来源是隐式重试。模型超时、返回格式不对、工具调用失败框架层往往会自动重试。这些重试在业务日志里可能只记了一条成功但账单上每一次都算钱。第三个来源是任务类型混杂。你的系统里可能同时跑着意图识别、内容生成、代码补全、数据抽取好几类活它们对 Token 的消耗模式完全不同。如果只统计总量你永远不知道是哪一类在吃钱。提示如果你现在的监控只能看到总 Token 数和总调用次数那你其实处于账单盲区。这两个指标对优化几乎没有指导意义。2.3 洞察台要解决的核心问题洞察台Caravel的设计目标很明确把每一次模型调用都打上足够的标签让你能按任务类型、Agent 链路、工具调用、用户会话等多个维度去聚合消耗。Caravel 这个词本身是轻快帆船的意思取的就是在数据的海洋里灵活穿行的意象。它要回答的问题就三个哪类活最费为什么费能不能省下面我按搭建顺序一步步讲。3. 洞察台的整体设计思路3.1 核心思路调用即事件整个方案的地基是一个很朴素的想法——把每一次模型调用当成一个事件来记录。这个事件里要带上足够多的上下文标签让后续分析能切片。具体来说每次调用发生时我们记录这些字段字段名含义示例trace_id一次用户请求的全局链路 IDtr_8f3a2bspan_id链路内单次调用的 IDsp_001task_type任务类型标签intent_recognizeagent_name哪个 Agent 发起的shopping_assistantmodel模型名gpt-4oprompt_tokens输入 Token3421completion_tokens输出 Token512tool_calls本轮触发的工具数2retry_count重试次数0latency_ms耗时2340timestamp时间戳1717000000这张表是整个洞察台的核心。你会发现它比普通的调用日志多了task_type、agent_name、tool_calls、retry_count这几个关键标签——正是这几个字段让哪类活最费这个问题变得可回答。3.2 为什么选事件流而不是直接查数据库有人会问为什么不直接在业务代码里写个计数器按任务类型累加不就行了我试过不行。原因有三个。第一累加会丢失明细你没法回溯某个具体会话为什么这么贵。第二Agent 的调用是异步并发的累加器会有竞态问题。第三你迟早会想按新的维度切分而累加器一旦写死维度就改不动了。事件流的思路是先无脑记录明细分析的时候再聚合。存储成本确实高一些但换来的是分析自由度。Caravel 用的是明细落盘 定时聚合的两层结构明细保留 30 天聚合结果长期保留。3.3 标签体系怎么设计才不返工标签体系是这套方案里最容易返工的地方我踩过坑这里重点说。task_type这个标签一定要在业务语义层定义而不是在技术层。什么意思不要用chat_completion这种技术动作当标签要用意图识别商品推荐售后问答这种业务动作。因为老板问的是哪类活费钱他关心的是业务不是技术。我的做法是维护一张任务类型字典表每个 Agent 在发起调用时显式传入任务类型。如果传了未知类型就落到unknown桶里然后定期 review 这个桶把新出现的类型补进字典。# 任务类型字典示例 TASK_TYPES { intent_recognize: 意图识别, product_recommend: 商品推荐, after_sale_qa: 售后问答, content_generate: 内容生成, code_complete: 代码补全, data_extract: 数据抽取, reflection: 反思校验, }注意reflection反思校验这类内部动作一定要单独打标。很多团队发现账单异常最后查出来是反思循环失控——模型反复自我检查一轮任务跑了二十几次反思调用。4. 核心细节解析与实操要点4.1 埋点位置的选择网关层还是 SDK 层埋点位置决定了你能拿到多少信息。两个选择网关层和 SDK 层。网关层埋点的好处是统一、无侵入所有调用都经过它。坏处是它拿不到业务语义——网关不知道这次调用是意图识别还是商品推荐它只看到一次 HTTP 请求。SDK 层埋点的好处是能拿到完整的业务上下文任务类型、Agent 名称、工具调用都能带上。坏处是需要改代码而且如果团队用了多种语言SDK 要维护多份。我的建议是两层都埋网关层兜底SDK 层补充。网关层保证不漏记哪怕 SDK 忘了传标签至少 Token 数是对的SDK 层负责打业务标签。两边用同一个trace_id关联分析时以 SDK 层为准网关层做校验。4.2 Token 计数的准确性陷阱Token 计数看着简单其实坑不少。第一个坑是不同模型的 Tokenizer 不一样。你不能用 GPT 的 tokenizer 去估算 Claude 的 Token 数误差能到 20% 以上。正确做法是优先用 API 返回的usage字段那是官方计费口径最准。第二个坑是流式响应的 usage 可能拿不到。有些 API 在流式模式下不返回 usage或者只在最后一个 chunk 返回。这时候你得自己用对应模型的 tokenizer 估算并且明确标注这是估算值。第三个坑是缓存命中的 Token 计费不同。现在很多模型对缓存命中的输入 Token 有折扣如果你不区分缓存命中和未命中算出来的成本会偏高。Caravel 里我专门加了cached_tokens字段来区分。# 成本计算示例注意缓存折扣 def calc_cost(usage, model_pricing): normal_input usage[prompt_tokens] - usage.get(cached_tokens, 0) cached_input usage.get(cached_tokens, 0) cost ( normal_input * model_pricing[input] cached_input * model_pricing[input_cached] usage[completion_tokens] * model_pricing[output] ) return cost4.3 链路追踪的 ID 传递Agent 的调用链路可能跨多个服务、多个进程trace_id必须能一路传下去。我用的是标准的 W3C Trace Context 格式在 HTTP header 里带traceparent。难点在于工具调用。当 Agent 调用一个外部工具工具内部又调了模型这个trace_id要能续上。我的做法是在工具调用的入参里显式塞一个_trace_context字段工具实现方负责把它透传给下游。如果工具是第三方不可控的那就只能断链这时候用parent_span_id记录我调用了这个工具工具内部的调用单独成链分析时通过时间窗口做近似关联。4.4 采样策略全量还是抽样全量记录明细存储压力确实大。一个中等规模的 Agent 系统一天几十万次调用很正常每次调用一条明细一个月就是千万级。我的策略是分层采样错误调用、高消耗调用超过阈值、新任务类型这三类全量记录普通调用按 10% 采样。这样既保证了异常可追溯又控制了存储成本。阈值怎么定我按 P95 消耗来定。先跑一周全量统计出各类任务的 P95 Token 消耗然后把这个值作为高消耗的阈值。低于阈值的采样高于阈值的全记。5. 实操过程与核心环节实现5.1 环境准备与依赖Caravel 我用的是一套轻量组合采集端用 Python SDK存储用 ClickHouse列式存储聚合查询快可视化用 Grafana。整套下来单机就能跑不需要复杂的集群。# 依赖清单 pip install clickhouse-driver pip install tiktoken pip install opentelemetry-api pip install opentelemetry-sdkClickHouse 建表语句重点是标签字段都建成低基数的 String方便做 group byCREATE TABLE model_calls ( trace_id String, span_id String, task_type LowCardinality(String), agent_name LowCardinality(String), model LowCardinality(String), prompt_tokens UInt32, completion_tokens UInt32, cached_tokens UInt32, tool_calls UInt8, retry_count UInt8, latency_ms UInt32, cost Decimal(10, 6), ts DateTime ) ENGINE MergeTree() PARTITION BY toYYYYMMDD(ts) ORDER BY (task_type, ts);LowCardinality这个类型很关键它把重复的字符串做了字典编码存储能省一大半查询还更快。ORDER BY (task_type, ts)是为了让按任务类型聚合的查询走索引。5.2 采集 SDK 的核心实现SDK 的核心是一个装饰器包住模型调用函数自动记录前后状态。import time import uuid from functools import wraps def track_model_call(task_type, agent_name): def decorator(func): wraps(func) def wrapper(*args, **kwargs): span_id fsp_{uuid.uuid4().hex[:8]} start time.time() retry 0 while True: try: response func(*args, **kwargs) break except Exception as e: retry 1 if retry 3: raise latency int((time.time() - start) * 1000) usage response.get(usage, {}) record { span_id: span_id, task_type: task_type, agent_name: agent_name, model: response.get(model), prompt_tokens: usage.get(prompt_tokens, 0), completion_tokens: usage.get(completion_tokens, 0), cached_tokens: usage.get(cached_tokens, 0), retry_count: retry, latency_ms: latency, ts: int(time.time()), } enqueue(record) # 异步写入队列 return response return wrapper return decorator这里有个细节enqueue是异步的不能阻塞主流程。我用的是一个内存队列加后台线程批量写入每 100 条或每 2 秒刷一次盘。这样即使 ClickHouse 短暂不可用也不会影响业务。5.3 成本计算的参数选择成本计算要维护一张模型价格表。这里我建议把价格表做成配置不要硬编码因为模型价格变动很频繁。MODEL_PRICING { gpt-4o: {input: 2.5e-6, input_cached: 1.25e-6, output: 1e-5}, gpt-4o-mini: {input: 1.5e-7, input_cached: 7.5e-8, output: 6e-7}, claude-3-5-sonnet: {input: 3e-6, input_cached: 3e-7, output: 1.5e-5}, }单位是每 Token 美元。注意input_cached通常比input便宜一半甚至更多这个折扣一定要算进去否则你会高估成本做出错误的优化决策。我实测过一个案例某团队以为自己的缓存没生效因为成本一直很高。后来把cached_tokens单独统计出来发现缓存命中率其实有 60%只是他们之前没区分把缓存命中的 Token 也按原价算了。5.4 聚合查询回答哪类活最费数据落盘后核心查询就一条 SQLSELECT task_type, count() AS call_count, sum(prompt_tokens completion_tokens) AS total_tokens, sum(cost) AS total_cost, avg(cost) AS avg_cost_per_call, sum(retry_count) AS total_retries FROM model_calls WHERE ts now() - INTERVAL 7 DAY GROUP BY task_type ORDER BY total_cost DESC;这条查询直接告诉你过去 7 天哪类任务总成本最高平均每次调用多少钱重试了多少次。但光看总成本还不够因为高总成本可能只是因为调用量大不代表费。我还会算一个单位成本指标——每完成一个业务动作的成本。比如每处理一个售后问题花多少钱这个指标才能反映效率。-- 单位业务成本 SELECT task_type, sum(cost) / count(DISTINCT trace_id) AS cost_per_task FROM model_calls WHERE ts now() - INTERVAL 7 DAY GROUP BY task_type ORDER BY cost_per_task DESC;5.5 可视化看板的搭建Grafana 里我搭了四块面板从粗到细层层下钻。第一块是总览总成本、总 Token、总调用数、平均延迟四个大数字一眼看全局。第二块是任务类型排行横向柱状图按总成本排序一眼看出哪类活最费。第三块是成本趋势折线图按天展示各类任务的成本变化用来发现异常增长。第四块是下钻明细点某个任务类型展开看它的调用链路找出最贵的那些具体会话。提示第三块趋势图是发现问题的关键。账单突然涨一定是某条曲线突然翘头。我遇到过一次是反思校验这条线三天内涨了 5 倍查下去发现是新上线的 Agent 反思逻辑写了个死循环。6. 常见问题与排查技巧实录6.1 账单涨了但调用量没涨怎么查这是最典型的情况。调用量平稳但成本上升通常有三个原因。第一是上下文变长。可能是某个功能上线后往 prompt 里塞了更多历史消息或知识库内容。查法对比prompt_tokens的日均值看是不是涨了。第二是模型被换了。可能某次发布把便宜的模型换成了贵的。查法按model字段分组看成本占比变化。第三是缓存失效。缓存命中率下降同样的调用变贵了。查法算cached_tokens / prompt_tokens的比值趋势。我整理了一张速查表现象可能原因排查字段调用量平、成本涨上下文变长prompt_tokens 均值调用量平、成本涨模型被换model 分组占比调用量平、成本涨缓存失效cached_tokens 比值调用量涨、成本涨更多重试增多retry_count 总和某类任务成本突增逻辑死循环单 trace 的 span 数6.2 单次会话成本异常高怎么定位有时候总量正常但个别会话贵得离谱。这时候要按trace_id聚合找出 span 数最多的那些会话。SELECT trace_id, count() AS span_count, sum(cost) AS session_cost FROM model_calls WHERE ts now() - INTERVAL 1 DAY GROUP BY trace_id ORDER BY session_cost DESC LIMIT 20;拿到最贵的 trace_id 后把它的所有 span 按时间排序拉出来就能看到完整的执行链路。我一般会重点看两个地方有没有同一个工具被反复调用有没有反思循环停不下来。6.3 埋点数据对不上账怎么办偶尔会遇到自己统计的成本和官方账单对不上。差异通常在 5% 以内是正常的时区、汇率、计费口径差异超过 10% 就要查。常见原因一是漏记了某些调用路径比如某个服务没接 SDK二是 Token 估算不准流式响应没拿到 usage三是价格表过期了。我的做法是每周做一次对账把官方账单和 Caravel 统计拉出来对比差异超过阈值就告警。这个习惯帮我抓到过好几次漏埋点的问题。6.4 几个我踩过的坑第一个坑忘了给异步调用打标。Agent 里有些调用是在后台线程发起的如果 SDK 的上下文传递没做好这些调用会落到unknown桶里。我后来强制要求所有调用必须显式传task_type不传就抛异常。第二个坑重试计数算重了。一开始我在装饰器里用局部变量记重试结果嵌套调用时计数串了。后来改成用 contextvar 传递才准确。第三个坑时区没统一。ClickHouse 存的是 UTCGrafana 显示的是本地时间对账时差了几个小时白白排查了半天。现在所有时间戳统一存 UTC展示层再转换。7. 优化动作从洞察到省钱7.1 按任务类型做差异化策略有了洞察数据优化就有了靶子。我的经验是不同任务类型要用不同的省钱策略。对于高频低价值的任务比如意图识别果断换小模型。这类任务通常输出很短小模型完全够用成本能降 90% 以上。对于低频高价值的任务比如复杂推理保留大模型但优化 prompt减少不必要的上下文。对于反思校验这类内部动作设置硬性上限。比如最多反思 2 轮超过就强制结束。这一条帮我省过一大笔钱。7.2 上下文压缩的实操上下文压缩是省钱的大头。我的做法是分层最近 3 轮对话保留原文更早的对话做摘要工具返回结果只保留关键字段。摘要本身也要花 Token所以要算账。如果一段历史原文是 2000 Token摘要成 200 Token但摘要调用本身花了 300 Token那只有在这段历史会被引用 2 次以上时才划算。这个账要算清楚别为了省而省。7.3 缓存策略的调整缓存命中能省一半以上的输入成本值得投入。关键是设计好缓存 key。我的做法是把 system prompt 和固定的 few-shot 示例放在最前面这部分内容稳定容易命中缓存。变化的部分放后面。实测下来把稳定内容前置后缓存命中率从 20% 提到了 65%输入成本直接砍半。8. 我个人的一点体会这套洞察台搭下来最大的价值不是省了多少钱而是让团队对钱花在哪有了共识。以前开会讨论成本大家各说各的现在打开看板哪类活费钱一目了然讨论直接进入怎么优化的环节。如果你现在还在用总 Token 数这种粗粒度指标管成本我建议先花两天把埋点做起来。不用一步到位先把task_type和trace_id这两个标签打上你就能回答 80% 的问题了。剩下的维度可以慢慢加。最后分享一个小技巧给成本设个日环比告警涨幅超过 30% 就通知。这个简单的告警帮我提前发现过好几次问题比月底看账单再补救强太多。