Prompt cache 命中率从 60% 拉到 95% 的 4 个偏方
t2 那篇讲了怎么启用 prompt cache——5 分钟 TTL、命中 0.1x 价、账单砍 60%。但实际上线后我命中率卡在 60%——因为很多请求的 cache 前缀**差几个字符**就 miss 了。这篇讲怎么把命中率拉到 95%,实测多砍 28% 账单。
实测环境:Claude Sonnet 4.5 · prompt 7.1K token · 50K 请求/天 · 优化前命中率 60% → 优化后 95% · 账单多砍 28%
为什么 t2 的 cache 跑不到 95%
t2 那篇讲了怎么启用 cache——加 cache_control: {type: "ephemeral"},让 Anthropic 自动缓存。但实际上线后:
优化前账单 (60% 命中率):
Cache miss: 50K × 70% × $3/M = $105/天
Cache hit: 50K × 30% × $0.3/M = $4.5/天
小计: $109.5/天
为什么只有 60% 命中?我排查了 4 天,发现 4 个”看着不起眼但致命”的坑。
坑 1:System prompt 里有时间戳
我的爬虫 agent system prompt 写了:
SYSTEM_PROMPT = f"""
你是 X 爬虫 agent。
当前时间:{datetime.now().isoformat()}
用户时区:{user.timezone}
"""
结果:每次请求 system prompt 都带不同的”当前时间”和”用户时区”。Anthropic cache 要求前 N 个 token 完全相同——这俩字段直接让我 cache miss。
修复:把动态信息挪到 user message,不进 cache 块。
# 错的写法:动态信息在 system prompt(不进 cache)
system = f"当前时间:{datetime.now().isoformat()}"
# 对的写法:动态信息在 user message
system = "你是 X 爬虫 agent。" # 稳定,进 cache
user_msg = f"用户问题(当前时间 {datetime.now().isoformat()}):{question}"
实测:改完命中 60% → 78%。
坑 2:Tools 数组顺序不一致
# 错的写法:tools 顺序动态变化
def get_tools(user):
tools = [CORE_TOOL]
if user.is_admin:
tools.append(ADMIN_TOOL)
if user.has_pro:
tools.append(PRO_TOOL)
return tools
# 同一个 user.is_admin 应该是 True,但 tools 顺序每次都重新构造
结果:tool 描述的 token 顺序变了 → cache miss。
修复:固定 tools 顺序,禁用项传 disabled: true 而不是移除。
def get_tools(user):
return [
{**CORE_TOOL, "input_schema": ...},
{**ADMIN_TOOL, "disabled": not user.is_admin},
{**PRO_TOOL, "disabled": not user.has_pro},
]
实测:改完命中 78% → 88%。
坑 3:Few-shot 示例里用了随机数据
# 错的写法:每次随机抽 few-shot 示例
import random
EXAMPLES = [
{"q": "...", "a": "..."},
{"q": "...", "a": "..."},
# ...
]
def build_prompt(question):
examples = random.sample(EXAMPLES, 3) # 每次都不同
return f"示例:{examples}\n问题:{question}"
结果:few-shot 顺序变了 → cache miss。
修复:固定 few-shot 顺序,或者干脆不放进 cache 块。
def build_prompt(question):
# few-shot 放 cache 块外面(不进 cache)
examples_text = "\n".join([f"Q: {e['q']}\nA: {e['a']}" for e in EXAMPLES])
return f"""[示例 - 不进 cache]
{examples_text}
[问题 - 进 cache]
{question}"""
实测:改完命中 88% → 92%。
坑 4:Cache TTL 边界效应
Anthropic 默认 TTL 是 5 分钟。如果你的请求是均匀分布的,命中率高;如果是突发分布(比如 1 分钟集中发 100 个请求,4 分钟空闲),TTL 边界会浪费。
时间线 (TTL 5min):
00:00:00 - 第一次请求,cache write
00:00:30 - 第二次,cache hit
00:01:00 - 第 N 次,cache hit
00:05:01 - 第一次 cache 过期!下次请求又要 cache write
00:05:30 - 第 N+1 次请求,cache miss → 重新 write
00:10:01 - 又过期
修复:用 1 小时 extended cache(beta)+ 自己控制 TTL。
# Anthropic 1h extended cache (beta)
response = client.beta.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=8192,
system=[
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral", "ttl": "1h"} # 1 小时 TTL
}
],
messages=[...]
)
| TTL | Cache hit 价 | 适用场景 |
|---|---|---|
| 5min (默认) | $0.30/M | 请求均匀分布 |
| 1h (extended) | $0.60/M | 请求突发分布 |
注意:1h 版本命中价是 5min 的 2 倍($0.60 vs $0.30),但减少 cache write 次数。
实测(我的场景:每分钟 50 个请求 burst):
- 5min TTL:命中 92%,write 频繁
- 1h TTL:命中 95%,write 极少
- 1h 版本账单反而更低(因为 write 价 $3.75/M 比 hit 价 0.30/M 高)
改完命中 92% → 95%。
优化后账单对比
| 项 | 优化前 (60%) | 坑 1+2 修 | 坑 3 修 | 坑 4 修(最终) |
|---|---|---|---|---|
| 命中率 | 60% | 88% | 92% | 95% |
| Cache miss ($3/M) | 50K×40% = 20K | 6K | 4K | 2.5K |
| Cache write ($3.75/M) | 50K×60% = 30K | 30K | 30K | 12.5K (1h) |
| Cache hit ($0.30/M 或 $0.60/M) | 50K×60% = 30K | 44K | 46K | 47.5K ($0.60) |
| 日账单 | $109.5 | $80.4 | $75.6 | $66.0 |
优化前: $109.5/天 = $3285/月
优化后: $66/天 = $1980/月
节省: $1305/月 (40% reduction)
对比 t2 那篇”启用 cache 砍 60%“——再叠加命中率优化,账单从 $3000/月 → $1980/月。
给也想优化 cache 的 4 条建议
-
System prompt 不放动态信息——时间戳、用户 ID、随机数都挪到 user message。这一条单独就能把命中拉到 78%。
-
固定 tools 数组顺序——禁用项用
disabled: true而不是删除。同一个 user 的 tools 顺序每次必须完全一致。 -
Few-shot 示例不放 cache 块——或者固定顺序。随机抽 few-shot 会让 cache miss。
-
突发场景用 1h extended cache——$0.60/M 比 $0.30/M 贵,但减少 write 次数反而省钱。请求均匀用 5min,请求突发用 1h。
现场:优化前后 cache 日志对比
优化前(60% 命中):
[10:00:00] cache miss - write
[10:00:01] cache hit
[10:00:02] cache hit
[10:00:30] cache hit (last)
[10:05:01] cache miss - write ← TTL 过期
[10:05:02] cache hit
[10:10:01] cache miss - write
优化后(95% 命中 + 1h TTL):
[10:00:00] cache miss - write
[10:00:01-10:59:59] cache hit (几乎全部命中)
[11:00:01] cache miss - write ← 1h TTL 过期
[11:00:02-11:59:59] cache hit
每天 cache write 次数:30K → 12.5K(减少 58%)。
附:完整的 cache 优化 checklist
# 检查清单(任何一项不符合都会降低命中率)
CHECKS = {
"system_prompt_no_dynamic": "system prompt 里没有 datetime / uuid / 随机数",
"tools_order_fixed": "tools 数组对同一 user 永远顺序一致",
"few_shot_static": "few-shot 示例顺序固定,或放在 cache 块外",
"cache_control_blocks": "在 messages 里正确放置 cache_control",
"ttl_appropriate": "5min 用于均匀请求,1h 用于突发请求",
"monitor_cache_hit_rate": "每日记录 cache_creation_input_tokens vs cache_read_input_tokens"
}
# 命中率计算
def cache_hit_rate(usage):
read = usage.cache_read_input_tokens
write = usage.cache_creation_input_tokens
miss = usage.input_tokens - read - write
return read / (read + miss) if (read + miss) > 0 else 0
这套 checklist 跑了 2 周,命中率稳定在 94-96%。
下一篇 T8 讲 tool nesting 的 5 个坑——比 T3 streaming cost 更深入一层,专门讲嵌套调用的稳定性。
— 怪招本 #012 · 2026-06-28