KV Cache 设计

某团队在系统提示词里加了一行实时时间戳,第二天首 token 延迟从 0.5 秒涨到 3-5 秒、账单翻倍——代码看着没问题,模型也没换。答案是那行时间戳让 KV Cache 每次请求都完全失效。缓存不是性能优化,是决定上下文该怎么设计的架构约束。

在进入故事之前,先把 KV Cache 的直觉建立起来。模型每生成一个 token,都要回头看一遍前文所有 token 的中间计算结果。如果每轮都从头算一次,开销会随上下文长度爆炸式增长。KV Cache 的做法是:把前文的中间计算结果缓存下来,下一轮只需要计算新增 token 的部分。前提是前缀完全不变——只要前缀里有一个字符被改写,缓存就全部作废,模型不得不从被修改的位置重算。顺带说明:本节讲到跨请求的“缓存命中”时,在 API 服务商的语境下叫 Prompt Cache——它是构建在推理引擎 KV Cache 之上的跨请求缓存。

理解了这一点,下面这个故事就一目了然。某团队的客服 agent 每天处理 10 万次对话,原本一切正常。某天工程师为了让 agent “知道”当前时间,在系统提示词里加了一行 Current time: {{now}},把时间戳实时注入进去。第二天监控告警:所有对话的首 token 延迟从 0.5 秒涨到 3-5 秒,月度推理账单几乎翻了一倍。代码看起来完全没问题,模型也没换——问题出在哪里?

答案是:那一行时间戳让 KV Cache 在每次请求都完全失效。系统提示词每次都不同,模型不得不从头重新计算前缀对应的所有键值对。这种“无形成本”在 agent 系统里反复出现——开发者写下的一行看似无害的代码,可能让整条推理链路慢一个量级。本页要讲的,就是如何避开这些陷阱。

技术门槛提示:本节涉及 Transformer 注意力机制和 KV Cache 的内部原理,技术密度较高。如果你不熟悉这些底层机制,可以跳过原理细节,只需记住以下三条核心结论:

  1. 系统提示词和工具定义一旦确定就不要改。 任何改动,哪怕多一个空格,都会导致缓存全部失效,延迟成倍增加、成本上升。
  2. 动态信息永远追加到末尾——时间戳、用户状态等变化的内容,作为新消息追加到对话末尾,而不是修改已有的系统提示词。
  3. 使用标准 API 格式,不要自行拼接消息:结构化消息会被 Chat Template 翻译成模型训练时见过的固定 token 序列;自行用字符串拼成 "USER: ... ASSISTANT: ..." 会偏离这种训练格式,削弱模型的多步思考能力。

这三条结论背后的直觉其实很简单:大模型在处理上下文时,会把前面已经处理过的内容缓存起来,下次只需要处理新增的部分。就像做菜——如果前几步完全一样(同样的食材、同样的刀工),你可以直接从上次切好的地方继续;但如果前面任何一步变了,后面所有步骤都得重来。系统提示词和工具定义就是“前几步”,一旦改动,所有缓存的中间结果全部作废。

KV Cache 的原理与约束

要理解 KV Cache 的价值,先看看没有它时会发生什么。假设一个 agent 在进行第 6 轮对话,上下文已经累积了 2000 个 token。在没有缓存的情况下,模型每生成一个新 token,都需要重新计算这 2000 个 token 的 K、V 向量——相当于重跑整个前缀的前向计算。尽管前 5 轮的内容完全没变,第 6 轮仍要像第 1 轮那样从头计算整个前缀。无缓存时,prefill 阶段的注意力计算量随上下文长度平方级增长,随着对话深入,延迟和成本都会急剧攀升。这对于需要几十轮工具调用的 agent 任务来说是不可接受的。

使用 KV Cache 时,历史 token 的 K、V 向量算过一次后就缓存起来,生成新 token 时只需计算它自身的 K、V,再与缓存的部分一起完成注意力计算。需要注意的是,KV Cache 省去的是历史 token 的 K、V 投影重算;但每个新 token 的注意力计算仍要遍历全部缓存的 K、V,计算量随上下文长度线性增长——这正是长上下文解码越来越慢、KV Cache 的显存与带宽成为推理瓶颈的原因。

为什么修改前缀会导致缓存全部失效? 大语言模型由多层 Transformer 堆叠而成,每一层都独立生成自己的 K、V 缓存,且层层串联:第 1 层的输出喂给第 2 层作为输入。因此,如果修改了第 1 个 token(比如系统提示词改了一个字),第 1 层的输出就变了,第 2 层的输入随之改变,逐层向下传导——所有层的缓存都必须重算。这就是为什么反复强调“系统提示词一旦定下来就不要改”。

KV Cache 与 Prompt Cache:两个层级的缓存

需要区分两个容易混淆的概念。KV Cache 是模型内部的优化——在一次推理过程中,缓存已计算的 token 的键值对,避免重复计算。Prompt Cache 则是 API 服务层的优化——跨多次 API 请求之间,缓存相同前缀的计算结果。两者原理相似(都利用前缀不变性),但作用层级不同:KV Cache 加速单次请求内的 token 生成,Prompt Cache 减少跨请求的重复计算成本。缓存读取的成本远低于首次计算——以 Anthropic、DeepSeek 为例约为十分之一。各家启用方式差异不小:Anthropic 需要在请求中显式设置 cache_control 断点才会缓存(并非自动命中),缓存写入有约 1.25 倍加价,且有最小可缓存长度和 TTL 限制(默认约 5 分钟,过期即失效);OpenAI 则是自动前缀缓存,无需显式声明。

缓存作为架构约束

在生产级的 agent 系统中,缓存不仅仅是性能优化手段——它是一个架构约束,决定了系统中许多看似无关的设计决策。Claude Code 的实践揭示了一个深层模式:当 Prompt Cache 的经济效益足够显著时,缓存一致性会反过来主导系统的架构选择:

  • 提示词的结构由缓存边界决定。系统提示词被一个缓存边界一分为二——标记之前可跨用户、跨会话全局缓存,标记之后包含用户和会话特定信息。每个动态运行时条件若放在边界之前,就会把缓存键的变体数量翻一倍(N 个二值条件产生 2^N 种组合),因此所有动态元素都被严格归到边界之后。
  • 子 agent 必须与父 agent 字节级对齐。子 agent 的提示词、工具定义、模型配置、消息前缀必须与父 agent 的缓存键逐字节匹配,才能命中同一份 Prompt Cache。
  • 工具结果的替换字符串在首次出现时就被冻结。大型工具输出被替换为摘要预览后,替换字符串会被持久化——即使会话重启,也用完全相同的字符串,保证恢复后的字节流与缓存一致。

核心启示是:缓存经济性不是事后优化,而是前置约束。缓存键的一致性要求会渗透到提示词设计、多 agent 协调、会话恢复等各个层面,越早纳入架构设计,后续工程代价越小。

工程实践

这三条架构约束在 Zapvol 里是同一套机制的落地。对 Anthropic 模型,每步在追加瞬态 reminder 之前由 markPrefixCacheBoundary() 放最多 4 个 ephemeral 断点,读时取最长命中前缀——多个锚点让缓存在 tail 跳变或压缩重写了部分前缀时仍有落点:system 块恒在(整任务稳定)、开头的 rolling 摘要锚、当前轮边界、以及 reminder 之前的 tail。压缩产出的摘要与工具结果替换串都按内容 hash 寻址、字节稳定后跨轮存活,正是上面“替换字符串冻结”与“会话恢复字节一致”的兑现。四锚的完整机制见压缩

KV Cache 未必是一次性的:可编辑、可组合的“笔记”

以下是来自研究前沿的延伸阅读,Zapvol 尚未落地,保留在此作为演进指引;前面的三条实践结论才是当前生产系统应当遵守的地基。

本页到此为止都建立在一条铁律上:前缀里改一个字节,后面的缓存就全废。这条铁律在今天的推理引擎里确实成立,但未必是必然的。松动它的出发点是一个反直觉的观察:在 prefill 阶段,模型其实在“做笔记”——读到上下文里的某个字段(比如“用户所在城市:北京”)时,它并不是把字段原封不动缓存下来,而是顺手把“这个字段意味着什么”的结论写进了后面每一层的 KV 状态里。测量发现,一个字段自己那几个 token 的 KV 对最终决策的贡献往往不到 1%——真正影响输出的,是它在下游留下的那些“读书笔记”。

这个发现打开了两种以前认为不可能的操作。其一是编辑(Editing):既然结论已经写进下游笔记,改掉一个字段后,只要模型有显式的思考链(CoT),就能让这处改动顺着已缓存的思考传播下去,用大约 1% 的算力得到与整段重算一致的结果;反过来,没有 CoT 时孤立地改字段会被忽略——结论早已烘焙进下游状态,却没有一条思考路径去更新它,这是一条重要边界。其二是组合(Composition):把一段预先算好的“技能”缓存,通过旋转位置编码(RoPE)挪到新位置,直接拼接进另一段上下文而不必重算注意力——于是“用模块化缓存块拼出一个长上下文”从 O(L²) 的重算降到 O(L) 的拼接,质量却与完整重算无法区分。

打个比方:读一份厚文档时,你不会每改一个事实就从头重读,而是靠页边笔记——笔记里已经写着“所以这意味着 X”。某个事实变了,只需修正那条笔记,它喂养的结论就跟着更新;又因为笔记用一种可搬运的速记写成,你还能把上次为别的问题记的一页,重新编号后(这就是 RoPE 重定位)粘到新问题里复用。论文在 vLLM 上实现后,首 token 延迟(p90)最高有几十到几百倍的下降、前缀缓存命中率约 98.5%,而输出与逐字重算在决策上完全一致(跨 12 个模型,logit 余弦相似度 0.90–0.999)。

对 agent 而言,这意味着那个被反复重建的长上下文——换一批工具、更新一个记忆字段、注入一条新状态(正是状态栏要做的事)——也许不必每轮都推倒重来。它指向“上下文可变、但缓存收益还在”的可能:把上下文的组装从 O(L²) 的重算变成 O(L) 的“笔记拼接”。这仍属研究阶段,本页前面的三条实践结论在当前生产系统中依然是应当遵守的默认原则。

相关阅读

  • 提示工程——静态前缀里到底写什么、怎么排布让缓存生效。
  • 上下文压缩——压缩如何与缓存共存:只追加、按 hash 寻址、可确定性重推。

来源

  • Prompt caching — Anthropic, Claude API 文档
  • Li, Bojie. Models Take Notes at Prefill: KV Cache Can Be Editable and Composable. arXiv:2606.17107, 2026
这页有帮助吗?