用提示词缓存砍掉 LLM API 成本:官方定价机制与实操写法
提示词缓存(prompt caching)的原理很简单:请求之间重复出现的提示词前缀,由模型服务商直接复用上次的计算结果,这部分 token 只按正常输入价的约 10% 计费——在 Anthropic 和 OpenAI 当前的模型上都是九折上下的让利,准确说是打一折。只要你的应用带着长系统提示、跑 Agent 循环,或者每次请求都塞进同一批检索文档,缓存几乎一定是降本最大的那一个开关。下面把缓存到底存了什么、两家官方的计价规则、CaMeL Hub 上缓存怎么算钱、以及怎么写提示词才能真正命中,逐一说清楚。
提示词缓存到底缓存了什么
LLM 在吐出第一个字之前,要先把整段提示词完整过一遍前向计算,建立内部的注意力状态(KV cache)。提示词有 5 万 token 时,这步预填充(prefill)占掉大头的算力和费用。问题在于:如果其中 4.8 万 token 和你几秒钟前的上一次请求一模一样,服务商其实在重复劳动。
缓存做的事就是把这段前缀的计算结果暂存起来,下一个请求只要前缀完全一致,就直接跳过重算。省下来的是真实算力,所以服务商愿意把大部分让给你:命中缓存的 token 按远低于正常输入价的费率结算。
有一个机制细节决定了后面所有实操建议:缓存是严格按前缀精确匹配的。OpenAI 的缓存文档原话是 "Cache hits are only possible for exact prefix matches within a prompt"——第 100 个字符变了,第 99 个字符之后的缓存全部作废。记住这一条,省钱技巧就都能自己推出来。
什么场景最吃缓存:长系统提示、Agent 循环、RAG
缓存要起作用,前提是提示词里有一大块内容在缓存有效期内反复出现。实际业务里主要是三类:
- 长系统提示——客服机器人挂着两万 token 的规则文档和示例,每条用户消息都要重发一遍。开了缓存,第一次全价,之后每次都按一折算。
- Agent 循环——写代码、调工具的 Agent 是最极端的受益者:每一步工具调用都在旧上下文后面追加一点再整体重发,40 步的任务等于把不断变长的前缀重算 40 次。缓存密集的 Agent 流量里,输入 token 有九成以上是缓存读取,很常见。
- RAG 与长文档问答——用户围绕同一批检索结果或同一份合同连续提问时,把文档放在提示词开头,后续每个问题都在最贵的那部分上命中缓存。
- 反过来,这些场景缓存帮不上忙:一次性的独特内容、低于最小可缓存长度(通常 1,024 token)的短提示词、以及请求间隔太长导致缓存过期的稀疏流量。
Anthropic 官方定价(claude.com/pricing):命中 0.1 倍,写入 1.25 倍
Anthropic 的缓存是显式的:你在请求里用 cache_control 标记缓存断点,用量对象里分别报告缓存写入和读取。根据 Anthropic 提示词缓存官方文档,相对基础输入价的倍率是:默认 5 分钟有效期的缓存写入 1.25 倍,可选 1 小时有效期的写入 2 倍,缓存读取(命中)0.1 倍。而且每次命中都会刷新有效期,持续活跃的会话缓存不会断。
换算成美元(官方价目表):Claude Opus 4.8 输入价 $5/百万 token,5 分钟缓存写入 $6.25/百万,命中只要 $0.50/百万。这笔账很好算——写入多付的 25%,第一次命中(省 90%)就赚回来了。只要一段前缀在 5 分钟内会被复用哪怕一次,就值得缓存。
还有个门槛要知道:最小可缓存长度因模型而异,同一份文档写明 Claude Opus 4.8 和 Claude Sonnet 5 是 1,024 token,其他型号在 512 到 4,096 之间。不够长的前缀不会报错,只是按普通输入原价处理,没有折扣。
import anthropic
client = anthropic.Anthropic(
api_key="sk-...", # CaMeL Hub 的 API key
base_url="https://api.camel-hub.com",
)
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": LONG_SYSTEM_PROMPT, # 规则文档、工具说明、示例
"cache_control": {"type": "ephemeral"}, # 缓存断点
}
],
messages=[{"role": "user", "content": question}],
)
print(resp.usage)
# cache_creation_input_tokens=... <- 按输入价 1.25 倍计费
# cache_read_input_tokens=... <- 按输入价 0.1 倍计费
OpenAI 官方定价:自动生效,缓存输入按一折
OpenAI 走的是另一条设计路线:全自动。官方缓存指南写明 "Prompt Caching works automatically for eligible requests, with no code changes required"——1,024 token 以上的提示词自动参与缓存,命中数量在响应的 prompt_tokens_details.cached_tokens 字段里。
折扣力度和 Anthropic 同一量级。根据 OpenAI 官方定价页(developers.openai.com/api/docs/pricing),当前 GPT 系列的缓存输入统一按正常输入价的 10% 计费:gpt-5.5 输入 $5.00/百万 token,缓存后 $0.50;gpt-5.4 是 $2.50 对 $0.25;gpt-5.4-mini 是 $0.75 对 $0.075。
写入费用则分代际:官方指南说明 GPT-5.6 系列之前的模型缓存写入不额外收费,GPT-5.6 及之后写入按未缓存输入价的 1.25 倍——和 Anthropic 的结构对齐了。缓存寿命也随 GPT-5.6 加长:缓存前缀"至少 30 分钟内可复用";更早的模型大约在闲置 5–10 分钟后失效,上限一小时。
{
"usage": {
"prompt_tokens": 12083,
"completion_tokens": 486,
"prompt_tokens_details": {
"cached_tokens": 11776
}
}
}
在 CaMeL Hub 上,缓存命中怎么计费
CaMeL Hub 不会把缓存价"抹平"成统一输入价,而是把服务商的缓存让利原样传导给你:命中缓存的那部分输入 token,按服务商的缓存读取费率结算,而不是按正常输入价。每笔请求的明细都在用量日志里——缓存 token 数与普通输入、输出 token 并列记录;只要缓存帮你省了钱,日志里会把不打折的原价划掉,旁边是实付金额,省了多少一眼可见。
两种调用方式都完整保留各自的缓存机制:走 Anthropic 原生的 /v1/messages 端点、自己打 cache_control 断点调 Claude 也行;走 OpenAI 兼容的 /v1/chat/completions 端点、让 GPT 系列自动缓存也行。一个 API key、一个 base URL,两家的缓存都照常工作。
实际能省多少?我们在生产流量上做过统计:缓存密集的 Claude 工作负载(以编程 Agent 和长上下文助手为主)整体成本节省约 30%——这是真实生产环境的内部实测数据,不是推算。你的数字取决于命中率:短提示词的普通聊天应用可能只有个位数,高频 Agent 流水线则完全可以超过 30%。
怎么组织提示词,让命中率最大化
既然匹配规则是前缀精确一致,优化思路就一句话:稳定的内容放前面,易变的内容放最后。以下几条覆盖了实际流量里绝大多数"本可以命中却没命中"的情况:
- 静态内容固定顺序放最前:系统提示 → 规则/参考文档 → 工具定义与示例。请求之间不要调换这些块的顺序。
- 易变值绝不放前缀:时间戳、请求 ID、会话名、随机问候语只要出现在系统提示开头,命中率直接归零。统一挪到最后一条用户消息的末尾。
- 历史只追加、不改写:对话历史保持 append-only。编辑、总结或重排早前轮次,会让那个位置之后的缓存全部失效——确实要压缩历史时,低频操作,认一次冷启动。
- 序列化要确定性:用结构化数据拼提示词时,固定 JSON 的键序和空白符。语义相同但键序不同的两份数据,就是两个不同的前缀。
- 过最低门槛:两家都要求约 1,024 token 起才可缓存,不必刻意凑长度,但短提示词别指望这项折扣。
- 保持缓存温热:命中必须落在有效期内(Claude 默认 5 分钟、每次命中续期;GPT-5.6+ 至少 30 分钟)。批量任务里,共享前缀的请求连续发,比摊开几小时零散发划算得多。
from datetime import datetime, timezone
# 反例 - 易变数据放开头:前缀每次都不同,永远无法复用
system = (
f"当前时间:{datetime.now(timezone.utc)}\n"
"你是客服助手。\n"
f"{POLICY_DOC}"
)
# 正例 - 静态前缀在前,易变数据放末尾
system = f"你是客服助手。\n{POLICY_DOC}"
user = (
f"{question}\n\n"
f"(当前时间:{datetime.now(timezone.utc):%Y-%m-%d %H:%M} UTC)"
)