# 大模型缓存机制完全指南：一篇讲透 KV Cache 到 Claude Code 的省钱工程

> 龙虾AGI通用实验室 · 2026-05-19

## 一

写这篇文章的起因，是一次很具体的困惑。

我用 Claude Code 做一个项目，聊了大概十来轮。中途看了一眼 token 消耗统计，发现一个奇怪的数字——十轮对话，总共消耗了将近 25 万 input token。

25 万。我回头翻了翻聊天记录，自己输入的内容加起来撑死三千 token。模型回复的部分多一些，大概一万出头。那剩下的二十多万 token，消耗在什么地方了？

这个问题后来带着我翻了 Claude Code 的源码、读了 Anthropic 的缓存文档、在本地跑了实验。一路追下去，发现背后是一整套围绕 KV Cache 的工程设计。弄明白这套机制之后，同样的工作习惯可以让套餐额度多撑好几倍。

这篇文章就是把这个追问过程完整写出来。

## 二

先回答那个最直接的问题：那二十多万 token 去哪了。

答案出乎意料地简单——大模型的 API 是无状态的。

这句话的意思是：服务端不记得你之前说过什么。每一次你发送消息，客户端都会把**完整的对话历史**从头到尾重新发一遍。你只发了一句"帮我改下这个函数"，实际上发出去的是：系统提示词两万 token、工具定义八千 token、CLAUDE.md 两千 token、前面所有轮次的对话记录、再加上你这句话。

第 1 轮，发送三万 token。第 2 轮，发送三万一千。第 10 轮，发送超过四万。但十轮下来你自己新输入的内容，加起来不到三千。

这就是那二十多万 token 的去向——绝大部分花在了重复发送已经发过的内容上。

网页端 claude.ai 的情况一样，只是固定开销小一些（没有 CLAUDE.md、工具定义更少）。但对话越长，历史消息的重复发送量同样在累积。这不是 Claude 独有的问题，所有基于 API 的大模型对话都是这个结构。

## 三

知道了 token 花在哪之后，紧接着的问题是：既然每一轮都在重复发送，那有没有办法不重复计算？

有。这就是 KV 缓存。

但在讲缓存之前，需要先说清楚"重复计算"到底在计算什么。大模型处理输入的时候，不是简单地"阅读"文字。每个 token 经过模型的每一层（Opus 有很多层 Transformer），会产出两组向量——Key 和 Value。新 token 生成时，它算出自己的 Query，去和前面所有 token 的 Key 做匹配，从对应的 Value 中提取信息。这就是注意力机制的核心：Q 问问题，K 提供索引，V 提供内容。

这三个角色中，Q 每次都不一样（因为每次生成的新 token 不同），但 K 和 V 有一个关键性质：**前面 token 的 K 和 V，一旦算出来就不会再变。**

为什么不变？因为当前所有主流大模型都是 Decoder-only 架构，用的是因果掩码：每个 token 只看它前面的 token。后面新来了什么，跟它完全无关。

        T₁   T₂   T₃   T₄(新)  T₁    ✅    ❌    ❌    ❌  T₂    ✅    ✅    ❌    ❌  T₃    ✅    ✅    ✅    ❌    ← T₃ 的 KV 不因 T₄ 的出现而改变  T₄    ✅    ✅    ✅    ✅    ← T₄ 只需要算自己的 KV

既然前面的 KV 算完就固定了，那上一轮已经算过的 KV，这一轮就不用再算了，直接从内存里拿就行。结果在数学上完全一致，精确复用。

从 GPU 重新计算，到从内存读取，速度差两到三个数量级。模型越大、层数越多，每个 token 的 KV 计算越昂贵，缓存省下的就越多。

这就是 KV 缓存的全部原理。没有什么神秘的优化技巧，就是**避免对同样的输入做重复的数学运算**。

顺便提一点：KV 的计算是确定性的——同样的输入加同样的权重，每次算出来的 KV 一模一样，所以可以被精确缓存。但模型的输出不是确定性的，同一个问题问两遍，回答可能完全不同。这个看似矛盾的现象涉及 GPU 浮点运算和采样机制，是另一个话题了，准备单独写一篇。

## 四

到这里，事情看起来已经很清楚了：前面算过的 KV 缓存住，后面直接用，省了大量重复计算。

但如果你往下想一步，会发现一个不那么显然的问题——缓存怎么知道"前面的内容没变过"？

毕竟两次请求之间，用户可能改了 CLAUDE.md，可能加了一个 MCP 工具，可能删了一条历史消息。服务端怎么判断哪些部分可以复用、哪些必须重算？

答案是一条极其简单的规则：**从第一个 token 开始逐个比对，完全一致的部分直接复用，遇到第一个不同的 token 就停下，从这里开始往后全部重算。**

就这一条。

它的推论很重要：如果中间某个位置变了，哪怕只变了一个空格，后面的所有内容——即使一字不差——也全部作废，必须重算。

上一次: [系统提示][工具定义][msg1][msg2][msg3]     → 写入缓存这一次: [系统提示][工具定义'][msg1][msg2][msg3][msg4]                          ↑ 这里变了        ←── 命中 ──→ ←────── 全部重算，msg1-3 一字没动也没用 ──────→

理解了这条规则，后面很多东西就不用死记了，可以自己推导出来：为什么改 CLAUDE.md 会让缓存大面积失效、为什么切换模型最贵、为什么 /compact 有副作用。都是这一条规则的推论。

它也直接决定了一个工程设计原则：**最不可能变的内容放最前面，最容易变的内容放最后面。** 这样即使末尾变了，前面一长串前缀还能命中缓存。

## 五

Claude Code 的请求结构，就是这个原则的直接体现。

翻源码会发现，每次 API 调用发出去的不是一整块文本，而是一个严格按"变化频率从低到高"排列的多层结构：

最前面（几乎不变）──────────────────── 最后面（每轮都变）Block 3          Block 4        工具定义        消息历史全局静态指令      CLAUDE.md      session 内冻结   每轮增长

Block 3 放在最前面，因为它全世界所有 Claude Code 用户完全相同——同一份行为规则、同一套指令。张三早上 9 点发了一次请求，Block 3 的 KV 写入缓存；李四 9 点 01 分发请求，Block 3 直接命中。两个人不认识，不在同一个国家，但他们的请求前缀一模一样，所以缓存可以共享。

这也回答了一个之前讨论中遇到的问题：不同用户之间怎么共享缓存？不需要共享对话，不需要有任何关系——**只要请求的 token 序列从头开始有一段完全相同，就能复用。** 缓存系统像一张巨大的哈希表，同时存着成千上万条不同前缀的缓存条目，请求进来后匹配最长的那条。

CLAUDE.md 排在 Block 3 后面，因为它在同一个项目内保持稳定，但不同用户、不同项目之间不同。工具定义再往后，在同一个 session 内一般不变。消息历史放最后——每轮都有新内容追加，但追加发生在末尾，前面一长串前缀完好无损。

源码中实现这个分层的几个关键函数：getSystemPrompt() 组装系统提示词，splitSysPromptPrefix() 按 DYNAMIC_BOUNDARY 切分静态与动态部分，buildSystemPromptBlocks() 添加缓存标记，addCacheBreakpoints() 在最后一条消息上设置缓存断点。

网页端 claude.ai 的结构类似但更轻量——没有 CLAUDE.md，工具定义更少，固定开销低于 Claude Code。这也是为什么做同样的事情，Claude Code 和网页端的 token 消耗并不相同。

## 六

讲到这里，原理部分基本讲完了。缓存存的是 KV 张量，复用靠前缀匹配，Claude Code 的请求结构为前缀匹配做了精心排列。

但还有一个实际问题：缓存不是永久的。它有保质期。

默认 TTL 是 5 分钟，可选 1 小时（写入成本不同：5 分钟的写入是基础价格的 1.25 倍，1 小时是 2 倍，读取都是 0.1 倍）。

这里有一个容易被忽略的细节：**每次缓存被命中时，TTL 计时器会重置。** 也就是说，如果你每隔一两分钟发一条消息，计时器不断刷新，缓存其实永远不会过期。只有连续 5 分钟完全没有请求，缓存才会失效。所以持续编码的场景下 5 分钟 TTL 够用，真正吃亏的是"写一段、思考五分钟、再写一段"的工作节奏。

2026 年 3 月发生了一件让社区不太高兴的事：Anthropic 把 Claude Code 的默认 TTL 从 1 小时静默降回了 5 分钟。社区通过分析本地日志文件（~/.claude/projects/ 下每条响应包含 ephemeral_5m_input_tokens 和 ephemeral_1h_input_tokens 字段）发现了这个变化。目前 Max 订阅用户仍有 1 小时 TTL，Pro 用户默认 5 分钟，由服务端控制，用户没法手动改。

知道了前缀匹配和 TTL 这两条规则之后，什么操作省钱、什么操作烧钱，就不用背清单了，可以自己推导。

## 七

我试着推导一遍。

**同一个 session 持续对话，为什么省钱？** 因为每轮只在末尾追加新消息，前面的前缀不变，缓存全部命中。10 轮对话，只有第 1 轮是全价写入，后面 9 轮的重复部分都按 1/10 计费。

**频繁开新 session，为什么贵？** 因为每个新 session 都是冷启动。系统提示词、工具定义、CLAUDE.md——这些内容要重新全价计算一遍。唯一能命中的是 Block 3（全球共享的静态指令），其他部分全部从头来过。算一下：10 轮对话，同一 session 约 60K 等价 token，每次新开 session 约 255K，差了 4 倍多。

**改 CLAUDE.md 为什么伤缓存？** 因为 CLAUDE.md 在请求结构中排在 Block 3 后面。它一变，从它开始往后的所有内容——工具定义、全部消息历史——都跟缓存里存的对不上了，全部重算。Block 3 还能命中，但那只是前缀中的一小段。

**切换模型为什么最贵？** 因为不同模型的权重不同，同样的输入在不同模型上算出的 KV 完全不一样。Sonnet 的缓存给 Opus 用不了，反过来也一样。切一次模型，整个上下文的 KV 全部作废，相当于最彻底的冷启动。

**增删 MCP 工具为什么有影响？** 因为工具的 schema 定义排在 CLAUDE.md 之后、消息历史之前。增删一个工具，schema 变了，从这个位置开始后面全部失效。

**/btw 命令为什么省钱？** 这是 Claude Code 的一个设计。用 /btw 前缀发消息时，Claude 能看到当前对话的全部内容，但交互结果不会写入主对话历史。问了个语法问题、确认了一个库的用法，主对话的上下文长度没有增长，缓存前缀不变。如果用普通消息问同样的问题，这个问答会永久留在历史里，后续每一轮都要带着。

**分叉呢？** 分叉点之前的前缀不变，缓存可以命中；分叉点之后是新内容，全价计算。所以分叉对缓存是友好的。但在多个分支之间频繁切换就不太好了——每次切换都可能在分叉点之后触发缓存重写。

每一条都不用死记，回到第四章那条规则，都能推出来。

## 八

讲完了"什么操作烧钱"，还有一个东西值得单独拿出来说，因为它的处境比较尴尬——/compact。

/compact 做的事情是把对话历史压缩成摘要，腾出上下文空间。听起来是好事。但结合前缀匹配规则想一下：压缩改变了消息历史的内容，也就改变了请求的 token 序列，从变化点开始所有缓存全部失效。

这就是 /compact 的矛盾——**它在解决上下文空间问题的同时，制造了缓存成本问题。**

Claude Code 有三层上下文管理：微压缩（60-70% 时清除旧工具输出）、自动压缩（95% 时全量摘要）、手动 /compact。三层都会改变消息历史，三层都导致缓存断裂。

什么时候不该用？上下文还有充足空间，只是觉得"历史太长了想清理"。这时候 compact 没有解决任何实际问题，反而让下一轮请求多花一次完整的缓存写入费。微压缩也一样——上下文没满的情况下清除旧工具输出，省出来的空间不急需，缓存断裂的代价倒是实实在在的。

什么时候该用？上下文确实快满了。建议在 60% 左右时主动执行，而不是等到 95% 被动触发。原因是 60% 时模型还能看到完整的未压缩历史，生成的摘要质量更高。到了 80-95%，可能已经微压缩过一轮，模型是在对一个已经退化的视图做总结。执行时带上保留指令：

/compact 保留：当前修改的文件列表、选择 PostgreSQL 的原因、auth.ts 中未修复的错误

还有一条经验：长期有效的规则放 CLAUDE.md，不要指望 compact 会保留。有人报告过 compact 后模型忘了 prompt 中的访问控制规则，开始回应本应拒绝的请求。CLAUDE.md 不受 compact 影响，每轮都会加载。

另外，如果老上下文里大部分是已完成的子任务、被否决的方案、过时的讨论，开新 session 用一两句话概括背景可能比 compact 更划算——既不用付臃肿历史的缓存读取费（走缓存也有 10%），也避免了 compact 的缓存断裂。

## 九

写这篇文章的过程中，还发现了一个之前没注意到的细节：网页端的 artifact 和 Claude Code 的文件，在上下文中的存在方式完全不同。

在 claude.ai 里生成一个 artifact——比如一份五千 token 的文档——这个文档的完整内容会嵌在对话历史中。之后每一轮对话都要带着它。走缓存按 10% 计费，等价 500 token 一轮，20 轮下来就是等价一万 token 的额外开销。就因为之前生成了一个可能已经不需要再看的文档。

Claude Code 不一样。文件通过 Write 工具写入磁盘，上下文中只记录工具调用的结果信息，不携带文件全文。后续对话不会反复带着整个文件。

但如果在 Claude Code 里反复用 Read 工具读同一个文件，每次读取都会把内容重新加载到上下文——第 3 轮读了一次，第 7 轮又读了一次，上下文里就有两份。第 3 轮的那份在后续走缓存（10%），第 7 轮的那份是新增内容，全价。读一次花一次，读两次花两次。

还有 sub-agent。Claude Code 处理复杂任务时会启动 sub-agent（Explore agent 搜代码、Plan agent 做规划），每个 sub-agent 是独立的 API 调用。它能复用主线程的缓存吗？几乎不能——工具集不同（sub-agent 只有主线程工具的子集）、消息历史独立、可能用不同模型。三条加起来，前缀完全对不上。每个 sub-agent 基本等于一次迷你冷启动。所以 Claude Code 不会滥用 sub-agent，简单的文件搜索直接 Grep/Glob 就行，比启动一个 Explore agent 便宜得多。

## 十

最后说一个经常被忽视的事。

前面九章全在讲怎么省钱，但上下文管理其实还影响另一个维度——回答质量。

大模型对上下文中所有 token 做注意力计算，但注意力分配并不均匀。两个已知的问题：模型对开头和结尾的内容关注度高，中间部分容易被忽视（学术上叫 "lost in the middle"）；无关信息越多——已完成的任务、否决的方案、过时的讨论——模型对关键信息的关注度越低。

一个 20K token 的精炼上下文，在回答质量上往往优于 150K token 的臃肿上下文。Opus 支持 1M token 的上下文窗口，但塞满不等于最优。

产品层（claude.ai 和 Claude Code）一直在背后做上下文管理——接近窗口限制时自动压缩、清除旧输出、必要时全量摘要。你从未遇到"请求被拒绝"的情况，不是因为上下文无限大，而是产品层在背后替你做了裁剪。裸 API 不做任何管理，超出窗口限制直接返回错误。

写到这里，回头看最开始那个问题：十轮对话，25 万 input token，花在哪了？

现在答案很清楚了：花在重复发送上。而缓存机制的作用，就是把这些重复部分的计算成本降到原来的十分之一。理解了 KV 缓存的原理、前缀匹配的规则、Claude Code 的请求结构，就能推导出哪些使用习惯在保护缓存、哪些在破坏缓存。

省钱和保质量，在上下文管理这件事上指向同一个方向——减少无关信息，既降低 token 消耗，也提高模型对关键信息的注意力。在合适的时机开新 session、把 CLAUDE.md 写得精炼、用 /btw 处理不需要留痕的问题、不在上下文里堆积废旧信息——这些做法同时服务于这两个目标。

我建了一个AI学习群，目前几十来人，都是在真正动手学AI的人。如果你也在自己跑模型、写代码、做项目，欢迎加我微信，备注"你在做的AI方向"，我拉你进群。纯围观的就不加了，群里大家都在真搞。

![](images/ff0773/img_001.jpg)
