RH
RouterHub AI API 中转站导航与平台推荐
博客

Claude API 的 Prompt Caching 怎么用:从少花钱到少踩坑

你接入 Claude API 以后,最容易被忽略的一笔账不是输出 token,而是每次请求里反复发送的那一大段固定上下文。 系统提示词、工具定义、RAG 检索出的长文档、代码仓库规范、对话历史摘要。

muchacha 2026-05-10 04:36:18

正文

你接入 Claude API 以后,最容易被忽略的一笔账不是输出 token,而是每次请求里反复发送的那一大段固定上下文。

系统提示词、工具定义、RAG 检索出的长文档、代码仓库规范、对话历史摘要,这些内容经常一字不差地出现在很多次请求里。模型每次都要重新处理它们,你也会按完整输入 token 付费。Prompt caching 解决的就是这个问题:把稳定的 prompt 前缀缓存起来,后续请求命中缓存时,只为这部分 token 支付更低的读取成本。

这不是一个需要重做架构的优化。真正难的地方在于你要知道哪些内容适合缓存、缓存断点应该放在哪里,以及怎样判断它真的命中了。

先判断:你的请求里有没有“稳定前缀”

Prompt caching 适合这些场景:

  • 每次请求都会带同一段系统提示词。
  • 工具 schema 很长,而且工具列表不频繁变化。
  • RAG 会反复塞入同一批文档、手册或项目规范。
  • Agent 循环里会持续复用同一段任务背景。
  • 批处理任务在短时间内用同一套说明处理多条输入。

不适合的场景也很明确:

  • 每次请求都是一次性短 prompt。
  • 固定上下文很小,低于模型的最小缓存 token 门槛。
  • 你把时间戳、随机 ID、用户即时输入放进了 prompt 前缀,导致每次前缀都不同。
  • 输出 token 才是主要成本,输入 token 占比不高。

可以用一句话判断:如果一段 prompt 会在 TTL 时间窗口内被复用两三次以上,它就值得考虑缓存。

它缓存的不是“某个字段”,而是请求前缀

Prompt caching 的核心是“前缀匹配”。你在某个 content block 上放一个缓存断点,Claude 会把这个断点之前的内容当作可复用前缀。

后续请求只有在前缀保持一致时才容易命中。顺序、空格、工具定义、系统提示词、检索文档,只要在断点之前发生变化,都可能让缓存失效。

更稳妥的组织方式是把请求拆成几层:

工具定义(最稳定)
系统提示词(较稳定)
项目规范 / 知识库文档 / RAG 上下文(按任务复用)
历史摘要或阶段性上下文(短期复用)
当前用户输入(不缓存)

缓存断点应该尽量放在稳定块的最后,而不是放在动态输入后面。动态内容越靠后,缓存命中率越高。

最小实现:先缓存系统提示词

如果你只是想验证效果,可以先从系统提示词开始。下面是 TypeScript 写法:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

const REVIEW_RULES = `
你是一名代码审查助手。
审查重点包括:类型安全、异常处理、边界条件、性能风险和可维护性。
输出必须具体,不要给空泛建议。
`.trim();

export async function reviewCode(diff: string) {
  const response = await client.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1200,
    system: [
      {
        type: "text",
        text: REVIEW_RULES,
        cache_control: { type: "ephemeral" },
      },
    ],
    messages: [
      {
        role: "user",
        content: `请审查下面的代码变更:\n\n${diff}`,
      },
    ],
  });

  console.log({
    input_tokens: response.usage.input_tokens,
    cache_creation_input_tokens: response.usage.cache_creation_input_tokens,
    cache_read_input_tokens: response.usage.cache_read_input_tokens,
  });

  return response.content;
}

Python 里也是同一个思路:

import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

REVIEW_RULES = """
你是一名代码审查助手。
审查重点包括:类型安全、异常处理、边界条件、性能风险和可维护性。
输出必须具体,不要给空泛建议。
""".strip()


def review_code(diff: str):
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1200,
        system=[
            {
                "type": "text",
                "text": REVIEW_RULES,
                "cache_control": {"type": "ephemeral"},
            }
        ],
        messages=[
            {
                "role": "user",
                "content": f"请审查下面的代码变更:\n\n{diff}",
            }
        ],
    )

    print(
        {
            "input_tokens": response.usage.input_tokens,
            "cache_creation_input_tokens": response.usage.cache_creation_input_tokens,
            "cache_read_input_tokens": response.usage.cache_read_input_tokens,
        }
    )
    return response.content

第一次请求通常会看到 cache_creation_input_tokens,后续命中时会看到 cache_read_input_tokens。上线前不要只看“代码加了 cache_control”,要把 usage 指标打进日志,连续观察一段时间的命中率。

5 分钟还是 1 小时:别一上来就选长 TTL

Anthropic 的显式缓存断点支持短 TTL,也支持 1 小时 TTL。短 TTL 写入成本更低,适合高频交互、Agent 循环、批处理队列这种“短时间内连续请求”的场景。1 小时 TTL 的写入成本更高,适合低频但仍会复用同一前缀的任务,比如每十几分钟跑一次的定时分析、长时间代码审查会话、周期性报告生成。

一个实用规则:

场景 更适合的选择
聊天、Agent 循环、连续批处理 短 TTL
每隔十几分钟复用同一上下文 1 小时 TTL
单次请求或很短 prompt 不必缓存
前缀经常变化 先整理 prompt 结构

不要为了“缓存更久”直接选 1 小时。缓存写入也要付费,只有复用次数足够时才划算。

成本账怎么算

以一个约 2,000 token 的稳定前缀为例。如果它在短时间内被请求 10 次,没缓存时你会为这 20,000 个输入 token 按完整输入价付费。

加缓存后,第一轮会产生写入成本,后面命中的请求只产生读取成本。读缓存通常比完整输入便宜很多,所以收益来自“多次复用同一前缀”。

你可以在自己的服务里直接记录三类数字:

完整输入 token:usage.input_tokens
缓存写入 token:usage.cache_creation_input_tokens
缓存读取 token:usage.cache_read_input_tokens

再按官方价格表做一张周报:

节省的输入成本 =
  缓存读取 token 按完整输入价计算的成本
  - 缓存读取 token 实际读取成本
  - 缓存写入带来的额外成本

这比只看总账单更可靠,因为你能看到缓存到底是命中、失效,还是压根没有达到最小缓存门槛。

最常见的缓存失效原因

第一类是动态内容放错位置。比如你把 今天是 2026-05-03、请求 ID、用户昵称放进系统提示词开头,每次前缀都不同,缓存自然很难命中。把这些内容移到当前用户输入里,或者做成后置上下文。

第二类是工具定义不稳定。工具数组顺序变化、schema 字段顺序变化、某个描述字符串被拼接出不同空格,都可能影响前缀匹配。工具定义要集中生成,并保证顺序稳定。

第三类是文档块太小。不同模型的最小可缓存 token 门槛不一样,过短的系统提示词不会带来你期待的缓存写入。短 prompt 不要为了缓存硬凑长度,只有上下文确实会反复复用时才值得优化。

第四类是把缓存当成强一致存储。Prompt caching 是服务端优化能力,不适合承载业务状态。业务状态仍然应该放在你的数据库、队列或会话存储里。

更像生产环境的断点放法

复杂请求不要只放一个断点。你可以按稳定程度分层:

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1600,
  tools: [
    {
      name: "search_docs",
      description: "搜索内部文档",
      input_schema: {
        type: "object",
        properties: {
          query: { type: "string" },
        },
        required: ["query"],
      },
      cache_control: { type: "ephemeral" },
    },
  ],
  system: [
    {
      type: "text",
      text: STABLE_SYSTEM_PROMPT,
      cache_control: { type: "ephemeral" },
    },
  ],
  messages: [
    {
      role: "user",
      content: [
        {
          type: "text",
          text: PROJECT_GUIDE,
          cache_control: { type: "ephemeral" },
        },
        {
          type: "text",
          text: `当前问题:${question}`,
        },
      ],
    },
  ],
});

工具、系统提示词、项目文档都可以成为稳定前缀的一部分;当前问题不要放进缓存层。这样用户连续追问时,模型不需要每次都重新处理完整工具说明和项目资料。

上线前的检查清单

接入 Prompt caching 之前,先过一遍这几个问题:

  • 稳定前缀是否真的会复用?
  • 断点之前有没有时间戳、随机数、用户即时输入?
  • 工具定义和文档顺序是否稳定?
  • 固定上下文是否达到当前模型的最小缓存 token 门槛?
  • 日志里是否记录了缓存写入和缓存读取 token?
  • TTL 是否和真实请求间隔匹配?
  • 价格计算是否使用了你当前调用模型的官方价格?

只要这些问题答案清楚,Prompt caching 通常是 Claude API 集成里投入产出比很高的一项优化。它不会减少输出成本,也不会替你设计会话状态,但它能让那些反复出现的长 prompt 不再每次都按完整输入价付费。

真正值得养成的习惯是:把 prompt 当成可优化的工程资产。固定内容放前面,动态内容放后面;能复用的上下文保持稳定;上线后看 usage 指标,而不是靠感觉判断是否省钱。这样做以后,缓存不是一个临时省钱技巧,而会变成你设计 Claude 应用时的默认约束。

参考资料

相关阅读