← 返回资讯

Prompt Caching 省钱原理与实践:让重复内容少算一次钱

2026-06-15

如果你的每次 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_tokensusage.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 统计和当前定价页为准,这里给的是估算思路,不是承诺数字。

上线前自查清单

把下面几步走一遍,能避免接入了却没生效的尴尬:

  1. 打印一次实际前缀的 token 数,确认它超过了模型的最小缓存阈值,不确定就查当前的官方文档。
  2. 连续打两次一模一样的请求,看第二次响应里的 cached_tokens(OpenAI)或 cache_read_input_tokens(Anthropic)是不是大于 0——大于 0 说明命中了,一直是 0 就回头对照上面的踩坑记录排查。
  3. 检查前缀里有没有混入动态字段:grep 一下你拼接 system prompt 的代码,看有没有时间戳、请求 ID、用户名这类每次都变的东西混在本该固定的部分里。
  4. 在监控面板里加一条命中率曲线,不要只看省了多少钱,命中率骤降往往说明代码某处不小心又混入了动态内容,这条曲线通常比账单异常更早暴露问题。

常见失效原因排查

  • 修改了系统提示:哪怕加了一个空格也会使缓存失效,确保 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 部分(如”请根据以下资料回答,不要超出资料范围……”),这部分可以缓存。


延伸阅读: