Agent 的 Token 雪崩
重读那 10 行代码
上一篇写了 Agent 的核心伪代码。这次不问"它能做什么",问一个更实际的问题:它要花多少钱。
while not done:
response = model.chat(messages, tools=tool_definitions)
if response.has_tool_call:
result = execute_tool(response.tool_call)
messages.append(result) # ← 每次循环,messages 都在膨胀
continuemessages.append(result) 这一行是关键。Agent 每完成一轮工具调用,工具的返回结果就被追加到对话历史里。下一轮循环时,模型收到的不再是原始问题,而是原始问题 + 所有历史工具调用及其结果。
这不是"多调用一次就多花一份钱"这么简单。
一次对话的 token 账单
先建立基准。以 Claude Sonnet 为例,API 定价大概是:input token $3/百万,output token $15/百万。
一次普通对话:用户发 500 token 的问题,模型回 1000 token 的回答。成本大约是 $0.000015 + $0.000015 = $0.00003。不到千分之一分钱。谁都不会在意。
Agent 改变了这个计算方式。
复利效应
假设一个 Agent 任务需要 20 轮工具调用。简化一下,每轮模型输出 200 token 的工具调用 JSON,工具返回 500 token 的结果。
第 1 轮:模型收到 prompt + 用户问题。比如 2000 token。 第 2 轮:模型收到 2000 + 200(上一轮输出)+ 500(工具结果)= 2700 token。 第 3 轮:2700 + 700 = 3400 token。 第 20 轮:2000 + 19 × 700 = 15300 token。
看起来还行?但这是简化模型。实际情况里工具返回的结果远不止 500 token。一次 Read 可能返回几百行代码,3000-5000 token。一次 Bash 的 build 输出可能有上万 token。
重新算一下。假设平均每轮工具返回 3000 token,模型输出 300 token。
第 1 轮 input:2000。 第 5 轮 input:2000 + 4 × 3300 = 15200。 第 10 轮 input:2000 + 9 × 3300 = 31700。 第 20 轮 input:2000 + 19 × 3300 = 64700。
到第 20 轮,单这一轮的 input 成本就是第一轮的 32 倍。而且这不只是第 20 轮的代价——从第 1 轮到第 20 轮,每一轮都在付越来越贵的 input 成本。总 input token 消耗不是 20 × 2000 = 40000,而是这些轮次的累加,超过 664000 token。
一次普通聊天 $0.00003,同一个任务交给 Agent 可能花 $2-3。不是贵了一点,是贵了几个数量级。
这就是 token 雪崩。
KV Cache 的物理现实
成本只是表面问题。更深层的是性能退化。
上一篇讲过,推理分两个阶段:Prefill 一次性处理输入,Decode 逐 token 生成。Prefill 的耗时和 input token 数成正比。到第 20 轮,input 已经从 2000 涨到 64000+,Prefill 的时间也涨了 30 多倍。
KV Cache 也在同步膨胀。每一轮的 Prefill 都要把所有 token 的 Key-Value 存下来。20 轮下来,一个 Agent 会话的 KV Cache 可能吃掉几百 MB 显存。在 GPU 同时服务多个用户的场景下,这意味着能同时处理的请求数变少,整体吞吐下降。
所以长上下文的 Agent 不只是贵,还慢。而且不是线性变慢,是加速变慢——越往后越慢。
Prompt Caching:急救绷带
Anthropic 和 OpenAI 都提供了 prompt caching 机制。原理很简单:如果两次请求的 input 前缀相同,后一次可以复用前一次的 KV Cache,跳过 Prefill,直接从 Decode 开始。
Agent 的循环天然满足"前缀相同"这个条件。第 5 轮的 input 前 4 轮的内容和第 4 轮完全一样,只是末尾多了一轮工具结果。理论上大部分 input token 都可以命中 cache。
命中时成本降 90%。这看起来解决了雪崩问题。
但有一个矛盾。Prompt cache 有 TTL(Anthropic 是 5 分钟)。如果 Agent 在某一轮"思考"太久——比如模型生成了一长段推理——cache 就过期了。下一轮要重新 Prefill 全部内容,付全价。
Agent 的思考时间不可控。一个复杂任务可能需要在某几轮做深度推理,耗时超过 TTL。cache 过期的那一刻,之前积累的所有节省一笔勾销。
这不是 prompt caching 的设计缺陷,是它作为"急救绷带"的天然局限。它降低了稳态成本,但没有改变成本加速增长的结构。
控制雪崩的策略
没有银弹,但有几条实操原则。
工具返回要做截断。 Read 不需要返回整个文件,只返回相关行。Bash 输出太长就截断。每少返回一个 token,后面每一轮都省一个 token。这是杠杆率最高的优化。
把大任务拆成短对话。 一个 20 轮的 Agent 任务,不如拆成 4 个 5 轮的子任务。每个子任务用独立的上下文,总 input token 消耗从 664000 降到大约 80000。代价是子任务之间可能丢失一些上下文连续性。
用子代理隔离上下文。 Claude Code 的 Agent 工具就是这么做的。主代理把子任务委托给独立的子代理,子代理有自己的上下文窗口,执行完只返回摘要给主代理。主代理的上下文不会被子任务的工具调用细节污染。
及时压缩上下文。 当对话历史超过一定长度,主动做一次摘要,用摘要替换原始历史。这需要额外的 token 花在做摘要上,但后续每一轮都受益于更短的 input。阈值设在哪里,取决于任务对细节的敏感度。
这些策略的核心思路都一样:控制 messages 数组的长度。Agent 的 token 雪崩来自 messages.append(result) 的无条件累积。所有优化本质上都是在决定:哪些结果值得保留,哪些可以丢弃。
上一篇说 Agent 没有魔法,底层就是一个循环。这一篇的结论也类似——控制 Agent 成本没有魔法,底层就是管理好那个不断膨胀的数组。
← All posts