1. 从“重复计算”说起:为什么长会话必须把 Prompt Cache 当回事
如果你长期用 Claude Code 跑自动化任务或者做代码生成,一定会注意到一个现象:会话越聊越长,每次请求头里的 input_tokens 数量越来越夸张。刚开始可能只有几千,几轮之后轻松突破两三万,甚至跑到五六万。很多人的第一反应是“上下文窗口不够用”,其实窗口还远没触顶,真正让你心疼的是钱包和响应速度——因为你每次都在让模型把前面几十 K 的token重新算一遍。
这时候 Prompt Cache 就该登场了。简单说,Prompt Cache 是服务端对“已经算过的前缀”做的缓存:如果这次请求的文本前缀和上次完全一致,模型就不需要重新计算那部分注意力矩阵,直接读取缓存结果就行。Claude Code 这类 CLI 工具,每一次 API 调用几乎都把系统提示、项目记忆、工具定义、历史对话全部塞进请求里,其中能复用的静态内容占比极高,缓存做得好不好,直接决定你的使用成本和响应首字延迟。
这篇文章不讲那些“什么是缓存”的泛泛术语,而是从工程落地的角度,把 Prompt Cache 在 Claude Code 场景里的底层机制、设计取舍、验证方法和踩坑经验一次讲清楚。适合三类人看:用 Claude Code 做自动化流程的开发者、做 Agent 应用想要控制 API 成本的工程师、以及被 token 账单惊到想搞清楚到底花在哪了的用户。
1.1 每次请求都像把会议记录从头念一遍
假设你开一场长达两小时的项目会,每隔五分钟大家让你“复述一下到目前为止的完整结论”。正常人不会真把每一句话再念一遍,但 Claude Code 的底层模型做不到这种“记重点”的能力——它的无状态 API 设计决定了,每次请求都必须带上完整的上下文历史,模型才能知道你之前聊了什么。
于是多轮会话里,真正新增的内容往往只有最后几百个token,其余几万token全是重复携带的“历史包袱”。没有缓存的情况下,服务端针对这几万token重新做一遍全量注意力计算,耗费 GPU 算力、增加网络传输量、拉长首字生成时间,然后这笔开销原封不动地体现在你的 token 消耗里。
Prompt Cache 就是专门解决这个浪费的。它把“已经见过的前缀”在服务端暂存,下次遇到同样的前缀直接复用。用在 Claude Code 这种高频、长上下文交互场景,价值几乎是压倒性的:聊天历史越多,缓存收益越大。
1.2 KV Cache 和 Prompt Cache 不是一回事,但关系密切
很多人会把这两个概念搞混,实际它们处在不同层面。
KV Cache 是 Transformer 推理引擎内部的机制。模型生成第 N 个 token 时,需要让这个 token 和前面所有 token 做注意力计算。计算过程中每个历史 token 会产出 Key 和 Value 向量,这些向量被暂存下来,后续每个新 token 生成时直接复用,不用重新计算前面的 K、V。这是单次推理内部的缓存,属于“生成阶段加速”。
Prompt Cache 则是服务端对外提供的跨请求缓存机制。它复用的底层资源仍然是 KV Cache,但粒度更大——不是管单个 token,而是管“一段文本前缀”。如果两个请求的开头若干 token 完全一致,后面就可以跳过这些 token 的 K、V 计算,直接沿用缓存内容。API 层通常会通过 cache_read_input_tokens 和 cache_creation_input_tokens 这两个字段把这个过程暴露出来。
理解这两层关系有个现实的用处:Prompt Cache 可以帮你省掉“重复计算前缀”的开销,但不能省掉“扫描缓存本身”的开销。如果请求前缀频繁变化,缓存会频繁失效,此时反而不如老老实实缩短上下文更划算。
2. 缓存命中到底靠什么:前缀匹配、断点和 TTL 的玩法
知道了缓存有大用,接下来必须搞清楚它命中条件有多苛刻。Prompt Cache 不是“按内容模糊识别常见的段落”,而是严格的文本前缀匹配——注意,是从请求的第一个 token 开始,逐 token 连续匹配,任何一个地方不一致,后面全部失效。这个特性决定了你在组织 prompt 时的所有工程策略。
2.1 为什么说“从第一个 token 开始”是致命的约束
先看一个坏例子。假设你的请求结构是:
系统提示 + 项目规则 + 当前日期 + 用户问题其中“当前日期”每天或者每次请求都在变化。那么问题来了:日期这个动态 token 之前的部分可以命中,但它之后的“项目规则”和“用户问题”虽然完全一样,也不能命中——因为前缀匹配必须连续,中间断了一节,后续整段就没了。
这个场景在 Claude Code 里特别容易出现。很多人习惯在项目记忆文件或者提示词开头写一句“今天是星期几、现在是几点”,看起来无伤大雅,实际上足以让整段缓存失效。正确的做法是把这种动态信息挪到对话末尾,或者干脆不要放在模型上下文的关键路径上。
Claude Code 的请求结构里,系统提示和项目记忆通常位于最前面,接着是工具定义、历史对话,最后才是你当前这轮输入。其中改动频率最低的是系统提示和工具定义,改动频率最高的是历史对话中新增的那一小段。为了让前缀尽可能稳定,工程上要做的事情本质上只有一件:把稳定内容往前放,把易变内容往后放,并且保证稳定内容中间不被插入任何变量。
2.2 cache_control 断点到底是干什么的
Prompt Cache 虽然在服务端自动启用,但它还提供了一个显式控制机制叫 cache_control,用来标记“在这个位置创建缓存断点”。理解断点的最佳方式是类比切蛋糕:整段上下文是一块大蛋糕,如果你只在开头和结尾做了断点,中间全部切成一块,那么只要中间任何一个位置发生变化,整块缓存都作废;如果你在多个固定位置打上断点,比如系统提示后一个、工具定义后一个、大段文件内容后一个,那么只有发生变化的那一段需要重新计算,其余段仍能命中。
在 Claude Code 内部,系统提示、CLAUDE.md 记忆、工具定义这些大块静态内容非常适合作为断点位置。但普通用户通常不会直接操作底层请求,所以你能间接控制的,是保持这些静态块内容稳定、顺序不变。如果你通过某种方式往系统提示中间动态塞数据,相当于在蛋糕中间乱切一刀,断点布局全乱。
如果你直接在 SDK 层面调用 API,常见的显式断点写法大致是这样:
from anthropic import Anthropic client = Anthropic() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2048, system="你是一个严谨的代码审查助手。", messages=[ { "role": "user", "content": [ { "type": "text", "text": "这里是完整的项目规范和工具说明……(较长且稳定)", "cache_control": {"type": "ephemeral"} } ] }, { "role": "user", "content": "请基于上述规范,审查下面这段代码:\n```python\n...\n```" } ] ) print(response.usage)把大段静态文本放在第一条 user 消息里并打上断点,之后的所有动态内容作为后续消息追加,这样静态前缀就能持续命中。写缓存断点有几个原则:每个断点之后的文本要尽量稳定,断点本身不要过多(策略上一般是 1 到 4 个),以及不要把太小的片段单独设断点,否则管理成本和收益不成比例。
2.3 TTL:缓存不是永久的,超时就得重来
Prompt Cache 有一个时间属性叫 TTL(生存时间)。通俗讲,服务端不会把你的缓存永久保存,而是在一段时间不读取后自动清掉。不同配置下 TTL 不同,常见的机制是短 TTL(例如 5 分钟)和长 TTL(例如 1 小时)。关键点是:每次命中读取通常会刷新 TTL 窗口,但如果你停顿超过窗口时间,缓存就失效,下一次请求会重新产生 cache_creation 开销。
这对 Claude Code 的实际影响非常明显:在一个高频交互的会话里,每条消息间隔通常只有几十秒,缓存可以无缝保持;但如果聊到一半你去干了别的事,过了十几分钟再回来继续,第一轮请求很可能就是“全量重新计算”,你会看到那一轮的 token 消耗明显增大、响应变慢。这属于正常现象,不是 bug。
顺带提一个很多人忽略的点:TTL 窗口内多次读取缓存,服务端仍然会计费和读取缓存相关的 token 用量,但计费单价远低于全量重新计算。所以长会话里偶尔一两轮 miss 没关系,整体命中率只要保持在较高水平,成本优化效果就很明显。
3. Claude Code 里的缓存工程:从上下文组装到实测验证
前面讲的都是机制层面的东西,这一节落到 Claude Code 这个具体工具上。它作为一款命令行编程助手,在组装上下文时有自己的顺序和逻辑,理解这个顺序,你才能设计出“缓存友好”的会话结构。
3.1 Claude Code 的上下文组成结构
Claude Code 每次调用模型时,上下文的组装大体遵循这样一个顺序:
- 系统提示:包含 Claude Code 自身的行为规则、工具说明、使用边界。
- 项目记忆:来自项目级和用户级的 CLAUDE.md 文件。
- 自动加载的上下文:比如你当前打开的文件片段、最近的命令输出。
- 工具定义:Claude Code 暴露给模型的一整套工具的描述信息。
- 对话历史:包括之前的用户请求、助手回复、工具调用结果。
- 当前输入:你这一轮真正的问题或指令。
这个顺序对缓存有一个关键启示:前面的部分天然是稳定前缀,后面的部分天然是易变后缀。你越能做到“前面几乎不变”,缓存命中率就越高。反过来,如果你在 CLAUDE.md 里频繁写入会变的临时状态,等于主动破坏自己的前缀稳定性。
3.2 CLAUDE.md 的写法直接影响缓存,别把它当便签
Claude Code 的 CLAUDE.md 是项目记忆的核心载体,我见过最多的误用就是把它当成一个“随手记录当前任务进度”的便签。今天加一行“正在修复登录模块”,明天删掉换成“正在优化性能”,整个文件内容一直在变。
问题在于 CLAUDE.md 位于请求前缀的头部区域。它一变,后面系统提示之后的所有 token 都跟着失效。一次看似无害的修改,可能让接下来几十轮请求的缓存命中率从 70% 直接掉到 5% 左右。
正确的做法是把 CLAUDE.md 当作“稳定的项目宪法”来维护:放技术栈说明、代码风格规范、常用命令、架构决策、目录结构约定,这些内容一次写好,尽量少改。至于当前任务进度、临时的调试信息、待办事项,应该通过对话输入或者临时文件传给 Claude Code,而不是写进记忆文件。
我在实际使用中还会定期做一次整理:如果发现某条 CLAUDE.md 里的规范已经被后续内容覆盖,我会及时删掉旧条目,因为冗余的规则不仅浪费上下文空间,还会让系统在这个位置的注意力被无关信息稀释,间接加剧后文要说的迷失问题。
3.3 实测:怎么验证缓存是否真的命中
不能光凭感觉说“缓存生效了”。如果你直接用官方 SDK 调 API,每次返回的响应里会带 usage 对象,其中有几个字段值得关注:
- cache_creation_input_tokens:本轮新写入缓存的 token 数量。
- cache_read_input_tokens:本轮从缓存中读取的 token 数量。
- input_tokens:本轮实际计费的非缓存输入 token 数量。
判断命中率的公式很简单:
缓存命中率 = cache_read_input_tokens / (cache_read_input_tokens + cache_creation_input_tokens + input_tokens)在 Claude Code 场景里,如果你开启 verbose 模式或者 debug 日志,也能看到类似的请求和响应明细。我在调优时一般会连续跑三轮同样的任务,对比第二轮的 cache_read 和第一轮的 cache_creation,如果第二轮 read 接近第一轮 creation 的总量,说明前缀很稳定;如果第二轮仍然大量 creation,说明前缀里有某个变量在捣乱。
另外一个实操技巧:中途故意停顿超过 TTL 窗口后再发一条消息,如果那一轮 cache_read 接近 0、cache_creation 显著上升,说明 TTL 机制按预期工作。这样既能验证缓存,也能给你的工作习惯提个醒——长间隔后的第一轮请求成本会更高,不属于异常。
4. 长上下文的另一面:Lost in the Middle 与缓存设计的冲突
聊缓存聊到兴头上,很容易忽略一个更隐蔽的问题:当上下文很长时,模型对“夹在中间”的信息关注度会明显下降。这就是常说的大模型“中间迷失”现象。它和 Prompt Cache 并不直接冲突,但在工程实践中经常会产生拉扯。
4.1 为什么模型会迷失在中间
Transformer 的自注意力机制让序列里所有 token 两两之间都能计算关联,但长序列下注意力分布并不是均匀的。大量实验和线上观测都显示,模型对上下文开头和结尾的信息利用得更好,对中间部分则容易“一带而过”。这有点像主持人给你念了一长串任务清单,你往往只记得最前面的几条和最后强调的几条,中间的细节迷迷糊糊。
在 Claude Code 里,历史对话、大段代码文件、工具输出通常都排在中间位置。如果某个关键约束条件恰好被夹在两段巨长的文件内容中间,模型很可能会忽略它,造成“明明写了规则却总是绕开”的现象。这不是缓存导致的问题,但缓存设计如果不考虑它,会把问题放大。
4.2 缓存命中和信息位置经常互相打架
最典型的冲突场景是:为了缓存,你想把大段静态内容往前放;但为了模型注意力,你又希望关键指令靠近末尾。这两件事在某些情况下会矛盾。例如你想让 Claude Code 始终遵守一条安全准则,理想做法是放在系统提示开头(缓存友好),但开头如果被一大段冗长的工具说明占据,这条准则反而容易被稀释;放在对话末尾则更醒目,但末尾是动态区,无法长期作为稳定前缀缓存。
这个矛盾没有绝对解,只能做工程权衡。我的经验是分层处理:最重要、最高优先级的规则放在系统提示或 CLAUDE.md 最显眼的位置,哪怕牺牲一点注意力也要保证它稳定存在;而那些针对单轮任务的临时指令,放在当前用户消息的末尾,让模型紧盯着最近的指令执行。这样既保证了高频规则的缓存稳定性,又利用“结尾优势”来引导短期行为。
4.3 自动压缩历史:缓存必须面对的代价
Claude Code 在上下文接近窗口上限时,会自动把之前的对话做摘要压缩。这个功能很实用,但它有一个容易被忽略的副作用:压缩后的历史文本内容和原始文本不一样,因此以前缀为基础的缓存会大幅失效。压缩那一轮以及接下来几轮,你会看到明显的 cache_creation 上涨。
这不是说自动压缩不好,而是提醒你它在“节省窗口空间”和“牺牲缓存命中”之间做了交换。如果你发现自己频繁触发自动压缩,并且整体缓存命中率偏低,可以考虑两个方向:一是精简 CLAUDE.md 和自动加载的文件内容,减少无谓的上下文占用;二是对工具输出做摘要而不是把原始输出全部塞进历史。前者省窗口,后者也能间接省缓存空间。
5. 模拟项目:某跨平台代码生成工具的缓存调优实录
这里的案例来自一个模拟的自研项目,就叫它“某跨平台代码生成工具”。这套工具基于 Claude Code 封装了一个自动化流程,每次运行需要加载约 2 万到 3 万 token 的规范文件、教程片段和代码示例,然后连续执行十几轮生成、校验、修正的循环。最初我对缓存没有概念,结果就是每一轮都在全量计算,费用高得离谱,响应也慢。
5.1 第一次调优:把会变的内容从记忆文件里挪走
最初,CLAUDE.md 里混合了大量“本次任务进度”信息,比如“当前已经生成三个模块,下一步处理订单模块”。这些内容每执行一轮就变一次,导致整个前缀反复失效。某次我抓了一下 usage,发现 cache_read 一直很低,绝大多数 token 都在走 cache_creation。
我做的第一个改动就是强制规定:CLAUDE.md 只写与代码生成规范、输出格式、命名约定、常用框架相关的静态内容;所有任务进度通过命令参数传给脚本,由脚本拼到每轮对话的最新用户消息里。改完之后,系统提示和 CLAUDE.md 那一大段基本稳定了,后续轮次的 cache_read 立刻上来。
第一次优化后的效果:同样一套规范,优化前每轮请求的 cache_creation 高达 2 万 token,优化后降到了几千,cache_read 占比从不到 10% 提升到了 60% 左右。但这时候离理想状态还差很远。
5.2 第二次调优:大文件加载顺序和段落组织
问题出在“教程片段”和“代码示例”这些大文件上。最开始我把所有规范文件拼成一个大字符串,统一放在系统提示之后。表面上它们内容稳定,但因为文件太多、太长,中间某个小章节经常需要调整措辞,一调整,后面所有内容全部 miss。更麻烦的是,这个拼出来的大字符串每次启动时还会因为文件路径顺序的随机变化而重新排序,等于自己制造了不必要的变动。
我做的第二个改动有两部分:第一,固定文件加载顺序,按“框架说明 -> 组件规范 -> 模板示例 -> 可复用组件”的顺序拼接,不允许随机排列;第二,把超长的大文件拆成多段,在关键段落位置打上断点,这样某一段被修改时,只有它和它之后的段需要重算,前面的段仍然命中。
这次调优之后,整个流程的 cache_read 占比稳定在 85% 到 90% 之间。最直观的感受是每轮请求的首字响应速度明显变快,而且账单数字降了一大截。
5.3 第三个隐患:工具输出和对话历史的膨胀
最后一类问题是工具输出。这个工具脚本每轮都会执行一些本地命令,然后把原始输出交给模型判断。最开始我把每次完整的命令输出都塞进对话历史,几轮下来历史变得又长又杂。这里出现了双输:历史越长,上下文窗口越紧张,同时历史文本一旦变化,前缀匹配能覆盖的范围就越小。
我的解决方案是加一个摘要步骤:每次工具执行后,先让一个小模型或规则引擎把输出压缩成几十个 token 的关键结论,再把结论写入历史。这样既保留了需要的信息,又大幅减少了动态 token 的数量。做完这一步,同一轮循环里后几条消息的 cache_read 又往上提了一截,因为动态部分变小了,稳定前缀能覆盖更多轮次。
5.4 调优结果对照
整理一下这个模拟项目优化前后的数据,虽然具体数字因项目而异,但量级值得参考:
| 指标 | 优化前 | 第一次优化后 | 第二次优化后 | 第三次优化后 |
|---|---|---|---|---|
| 单轮平均输入 token | 约 32000 | 约 30000 | 约 28000 | 约 22000 |
| cache_read 占比 | <10% | 约 60% | 约 85% | 约 90% |
| cache_creation 平均 token | 约 28000 | 约 10000 | 约 4000 | 约 2000 |
| 平均首字响应速度 | 慢,体感明显 | 有所改善 | 明显加快 | 流畅 |
这个项目让我彻底意识到,Prompt Cache 不只是一个“服务端自动帮忙”的功能,它更像一种 prompt 设计方法论:稳定前缀是资产,动态内容是负债,每一轮会话你都在做资产和负债的权衡。
6. 常见问题定位与排查速查
调优过程中会遇到各种奇奇怪怪的现象,这里整理一个常见问题速查表,都是我实际踩过或者帮别人排查过的类型。
| 现象 | 可能原因 | 排查方向与解决建议 |
|---|---|---|
| 缓存命中率一直是 0 | 前缀里存在高频变化的变量,例如时间戳、随机 ID、临时路径 | 检查请求开头是否有动态内容,把稳定段和动态段彻底分离 |
| 命中率突然从 80% 降到 10% | 固定前缀区某处被修改,比如 CLAUDE.md 新增了一条临时任务记录 | 查看近期是否有文件或配置改动,回滚或把临时记录移到对话尾部 |
| 上下文自动压缩后立刻变慢 | 压缩改变了历史文本,缓存前缀失效 | 属正常现象;优化方向是减少无谓上下文占用,降低压缩触发频率 |
| 停顿几分钟后第一轮请求特别贵 | TTL 窗口已过,缓存被清空 | 属正常机制;如果高频使用,尽量保持交互间隔,或在代码里预判处理 |
| 显式设置 cache_control 后没效果 | 断点位置选在了会变的内容后面 | 断点的价值在于隔离静态段,把断点放在大段静态内容的末尾,而不是动态内容中间 |
| 工具名称或描述频繁变化 | 动态注册工具导致工具定义区不稳定 | 固定工具列表和描述文案,工具的新增留在会话较后阶段再暴露 |
排查缓存问题有一个通用的操作顺序:先抓 usage 数据,确认 cache_read 和 cache_creation 的比例;然后逐步缩小请求前缀,用二分思想定位“从哪个 token 开始不再命中”,一般很快就能找到动态内容的位置。再强调一次,中间插入哪怕一个 token 的变动,后面整段都会前功尽弃,所以“最小扰动原则”永远是第一位的。
7. 个人经验与最终建议
把这个项目调完以后,我最大的感受是:Prompt Cache 不是什么高深莫测的技术,但它的工程细节多到能让人栽无数跟头。下面几条是我自己的经验总结,不一定适用于所有场景,但多数情况下能帮你少走弯路。
第一,重视 CLAUDE.md 和其他记忆文件的稳定性。它是最容易被忽略的缓存杀手,也是最容易修复的收益来源。我每次写新的自动化项目,都会先花时间把记忆文件结构定好,锁定哪些内容属于静态规则、哪些内容走动态输入,避免中途反复改。
第二,用数据说话,别猜。如果你在 API 层面做了封装,尽量把 usage 字段记录到日志里,按轮次统计 cache_read 和 cache_creation。几次对比下来,你就能清楚知道自己改动的正负效果。Claude Code 使用者也可以开启详细日志,至少能判断哪一轮明显慢了。
第三,关键内容既要考虑缓存,也要考虑位置。缓存让你省钱,但如果不顾 Lost in the Middle 的问题,省下来的钱可能变成返工时间。高优先级规则放稳定前缀区,短期指令放对话末尾,长期在这种权衡中形成的习惯,才是真正的收益。
最后再说一个很多人没注意的小技巧:如果你经常启动很长的会话,可以刻意把“每次启动时固定不变的一大段内容”作为第一条消息发出去,而不是合并到系统提示里。这样一来,后续同一会话内的轮次都能命中这段内容,跨会话只要内容相同也能命中。在实际操作中,这个小习惯往往能把看似普通的流程一举带入缓存高命中区间。