Claude Code 的 prompt caching 设计有多聪明

那次我改了 CLAUDE.md,Claude 半天没理我
我前几天给一个项目加了条规则到 CLAUDE.md:「测试用 vitest,不要用 jest」。
存盘、回到 Claude Code 对话框、让它给某个工具函数补单测——它继续用 jest 的语法写。
我又强调了一遍。它说「好的我会用 vitest」,然后下一段就是 import { describe, it, expect } from '@jest/globals'。
第一反应是「Claude 是不是疯了」。第二反应是「我是不是 CLAUDE.md 写得不对」。
真相是第三个:
项目级 CLAUDE.md 在会话开始时读一次,中期改完不生效。
想让新规则生效,要么 /clear,要么重启会话。
我去翻 Claude Code 的 prompt caching 文档准备写个 issue,结果读完发现,这不是 bug,是个主动选择的设计——而且整个 Claude Code 里你觉得「奇怪」的行为,几乎都能用同一条规则解释。

一条简单规则,推导出全部行为
Claude Code 的每次请求,本质是把过去所有上下文连成一长串发给模型,新内容附在末尾。
API 缓存的方式是前缀匹配——这次请求的开头和最近一次缓存内容比对,匹配多少就跳过多少计算。
匹配是精确的:
前缀里任何位置变了一个字符,后面全部要重算。
为了让前缀尽可能稳定,Claude Code 把请求分成三层,稳定的在前,易变的在后:
| 层 | 内容 | 什么时候变 |
|---|---|---|
| 系统提示 | 核心指令、工具定义、输出样式 | MCP 接入/断开、Claude Code 升级 |
| 项目上下文 | CLAUDE.md、自动内存、规则 | 会话开始、/clear、/compact |
| 对话 | 你的消息、Claude 的回复、工具结果 | 每个回合 |
这张表第三列没有「中期编辑 CLAUDE.md」——因为它根本不触发重新加载。
这就是钩子段那个谜的答案:项目上下文层只在会话开始时构建一次。
不构建,缓存就不会失效;不失效,你的修改就也不被读到。一体两面。

设计哲学:宁可让你的修改暂时失效,也要保住前缀
Claude Code 的工程师不是没想到你会想中期改 CLAUDE.md。
他们的选择是:不做这个功能。
如果允许中期改 CLAUDE.md 生效,意味着项目上下文层会在会话中变,意味着前缀失效,意味着下一回合的 token 全部按未缓存价格重新计费。
对一个动辄几万 token 项目上下文的开发者,一次失效的代价可能是几十次正常回合的总和。
所以设计选择是:让你的修改暂时失效,但缓存活着。想生效的代价是显式的 /clear 或重启——你知道自己在付什么代价。
同样的逻辑解释一堆「奇怪」行为:
- 输出样式中期切换不生效——它在系统提示里
- 新装 MCP 服务器要重启——MCP 工具定义在系统提示里
paths:规则要等 Claude 首次读匹配文件才加载——避免一开始就把所有规则塞进上下文
这套设计背后的产品决策标准是:
「缓存安全」优先于「即时反映用户操作」。
如果你在做自己的 agent 应用,这点很值得抄:
什么放进系统提示、什么放进对话历史,直接决定了你的 agent 在长会话里是省钱机器还是吞钱怪兽。
那些悄悄让缓存失效的操作
CLAUDE.md 中期改是「让修改暂时失效来保住缓存」。反过来,有一类操作是「你以为没什么,实际把缓存炸了」。它们才是隐性成本的来源。
1. 切换模型(/model)
每个模型有自己的缓存。
你从 Sonnet 切到 Opus 看一眼,下一回合整个对话历史按未缓存重新计费。
订阅用户感觉不到——你不付 token 钱。按 API 调用的用户,长会话切一次就要心疼一下。
2. opusplan 模式
这个是隐藏雷区。
opusplan 在 Plan Mode 期间用 Opus,执行时切回 Sonnet。
意思是:每次进或出 Plan Mode,都是一次模型切换。 每次都会重建缓存。
如果你养成「先 plan 再写」的习惯,在 opusplan 下,每一轮都在交「缓存重建」的税。
3. MCP 服务器后台断线重连
你什么都没动,但 MCP 服务器的 stdio 进程退出了、或 HTTP 会话过期了、或暂时故障后自动重连——工具定义在系统提示里,工具集变了,缓存就炸了。
这一条最防不胜防,你不知道它什么时候发生。
4. 升级后 resume 长会话
claude --resume 把你拉回之前的长对话。
如果中间 Claude Code 自动更新过,新版本的系统提示和旧的不一样,整个历史按新前缀重新处理一遍。
会话越长,这一次 resume 的代价越大。
你可能正想接着干一件事,结果发现这是你今天最贵的请求。
设计的优雅之处:同一条规则解释「缓存友好」的操作
如果只看上面的失效列表,你会觉得 prompt caching 是个布满陷阱的东西。
翻到文档另一面会发现——同样一条「前缀匹配」规则,也推导出一整套相反的事实:
- 编辑代码文件不会让缓存失效。文件内容只在 Claude 读它时进入上下文,你后面改了它,Claude Code 只是附加一个
<system-reminder>提示文件变了,前缀完全不动 /rewind比/compact更省。/rewind回到更早的回合,前缀还是原来缓存里那段,直接命中;/compact是把历史压缩成摘要,等于构造了一段全新的前缀,要重新预热- 技能 / 命令作为用户消息附加。它们在调用点插一段用户消息,前缀不动
/recap是命令输出,不替换历史——和/compact表面像,本质相反- 权限模式切换是缓存安全的(除非你用
opusplan,那它顺带切模型)
整套行为只用一条规则就能推导:
改的位置离对话末尾越近,代价越小。
没有特例,没有「除了 X 之外」。
这是设计上最舒服的地方——你不用记一长串规则,只要记住「前缀匹配,精确,从头算」这一句话,所有现象都讲得通。
我看过不少 LLM 应用做缓存,大多数是把它当个 feature 加在某个角落,缓存命中是 bonus、不命中是默认。
Claude Code 反过来:
缓存命中是默认,失效是要付出代价的偏离。
这是产品哲学上的层级差异。
我的评价:借鉴 + 不喜欢
值得借鉴的:
- 分层是个干净的抽象——系统提示 / 项目上下文 / 对话三层,清楚到能直接抄进自己的 agent 设计
- 「缓存安全」作为产品决策标准比「用户操作要立刻生效」更优先,反直觉但合理
- 一条规则解释所有行为是设计水平的体现,大部分工具做不到
不喜欢的地方,也得说:
心智负担其实不小。 文档讲得清楚,但大多数用户不会去翻。结果是 Claude Code 的随手用户,踩坑了也不知道为什么贵。
opusplan 的隐性成本文档没强调够。 它被推荐给 Plan Mode 重度用户,但每次进出 Plan Mode 都付一次切换税——这点应该在 /model opusplan 选项里就提示。
worktree 不共享缓存对并行开发不友好。 同一个仓库开三个 worktree 干三件事,缓存完全独立。文档承认这点,但没给解决方案。
MCP 自动重连的不可见性令人不安。 你在做正事,缓存在后台被悄悄炸,statusline 不点开你不知道。
适合谁看这份文档:
- 做 agent 应用的开发者——这是个能直接抄的优秀范式
- Claude Code 重度用户(每天用、长会话、按 token 付费)——读完能省钱
- 对「为什么 AI 工具这样设计」好奇的人
不适合:
- 偶尔用一下的人——感受不到这层设计的重量,看了等于白看
你下次该做什么
下次 Claude Code 突然变慢或账单异常,记得先看一眼 statusline 里的 cache_creation_input_tokens 和 cache_read_input_tokens——前者突然变大,说明你刚做了一件让前缀失效的事。
你在用 Claude Code 时,有没有遇到过「看起来像 bug,后来发现是设计」的行为?评论区聊聊。