· 6 分钟 · API

详解提示词缓存:何时能省钱,何时不能

详解提示词缓存:何时能省钱,何时不能

一个周一的凌晨 9:12,我们的一个客服智能体的成本开始比预期高出 4x。

模型没有产生幻觉,流量图没有出错,也没有人意外地从 Haiku 切换到 Opus。问题更简单:每个请求都包含同一份长度为 42,000 个 token 的政策手册,但我们的提示词缓存命中率几乎为零,因为我们在手册前面加上了每次更新的时间戳和用户专属元数据。

内容相同。模型相同。应用相同。却没有节省任何缓存成本。

提示词缓存听起来像是会自动生效的功能,直到你尝试用它降低账单。实际上,它只对一种非常特定的提示词结构有利:在多个请求中重复使用的一段较长且稳定的前缀。只要理解这句话,通常就能预测缓存是否会省钱,还是悄无声息地毫无作用。

提示词缓存究竟缓存什么

提示词缓存不是语义记忆。提供商并不是在说:“这看起来和你之前发送的内容相似。”它更接近于:

“对于这个模型和缓存策略,这个请求的开头与之前的 token 序列足够匹配,因此我们可以复用该前缀的内部计算。”

这一区别很重要。

当你反复发送相同的长前缀时,提示词缓存通常会有所帮助:

  • 系统指令
  • 工具定义
  • 长篇文档集
  • 产品目录
  • 代码库上下文
  • 法律或政策手册
  • 少样本示例
  • 在多个请求中重复使用的静态检索结果

如果每个请求的开头都不同,缓存通常帮助不大:

  • 顶部的动态时间戳
  • 共享内容之前的用户专属数据
  • 随机排列的示例顺序
  • 可缓存部分中每个请求各不相同的 UUID
  • 顺序或格式发生变化的检索片段
  • 放在稳定指令之前的聊天历史

工程上的核心做法是:将稳定且计算成本高的上下文放在前面,将易变的上下文放在后面。

Anthropic 与 OpenAI:目标相似,控制方式不同

Anthropic 和 OpenAI 都支持提示词缓存,但开发者的使用体验有所不同。

Anthropic 提供的提示词缓存控制更为显式。你可以为内容块标记缓存控制设置,然后查看用量字段,例如 cache_creation_input_tokens 和 cache_read_input_tokens。这样更容易判断你是在创建缓存条目,还是从中读取内容。

在支持的模型上,OpenAI 的提示词缓存更加自动化。当提示词前缀被重复使用时,缓存的输入 token 可能会按不同价格计费,并体现在用量元数据中。通常,你不需要以同样的方式设置显式缓存断点;而是要组织好请求,确保可复用前缀在字节和 token 层面保持稳定。

实际差异在于控制能力与便利性之间的取舍。

维度Anthropic 式显式缓存OpenAI 式自动缓存
开发者控制能力较高;可以标记可缓存的内容块较低;由提供商检测可复用前缀
可调试性通常可通过缓存创建和读取计数更清楚地了解情况取决于模型/API 提供的用量元数据
提示词结构的重要性极高极高
最适用场景大型静态内容块、工具、文档、示例多次调用中重复使用的长前缀
常见失效原因缓存断点放在动态内容之后缓存尚未匹配到内容,前缀就已发生变化
理解方式“我在写入和读取一个近似有名称的前缀”“提供商识别出了这个重复出现的前缀”

实现细节因模型和提供商而异,而且仍在持续演进。但架构层面的规则是稳定的:缓存依赖重复的前缀,而不是相似的提示词。

定价模型:创建与读取

理解提示词缓存的一种实用方法,是将输入 token 分成三类:

  1. 普通输入 token:按常规输入价格计费。
  2. 缓存创建 token:用于创建缓存条目的 token,通常按加价或专门的写入费率计费。
  3. 缓存读取 token:从已有缓存条目中读取的 token,通常按折扣费率计费。

Anthropic 通过以下用量字段直接体现这种区别:

{
  "usage": {
    "input_tokens": 812,
    "cache_creation_input_tokens": 42000,
    "cache_read_input_tokens": 0,
    "output_tokens": 684
  }
}

在后续请求中,如果前缀命中缓存,你希望看到的结果应更接近:

{
  "usage": {
    "input_tokens": 936,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 42000,
    "output_tokens": 711
  }
}

首次调用创建缓存。后续调用从缓存中读取。

经济效益取决于提供商当前的定价、缓存 TTL 和模型。由于价格会变化,我更倾向于在代码中用变量建模,而不是将具体数字硬编码到设计文档中。

下面是我在判断某条调用路径是否值得实现缓存时使用的一个小型 Python 函数:

def cache_savings(
    cached_tokens: int,
    normal_input_per_mtok: float,
    cache_write_per_mtok: float,
    cache_read_per_mtok: float,
    requests: int,
) -> dict:
    mtok = cached_tokens / 1_000_000

    no_cache = requests * mtok * normal_input_per_mtok
    with_cache = (
        mtok * cache_write_per_mtok +
        (requests - 1) * mtok * cache_read_per_mtok
    )

    return {
        "no_cache": round(no_cache, 6),
        "with_cache": round(with_cache, 6),
        "savings": round(no_cache - with_cache, 6),
        "savings_pct": round((1 - with_cache / no_cache) * 100, 2),
    }


print(cache_savings(
    cached_tokens=42_000,
    normal_input_per_mtok=3.00,
    cache_write_per_mtok=3.75,
    cache_read_per_mtok=0.30,
    requests=100,
))

这里的价格仅用于示例。关键在于计算方式。

对于长度为 42,000 个 token 的可复用前缀和 100 次请求,缓存通常效果很好。如果前缀相同但只有一次请求,而写入价格又高于普通输入价格,那么缓存反而可能更贵。

这就是第一个重要的易踩坑之处:提示词缓存依靠成本摊销。如果不重复使用缓存的前缀,就无法获得收益。

盈亏平衡公式

你可以用以下公式估算盈亏平衡点:

no_cache_cost = N * T * normal_price

cache_cost = T * write_price + (N - 1) * T * read_price

其中:

  • N 是使用同一前缀的请求次数
  • T 是可缓存的 token 数量
  • normal_price 是普通输入 token 的价格
  • write_price 是缓存创建价格
  • read_price 是缓存读取价格

满足以下条件时,缓存可以省钱:

T * write_price + (N - 1) * T * read_price
  <
N * T * normal_price

该 T 在只考虑成本的盈亏平衡计算中会被约去,这意味着复用次数和相对价格决定了是否值得使用缓存。但 token 数量在实际运行中仍然很重要:在 500 个 token 上节省 80% 的成本微不足道;在 80,000 个 token 上节省同样的比例,就会成为账单上值得关注的一项。

一个实用的粗略估算版本:

break_even_requests =
  (write_price - read_price) / (normal_price - read_price)

如果缓存写入仅比普通输入稍贵,而读取便宜得多,那么达到盈亏平衡所需的复用次数可能很少。如果缓存写入很贵,或者前缀很少重复,缓存的收益就很不稳定。

如何组织提示词以保持前缀稳定

最有效的提示词缓存优化不是某个 API 标志,而是提示词的布局。

不佳的布局:

Request ID: req_8f19a
Current time: 2026-02-14T09:12:33Z
User: customer_4812
Locale: en-US

SYSTEM INSTRUCTIONS:
...

POLICY MANUAL:
[42,000 tokens of stable text]

USER QUESTION:
Can I get a refund after 45 days?

这会使前缀缓存失效,因为提示词从开头就出现了差异。

更好的布局:

SYSTEM INSTRUCTIONS:
...

POLICY MANUAL:
[42,000 tokens of stable text]

DYNAMIC REQUEST METADATA:
Request ID: req_8f19a
Current time: 2026-02-14T09:12:33Z
User: customer_4812
Locale: en-US

USER QUESTION:
Can I get a refund after 45 days?

稳定的内容块放在前面。动态的内容块放在后面。

对于 Anthropic 式显式缓存,其概念结构可能如下:

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 800,
  "system": [
    {
      "type": "text",
      "text": "You are a support policy assistant..."
    },
    {
      "type": "text",
      "text": "Refund policy manual v2026-02-01:\n\n...",
      "cache_control": {
        "type": "ephemeral"
      }
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "Can I get a refund after 45 days?"
    }
  ]
}

具体请求结构因 API 版本和 SDK 而异,因此请将它视为结构示意,而不是可以直接复制粘贴的代码。关键是将较大的稳定内容块明确标记为可缓存,并使其在各次调用之间保持不变。

对于 OpenAI 式自动缓存,即使你不手动标记内容块,提示词结构也同样重要:

{
  "model": "gpt-5.5",
  "messages": [
    {
      "role": "system",
      "content": "You are a support policy assistant...\n\nRefund policy manual v2026-02-01:\n\n..."
    },
    {
      "role": "user",
      "content": "Can I get a refund after 45 days?"
    }
  ]
}

如果系统消息在每个请求中都会变化,就不应期待持续稳定地从缓存读取。

一种实用的架构模式

对于生产系统,我喜欢在应用代码中明确定义缓存的提示词前缀。

一种简单的模式:

prompt_prefix/
  support_policy_v17.txt
  tool_schema_v4.json
  examples_v9.md

runtime_request/
  user_question
  user_profile
  retrieved_recent_orders

然后像这样组装请求:

from pathlib import Path

POLICY_PREFIX = Path("prompt_prefix/support_policy_v17.txt").read_text()
TOOL_SCHEMA = Path("prompt_prefix/tool_schema_v4.json").read_text()
EXAMPLES = Path("prompt_prefix/examples_v9.md").read_text()

def build_prompt(user_question: str, user_profile: dict) -> str:
    stable_prefix = f"""
You are a support policy assistant.

TOOLS:
{TOOL_SCHEMA}

POLICY:
{POLICY_PREFIX}

EXAMPLES:
{EXAMPLES}
""".strip()

    dynamic_suffix = f"""
USER PROFILE:
{user_profile}

USER QUESTION:
{user_question}
""".strip()

    return stable_prefix + "\n\n--- DYNAMIC REQUEST ---\n\n" + dynamic_suffix

实际上,我还会为前缀设置版本。如果政策手册发生变化,我希望这是一次有意触发的缓存失效事件:

sha256sum prompt_prefix/support_policy_v17.txt

将这个哈希值与模型名称、缓存创建 token 数、缓存读取 token 数和请求路由一起记录到日志中。如果缺少这种可观测性,缓存问题会很难排查,因为响应仍然正常,只是成本更高。

一条有用的日志记录如下:

{
  "route": "support.answer",
  "model": "claude-sonnet-4-6",
  "prefix_hash": "9d3a4a...",
  "input_tokens": 932,
  "cache_creation_input_tokens": 0,
  "cache_read_input_tokens": 42000,
  "output_tokens": 688,
  "latency_ms": 1840
}

不要仅凭账单进行优化。应按调用路径记录缓存行为。

延迟:通常会改善,但并非魔法

提示词缓存可以降低延迟,因为模型提供商能够复用缓存前缀的计算结果。但除非已在自己的工作负载上进行测量,否则我不会在制定 SLA 时假定延迟会降低一个固定值。

生产环境中的实际情况更复杂:

  • 短提示词可能不会带来明显的延迟变化。
  • 较长的缓存前缀通常能缩短首个 token 的响应时间。
  • 输出长度仍然是许多请求耗时的主要决定因素。
  • 工具调用、网络延迟和排队可能掩盖缓存带来的收益。
  • 如果写入比读取更慢,缓存未命中会导致尾部延迟骤增。

贴近实际情况的基准测试设置很简单:

# warm cache
python run_eval.py --case support_refund --repeat 1

# measure cache reads
python run_eval.py --case support_refund --repeat 50 --jsonl out.jsonl

# inspect p50/p95 by cache_read_input_tokens > 0
python analyze_latency.py out.jsonl

分别测量缓存命中和缓存未命中的请求。将它们混在一起,只会得到一个无法解释任何一种情况的数字。

我在实际系统中见过的常见陷阱

1. 动态内容放在稳定内容之前

这是最常见的问题。时间戳、请求 ID、功能开关配置快照、用户区域设置或实验分组信息被放在系统提示词的最前面。

把这些内容移到后面。

2. “等价”的 JSON 并不等同

这两个片段在语义上完全相同,但在缓存层面未必相同:

{"name":"refund_lookup","timeout":3,"strict":true}
{
  "strict": true,
  "timeout": 3,
  "name": "refund_lookup"
}

如果缓存前缀包含工具 schema 或配置,请使用确定性序列化:

import json

def stable_json(data: dict) -> str:
    return json.dumps(data, sort_keys=True, separators=(",", ":"))

这样可以避免因键顺序或空白字符变化而意外导致缓存未命中。

3. 检索可能破坏可缓存性

RAG 系统经常这样构建提示词:

Retrieved documents:
[doc A]
[doc B]
[doc C]

Instructions:
...

如果每次查询的检索结果不同,前缀也会随查询而变化。如果检索到的上下文确实是动态的,这样没有问题,但不要把稳定的指令放在它后面。

更好的做法:

Instructions:
...

Long-lived product documentation:
[stable docs]

Query-specific retrieved documents:
[doc A]
[doc B]
[doc C]

对于请求量较大的系统,可以考虑将上下文拆分为:

  • 全局稳定的指令
  • 租户内稳定的文档
  • 针对用户或查询的检索内容

每一层的缓存行为都不同。

4. 缓存 TTL 很重要

有些缓存条目会过期。如果流量呈突发性,一个前缀可能在十分钟内被大量复用,之后便无人使用。延长缓存存活时间可能有所帮助,但不同提供商和模型的定价或可用性可能不同。

这时,请求分布就很重要。“我们每天收到 10,000 个请求”不如“上午 9 点到 10 点之间,针对这个完全相同的前缀,我们每分钟收到 200 个请求”有用。

5. 模型变化会让原有假设失效

不能假定为某个模型缓存的前缀可以供另一个模型复用。如果你在 Claude Opus 4.8、Sonnet 4.6、GPT-5.5、Gemini 3,或 Llama、Qwen、DeepSeek、MiniMax、Kimi 等开放模型之间进行路由,请按模型跟踪缓存行为。

多模型路由很有用,但会让成本摊销更复杂。如果流量分到五个模型,每个模型收到的重复前缀可能会更少。

6. 提示词个性化可能适得其反

团队经常对系统提示词进行个性化处理:

You are helping {{customer_name}}, who is on plan {{plan_name}}.

这看起来无伤大雅,但如果它出现在缓存内容之前,每个客户就会有不同的前缀。除非个性化内容确实会改变模型对稳定内容块的理解,否则应将其放在后面。

提示词缓存何时能省钱

以下情况通常非常适合使用缓存:

  • 可缓存的前缀较大,通常包含数千甚至数万个 token。
  • 完全相同的前缀被多次复用。
  • 前缀在不同请求之间保持稳定。
  • 流量在缓存存活时间内到达。
  • 你能观测缓存创建和缓存读取的 token 数。
  • 路由中的提示词组成可预测。

具体示例:

  • 编程助手反复包含相同的代码仓库结构图和风格指南。
  • 客服机器人在回答每个退款问题时都包含相同的政策手册。
  • 智能体在每次请求中都使用大型静态工具 schema。
  • 内部研究助手为团队提供相同的语料库概览。

对于这些工作负载,提示词缓存可能是最有效的成本优化手段之一,因为它不需要降低上下文质量。你仍然保留长提示词,只是不必每次都按完整的输入价格付费。

提示词缓存何时没有帮助

以下情况下,缓存收益有限,甚至可能产生负面效果:

  • 提示词很短。
  • 前缀很少被复用。
  • 大部分上下文都是针对特定查询的检索内容。
  • 动态数据出现在稳定数据之前。
  • 你在实验期间不断修改指令。
  • 流量稀疏地分散在许多模型、租户或提示词变体之间。
  • 缓存写入成本较高,而读取次数太少,无法摊销写入成本。

评估时的一个常见陷阱是:在 notebook 中,提示词缓存看起来效果不佳,因为每个案例只运行一次。到了生产环境,数千名用户共享同一个稳定前缀,效果就非常好。反过来的情况也会发生:演示程序循环运行同一个提示词 100 次,显示出显著的成本节省,但生产环境中的租户定制却阻碍了复用。

使用真实的请求分布进行测试。

简单的缓存就绪检查清单

在启用或依赖提示词缓存之前,我会检查:

  • 是否至少有一个长度超过几千个 token 的稳定前缀?
  • 它是否位于所有动态内容之前?
  • 序列化是否具有确定性?
  • 是否对前缀进行了版本管理并计算了哈希?
  • 我能否记录缓存创建和缓存读取的 token 数?
  • 我是否知道缓存存活时间内的预期复用次数?
  • 我是否分别测量过缓存命中和未命中的延迟?

如果有三个或更多问题的答案是“否”,缓存可能仍然有效,但你是在凭猜测做判断。

实践要点

提示词缓存不是神奇的压缩技术。它复用的是稳定的 token 前缀,其经济收益来自一次性支付创建成本,再多次支付较低的读取成本。

工程实践方法很直接:

  • 将大段稳定内容放在最前面:指令、工具、文档、示例。
  • 将时间戳、用户元数据、检索结果和请求 ID 移到可缓存前缀之后。
  • 跟踪 cache_creation 与 cache_read 按路由和模型统计的 token 数。
  • 对 JSON 工具 schema 和配置使用确定性序列化。
  • 根据实际提供商的价格和流量分布,建立盈亏平衡模型。
  • 谨慎处理多模型路由、租户定制和短提示词。
  • 分别测量缓存命中和未命中的延迟,不要仅凭平均值判断。

我见过最好的提示词缓存优化一点也不复杂:我们把 42,000 个稳定 token 移到了 12 个动态 token 前面。模型行为没有变化,账单却变了。

A

本博客由 AI Prime Tech 发布。文章借助 AI 辅助起草;在据此采取行动前,请对照官方文档核实模型 ID、价格和限额等特定版本的细节。

以更低价格接入 Claude API

一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。

获取 API 密钥 →
AI Prime Tech 是独立的第三方 API 网关。Claude™ 和 Anthropic® 是 Anthropic, PBC 的商标。此处不暗示任何关联或背书。