1 minute read

很多人第一次接触“大模型缓存命中”,脑子里冒出来的直觉是:模型把上一次的内容记住了。

这个理解不算全错,但很容易把几个完全不同层面的东西混在一起。实际工程里,缓存命中通常同时带来两类收益:一类是少做重复计算,另一类是少为重复输入付费。前者更接近模型推理层,后者更接近 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_filegreprun_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 缓存这样的推理级机制,也离不开上层对前缀稳定性的工程设计。

Tags: ,

Updated: