缓存命中到底省了什么:从 KV 缓存到 Agent 里的 token 成本
很多人第一次接触“大模型缓存命中”,脑子里冒出来的直觉是:模型把上一次的内容记住了。
这个理解不算全错,但很容易把几个完全不同层面的东西混在一起。实际工程里,缓存命中通常同时带来两类收益:一类是少做重复计算,另一类是少为重复输入付费。前者更接近模型推理层,后者更接近 API 和 Agent 产品层。两者经常一起出现,所以最后都被压成了一句话:缓存命中了,token 省了。
问题是,这句话太粗了。
如果只是想知道产品账单为什么降了,它够用;但如果你在做 Agent、在调延迟、在看推理链路,光说“省 token”就不够了。因为这里至少有三层缓存,经常一起出现,却不是一回事。
先把三类缓存分开
最直观的是结果缓存。
如果请求内容、模型版本、参数配置都一样,系统可以直接返回上一次的输出,不再重新调用模型。这一层收益最大,因为整次推理都被跳过去了。问题也很明显:命中条件非常苛刻。提示词换一个字、参数动一下、模型版本升级,通常都要重新算。
第二层是前缀缓存,也就是很多平台口中的提示词缓存(prompt caching)。
它缓存的不是最终答案,而是“这段输入前缀已经处理过”。新请求如果和历史请求共享一段完全一致的前缀,系统就没必要从头处理整段输入,只需要处理后面新增的部分。这个能力和 Agent 的关系最直接,因为 Agent 的请求前面往往挂着一大段稳定内容:
- 系统提示词
- 工具定义和结构描述(schema)
- 安全约束、权限边界、执行规则
- 长期记忆摘要
- 项目背景或任务上下文
这些内容在多轮任务里经常不怎么变,真正变化的通常只有用户新消息、工具输出和少量状态信息。前缀缓存能不能命中,直接决定系统是不是在反复为同一段上下文买单。
第三层才是 KV 缓存(KV Cache)。
KV 缓存属于 Transformer 推理阶段对注意力计算的复用。它缓存的是各层注意力里已经算过的键向量(Key)和值向量(Value)张量,不是最终答案,也不是账单层直接暴露给用户的“优惠项”。
这里有一个很重要的边界:KV 缓存并不天然等于跨请求共享缓存。
在单次生成过程中,它首先服务的是同一请求里的连续解码;只有模型服务或推理引擎额外实现了前缀复用、会话复用或跨请求缓存,历史请求里的前缀才有机会被后续请求继续利用。
所以更准确一点说:
- 结果缓存:复用最终输出
- 前缀缓存:复用已经处理过的输入前缀
- KV 缓存:复用前缀在注意力计算中的中间状态
三者经常串在一条链路上,但关注点完全不同。业务系统更关心命中率、延迟和成本,模型服务更关心预填充(prefill)、解码(decode)、显存占用和吞吐。
KV 缓存到底缓存了什么
KV 缓存的价值,要从自回归生成的工作方式看。
Transformer 在生成下一个 token 时,不是只看当前位置,而是要结合前面整段上下文。进入注意力层后,每个 token 会产生三组向量:查询向量(Query)、键向量(Key)、值向量(Value)。可以粗略理解成:
- 查询向量决定当前 token 正在找什么
- 键向量决定历史 token 提供什么索引信号
- 值向量决定历史 token 真正携带什么内容
生成新 token 时,模型会用当前查询向量和历史所有键向量计算相关性,再对对应的值向量做加权汇总。如果每生成一个新 token,都把前面整段上下文的键向量和值向量重新算一遍,重复计算会非常多,尤其是在长上下文里。
KV 缓存的做法很直接:前缀部分既然已经算过,就把这些键向量和值向量保留下来;后续继续生成时,只为新增 token 计算新的键向量和值向量,再和已有缓存一起参与注意力计算。
这也是为什么长上下文请求里,第一阶段通常更慢,后续连续生成更快。前者主要是预填充(prefill),要把整段输入先过一遍模型;后者主要是解码(decode),重点变成“在已有前缀基础上继续往后生成”。
注意力常见写法是:
\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]公式本身不复杂,真正影响成本的是:长前缀里的 (K) 和 (V) 一旦已经得到,下一步还要不要重复准备。KV 缓存的意义就在这里。
缓存命中到底命中了什么
缓存命中不是“意思差不多”,而是“可复用的输入前缀完全一致”。
这里的一致,通常不是按自然语言语义判断,而是按实际 token 序列、请求配置,甚至会话上下文来判断。所以缓存命中经常比想象中脆弱。看起来只是很小的改动,实际上已经足够让前缀失效:
- 多了一个时间戳
- 工具结构描述(schema)字段顺序变化
- 系统提示词里插入一段运行状态
- 历史摘要每轮都重新改写
- 模型版本或推理参数发生变化
从效果上看,大致可以分成两种:
- 完全命中:整段可复用前缀一致
- 部分命中:只有前半段一致,后面是新增内容
真实系统里更常见的是部分命中。尤其是 Agent、多轮对话、代码修复、工作流编排这类任务,请求结构几乎总是“稳定前缀 + 新增尾部信息”。
为什么 Agent 特别容易从缓存里拿到收益
Agent 和普通聊天最大的区别,不是“更聪明”,而是它每一轮都背着更多固定负担。
普通对话也有上下文,但 Agent 往往会固定携带更多系统级内容,包括角色约束、工具定义、执行规则、记忆摘要和项目背景。这些内容一长,重复输入的成本就会迅速放大。
看一个简化过的例子:
- 系统提示词:
2500 tokens - 工具定义:
1800 tokens - 安全规则和执行约束:
1200 tokens - 历史摘要和工作记忆:
1500 tokens - 当前用户新增指令:
120 tokens - 本轮工具返回结果:
300 tokens
如果前面几项基本稳定,那么一轮请求里真正新增的内容可能只有几百 token。没有缓存时,前面的六七千 token 每轮都要重新处理;有前缀缓存时,这部分重复输入要么被复用,要么按更低成本计费,具体方式取决于模型服务商和系统实现。
这里也要把边界说清楚:不是所有平台都会把“命中的前缀”明确展示成缓存输入 token(cached input tokens),也不是所有系统都会因此直接减少账单。不同服务商在计费口径、缓存时长、会话边界、是否跨请求生效这些地方差异很大。能确定的是,只要前缀复用成立,重复工作就会减少;至于这部分收益最后表现成更低延迟、更低账单,还是更高吞吐,要看平台怎么暴露这项能力。
一个更贴近 Agent 的例子
假设你在做一个代码 Agent。每轮请求都包含下面几类固定信息:
- 编码规范和安全约束
- 当前仓库的目录摘要
- 工具定义,比如
read_file、grep、run_tests - 当前任务的长期工作记忆
第一轮请求进入模型时,系统需要先处理整段前缀。假设这部分一共 7000 tokens。
第二轮里,用户只补了一句:“别改接口,尽量兼容旧逻辑。” 真正新增的内容很少。如果前缀保持稳定,系统只需要在这 7000 tokens 之后处理新增内容;如果前缀被打乱,比如时间戳更新了、工具 JSON 重排了、历史摘要被整体改写了,缓存就可能失效,系统又要把前面的 7000 tokens 重走一遍。
这类差异在单轮里不一定夸张,但在 10 轮、20 轮的 Agent 任务里会被放大。前缀越长、轮数越多,重复输入造成的成本就越明显。
这也是为什么很多 Agent 系统做到后面,都会开始关注“前缀稳定性”。这不是文案问题,而是成本和延迟问题。
KV 缓存省的是计算,为什么最后经常表现成“省 token”
如果只看模型内部,KV 缓存的主要收益是减少重复计算,缩短解码阶段的开销,并改善长前缀场景下的推理效率。严格来说,token 本身没有消失;消失的是“每次都从头处理这些 token”的成本。
但到了产品层,这种复用能力经常会被包装成更容易理解的指标:
- 缓存 token(cached tokens)
- 提示词缓存(prompt caching)
- 会话复用(session reuse)
- 前缀复用(prefix reuse)
一旦平台把“可复用前缀”作为显式能力暴露出来,用户就会在计费或监控面板上看到 token 维度的变化。于是“KV 缓存省 token”这个说法流行起来了。
更准确的表述应该是:KV 缓存先省掉了重复计算,平台再把这种复用转化成可感知的成本收益。在某些系统里,这个收益表现为更低账单;在另一些系统里,它主要表现为更低延迟或更高吞吐。
为什么很多 Agent 系统命中率并不高
理论上,Agent 很适合缓存;实际工程里,命中率却经常不稳定,问题通常不在模型本身,而在请求构造方式。
常见原因有几个。
动态字段放得太靠前。时间戳、随机 ID、跟踪 ID(trace id)、实时环境信息,只要进入前缀,就会让每轮请求都不一样。
工具定义不稳定。很多框架每轮重新拼装工具结构描述(schema),字段顺序、空值处理、默认值展开方式都可能变化。业务上看“还是那组工具”,token 序列却已经不是同一份。
历史记忆反复重写。如果每轮都生成一份“截至当前的全量摘要”,缓存很难持续命中。前缀复用更喜欢只追加(append-only)的结构,而不是每轮重写前文。
请求参数频繁变化。模型版本、采样参数、系统模板、工具开关是否参与缓存键计算,取决于具体实现,但这些变化通常都会降低复用概率。
缺少会话黏性。有些缓存只在单机、单进程或单会话里有效,请求一旦切到另一台机器,前面积累的缓存可能直接丢掉。
真想把命中率做上去,可以先改这些地方
先固定最贵的前缀。系统提示词、规则、工具定义,能稳定就尽量稳定,不要每轮重排,不要把无关动态信息混进去。
把动态内容放到后面。用户新消息、工具输出、实时状态、临时变量,尽量统一追加在尾部,让前缀保持稳定。
长期记忆尽量增量追加,而不是全量改写。前缀缓存更适合处理“前面不变,后面增长”的结构。
工具结构描述(schema)的序列化方式要固定。字段顺序、默认值、空字段处理方式都应该一致。很多缓存问题最后不是算法问题,而是序列化细节问题。
能做结果缓存的步骤,单独拿出来做。比如固定文档读取、固定配置解析、确定性较强的中间转换,这些步骤没必要每轮都重新交给模型。
监控不要只看总 token。更有诊断价值的指标通常包括:
- 缓存 token(cached tokens)占比
- 首 token 延迟
- 平均每轮新增 token
- 前缀长度稳定性
- 单任务累计输入成本
这些指标更容易帮助你分清,问题到底出在“上下文太长”,还是“前缀根本不稳定”。
回到 KV 缓存,它为什么仍然是基础设施
在 Agent 产品里,大家更常讨论提示词缓存,因为这是离业务最近的一层。但只要底层模型还是按自回归方式生成,前缀就一定存在;只要前缀存在,就绕不开“已经算过的中间状态能不能复用”这个问题。
上下文越长、回合越多、并发越高,KV 缓存的价值越明显。它决定的不是文案层面的“缓存”两个字,而是模型服务是否能在长前缀场景下维持合理的延迟、吞吐和资源开销。
如果把前面的关系压成一句话,可以这样说:
Agent 场景里,缓存命中表面上是在省 token,底层其实是在避免系统重复处理同一段前缀;而这种复用能否成立、能做到什么程度,离不开 KV 缓存这样的推理级机制,也离不开上层对前缀稳定性的工程设计。