Skip to content
wangx
Go back

Claude Code 的 prompt caching 设计

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.md 写新规则;右:Claude Code 无视规则用错语法

一条简单规则,推导出全部行为

Claude Code 的每次请求,本质是把过去所有上下文连成一长串发给模型,新内容附在末尾。

API 缓存的方式是前缀匹配——这次请求的开头和最近一次缓存内容比对,匹配多少就跳过多少计算。

匹配是精确的:

前缀里任何位置变了一个字符,后面全部要重算。

为了让前缀尽可能稳定,Claude Code 把请求分成三层,稳定的在前,易变的在后:

内容什么时候变
系统提示核心指令、工具定义、输出样式MCP 接入/断开、Claude Code 升级
项目上下文CLAUDE.md、自动内存、规则会话开始、/clear/compact
对话你的消息、Claude 的回复、工具结果每个回合

这张表第三列没有「中期编辑 CLAUDE.md」——因为它根本不触发重新加载。

这就是钩子段那个谜的答案:项目上下文层只在会话开始时构建一次。

不构建,缓存就不会失效;不失效,你的修改就也不被读到。一体两面。

四个连续请求:前三个回合命中缓存前缀,第四个回合系统提示变化导致完全 cache miss

设计哲学:宁可让你的修改暂时失效,也要保住前缀

Claude Code 的工程师不是没想到你会想中期改 CLAUDE.md。

他们的选择是:不做这个功能。

如果允许中期改 CLAUDE.md 生效,意味着项目上下文层会在会话中变,意味着前缀失效,意味着下一回合的 token 全部按未缓存价格重新计费。

对一个动辄几万 token 项目上下文的开发者,一次失效的代价可能是几十次正常回合的总和。

所以设计选择是:让你的修改暂时失效,但缓存活着。想生效的代价是显式的 /clear 或重启——你知道自己在付什么代价。

同样的逻辑解释一堆「奇怪」行为:

这套设计背后的产品决策标准是:

「缓存安全」优先于「即时反映用户操作」。

如果你在做自己的 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 是个布满陷阱的东西。

翻到文档另一面会发现——同样一条「前缀匹配」规则,也推导出一整套相反的事实:

整套行为只用一条规则就能推导:

改的位置离对话末尾越近,代价越小。

没有特例,没有「除了 X 之外」。

这是设计上最舒服的地方——你不用记一长串规则,只要记住「前缀匹配,精确,从头算」这一句话,所有现象都讲得通。

我看过不少 LLM 应用做缓存,大多数是把它当个 feature 加在某个角落,缓存命中是 bonus、不命中是默认。

Claude Code 反过来:

缓存命中是默认,失效是要付出代价的偏离。

这是产品哲学上的层级差异。

我的评价:借鉴 + 不喜欢

值得借鉴的:

不喜欢的地方,也得说:

心智负担其实不小。 文档讲得清楚,但大多数用户不会去翻。结果是 Claude Code 的随手用户,踩坑了也不知道为什么贵。

opusplan 的隐性成本文档没强调够。 它被推荐给 Plan Mode 重度用户,但每次进出 Plan Mode 都付一次切换税——这点应该在 /model opusplan 选项里就提示。

worktree 不共享缓存对并行开发不友好。 同一个仓库开三个 worktree 干三件事,缓存完全独立。文档承认这点,但没给解决方案。

MCP 自动重连的不可见性令人不安。 你在做正事,缓存在后台被悄悄炸,statusline 不点开你不知道。

适合谁看这份文档:

不适合:

你下次该做什么

下次 Claude Code 突然变慢或账单异常,记得先看一眼 statusline 里的 cache_creation_input_tokenscache_read_input_tokens——前者突然变大,说明你刚做了一件让前缀失效的事。

你在用 Claude Code 时,有没有遇到过「看起来像 bug,后来发现是设计」的行为?评论区聊聊。


Share this post on:

Previous Post
同时开三个终端跑三个模型,Claude Code 也能做到
Next Post
anthropic-agent-teams