通过提示词缓存技术让大模型推理速度提升 n 倍、成本降低 xx%
前言
今天学习了一下大模型的提示词缓存功能,读文档时更多的是在教学它的优势、使用方法、使用场景,
但没有告知技术原理,于是找大模型尝试了解了一下,然后干脆让大模型帮我写了一篇技术文档。
以下是大模型生成:
目录
-
1. 什么是 Prompt Caching -
2. 缓存带来的好处 -
3. 大模型计算基础 -
4. 缓存的原理 -
5. 缓存匹配机制 -
6. 如何使用 -
7. 适用场景 -
8. 总结
1. 什么是 Prompt Caching
1.1 直观示例
假设你有一本 10 万字的小说,需要基于这本书回答两个问题:
场景:
系统提示词: "以下是《傲慢与偏见》的完整内容:[10万字小说文本]"
第一次问答:
# 问题 1
User: "这本书的主题是什么?"
# API 处理
- 将 10 万字小说 + 问题分解为 100,020 个 tokens
- 逐层计算所有 tokens(40 层 Transformer)
- 创建缓存(保存计算结果)
- 返回答案
耗时: 2.5 秒
成本: $0.375
第二次问答:
# 问题 2
User: "主要角色有哪些?"
# API 处理
- 发现系统提示词(10万字小说)与之前相同
- ✓ 从缓存直接加载已计算的结果(100,000 tokens)
- ✗ 仅计算新问题的部分(20 tokens)
- 返回答案
耗时: 0.4 秒(快了 6 倍!)
成本: $0.03(便宜了 90%!)
1.2 定义
Prompt Caching(提示词缓存) 是一种优化技术:
-
• 缓存什么: 大模型处理文本时的中间计算结果(Key-Value 矩阵) -
• 何时复用: 当新请求的开头部分与之前请求相同时,直接加载缓存 -
• 带来什么: 避免重复计算,大幅提升速度、降低成本
核心流程:
第一次请求: [长文本] + [问题1]
↓
完整计算所有 tokens
↓
保存计算结果到缓存 ← 创建缓存
↓
返回答案1
第二次请求: [长文本] + [问题2]
↓
检测到 [长文本] 部分相同 ← 前缀匹配
↓
✓ 加载 [长文本] 的缓存结果
✗ 仅计算 [问题2] 部分
↓
合并结果,返回答案2
2. 缓存带来的好处
2.1 性能对比
基于上述示例(10 万字小说 + 2 次问答):
|
|
|
|
|---|---|---|
| 处理的 tokens |
|
|
| 耗时 |
|
|
| 成本 |
|
|
2.2 成本详解
Claude API 定价(Sonnet 4.5):
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
第一次问答(创建缓存):
Input (创建缓存): 100,020 tokens × $3.75/1M = $0.375
Output: 50 tokens × $15/1M = $0.00075
总计: $0.375
第二次问答(读取缓存):
Input (读取缓存): 100,000 tokens × $0.30/1M = $0.030
Input (常规): 20 tokens × $3.00/1M = $0.00006
Output: 50 tokens × $15/1M = $0.00075
总计: $0.031
节省分析:
如果没有缓存,两次问答总成本:
2 × (100,020 × $3/1M) = $0.60
使用缓存,两次问答总成本:
$0.375 + $0.031 = $0.406
节省: ($0.60 - $0.406) / $0.60 = 32%
如果是 10 次问答:
不使用缓存: 10 × $0.30 = $3.00
使用缓存: $0.375 + 9 × $0.03 = $0.645
节省: 78%
3. 大模型计算基础
在理解缓存原理之前,我们需要了解大模型是如何处理文本的。
3.1 文本到输出的完整流程
输入文本: "这本书的主题是什么?"
↓
[1] Tokenization(分词)
将文本切分为 tokens
↓
[2] Embedding(嵌入)
每个 token 转换为高维向量(如 4096 维)
↓
[3] Transformer 层(重复 40+ 层)
│
├─ 自注意力机制(Attention)
│ ├─ 计算 Query (Q):当前关注什么
│ ├─ 计算 Key (K):提供什么信息 ← 缓存这个!
│ ├─ 计算 Value (V):具体内容是什么 ← 缓存这个!
│ └─ Attention 计算: Q 与 K/V 交互
│
└─ 前馈网络(FFN)
↓
[4] 输出层
生成下一个 token
↓
返回文本: "这本书的主题是爱情与社会阶级..."
3.2 Tokenization(分词)
将文本切分为模型的最小处理单位 – Token:
# 示例
文本: "这本书的主题是什么?"
↓
Tokens: ["这", "本", "书", "的", "主题", "是", "什么", "?"]
↓
Token IDs: [1234, 5678, 9012, 3456, 7890, 2345, 6789, 0123]
关键点:
-
• 中文:1 个字通常对应 1-2 个 tokens -
• 英文:1 个单词通常对应 1-2 个 tokens -
• Token 是计费和缓存的基本单位
在我们的示例中:
-
• 10 万字小说 ≈ 100,000 tokens -
• "这本书的主题是什么?" ≈ 20 tokens
3.3 Transformer 自注意力机制(核心)
这是大模型最关键、最耗时的部分。
简化理解:
假设要理解"这本书的主题是什么?"这句话中"主题"这个词:
步骤 1: 为每个词计算三个向量
- Query (Q):我(主题)想查询什么信息?
- Key (K):其他词能提供什么信息?
- Value (V):其他词的具体内容是什么?
步骤 2: 计算相关性
用"主题"的 Q 与所有词的 K 计算相似度:
- "这" 的相关性:0.1
- "本" 的相关性:0.2
- "书" 的相关性:0.8 ← 高相关
- "的" 的相关性:0.1
- "主题" 的相关性:0.5
- "是" 的相关性:0.1
- "什么" 的相关性:0.3
步骤 3: 加权求和
根据相关性,将所有词的 V 加权组合:
output_主题 = 0.1×V_这 + 0.2×V_本 + 0.8×V_书 + ... + 0.3×V_什么
为什么这里最耗时?
在我们的示例中(100,020 个 tokens):
单层 Transformer 需要计算:
- 100,020 个 Q 向量
- 100,020 个 K 向量 ← 这个可以缓存!
- 100,020 个 V 向量 ← 这个可以缓存!
- 100,020 × 100,020 的注意力矩阵(100 亿次计算!)
模型有 40 层:
- 总计算量 = 40 层 × 100,020 tokens = 4,000,800 次 K/V 计算
- 这就是为什么处理 10 万 tokens 需要 2-3 秒
4. 缓存的原理
4.1 缓存了什么?
答案:缓存的是每一层 Transformer 中所有 tokens 的 Key (K) 和 Value (V) 向量。
不是缓存:
-
• ❌ 原始文本 -
• ❌ Token IDs -
• ❌ Embedding 向量
而是缓存:
-
• ✅ 每一层的 K 矩阵(已经过线性投影计算) -
• ✅ 每一层的 V 矩阵(已经过线性投影计算)
4.2 缓存的结构
以我们的示例为例(100,000 tokens 的小说):
# 缓存的完整结构
cache = {
"id": "cache_abc123",
"created_at": 1704902400,
"ttl": 300, # 5 分钟有效期
# Token 序列(用于匹配)
"tokens": [1234, 5678, 9012, ...], # 100,000 个 token IDs
# 每一层的 KV Cache
"layers": [
# Layer 0
{
"keys": [
# Token 0 的 key 向量
[k0_head0, k0_head1, ..., k0_head31], # 32 个注意力头
# Token 1 的 key 向量
[k1_head0, k1_head1, ..., k1_head31],
# ... 100,000 个 tokens
], # Shape: (100000, 32, 128)
"values": [
# 结构与 keys 相同
...
] # Shape: (100000, 32, 128)
},
# Layer 1-39 结构相同
...
]
}
# 存储大小估算(假设 float16)
每层大小 = 100,000 tokens × 32 heads × 128 dim × 2 (K+V) × 2 bytes
= 1.6 GB
40 层总大小 = 1.6 GB × 40 = 64 GB
4.3 为什么缓存 K/V 就能加速?
第一次请求(创建缓存):
# 处理 100,020 个 tokens(小说 + 问题1)
for layer in range(40): # 40 层
for token in all_100020_tokens: # 每个 token
# 1. 计算 K 和 V(耗时!)
k = W_k @ embedding(token) # 矩阵乘法
v = W_v @ embedding(token) # 矩阵乘法
# 2. 保存到缓存
cache.layers[layer].keys.append(k)
cache.layers[layer].values.append(v)
总计算量: 40 层 × 100,020 tokens × 2 (K和V) = 8,001,600 次计算
耗时: ~2.5 秒
第二次请求(使用缓存):
# 检测到前 100,000 tokens(小说)相同
# 仅需处理新增的 20 tokens(问题2)
for layer in range(40):
# 1. 从缓存加载小说部分的 K/V(快速内存读取!)
cached_K = cache.layers[layer].keys # 100,000 个
cached_V = cache.layers[layer].values
# 2. 仅计算新问题的 K/V(仅 20 个 tokens)
new_K = []
new_V = []
for token in new_20_tokens:
k = W_k @ embedding(token)
v = W_v @ embedding(token)
new_K.append(k)
new_V.append(v)
# 3. 拼接
full_K = concat(cached_K, new_K) # (100020, 32, 128)
full_V = concat(cached_V, new_V)
# 4. 使用拼接后的 K/V 进行 Attention 计算
output = attention(Q, full_K, full_V)
总计算量: 40 层 × 20 tokens × 2 = 1,600 次计算
加速比: 8,001,600 / 1,600 = 5000 倍(仅 K/V 计算部分)
实际加速: ~6 倍(因为还有 Attention 计算等其他步骤)
4.4 为什么结果一致?
数学保证:
自注意力的计算公式:
Attention(Q, K, V) = softmax(Q · K^T / √d) · V
这个计算只依赖 K 和 V 的数值,不关心它们来自哪里:
# 场景 1:不使用缓存(全部计算)
K_all = compute_K([小说tokens] + [问题tokens])
V_all = compute_V([小说tokens] + [问题tokens])
result_1 = attention(Q, K_all, V_all)
# 场景 2:使用缓存(拼接)
K_cached = load_from_cache() # 小说部分
K_new = compute_K([问题tokens])
K_all = concat(K_cached, K_new) # 数值完全相同
V_cached = load_from_cache()
V_new = compute_V([问题tokens])
V_all = concat(V_cached, V_new)
result_2 = attention(Q, K_all, V_all)
# 验证
assert result_1 == result_2 # 完全相同!
只要 K/V 矩阵的数值相同,无论是实时计算还是从缓存加载,最终结果都完全一致。
5. 缓存匹配机制
5.1 前缀匹配原理
缓存通过精确的 token 序列前缀匹配来判断是否可以复用。
核心算法:
def find_cache_match(current_tokens, cache_db):
"""
查找与当前请求匹配的最长前缀缓存
"""
best_match = None
max_length = 0
for cache in cache_db:
# 逐个比较 token
match_length = 0
for i in range(min(len(current_tokens), len(cache.tokens))):
if current_tokens[i] == cache.tokens[i]:
match_length += 1
else:
break # 遇到不同就停止
# 必须 >= 1024 tokens 才能缓存
if match_length >= 1024 and match_length > max_length:
max_length = match_length
best_match = cache
return best_match, max_length
5.2 示例分析
第一次问答(创建缓存):
发送到 API:
System: [小说 100,000 tokens] + cache_control
User: [问题1 20 tokens] + cache_control
Token 序列:
[t1, t2, t3, ..., t100000, t100001, ..., t100020]
└──────── 小说 ────────┘ └──── 问题1 ────┘
创建缓存:
Cache_1.tokens = [t1, t2, ..., t100020]
Cache_1.layers[0-39] = 所有 tokens 的 K/V 矩阵
第二次问答(匹配缓存):
发送到 API:
System: [小说 100,000 tokens] + cache_control ← 与第一次相同
User: [问题2 20 tokens] + cache_control ← 不同的问题
Token 序列:
[t1, t2, t3, ..., t100000, t200001, ..., t200020]
└──────── 小说 ────────┘ └──── 问题2 ────┘
↑ 前 100,000 个 tokens 完全相同
匹配过程:
1. 比较 Cache_1.tokens 与当前 tokens
2. 前 100,000 个完全匹配 ✓
3. 第 100,001 个开始不同 ✗
结果:
- 匹配长度: 100,000 tokens
- 从缓存加载: 前 100,000 个 tokens 的 K/V
- 重新计算: 后 20 个 tokens (问题2)
5.3 缓存点的作用
在代码中,我们用 cache_control 标记缓存点:
response = client.messages.create(
system=[{
"type": "text",
"text": book_content, # 10万字小说
"cache_control": {"type": "ephemeral"} # ← 缓存到这里
}],
messages=[{
"role": "user",
"content": [{
"type": "text",
"text": "这本书的主题是什么?",
"cache_control": {"type": "ephemeral"} # ← 可选的第二个缓存点
}]
}]
)
缓存点规则:
如果只在 System 添加 cache_control:
缓存内容 = [System 全部内容]
如果同时在 System 和 User 添加:
缓存内容 = [System 全部内容] + [之前的所有 messages] + [当前 User]
关键:缓存点标记的是"从开头到这里"的所有内容,不是某一段!
# ❌ 错误理解:只缓存这一段
system = [
{"type": "text", "text": "Part A"},
{"type": "text", "text": "Part B", "cache_control": {...}}, # 错误!
{"type": "text", "text": "Part C"}
]
# Part C 不会被缓存
# ✓ 正确:缓存从开头到标记点的所有内容
system = [
{"type": "text", "text": "Part A"},
{"type": "text", "text": "Part B"},
{"type": "text", "text": "Part C", "cache_control": {...}} # 正确!
]
# 缓存 Part A + Part B + Part C
5.4 可视化匹配过程
请求 1: [━━━━━━━ 小说 100K ━━━━━━━][问题1 20]*
创建 Cache_1: 100,020 tokens
请求 2: [━━━━━━━ 小说 100K ━━━━━━━][问题2 20]*
匹配检测:
[━━━━━━━ 小说 100K ━━━━━━━] ← 与 Cache_1 完全匹配 ✓
└─────── 从缓存加载 ──────┘
[问题2 20] ← 重新计算 ✗
结果:
- Cache Read: 100,000 tokens
- Input: 20 tokens
- 总耗时: 0.4 秒
6. 如何使用
6.1 基本用法
import anthropic
client = anthropic.Anthropic(api_key="your_api_key")
# 第一次请求
response_1 = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
system=[{
"type": "text",
"text": book_content, # 长文本内容
"cache_control": {"type": "ephemeral"} # ← 标记缓存点
}],
messages=[{
"role": "user",
"content": "这本书的主题是什么?"
}]
)
# 查看缓存统计
print(f"Input tokens: {response_1.usage.input_tokens}")
print(f"Cache write: {response_1.usage.cache_creation_input_tokens}")
print(f"Cache read: {response_1.usage.cache_read_input_tokens}")
# 输出:
# Input tokens: 100020
# Cache write: 100020 ← 创建了缓存
# Cache read: 0 ← 第一次没有缓存可读
# 第二次请求(5分钟内)
response_2 = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
system=[{
"type": "text",
"text": book_content, # 相同的长文本
"cache_control": {"type": "ephemeral"}
}],
messages=[{
"role": "user",
"content": "主要角色有哪些?" # 不同的问题
}]
)
# 查看缓存统计
print(f"Input tokens: {response_2.usage.input_tokens}")
print(f"Cache write: {response_2.usage.cache_creation_input_tokens}")
print(f"Cache read: {response_2.usage.cache_read_input_tokens}")
# 输出:
# Input tokens: 20 ← 只计算新问题
# Cache write: 100020 ← 更新缓存
# Cache read: 100000 ← 从缓存读取!
6.2 缓存类型
|
|
|
|
|---|---|---|
ephemeral |
|
|
注意:
-
• 5 分钟内没有新请求,缓存会自动失效 -
• 最小缓存大小:1024 tokens -
• 建议每个请求使用 1-2 个缓存点
6.3 监控缓存效果
def analyze_cache_usage(response):
"""分析缓存使用情况"""
usage = response.usage
# 计算总输入
total_input = usage.input_tokens + getattr(usage, 'cache_read_input_tokens', 0)
# 缓存命中率
cache_hit_rate = (
getattr(usage, 'cache_read_input_tokens', 0) / total_input * 100
if total_input > 0 else 0
)
# 成本估算
cost = (
usage.input_tokens * 3.00 / 1_000_000 +
getattr(usage, 'cache_read_input_tokens', 0) * 0.30 / 1_000_000 +
getattr(usage, 'cache_creation_input_tokens', 0) * 3.75 / 1_000_000 +
usage.output_tokens * 15.00 / 1_000_000
)
print(f"总输入: {total_input:,} tokens")
print(f" ├─ 从缓存: {getattr(usage, 'cache_read_input_tokens', 0):,} ({cache_hit_rate:.1f}%)")
print(f" └─ 新计算: {usage.input_tokens:,}")
print(f"缓存写入: {getattr(usage, 'cache_creation_input_tokens', 0):,}")
print(f"估算成本: ${cost:.6f}")
# 使用
analyze_cache_usage(response_2)
# 输出:
# 总输入: 100,020 tokens
# ├─ 从缓存: 100,000 (99.9%)
# └─ 新计算: 20
# 缓存写入: 100,020
# 估算成本: $0.030081
7. 适用场景
7.1 推荐使用的场景
✅ 长文档问答
-
• 基于书籍、论文、技术文档的多次查询 -
• 代码库分析(多次问不同问题)
✅ 固定提示词的应用
-
• 系统提示词很长(如详细的角色设定、规则说明) -
• 工具文档、API 文档等固定参考资料
✅ 批量处理
-
• 对同一份文档的多个问题批量回答 -
• 客服机器人(产品手册固定,用户问题不同)
7.2 不推荐使用的场景
❌ 单次请求
-
• 只问一个问题就结束,无后续请求
❌ 短文本
-
• 上下文少于 1024 tokens(不满足最小缓存大小)
❌ 动态内容
-
• 每次请求的内容都不同(无法匹配缓存) -
• 包含时间戳、随机数等变化内容
7.3 决策树
是否有长文本(> 1K tokens)?
├─ 否 → ❌ 不使用缓存
└─ 是 → 是否有多次请求?
├─ 否 → ❌ 不使用缓存
└─ 是 → 内容是否相对固定?
├─ 否 → ❌ 不使用缓存
└─ 是 → ✅ 使用缓存!
8. 总结
8.1 核心要点
-
1. Prompt Caching 是什么
-
• 缓存大模型计算的中间结果(K/V 矩阵) -
• 通过前缀匹配复用已计算的部分 -
• 只需重新计算新增内容
-
2. 缓存的好处 -
• 速度: 6-7 倍加速(从 2.5 秒降至 0.4 秒) -
• 成本: 90% 折扣(缓存读取仅 0.1× 价格) -
• 适用: 长文档、多次请求、固定内容 -
3. 工作原理 -
• 大模型通过 Transformer 逐层处理 tokens -
• 每层计算 K/V 矩阵最耗时 -
• 缓存保存所有层的 K/V,下次直接加载 -
• 前缀匹配确保精确复用 -
4. 如何使用 -
• 在固定内容末尾添加 cache_control: {type: "ephemeral"} -
• 缓存有效期 5 分钟 -
• 最小 1024 tokens -
• 监控 cache_read_input_tokens确认命中


