04-KV Cache原理与缓存友好的上下文设计:前缀复用、缓存作为架构约束
系列的最后一篇,聊一个平时看不见、却直接决定 Agent 成本和延迟的东西:KV Cache。上一篇说过,上下文是 Agent 的眼睛;这一篇讲的是——这双眼睛每次“重新看一遍世界”的代价,以及如何让这个代价尽可能小。理解了 KV Cache,你写 Agent 时的很多“玄学优化”(为什么 system 提示词要放最前面、为什么别在提示词里塞时间戳)会瞬间变成有理有据的工程决策。
一、KV Cache 原理:注意力机制里的“复用艺术”
先补一点底层知识。现代 LLM 的核心是注意力机制(Attention):每个 token(词元)在生成输出时,要“回看”前面所有 token,计算彼此的关联权重。这个回看动作依赖每个 token 的两个向量:K(Key,钥匙)和 V(Value,内容)。
关键点来了:K 和 V 只由 token 自身和位置决定,与后面生成什么无关。也就是说,处理第 100 个 token 时,前 99 个 token 的 K/V 向量不会变。那何必每生成一个新 token 就把前文全部重算一遍?把它们缓存下来,只算新 token 自己的 K/V,这就是 KV Cache。
没有 KV Cache 时,注意力计算是 O(N²) 级的——每一步都和前面所有 token 两两配对。缓存之后,生成阶段每个新 token 只需 O(N) 的计算量。再加上 Transformer 是多层串联结构(几十上百层,每层各自有 K/V),前面 token 在每一层的 K/V 都能被后续计算复用,这就是“前缀复用”的物理基础:只要前缀一个字节都不变,这些计算结果就是完全可复用的。
这也解释了推理服务里的两个阶段:prefill(处理你的输入提示,计算量大,与输入长度平方相关)和 decode(逐 token 生成,每步便宜)。KV Cache 的本质,就是把 prefill 阶段算出来的 K/V 存下来,避免重复 prefill。
二、两个层级的缓存:KV Cache 与 Prompt Cache
- KV Cache:模型服务端按前缀逐层缓存的 K/V 张量,物理存在显存里,对用户透明。
- Prompt Cache(前缀缓存):推理平台暴露出来的机制——如果你的请求前缀与近期某次请求完全一致,直接命中缓存的 KV,跳过 prefill。命中一次,费用可能降到原来的十分之一,首 token 延迟从秒级降到毫秒级。
注意“完全一致”四个字:是字节级的前缀匹配。前缀中任何一个字节变了,从那个字节起,后面所有层的缓存全部作废。这就是为什么它对上下文组织方式极其敏感。
三、五种常见的“缓存杀手”上下文模式
对照检查你的 Agent 代码,以下每一种都在悄悄让缓存失效:
1. 动态系统提示词(塞时间戳)
❌ system: 你是无人售货柜客服。当前时间:2026-08-18 14:23:07每次请求时间都变,而 system 在最前面——第一句话就变了,整个上下文的缓存全灭。正确做法:系统提示词保持静态;当前时间如果真需要,作为最后一条 user/tool 消息注入,只“脏”末尾一小段。
2. 动态用户配置放在前缀
把用户昵称、会员等级、柜机编号等每次可能变化的信息写进 system。正确做法:基础 system 固定,个性化信息放到对话靠后位置或首轮 user 消息里。
3. 工具定义动态排序
今天按字母序排工具,明天按调用频率排——顺序一变,序列化后的字节就变,缓存失效。正确做法:工具列表及顺序冻结,只做增量追加,不重排。
4. 滑动窗口裁剪历史
为了省 token,把最老的消息成批删掉。窗口一滑,前缀变了,缓存照样失效。正确做法:优先压缩/摘要旧对话并放到文档或工具结果里按需检索,而不是在前缀处开刀。
5. 文本格式化的“手痒”
每次请求都重新格式化上下文:JSON 重新序列化(键序可能变)、浮点数精度抖动、随机空格换行。字节一抖,前缀崩塌。正确做法:任何进入前缀的内容都做字符串冻结——序列化一次,存成常量,之后原样复用。
四、缓存作为架构约束:像对待数据库 Schema 一样对待前缀
李博杰在书中提出了一个很硬核的观点:缓存不是性能优化的可选项,而是架构约束——你设计 Agent 上下文结构时,必须像设计数据库 Schema 一样,把“什么在前缀、什么在末尾、什么会变”当作一等公民来设计。
业界的最佳实践可以佐证:
- Claude Code 的缓存边界:Anthropic 的 API 允许显式标记缓存断点,Claude Code 把 system 提示和工具定义放在缓存边界之前并保持冻结,对话历史放边界之后,前缀命中率极高。
- 子 Agent 的字节级对齐:父 Agent 派生多个子 Agent 时,让所有子 Agent 共享完全相同的 system + 工具定义前缀(字节级一致),一次 prefill,全家复用。
- 工具结果字符串冻结:工具返回的结果一旦进入上下文,就不再二次加工,保证后续轮次前缀稳定。
我们自己在做无人零售客服平台时同理:所有租户共享一份基础 system 提示与工具集(命中前缀缓存),租户差异配置在运行时通过工具查询注入到对话中段,而不是拼进 system。这一个设计决策,让线上推理成本降了一个量级。
五、前沿:KV Cache 的可编辑与可组合
传统 KV Cache 是“只读”的——改一个 token,后面全重算。前沿研究方向正试图让它像笔记一样可编辑:定位并修改某段前缀对应的 KV 向量而不作废整体缓存;以及跨请求、跨会话组合缓存块(比如把“工具定义块”的 KV 直接拼到不同会话前)。这有点像人类的读书笔记:不必重读全书,只翻笔记增量。这个方向一旦成熟,“缓存作为架构约束”会演变成“缓存作为可组合的存储层”,值得持续关注。
小结:三条核心结论
- 系统提示词固定:任何每次请求都变的内容,禁止出现在前缀,时间戳尤其如此。
- 动态信息放对话末尾:上下文按“稳定在前、易变在后”排列,让缓存失效范围最小化。
- 保持消息格式稳定:工具顺序冻结、序列化结果字符串冻结,前缀字节级一致才能命中 Prompt Cache。
一句话收束整个系列:Agent = LLM + 上下文 + 工具;上下文喂得好不好,看上下文工程;上下文喂得贵不贵、快不快,看 KV Cache 意识。设计与成本,从来是一体两面。