龙虾AGI通用实验室Lobster AGI Lab · est. 20262026 · 09 · 16
首页 / 文章 / AI持续学习
AI持续学习

大模型缓存机制完全指南:一篇讲透 KV Cache 到 Claude Code 的省钱工程

格里灰丝2026-05-19约 6949 字Markdown 原文

写这篇文章的起因,是一次很具体的困惑。

我用 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方向",我拉你进群。纯围观的就不加了,群里大家都在真搞。

上一篇Claude Code的上下文对话想分叉怎么办?我有一个技巧下一篇大模型掷骰子吗?拆解 AI 输出的确定性边界