详解提示词缓存:何时能省钱,何时不能
一个周一的凌晨 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 分成三类:
- 普通输入 token:按常规输入价格计费。
- 缓存创建 token:用于创建缓存条目的 token,通常按加价或专门的写入费率计费。
- 缓存读取 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 前面。模型行为没有变化,账单却变了。
一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。
获取 API 密钥 →