KV Cache上下文设计

作者:CherryYang 发布时间: 2026-10-14 阅读量:1 评论数:0

KV Cache 与上下文设计:为什么 Agent 要保持前缀稳定?

本文来自李博杰《深入理解 AI Agent》2.3 节整理得到


KV Cache 是大语言模型推理中的一项基础优化:通过缓存历史 Token 的中间计算结果,减少重复计算。但在 Agent 系统中,缓存还会影响上下文的组织方式。系统提示词是否稳定、工具定义是否固定排序、动态状态插入到什么位置,都可能决定已有缓存能否复用,进而影响推理延迟和成本。

要理解这些影响,需要先弄清 KV Cache 如何产生与复用,再区分推理服务中的 Prompt Cache,最后分析上下文编辑、缓存拼接时可能遇到的正确性问题。

从 API 消息到模型真正处理的 Token

Agent 请求中的 messages 是 API 层面的结构化消息列表,包含 system、user、assistant 和 tool 等角色。推理服务会通过 Chat Template 将这些消息转换成模型使用的 Token 序列。以 Qwen 系列使用的一种格式为例:

API 消息:
{"role": "system", "content": "你是一个助手"}
{"role": "user",   "content": "你好"}

模型输入(示意):
<|im_start|>system
你是一个助手<|im_end|>
<|im_start|>user
你好<|im_end|>
<|im_start|>assistant

这些特殊标记告诉模型消息的角色与边界,工具调用与工具结果也遵循相应的格式。不同模型的 Chat Template 不完全相同,因此应该让推理框架按照目标模型的协议序列化消息,而不必手动拼接 USER: ... ASSISTANT: ...。尤其在工具调用循环中,把 tool 结果误标为 user,可能改变模型对对话结构的判断,部分模型还会改变对历史思考内容的处理方式。

这也解释了为什么缓存设计关心最终 Token 前缀。API 层看起来只是改动了某个字段,进入模型后却可能改变靠前的 Token,进而影响后续缓存的复用。

KV Cache 到底缓存了什么?

Transformer 的注意力机制可以直观理解为:当前 Token 生成 Query(Q),与已有 Token 的 Key(K)计算相关性,再对对应的 Value(V)进行加权汇总。实际计算涉及多个注意力头和多层网络,每层都产生自己的 K、V。

模型处理输入和生成输出可以分成两个阶段:

Prefill
一次性处理输入 Prompt,计算各层 Token 的 KV。
→
KV Cache
保存已处理 Token 的 K、V,供后续 Attention 读取。
→
Decode
逐步生成输出,读取历史 KV,并扩展缓存。
图 1:单次推理中 KV Cache 的产生和使用

假设输入有 10,000 个 Token。Prefill 完成时,模型已经保存了它们的 KV。之后模型生成第 400 个输出 Token 时,仍需要通过 Attention 读取历史 Token 的 KV,但不必再次对整段历史执行完整的 Transformer 前向计算。

这里要区分两个操作:读取历史 KV 与重新计算历史 KV。 KV Cache 省掉了大量重复计算,同时也带来了显存占用和历史 KV 读取的带宽成本。上下文越长,每一步 Decode 需要访问的历史信息通常越多,长上下文推理仍然可能变慢。

为什么前缀变化会使后面的缓存失效?

假设 Agent 使用一段很长的系统提示词,每次请求都向其中注入当前时间:

请求 A
固定前缀 · 500 Token
时间戳 A
固定后缀 · 1,500 Token
请求 B
可复用前缀
时间戳 B
后缀需重新处理
图 2:时间戳位于前部时,普通前缀缓存只能命中首个不同 Token 之前的部分

后面 1,500 个 Token 的文字完全相同,为什么还需要重算?因为 Decoder-only Transformer 使用因果注意力(Causal Attention):后面的 Token 可以读取前面的信息,其隐藏状态可能包含时间戳带来的影响。下一层的 K、V 又是由上一层的隐藏状态计算得到的,于是差异能够跨 Token、跨层传播。

这里有一个值得单独记住的细节:如果某个后续 Token 自身没有变化,位置也相同,那么在通常的 Transformer 结构中,它的第一层 K、V 仍可相同;变化的时间戳可能先影响第一层 Attention 的输出,再传递到第二层乃至更高层的 KV。不能简单断言每一层的 KV 数值都一定变化。

不过,普通前缀缓存要求安全、完整地复用已有计算。一旦遇到首个不同 Token,它通常就停止复用后面的整段缓存。缓存无法被标准机制精确复用,不代表其中每个数值实际上都发生了变化。

因此,在工程上应尽量保持系统提示词、工具定义及顺序稳定,把时间戳、当前状态等动态信息放在靠后的消息中,必要时通过工具按需读取。模型最终仍然能看到动态信息,但前面的长公共前缀更容易命中缓存。

KV Cache 与 Prompt Cache:同一份计算结果,两种复用范围

KV Cache 和 Prompt Cache 经常被混用,但作用范围不同。KV Cache 是模型推理时保存历史 K、V 的基础机制;Prompt Cache 则在多个独立请求之间匹配公共前缀,并复用之前保存的 KV。在 vLLM 中,Automatic Prefix Caching(APC)就是这一思路的实现。

KV CachePrompt Cache
主要作用避免同一请求的 Decode 重算历史状态避免不同请求重复 Prefill 相同前缀
使用时机当前请求的 Prefill / Decode新请求到来、匹配已有前缀时
是否需要跨请求管理不一定需要缓存索引、共享与淘汰机制
缓存内容各层历史 Token 的 K、V通常复用已有的前缀 KV

例如一个客服 Agent 的前 10,000 个 Token 是固定政策文档,不同用户分别提出不同问题。即使两次 API 请求是独立的,只要推理服务支持跨请求缓存,且对应缓存没有被驱逐,第二次请求就可能复用此前的政策文档 KV。

反过来,如果引擎只有单次请求内的 KV Cache,第二次请求即使输入完全相同,也仍然可能重新 Prefill。KV 物理上曾经计算过,并不意味着引擎一定有跨请求查找和引用它们的机制。实际命中还受缓存块粒度、模型配置、缓存容量和服务商策略影响。

缓存经济性如何影响 Agent 架构?

Agent 上下文管理中的几种常见问题,可以归结为同一个工程约束:尽量保留稳定的公共前缀,将频繁变化的信息向后放置。

把 Current time: {{now}} 写在 System Prompt 开头,会使后面的大量上下文需要重新处理;按调用频率不断调整工具定义的顺序,也会破坏原本稳定的工具前缀。把工具结果伪装成普通用户消息,则还可能破坏模型所需的角色结构。

另一种容易被忽视的情况是对话历史管理。直接删除最早的几轮消息虽然缩短了上下文,却改变了前缀,而且可能丢掉关键工具结果,导致 Agent 重复执行已经完成的操作。需要压缩历史时,应有意识地保留关键事实、工具结果及任务状态,并接受在压缩边界重新建立缓存的成本。

这并不意味着所有动态信息都应该无限追加。随着状态更新不断累积,旧状态可能与新状态形成冲突,上下文也会持续增长。长期运行的 Agent 需要状态版本、权威数据源和适当的压缩策略。

研究延伸:KV Cache 可以编辑和拼接吗?

传统 Prefix Caching 只能精确复用连续不变的前缀。但《Models Take Notes at Prefill: KV Cache Can Be Editable and Composable》提出了一个值得关注的观察:在 Prefill 过程中,模型可能已经通过 Attention 将前面的事实和规则整合到后续 Token 的隐藏状态中。这些高层 KV 有时携带了对最终决策有影响的中间结果,好像模型在阅读时留下了“笔记”。

例如政策规定 pending 可以取消订单,shipped 不允许取消。分别对这两种状态进行 Prefill,得到旧缓存 A 和新缓存 B,再人为构造一份混合缓存(Hybrid Cache):

旧缓存 A: [旧前缀] [pending 的 KV] [旧下游 KV]
新缓存 B: [新前缀] [shipped 的 KV] [新下游 KV]

混合缓存: [旧前缀] [shipped 的 KV] [旧下游 KV]

这里的“下游 KV”指字段后面的 Token 对应的各层 KV。它们在旧 Prefill 时已经有机会读取 pending 及相关政策,因此可能带着旧状态形成的决策特征。混合缓存是通过 KV 张量切片替换人工构造的实验状态,并非一次自然完整的 Prefill。

如果在混合缓存上重新计算决策读取位置,模型可能仍然选择“允许取消”。这说明仅刷新字段自身的 KV,未必足以更新模型在下游状态中已经形成的结论。论文将这种缓存更新称为 Editing,并探索了局部更新、选择性重算和追加修正信息等方法。显式思考链在部分实验中能够帮助模型重新读取新字段,但效果具有模型与任务条件。

更稳妥、也更容易在现有推理引擎上实现的策略是 Append-only(追加修正):保留原来的长前缀,在末尾追加“订单状态现已变更为 shipped”,让模型在新决策中考虑更新信息。它有利于缓存命中,但仍需要处理旧状态冲突与长上下文膨胀。

另一条研究方向是 Composition(缓存组合)。假设一个 Agent 有几十份固定 Skill,可以分别预计算它们的 KV,使用时将需要的 Skill 拼接到上下文中。RoPE(旋转位置编码)允许对缓存的 Key 施加位置旋转,从而将一个缓存块迁移到新的 Token 位置,而无需从头执行整个块的 Transformer 计算。

它有一个重要的正确性边界:RoPE 修正位置信息,不能自动补齐独立 Prefill 时缺失的跨块 Attention。

完整 Prefill:     [System A] → [Skill B 读取 A] → [Decision]
独立 Prefill:     [System A]    [Skill B 独立计算]
                              ↓ 拼接
                  [KV_A] + [KV_B] → [Decision]

在独立 Prefill 中,Skill B 计算自己的 KV 时没有看到 System A,自然也没有把来自 A 的信息写入自身的高层 KV。拼接之后,新计算的 Decision Token 可以同时读取 A、B,但不保证与完整 Prefill 的中间计算和最终 Logits 完全相同。对于强依赖的模块,可能需要重算边界或更多下游状态。

原论文在特定模型与负载下报告了显著的延迟改善,但这些实验结果不能直接推广为任意 KV 编辑和拼接都具有无损正确性。目前,稳定前缀、标准消息格式、动态信息后置依然是生产级 Agent 的默认做法。

综合练习

练习一:定位缓存失效边界

一个 Agent 每轮请求都使用如下上下文:

[固定 System Prompt:500 Token]
[当前时间:10 Token,每轮更新]
[固定工具定义和说明:3000 Token]
[用户消息]

假设两次请求只有时间发生变化,且新的时间从其首个 Token 起便与旧时间不同。推理服务支持跨请求前缀缓存。请回答:

  1. 第二次请求最多可以安全复用多少个连续前缀 Token 的 KV?
  2. 时间后面的 3000 个 Token 文本完全相同,为什么普通前缀缓存仍不能直接复用它们的完整跨层 KV?
  3. 如果要优化命中率,应该怎样调整上下文顺序?

参考答案:

  1. 可以复用前 500 个 Token 对应的前缀缓存;具体推理引擎可能采用块粒度缓存,所以实际可复用量可能更少。
  2. 后续 Token 在更高层的隐藏状态可能吸收前面的时间信息。即使这些 Token 的第一层 KV 可以保持相同,完整跨层缓存仍无法安全视为相同。
  3. 将稳定的系统提示词及工具定义放在前面,把当前时间放在后面的动态消息中,或在真正需要时通过时间工具获取。这样可以扩大连续不变的前缀。

练习二:缓存复用与订单决策正确性

某订单 Agent 使用固定退款政策 Skill A 和动态订单状态 Skill B。状态最初为 pending,随后变为 shipped。开发者考虑三种方案:

  • 方案一:只把字段自身的 KV 替换为 shipped,保留旧下游 KV。
  • 方案二:独立 Prefill A、B,重新计算 B 后使用 RoPE 拼接。
  • 方案三:保留稳定前缀,在末尾追加最新订单状态,再让 Agent 决策。

请分析三种方案能否保证与完整 Prefill 的决策一致。若 Agent 还要真正执行 cancel_order,需要增加什么保护?

参考答案:

方案一可能继续使用旧下游 KV 中的决策特征;仅替换字段自身的 KV 并不能保证正确。方案二可以节省预计算 Skill 的重复工作,但 B 独立 Prefill 时缺少与 A 的跨块 Attention,RoPE 只能解决位置问题,因此无法保证与完整 Prefill 等价。方案三可以在标准 Prefix Caching 上实现高复用,工程复杂度较低,但模型仍可能忽略修正信息或受到旧状态干扰,因而同样无法提供严格的决策保证。

涉及真实订单操作时,应由订单服务在执行取消操作的同一原子事务内验证当前状态,例如使用 cancel_order(order_id, expected_status=pending) 的条件更新语义。这样,即使 Agent 根据过期状态发出了请求,也不能越过业务状态约束。缓存负责降低推理成本,执行侧的原子校验负责业务正确性。

参考资料

评论