专栏 Agent 工程

缓存利用率:前缀复用与失效代价

一次长对话跑到第五轮,账单上的输入是八万 token。

缓存利用率:前缀复用与失效代价

一次长对话跑到第五轮,账单上的输入是八万 token。

新增的内容只有一条工具结果,两千字。剩下的七万多,全是前四轮已经发过的东西。

如果缓存正常,这七万多应该按缓存读取计价,便宜一个数量级。如果没有命中,就要按全新输入重新算一遍——同一个前缀,每一轮都在重算。

一、Agent 的请求,九成是重复的

这是 Agent 和普通聊天的结构差异。

普通对话,每一轮新增一问一答。Agent 每一轮要新增:一条工具调用、一条工具结果,可能还有一条系统提醒。而前面所有的历史,一个字都没变,还要原样再发一次。

请求的体积随轮数线性增长,其中绝大部分是上一轮已经发过的字节。

所以缓存对 Agent 不是优化项,是成本结构的基本盘。

二、规则很简单,也很难

主流厂商的 prompt cache 都遵循同一条规则:按精确的字节前缀匹配。

发过去的请求,从第一个字节开始比对。前面有一段和上次完全一样,这段就能命中缓存;从第一个不同的字节开始,后面全部按新内容算。

三个直接推论:

  • 命中是前缀式的,中间断一处,后面全废;
  • 缓存有 TTL,Anthropic 默认五分钟,可以花钱延长到一小时;
  • 写入缓存比普通输入贵,读取比普通输入便宜很多。所以它的价值在于重复使用——只用一次的前缀,开缓存反而更亏。

规则本身不复杂,难的是它给架构加了一条硬约束:已经发出去的内容,不能再改。

三、最贵的一类错误

有一个真实的例子,值得完整讲一遍。

长上下文时,系统需要定期提醒模型”开头那些规则仍然有效”。最初的实现很自然:在发送前,把这段提醒拼到最后一条消息的末尾。

问题在于,”最后一条消息”每一轮都不同。

第一轮它是工具结果 A,第二轮是工具结果 B。于是同一条历史记录,这轮带着提醒发出去,下轮不带。字节变了,缓存从那条消息开始失效,后面全部重建。

实测数据是四轮连续的缓存写入量:2802、7228、7422、519。每一轮都在重新创建,一次都没命中——尽管缓存开关是开的。

修法只有一句话:追加,不要改写。

把提醒作为一条独立的新消息加在历史末尾。前面所有字节一个不动,缓存前缀照样命中;只有新增的这一条要计价,而这本来就是任何新消息都必须付的成本。

这个改动还顺带修了另一个 bug:提醒之前是拼在工具结果的字符串里的,模型读到”文件内容 + 一段指令”,分不清哪部分是文件、哪部分是系统说的,一度判断仓库里藏了提示注入,连续三轮报警。拆成独立消息之后,语义也清楚了。

一个”就地改写”的小聪明,同时破坏了缓存和语义。这类错误很难靠测试发现,因为功能看起来完全正常——只是账单贵了十倍。

四、由此得出的结构约束

把上面那条经验推广,就是一套上下文组织规则:

规则 原因
只追加,不改写 改一个字节,后面全失效
稳定的放前面 system 提示、早期历史最不容易变
易变的放后面 时间戳、提醒、临时状态
不要在中段插入 插入点之后的内容全部重建
不要动态重排 顺序变了等于内容变了

最理想的结构是一个单调增长的前缀:system 永远在最前且不变,历史像日志一样只往后长,所有动态内容都在末尾。

这也解释了为什么很多 Agent 把”每轮都变”的东西设计成独立消息而不是字段:字段会改已有对象,消息只会在末尾多一条。

五、命中率怎么看

光看总 token 不够,要看三个数:

输入(未命中) + 缓存写入 + 缓存读取

命中率通常按 缓存读取 / 三者之和 算。分母必须包含写入,因为那是这一轮实际处理的输入量——只算未命中的部分,会把”全量重建”算成 100% 命中率。

健康的长对话,缓存读取应该占输入的大头。如果连续几轮都是”写入很大、读取接近零”,说明前缀一直在变,有东西在偷偷改写历史。

六、缓存和压缩天生冲突

第三篇讲过上下文压缩:把旧历史摘要成一段,替换原文。

从缓存的角度看,这是一次全量失效。历史被重写,前缀全变了,下一轮必须从头建立缓存。

这不算缺陷,是必然的代价。但它带来一个策略上的结论:压缩要一次到位,不能频繁小步做。

如果每几轮就压一次,每次压完都要重建缓存,成本会比不压还高。正确做法是等到确实需要时压一次,压到一个能撑很久的程度。

同理,任何”定期整理历史”的机制都要先问一句:这次整理值不值得让缓存全部作废。

七、为什么值得单独讲

因为它同时影响三件事,而且方向一致:

成本。 Agent 的 token 大头在重复前缀,命中与否差一个数量级。

延迟。 缓存命中的部分不需要重新计算,首字延迟明显更低。

架构。 它把”历史不可变”从建议变成了约束。任何想在历史中间动手脚的设计,都要先算这笔账。

所以缓存不是一个可以事后加的优化开关。它是一条从第一天就要遵守的规则:把上下文当成只追加的日志,而不是可以随便编辑的文档。


会话能恢复、缓存能命中,这两件事其实指向同一个要求:让每一轮发出去的东西,尽可能和上一轮一样。

Agent 的很多设计看起来奇怪,追到底都是这一条。

不过这些讲的都是请求”里面”的事:内容怎么组织、状态怎么保存。往外还有三件事——数据怎么流出来、请求最终发给了谁、以及图片怎么塞进去。