关于Anthropic SDK设置缓存点
Anthropic 的 Prompt Caching 缓存的不是“回答”,也不是“聊天记忆”,而是重复输入前缀的中间计算结果。
所以即使开启缓存,你每一轮仍然要把历史消息重新发送给 API。只不过前面一大段如果与上次完全相同,模型不用重新计算,速度更快、输入费用更低;回答仍然会重新生成。Messages API 本身依旧是无状态的。(Claude Platform)
一、Anthropic 到底缓存了什么
假设每次请求的完整输入是:
(大部分是这样子的)
第一次:
tools + system + 用户消息1
第二次:
tools + system + 用户消息1 + 助手回复1 + 用户消息2
这种叫缓存前缀
1 | tools |
我们在某个位置放一个缓存断点:
1 | [ tools + system + 历史消息 ] ← 缓存前缀 |
第一次请求:
1 | 发现没有缓存 |
第二次请求:
1 | 发现前缀完全相同 |
这里缓存的是类似模型注意力计算产生的 KV Cache,不是把 Claude 上一次回答直接拿出来。因此即使输入完全相同,输出仍可能因为采样参数等因素发生变化。(Claude Platform)
第二次请求的开头大量内容与第一次相同。Anthropic 可以直接复用已经处理过的部分,只处理新增的后缀。
cache_control 就是在告诉 Anthropic:
请把从请求开头到这里为止的内容作为一个可复用前缀。
Anthropic 的缓存顺序是:
1 | tools → system → messages |
并且会查找之前请求中最长的相同前缀。
二、cache_control 有两种放法
现在最容易绕晕的地方来了:
自动缓存和显式缓存都使用
cache_control,区别只是它放在哪里。
1. 顶层 cache_control:自动缓存
1 | response = client.messages.create( |
这里的意思是:
请自动寻找当前请求中最后一个可以缓存的内容块,并把缓存断点放在那里。
它特别适合不断增长的多轮对话。(Claude Platform)
例如第一轮:
1 | system |
第二轮:
1 | system |
第三轮:
1 | system |
缓存断点会随着对话自动往后移动,所以你不需要每轮手动修改消息中的标记。
2. 内容块里的 cache_control:显式缓存
假设你有一份很长、长期不变的系统规则:
1 | system = [ |
然后正常请求:
1 | response = client.messages.create( |
这里有一个非常重要的细节:
1 | { |
不是只缓存 very_long_project_document 这一个块,而是缓存:
1 | 从整个请求开头 |
也就是缓存的是一个累积前缀:
1 | tools |
Anthropic 的前缀顺序固定为:
1 | tools → system → messages |
所以通常应把长期稳定的内容放前面,把每次变化的内容放后面。(Claude Platform)
三、自动缓存和显式缓存怎么选
| 场景 | 推荐方式 |
|---|---|
| 普通多轮聊天 | 顶层自动缓存 |
| 长 system prompt | 显式缓存 |
| 固定知识文档 + 不同问题 | 显式缓存 |
| 大量固定工具定义 | 显式缓存 |
| 对话历史不断增长 | 自动缓存 |
| 不同部分更新频率不同 | 多个显式断点 |
| 系统提示固定,同时还想缓存对话 | 显式 + 自动组合 |
还可以同时使用:
1 | response = client.messages.create( |
不过每次请求最多可以使用四个缓存断点,顶层自动缓存也会占用其中一个。(Claude Platform)
四、为什么叫 ephemeral
目前支持的缓存类型是:
1 | {"type": "ephemeral"} |
ephemeral 的意思是:
1 | 临时的、短暂存在的 |
默认有效期是五分钟:
1 | cache_control={ |
等价于:
1 | cache_control={ |
也可以设置为一小时:
1 | cache_control={ |
TTL 是从最近一次命中或写入附近开始维持的;在有效期内再次命中,会延长缓存的可用时间,而不需要重新支付缓存写入费用,但仍会按缓存读取 token 计费。(Claude Platform)
五、缓存为什么能省钱
Anthropic 当前的价格倍率大致是:
1 | 普通输入:1 倍 |
也就是第一次缓存稍贵,但之后读取特别便宜。(Claude Platform)
假设固定前缀有 10,000 tokens。
没有缓存,调用三次:
1 | 10,000 × 3 |
五分钟缓存:
1 | 第一次写入:10,000 × 1.25 |
调用次数越多,收益越明显。
因此它特别适合:
1 | 长系统提示 |
六、怎样判断到底有没有命中
返回值的 usage 中主要看三个字段:
1 | response.usage.input_tokens |
含义分别是:
1 | input_tokens |
总输入量是:
1 | total_input_tokens = ( |
官方 API 也是按照这三个字段之和表示完整输入 token。(Claude Platform)
第一次请求可能是:
1 | cache_creation_input_tokens = 5000 |
第二次相同前缀:
1 | cache_creation_input_tokens = 0 |
自动缓存的多轮聊天里,第二次通常可能同时出现:
1 | cache_read_input_tokens > 0 |
因为旧对话从缓存读取,而新增对话尾部又被写入新缓存。
七、流式请求怎么查看 usage
你的程序使用的是:
1 | with client.messages.stream(...) as stream: |
在真正的 Anthropic API 中,可以这样写:
1 | with client.messages.stream( |
get_final_message() 会返回 SDK 在流式过程中累计完成的最终 Message 对象,其中包含 usage。(GitHub)
八、缓存命中的严格条件
1. 前缀必须完全一致
这些区别都可能破坏缓存:
1 | 多一个空格 |
缓存命中要求断点之前的内容完全一致。输出不会被缓存,只会重新生成。(Claude Platform)
例如这种设计不太好:
1 | system = f""" |
因为每次请求时间都不同:
1 | 第一次:当前时间 21:30:01 |
整个后续前缀都会变化。
应改成:
1 | system = [ |
这样长文档留在稳定前缀里,动态时间放在缓存断点后面。
2. 内容太短可能不会缓存
不同模型都有最小可缓存 token 数量。
如果没达到阈值,API 通常不会报错,而是静默地不缓存:
1 | cache_creation_input_tokens = 0 |
因此不要拿一句:
1 | 你好呀 |
测试缓存,然后疑惑为什么始终没有命中,小笨瓜会被缓存机制气鼓鼓的。应当用足够长的 system prompt、文档或对话测试。(Claude Platform)
3. 第一个请求必须开始返回后,缓存才可用
缓存不是在请求刚发送出去时就立即可用,而是在第一个响应开始后才可以被后续请求命中。
因此同时并发发送十个完全相同的新请求:
1 | await asyncio.gather(*tasks) |
不一定都能命中第一个请求建立的缓存。需要先让“预热请求”开始响应或完成,再发后续并发请求。(Claude Platform)
4. 有一个 20 内容块回溯窗口
Anthropic 会从缓存断点向前寻找以前写入过的缓存,但最多向前检查 20 个内容块。
普通聊天每轮增加两三个块通常没问题;如果一次 Agent 循环加入大量工具调用和工具结果,旧断点可能超出回溯范围,这时可以增加显式缓存断点。(Claude Platform)
这个属于进阶优化,现在知道有这么个机制就够啦。
九、最重要:你当前程序用的其实不是 Anthropic 后端
你文件里配置的是:
1 | BASE_URL = "https://api.deepseek.com/anthropic" |
所以你只是使用 Anthropic Python SDK 和 Anthropic 请求格式,实际请求发给的是 DeepSeek。
DeepSeek 官方兼容性文档明确写了:消息块、工具定义、工具结果里的 Anthropic cache_control 会被忽略。(DeepSeek API Docs)
DeepSeek 使用的是自己的自动上下文缓存:
1 | 默认自动开启 |
它同样要求前缀重复,而且只缓存输入计算,回答依然重新生成。(DeepSeek API Docs)
所以要把两种机制分开:
1 | 真正的 Anthropic API |
也就是说,在你当前这份聊天程序里:
1 | messages.append(...) |
每轮保持原历史消息完全不变,只在结尾追加新消息,本身就很适合 DeepSeek 的自动前缀缓存。你不需要给每个消息手动塞 cache_control。
真正理解成一句话就是:
自动缓存帮你决定“断点放哪里”;块级
cache_control让你自己决定“稳定前缀到哪里结束”。而你目前使用的 DeepSeek 后端,又是另一套默认自动缓存机制。
例子
假设用户要求:“读取 app.py,告诉我入口在哪里。”
第一次请求大概是这样:
1 | { |
这里写入了三个缓存点:
1 | 工具定义末尾 |
Claude 判断需要读取文件,返回:
1 | { |
MewCode 执行 ReadFile,拿到文件内容。此时会发起第二次请求。
第二次请求
1 | { |
注意:每次请求都会携带完整对话历史,不是只发送新增的工具结果。
但第二次请求的前半部分和第一次相同:
1 | 相同 tools |
因此 Anthropic 可以复用缓存,只处理新增部分:
1 | assistant 的 tool_use |
然后 Claude 返回最终答案:
1 | { |
如果用户继续问“那 Agent 在哪里创建?”,第三次请求还会携带前面的全部历史:
1 | tools |
所以完整对话会越来越长,而 Prompt Cache 的作用就是复用前面没有变化的部分。ConversationManager 负责保存这份完整历史,serialization.py 把它转换成上面的 Anthropic 格式。
如果设置三个缓存点
用户消息末尾的缓存点包含:
1 | tools + system + messages |
三个缓存点实际上是逐层扩大的:
1 | 缓存点 1:tools |
Anthropic 会优先寻找“最长的可命中前缀”。前面的缓存点主要充当后备。
例如完整缓存没有命中:
1 | tools 相同 |
此时:
tools + system + messages未命中tools + system仍然可以命中
再例如 system prompt 变化:
1 | tools 相同 |
此时还能命中最前面的 tools 缓存。
可以把它理解成三个存档点:
1 | tools ──●──────── system ──●──────── messages ──● |
| 发生的变化 | 最长可能命中 |
|---|---|
| 只新增消息 | 点3中的旧消息前缀 |
| 消息历史被压缩/改写 | 点2:tools + system |
| system prompt 改变 | 点1:tools |
| 工具定义改变 | 全部失效 |
所以你说得对:在一个非常简单、历史永远只追加且不超过回看范围的聊天程序中,只标记最后一条 user 消息通常就够了。
MewCode 设置三个缓存点,是因为它还会:
- 压缩或重写对话历史
- 截断工具结果
- 动态发现、启用工具
- 改变 Plan/Coordinator 等提示内容
多个缓存点不是为了重复缓存三遍,而是为了在后半段变化时,尽可能保住前面仍然稳定的部分。最终计费和复用通常以“最长命中的那个前缀”为主。


