Caching 解决什么问题
典型 RAG / 多轮对话场景:
- system prompt(角色、规则)—— 每次都一样,几千 token
- 上传的文档 / 代码库 —— 会话内不变,几万 token
- 对话历史 —— 前 N 轮累计下来也不小
- 用户当前新问题 —— 很短,几十 token
没缓存:每次都付全额 input 价。开了缓存:前三项命中后只付 10% 价,第 4 项照常付全价。
价格模型
| 操作 | 价格 |
|---|---|
| 普通 input | 1x(基准价) |
| Cache Write(创建缓存) | 1.25x — 一次性写入费 |
| Cache Read(命中缓存) | 0.1x — 十分之一 |
| 1 小时 TTL Write | 2x — 想缓存久就加钱 |
只要同一缓存块被复用 3 次以上,整体成本就低于不缓存——所以命中率至关重要。
怎么打 cache_control
在 content block 上打标记,告诉 Anthropic "这里之前(包括这里)的内容可以缓存":
const msg = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: [ { type: "text", text: "你是金融数据分析师..." }, { type: "text", text: longCompanyKnowledge, // 10000 tokens cache_control: { type: "ephemeral" }, // ← 这里前面都缓存 }, ], messages: [ { role: "user", content: "这份财报的关键风险是什么?" }, ], }); console.log(msg.usage); // 第 1 次:{ // input_tokens: 20, // 没缓存的部分(user query) // cache_creation_input_tokens: 10050, // 写缓存 @ 1.25x // cache_read_input_tokens: 0 // } // 第 2 次(5 分钟内换个问题):{ // input_tokens: 22, // cache_creation_input_tokens: 0, // cache_read_input_tokens: 10050 // 命中 @ 0.1x // }
四个可缓存位置
system
system prompt 的 text block,最常用
tools
工具定义数组,打在最后一个工具上,前面所有工具全缓存
messages
对话中的某个 block,可用于锁定历史上下文
document / image
大型 PDF / 多张图,打 cache_control 后长上下文可复用
缓存 TTL
两档:
- 5 分钟 ephemeral(默认):每次命中都刷新 5 分钟 TTL —— 实际"活跃就不会消失"
- 1 小时 extended(
{"type": "ephemeral", "ttl": "1h"}):写入贵一倍(2x),但适合"1 小时内偶尔调一次"
// 1 小时缓存(贵但稳) cache_control: { type: "ephemeral", ttl: "1h" }
命中条件:精确字节匹配
最关键的规则
缓存命中靠 前缀精确匹配——从消息第一个 byte 开始,到 cache_control 打的点,必须完全相同。差一个空格就 miss。
缓存命中靠 前缀精确匹配——从消息第一个 byte 开始,到 cache_control 打的点,必须完全相同。差一个空格就 miss。
这意味着:
- system prompt 不要动态插入时间戳/随机 ID
- 工具列表顺序固定
- 知识文档不要每次重新拼接(字节不一样)
- user 消息尽量放在缓存断点之后
多层 breakpoints
最多打 4 个 cache_control 断点,用于不同更新频率的内容:
// 层 1:极稳定(公司知识,每周更新) { type: "text", text: companyDocs, cache_control: { ttl: "1h" } }, // 层 2:稳定(用户画像,每天更新) { type: "text", text: userProfile, cache_control: { type: "ephemeral" } }, // 层 3:近期对话(每轮追加) { type: "text", text: recentContext, cache_control: { type: "ephemeral" } }, // 层 4:当前问题(不缓存) { type: "text", text: currentQuestion },
层次清晰,每层独立命中——即使某层变了,前面的还能复用。
实战:RAG 场景
async function ragQuery(userQuestion: string, retrievedDocs: string[]) { // 把 RAG 检索到的 N 段原文拼接 const context = retrievedDocs.join("\n\n---\n\n"); return client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: [ { type: "text", text: "你是知识库助手。基于下面的资料回答,不要编造。" }, { type: "text", text: `<documents>\n${context}\n</documents>`, cache_control: { type: "ephemeral" }, }, ], messages: [{ role: "user", content: userQuestion }], }); }
同一组检索结果连续问 5 个问题,输入 cost 降到原来的 ~28%(5 次里 1 次 1.25x 写 + 4 次 0.1x 读)。
实战:长会话 Agent
// 对话累积时,周期性打 cache_control 锁定历史 const history: MessageParam[] = [...]; // 每 10 轮在最后一条 assistant 消息上打点 if (history.length % 20 === 0) { const last = history[history.length - 1]; if (Array.isArray(last.content)) { last.content[last.content.length - 1].cache_control = { type: "ephemeral" }; } }
实战:Tool-heavy Agent
tools: [
{ name: "search", description: "...", input_schema: {...} },
{ name: "fetch_url", description: "...", input_schema: {...} },
{ name: "query_db", description: "...", input_schema: {...} },
{
name: "email_send",
description: "...",
input_schema: {...},
cache_control: { type: "ephemeral" }, // 最后一个工具打点,前面所有工具一起缓存
},
],
10+ 工具的 agent 每次调用都带满工具定义,缓存命中后几乎不花 tool 定义的钱。
监控命中率
每次响应的 usage 都有三件套,积累起来:
function logCacheStats(usage: Anthropic.Usage) { const total = usage.input_tokens + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0); const hitRate = (usage.cache_read_input_tokens ?? 0) / total; console.log(`hit rate: ${(hitRate * 100).toFixed(1)}%`); }
生产系统应该把命中率作为 SLO 监控——命中率骤降通常意味着有人改了 system prompt。
MISS 调试清单
打了 cache_control 但 cache_read_input_tokens = 0?按顺序排查:
- 缓存已过期 —— 上次写入超过 5 分钟没读(或 1 小时 TTL 版本的 1 小时)
- 前缀差异 —— 某个字节不一样了(时间戳/随机值?)
- 缓存块太小 —— 最小 1024 tokens(Sonnet)或 2048(Haiku)才能缓存,小于不生效
- 模型变了 —— Sonnet 和 Haiku 的缓存不互通
- 参数变了 ——
temperature/top_p等只要变了,整条都 miss
最小缓存块
| 模型 | 最小缓存 tokens |
|---|---|
| Opus 4.x | 1024 |
| Sonnet 4.x | 1024 |
| Haiku 4.x | 2048 |
短 prompt(几百 token)缓存不了——其实本来也省不下多少钱。
什么时候不该用
- 一次性的 one-shot 调用 —— 写入成本 1.25x 收不回
- prompt 每次都在变(例如含用户 ID/时间戳)—— 永远 miss,白交钱
- 缓存块小于最小值 —— 不生效
- 模型经常切换 —— 每个模型独立缓存,切模型就重写
和 OpenAI 的 "Automatic" 缓存对比
| Anthropic | OpenAI | |
|---|---|---|
| 打点 | 显式 cache_control | 自动(>1024 tokens 的前缀) |
| 折扣 | 0.1x 读 | 0.5x 读 |
| 写入费 | 1.25x(额外 25%) | 无 |
| TTL | 5 分钟 / 1 小时可选 | ~10 分钟,不可控 |
| 控制 | 精细,多断点 | 粗放,黑盒 |
显式控制是 Anthropic 的优势——想缓存就缓存,折扣也更深。
本章小结
cache_control: { type: "ephemeral" }打在 block 上,前缀全部被缓存- 读命中 0.1x,写入 1.25x,最少重复 3 次就赚
- TTL 5 分钟(默认)/ 1 小时(加钱 2x 写)
- 最多 4 个断点,按更新频率分层
- 命中靠前缀精确匹配——避免 prompt 里出现时间戳等浮动值
- 用
usage.cache_read_input_tokens监控命中率