为什么需要 Prompt 缓存
Prompt 链式调用 把大任务拆成多步,但有个隐形成本:
Step 1:[system 2000 token] + [用户问题 100] → 2100 token
Step 2:[system 2000 token] + [Step1输出 500] → 2500 token
Step 3:[system 2000 token] + [Step2输出 800] → 2800 token
...
system 那 2000 token 在每一步都被重复传输、重复计算。链路 10 步 = 20000 token 浪费在相同前缀上。
更典型的是 RAG 场景:
每次问答:
system prompt(角色定义 + 规则) 1500 token
检索到的资料片段 3000 token
用户问题 100 token
总计 4600 token
一天 10000 次调用 = 4600 万 token = ¥920/天
其中 system prompt 那 1500 token 是稳定的,重复传输 10000 次 = 1500 万 token 纯浪费。
Prompt 缓存的核心思想
让模型把稳定的前缀"记住",后续调用直接复用,只对新增部分计费和计算。
首次调用:
[system + 资料] 4500 token → 完整计算 → 缓存结果
后续调用(前缀相同):
[system + 资料] 命中缓存 → 不计费(或 1 折)
+ [新问题] 100 token → 只算这部分
成本从 4600 降到 ~460,降幅 90%
延迟从 2s 降到 0.4s,快 5 倍
四种缓存策略
策略 1:静态前缀缓存(最常用)
把不变的 system prompt + 固定资料放前面,变化部分放后面。
[system: 你是客服,规则如下...] ← 稳定,缓存
[资料: 产品手册 5000 字] ← 稳定,缓存
[对话历史] ← 半稳定,可缓存
[当前问题] ← 变化,不缓存
适用:客服问答、知识库问答、固定角色的 Agent。
策略 2:分层缓存
把 Prompt 分成多层,每层独立缓存:
Layer 1: system 角色(几乎永不变) → 长期缓存
Layer 2: 用户偏好(偶尔变) → 中期缓存
Layer 3: 会话上下文(每轮增长) → 短期缓存
Layer 4: 当前输入(每次变) → 不缓存
适用:长对话 Agent、个性化助手。
策略 3:资料块缓存(RAG 专用)
把知识库切成块,每块独立缓存,检索到哪块用哪块:
知识库 1000 个块,每块 500 token
用户问 A → 检索到块 12, 45, 78 → 这 3 块命中缓存
用户问 B → 检索到块 12, 99 → 块 12 命中,99 首次缓存
适用:大规模文档问答、企业知识库。
策略 4:Few-Shot 模板缓存
把 few-shot 示例放前面缓存,只换最后的问题:
[system + 10 个示例] ← 稳定,缓存(示例占大头)
[实际问题] ← 变化,不缓存
适用:分类、抽取、格式转换等需要大量示例的任务。
各模型的实现差异
Claude Prompt Caching(显式控制)
Anthropic 的 prompt caching 需要显式标记哪些块要缓存:
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-3-5-sonnet-20241022",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": system_prompt + documentation, # 稳定前缀
"cache_control": {"type": "ephemeral"} # 标记缓存
},
{
"type": "text",
"text": user_question # 变化部分
}
]
}
]
)
计费:
- 首次写入缓存:1.25x 价格(写入费)
- 命中缓存读取:0.1x 价格(1 折)
- 缓存有效期:5 分钟(每次命中刷新)
关键约束:
- 最小缓存块:1024 token(Sonnet)/ 2048 token(Opus)
- 最多 4 个 cache_control 标记
- 缓存按前缀匹配,前缀任一字符变化则失效
GPT-4o 自动缓存(隐式)
OpenAI 的缓存是自动的,无需标记:
from openai import OpenAI
client = OpenAI()
# 只要前缀相同,自动命中缓存
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt}, # 自动缓存
{"role": "user", "content": user_question}
]
)
计费:
- 命中缓存:0.5x 价格(5 折)
- 无写入费
- 缓存有效期:5-10 分钟(无官方保证)
关键约束:
- 最小缓存前缀:1024 token
- 完全前缀匹配(任何变化都失效)
- 不可控、不可观测(无法确定是否命中)
两者对比
| 维度 | Claude | GPT-4o |
|---|---|---|
| 控制方式 | 显式标记 | 自动 |
| 折扣 | 0.1x(1 折) | 0.5x(5 折) |
| 写入费 | 1.25x | 无 |
| 可观测 | 有 cache_read/input token | 无明确指标 |
| 多块缓存 | 支持(最多 4 块) | 单前缀 |
| 适合场景 | 长 RAG、多段资料 | 简单 system prompt |
成本计算实例
场景:客服 RAG,每次 4600 token,日均 10000 次
无缓存:
- 4600 × 10000 = 4600 万 token
- GPT-4o 价格 $2.5/百万 input = $115/天
GPT-4o 自动缓存(命中率 70%):
- 缓存命中:4600 × 7000 × 0.5 = 1610 万等价 token
- 未命中:4600 × 3000 = 1380 万 token
- 合计 2990 万等价 token = $74.75/天
- 省 35%
Claude 显式缓存(命中率 90%):
- 首次写入:4500 × 1000 × 1.25 = 562 万等价 token
- 命中读取:4500 × 9000 × 0.1 = 405 万等价 token
- 新增部分:100 × 10000 = 100 万 token
- 合计 1067 万等价 token = $26.7/天(按 $2.5/百万算)
- 省 77%
结论:长前缀、高命中场景,Claude 的 1 折优势碾压;短前缀、低命中场景,GPT 的自动缓存省心。
落地代码
Claude 分层缓存(system + 资料 + 对话)
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """你是客服助手。规则:
1. 仅基于资料回答
2. 标注引用
...""" # 1500 token,几乎不变
DOCUMENTATION = load_docs() # 5000 token,每天更新一次
def chat_with_cache(history, question):
resp = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=[
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"} # Layer 1
},
{
"type": "text",
"text": DOCUMENTATION,
"cache_control": {"type": "ephemeral"} # Layer 2
}
],
messages=[
*history, # Layer 3: 对话历史(也会被缓存)
{"role": "user", "content": question} # Layer 4: 不缓存
]
)
# 观察缓存命中
usage = resp.usage
print(f"输入: {usage.input_tokens}, 缓存写入: {usage.cache_creation_input_tokens}, 缓存命中: {usage.cache_read_input_tokens}")
return resp.content[0].text
输出示例:
首次调用:输入 100, 缓存写入 6500, 缓存命中 0 ← 写入缓存
5 分钟内:输入 100, 缓存写入 0, 缓存命中 6500 ← 命中,只算 100
GPT-4o 配合前缀对齐
from openai import OpenAI
client = OpenAI()
# 关键:保证前缀完全一致
def chat_aligned(history, question):
# system 必须完全相同(包括空格)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
*history,
{"role": "user", "content": question}
]
resp = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
# GPT 不返回明确命中指标,需通过 prompt_tokens 间接判断
return resp.choices[0].message.content
缓存命中率优化技巧
1. 前缀稳定性
✗ system: "现在是{当前时间},你是客服..." ← 时间变化导致整段失效
✓ system: "你是客服..." + user: "现在{时间},问题是..."
把变化内容移到后段,前缀保持稳定。
2. 资料排序固定
✗ 每次按相关性排序检索结果 → 顺序变 → 前缀变 → 缓存失效
✓ 固定排序(如按文档 ID)+ 在 user 部分标注"重点看第 3 条"
3. 对话历史压缩
✗ 把完整 10 轮对话塞进去 → 每轮增长 → 每次都写缓存
✓ 每 5 轮做一次摘要,压缩成 1 条 → 减少缓存写入频次
4. 批量请求合并
✗ 100 个独立问题,100 次调用
✓ 合并成 1 次调用(system + 资料缓存 + 100 个问题)
常见坑点
坑 1:缓存未命中却以为命中
Claude:可通过 cache_read_input_tokens > 0 判断
GPT: 无明确指标,只能通过成本下降间接推断
建议接监控统计实际命中率,别想当然。
坑 2:5 分钟有效期过期
低频应用(10 分钟一次)→ 缓存总过期 → 反而多付写入费
解决:对低频场景用 GPT 自动缓存(无写入费),或预热定时请求
坑 3:前缀长度不达标
Claude Sonnet 最小 1024 token
你的 system 只有 800 token → 不缓存
解决:补充 few-shot 示例凑到 1024+
坑 4:多用户混用缓存
✗ 用户 A 的对话历史进了缓存 → 用户 B 命中 A 的历史 → 泄露
✓ 每个用户独立会话,system + 资料共享缓存,对话历史不缓存
与其他技巧的关系
- 链式调用:链式调用的每步 system prompt 相同,最适合缓存
- RAG:RAG 的资料库缓存是降本关键
- Few-Shot 模板:示例放前面缓存,问题放后面
小结
Prompt 缓存是 LLM 工程化的"必修课"——不改变输出质量,只改变成本与延迟。掌握它,你能在不牺牲效果的前提下,把长上下文应用的成本压到可商业化水平。
记住一个原则:稳定的前缀往前放,变化的内容往后放,这是所有缓存优化的根基。