Prompt Caching 省钱原理与实践:让重复内容少算一次钱
如果你的每次 API 调用都带着同一段几百字的系统提示,你正在重复为同样的内容付费——Prompt Caching 就是为解决这个问题而生的。命中缓存后,重复前缀的计费成本可降至原价的 10%–25%,是所有省钱手段里单次效果最显著的一个。
Prompt Caching 是什么
Prompt Caching(提示词缓存)是部分大模型 API 提供的机制:当你的输入前缀与之前某次调用完全一致时,模型跳过对该前缀的重新处理,直接复用缓存的中间状态,并对命中部分收取更低的”缓存读取价”。
本质上节省的是: KV Cache 的重新计算开销。重复前缀的注意力计算结果被保存下来复用。
声明:以下节省幅度描述为截至 2026-06 的定性判断,仅供参考,以各厂商官方文档为准。
缓存到底缓存了什么:一次 attention 计算的复用
想搞懂”为什么改一个字符缓存就失效”,得往下看一层。Transformer 处理输入时,每个 token 的隐藏状态(hidden state)不是孤立算出来的,而是要对它前面所有 token 做一次 self-attention——也就是这个 token 要”看一眼”前面每个词,算出注意力权重再加权求和。这一步叫 KV(Key/Value)计算,是推理里最耗时的部分之一。
如果你这次调用的前缀和上次逐字节完全相同,模型服务端可以直接把上次算好的 KV 状态原样搬过来用,不用重新做那一整套矩阵运算。这就是 Prompt Caching 省下来的东西:不是”少读了几个字”,而是”少做了一整段前向计算”。
这也解释了两个反直觉的现象:
- 为什么改一个空格都会失效:哪怕只多了一个空格,从那个空格开始往后所有 token 的隐藏状态都要重新算一遍,因为每个 token 的 attention 依赖的是它前面的完整序列,不是单独存在的。
- 为什么可变内容必须放最后:如果你把用户输入插在系统提示中间,系统提示后半段所有 token 的隐藏状态都要跟着用户输入重算,等于把整个后半段缓存作废。只有把可变内容放在最末尾,才能保证前面那一大段的 KV 状态不受影响,被完整复用。
命中缓存的条件
不同厂商实现细节有差异,但以下规则普遍适用:
| 条件 | 说明 |
|---|---|
| 前缀完全一致 | 缓存基于输入前缀的精确匹配,改动一个字符即失效 |
| 最小长度阈值 | 通常需要前缀达到一定 token 数(如 1,024 token)才会被缓存 |
| 缓存有效期 | 缓存通常存活几分钟到数小时,超时需重建 |
| 前缀在前 | 可变部分(用户输入)必须放在固定前缀之后,不能插入中间 |
最重要的工程实践:把所有固定内容(system prompt、文档、示例)放到输入的最前面,把可变的用户消息放到最后。
各类场景节省幅度对比
| 场景 | 固定前缀占比 | 预期节省幅度 |
|---|---|---|
| 客服 Bot(长系统提示) | 60–80% | 显著,每次调用节省大半输入成本 |
| 文档问答(文档作为上下文) | 80–95% | 极高,文档不变时接近免费 |
| 代码补全(固定代码库片段) | 50–70% | 较高 |
| 纯聊天(无固定前缀) | <10% | 几乎无收益 |
如何在代码中启用
Anthropic Claude(显式标记)
response = client.messages.create(
model="claude-opus-4-5",
system=[{
"type": "text",
"text": "你是一个专业助手...", # 长系统提示
"cache_control": {"type": "ephemeral"} # 标记为缓存断点
}],
messages=[{"role": "user", "content": user_input}]
)
OpenAI(自动缓存)
OpenAI 对满足长度条件的前缀自动缓存,无需显式标记。调用返回的 usage 字段中会包含 cached_tokens 数量,可用于监控命中率。
进阶:多断点分层缓存
真实项目里前缀往往不是”一整块系统提示”这么简单,而是好几层,更新频率还不一样:工具定义(tools)几乎不变、长文档(如知识库片段)按天更新、对话历史每轮都在变。Claude 允许在一次调用里打多个 cache_control 断点(上限因模型而异,以官方文档为准),让不同更新频率的部分分别缓存:
response = client.messages.create(
model="claude-opus-4-5",
tools=[
{**tool_definition, "cache_control": {"type": "ephemeral"}} # 断点1:工具定义,几乎不变
],
system=[
{"type": "text", "text": long_system_prompt},
{"type": "text", "text": knowledge_doc, "cache_control": {"type": "ephemeral"}} # 断点2:知识库文档,按天更新
],
messages=[
*history, # 断点3可以打在历史消息的最后一条上,缓存到目前为止的对话
{"role": "user", "content": user_input}
]
)
每个断点标记的是”从上一个断点到这里为止”的一段内容,一旦命中,这一段就不用重新计算。分层的意义在于:知识库文档变了,也不会连累工具定义那段缓存失效;对话历史每轮增长,前面几轮依然可以复用。
真实踩坑记录
以下是接入时容易踩的坑,附具体现象和根因,照着排查能省不少时间:
- 现象:Claude 报
BadRequestError: cache_control is not supported for this content block type。根因:system参数传的是纯字符串而不是数组形式,cache_control只能挂在数组里的 block 对象上。修法:把system="你是..."改成system=[{"type": "text", "text": "你是...", "cache_control": {...}}]。 - 现象:
usage.cached_tokens或usage.cache_read_input_tokens一直是 0,明明系统提示写得很长。根因:三种可能——① 前缀没达到最小 token 阈值;② 两次调用间隔超过了缓存有效期,缓存已经过期被清掉;③ 高并发下缓存池容量有限,不是你独占,可能被其他请求挤出去了。排查顺序:先打印实际前缀 token 数确认过阈值,再检查两次调用的时间间隔,最后看是不是自己的 QPS 太低导致缓存总是”刚建好就凉”。 - 现象:多轮对话进行到第 5、6 轮时,缓存命中的 token 占比反而比第 2、3 轮低。根因:很多人会在每轮开始前对历史做”摘要压缩”或者在消息数组最前面插入当前时间戳之类的动态内容,这一改,前面已经缓存的那部分内容就跟上次不完全一致了,等于从头开始重新计费。修法:动态内容(时间、请求 ID)不要拼进会被缓存的前缀里,摘要压缩只对更早的历史做,不要碰最近几轮已经建立缓存的部分。
- 现象:流式(
stream=True)调用时看不到cached_tokens。根因:流式响应的 usage 信息通常只在最后一个 chunk(如 OpenAI 的stream_options={"include_usage": True})或消息结束事件里返回,不要在流式过程中的中间 chunk 里找这个字段。
值不值得为它改造代码
接入 Prompt Caching 不是免费的,你要改调用代码、要梳理清楚前缀里哪些是真固定的,还要在监控里加一个命中率指标。值不值得做,可以按这个表来判断:
| 场景特征 | 建议 |
|---|---|
| system prompt 超过 1,000 token,且调用频率高(分钟级以上) | 值得,收益明显大于改造成本 |
| system prompt 很短(几十字),或者调用频率极低(几小时一次) | 不值得,省下的钱可能还不够抵消开发时间 |
| 多租户场景,每个用户的固定前缀不一样 | 值得但要按用户分别设计前缀结构,不能一刀切 |
| 前缀里混杂大量动态字段(用户名、时间戳) | 先做前缀重构(把动态字段挪到最后),否则接入了也白接 |
简单成本估算
拿一个典型客服 Bot 场景举例:system prompt 2,000 token,用户输入平均 200 token,一天调用 10,000 次。不接入缓存时,这 10,000 次调用里 system prompt 部分要按原价全额计费 10,000 次。接入缓存后,假设命中率能做到 90%(第一次建立缓存正常计费,之后在有效期内持续命中),按前面提到的缓存读取价降至原价 10%–25% 估算,这 2,000 token 的固定部分,一天里绝大多数调用只花原来一到两成半的钱。用户输入那 200 token 因为每次都不同,该多少还是多少,不受影响。真实节省幅度以你账单上的 cached_tokens 统计和当前定价页为准,这里给的是估算思路,不是承诺数字。
上线前自查清单
把下面几步走一遍,能避免接入了却没生效的尴尬:
- 打印一次实际前缀的 token 数,确认它超过了模型的最小缓存阈值,不确定就查当前的官方文档。
- 连续打两次一模一样的请求,看第二次响应里的
cached_tokens(OpenAI)或cache_read_input_tokens(Anthropic)是不是大于 0——大于 0 说明命中了,一直是 0 就回头对照上面的踩坑记录排查。 - 检查前缀里有没有混入动态字段:grep 一下你拼接 system prompt 的代码,看有没有时间戳、请求 ID、用户名这类每次都变的东西混在本该固定的部分里。
- 在监控面板里加一条命中率曲线,不要只看省了多少钱,命中率骤降往往说明代码某处不小心又混入了动态内容,这条曲线通常比账单异常更早暴露问题。
常见失效原因排查
- 修改了系统提示:哪怕加了一个空格也会使缓存失效,确保 system prompt 在生产环境中完全固定
- 把用户信息混入前缀:如在 system prompt 里拼接用户 ID、时间戳,导致每次都不同
- 并发太低:某些厂商缓存需要一定频率维持,低 QPS 场景可能在有效期内没有第二次命中
- 模型升级:模型版本变更会使缓存失效,固定 model 版本号而非使用
latest别名
与其他省钱手段叠加
Prompt Caching 与其他手段不互斥,可以叠加使用:
- Caching + Batch API:批量非实时任务,前缀固定 + 批量折扣,双重节省
- Caching + 上下文压缩:对可变历史做摘要,把固定部分尽量前置,扩大缓存命中范围
- Caching + 小模型路由:简单任务用缓存命中的轻量模型,复杂任务再上旗舰
常见问题
缓存命中率怎么查?
OpenAI 在响应的 usage.prompt_tokens_details.cached_tokens 字段返回命中数;Anthropic 在 usage.cache_read_input_tokens 返回。建议在监控面板里跟踪命中率,低于 50% 说明前缀设计可能有问题。
第一次调用也会缓存吗?
第一次会正常计费并建立缓存,从第二次起命中才享受折扣。高并发场景下热身代价可忽略不计。
Prompt Caching 适合 RAG 场景吗?
RAG 的检索结果每次不同,不适合直接缓存。但如果有固定的 instruction 部分(如”请根据以下资料回答,不要超出资料范围……”),这部分可以缓存。
延伸阅读:
- 计费原理总览:大模型 token 计费完全指南
- 更多省钱技巧:大模型 API 成本优化 10 招
- 性价比模型选型:最便宜的大模型 API 盘点
- 实时价格对比:价格对比表工具
- Token 用量估算:Token 计算器
- token 成本专题:成本话题 Hub
- 接入托管服务:加入候补