Prefix Caching:Agent 时代的成本工程

先说那个让账单翻三倍的 bug,够蠢,但值得公开处刑。

四月份我们给一个 Agent 加了个「贴心」功能:系统提示词开头注入当前时间,格式精确到秒——「现在是 2026-04-17 14:32:08」。理由很正当,有些任务需要时间感知。上线两周后财务同学来问:这个服务的推理成本怎么涨了三倍?调用量没涨、模型没换、平均对话长度没变。

查了两天,最后在计费明细里看出端倪:缓存读取的占比从 70% 多掉到了几乎为零。每一轮请求都在全价计算完整上下文。原因就是那行时间戳——它在系统提示词的最前面,而且每秒都在变。前缀缓存是从第一个 token 开始逐字匹配的,第一行就不同,后面几万 token 的缓存全部作废。

一行代码,废掉整条缓存链,账单 ×3。修复方式是把时间戳挪到每轮用户消息的末尾,成本第二天就回去了。

花两分钟把机制搞清楚

这次事故之后我把 prefix caching 认真读了一遍,发现它的原理简单到可以两段话讲完,但工程含义被普遍低估。

先区分两个词。KV Cache 是单次生成内部的优化——自回归解码不重复计算已生成 token 的 key/value,这是推理框架的基本功,用户无感也无法干预。Prefix Caching 是多次请求之间的优化:这次请求的 prompt 前缀如果和之前的请求完全相同,推理侧直接复用算好的 KV 结果,只算增量部分。厂商的定价也体现这一点,缓存命中的输入通常只收原价的十分之一左右。

关键词是「完全相同」——字面意义的逐字匹配,撞上第一个不同字符,之后全部重算。这就是时间戳事故的机制:差异出现得越靠前,废掉的越多,而我们把差异放在了第一行。

Agent 负载天然适合缓存——如果你别搞砸

Agent 会话有个漂亮的结构性质:只追加。系统提示词和工具定义在最前且不变,历史消息按序排列,每轮只在末尾追加新内容。所以第 T 轮请求时,前 T-1 轮的全部内容都是可复用前缀,简化算一下命中率约等于 (T-1)/(T+1),20 轮的会话就是 90% 上下。我们修完 bug 后看真实数据,长会话的命中率确实稳定在 85%~92% 这个带里。

这也是各家敢把缓存价格打到一折的原因——Agent 负载天然对缓存友好,让利换规模,双赢。

但「天然适合」的另一面是「很容易人为破坏」。事故之后我把我们代码里所有可能打破前缀的地方审了一遍,列出来给大家对照:

会话中途改系统提示词。包括看起来无害的动态注入——时间戳、用户状态、随机 ID。改一个字,后面全废。动态信息一律放消息末尾。

中途变更工具列表。工具 schema 序列化在前缀里。我们有个功能是「按任务阶段挂载不同工具」,听起来很优雅,缓存视角看就是每次阶段切换全量 miss。后来改成会话开始就挂全量、用提示词控制阶段可用性——不优雅,但便宜。

「智能」压缩重写历史。有些压缩策略会改写中段历史。要算总账:压缩省的 token 未必抵得过缓存失效多花的。我们的压缩现在只做「截断+头部摘要」,摘要追加在末尾而不是改写中间。

路由不感知缓存。多模型分发如果按单次请求价格贪心,把连续会话在两个模型间倒腾,两边缓存都养不热。便宜模型的 miss 完全可能比贵模型的 hit 更贵。路由粒度必须是会话级,不是请求级。

归纳成一句设计规则:稳定的放前面,易变的放后面,中间不许动。

把命中率变成一个被盯着的指标

事故之后我们做的最重要的改变不是修那个 bug,是把缓存命中率纳入了日常监控。具体三件事:

一,网关侧按会话记录每次请求的 cached tokens / total input tokens,聚合出分服务的日命中率曲线。厂商返回的用量明细里都带缓存字段,解析出来就行,半天的活。

二,设告警线。我们的经验值:长会话型 Agent 服务命中率跌破 70% 告警。这条线三个月里响过两次,一次是有人往 system prompt 里加了动态内容(时间戳事故的复发,人类总是重复犯错),一次是一个新功能在会话中途重排消息顺序。两次都是告警响了才知道——这类退化没有任何功能症状,账单是唯一的受害者,不监控就永远发现不了。

三,接入新模型时把缓存行为写进评测项。各家的缓存实现有差异:有的自动缓存,有的要显式标记断点;最短可缓存前缀长度不同;缓存的存活时间(几分钟到几十分钟不等)也不同——低频服务可能每次请求都赶上缓存过期,标称的折扣根本吃不到。这些细节决定同一套架构在不同供应商上的真实成本,选型时不看,上线后看账单才知道。

两个容易想歪的地方

误区一:命中率高 = 成本低。不是。缓存读取仍然收费,而且长上下文每轮都要完整携带——第 50 轮时哪怕 95% 命中,那 5% 的绝对量也不小。前缀缓存降低的是重复计算的单价,上下文膨胀本身它管不了,该压缩压缩、该拆子代理拆子代理,两套手段是互补的。

误区二:反正能缓存,系统提示词写多长都不亏。这个推论危险。缓存解决计算成本,解决不了注意力成本——八千 token 的系统提示词即使全部命中,依然在稀释指令遵循率(指令预算的老话题),依然在 miss 的时候放大账单。正确姿势是三件套配合:指令预算决定前缀里放什么(少而精),前缀缓存决定怎么放(稳定不动),渐进式披露把低频知识挪出前缀按需加载。

我们按这个思路整改后,单任务推理成本降了差不多六成,缓存相关的贡献约一半。

给你两个可以立刻做的检查

写这篇的目的其实就是让你去做这两件事:

一,打开 API 账单,看缓存读取占输入 token 的比例。Agent 类负载低于 70%,基本可以断定有结构问题,照着上面那四条查。

二,grep 一下代码里所有修改「消息列表头部」的逻辑——动 system prompt 的、重排历史的、往前插内容的。每一处都值得盯着问一句:这个改动值多少缓存?

前缀缓存属于那种「不知道它存在系统也能跑,知道了才发现一直在漏钱」的机制。我们的学费是三倍账单跑了两周,希望你的便宜点。