跳转到内容

提示缓存优化策略

提示缓存优化策略

学习目标

读完本章后,你将能够:

  • 理解前缀匹配缓存的工作原理和失效规则
  • 正确放置缓存断点(cache_control breakpoints)
  • 诊断静默失效(silent invalidation)问题
  • 验证缓存命中情况并计算缓存命中率
  • 设计缓存感知的提示结构

核心概念

前缀匹配原理

提示缓存基于精确字节前缀匹配

关键规则

  1. 任何字节变化:前缀中任意字节的改变会使该断点及之后的所有缓存失效
  2. 渲染顺序toolssystemmessages
  3. 稳定优先:将不变的内容放在前面,变化的内容放在后面

为什么顺序很重要?

假设你的请求包含:系统提示 + 工具定义 + 用户消息。如果每次请求时工具定义都不同(如动态工具列表),但系统提示不变——把系统提示放在工具定义之前,系统提示的缓存仍然有效。


缓存断点放置

自动缓存

通过 cache_control: {type: "ephemeral"} 标记断点:

{
"system": [
{"type": "text", "text": "稳定的系统提示...", "cache_control": {"type": "ephemeral"}}
]
}

限制

  • 每个请求最多 4 个断点
  • 最小可缓存前缀约 1024 tokens(短于此时静默不缓存)

断点放置策略

位置建议原因
工具定义末尾✅ 放置工具列表通常稳定
系统提示末尾✅ 放置系统提示固定不变
消息列表末尾✅ 放置消息是变化的内容
动态内容中间❌ 避免每次变化使之后全部失效

最佳实践:缓存感知提示结构

[固定] 系统提示核心部分
[固定] 工具定义
↓ cache_control 断点 1
[半固定] 对话历史(前 N 轮)
↓ cache_control 断点 2
[变化] 最新用户消息

静默失效诊断

什么是静默失效?

静默失效指缓存看似正常但实际未被命中,不会报错,只是不会返回缓存数据。

常见静默失效原因

原因症状修复方法
datetime.now() 在系统提示中每次请求时间戳不同将时间戳移到消息部分
未排序的 JSON/dict每次序列化顺序不同使用 sort_keys=True
变化的工具集每次请求工具不同固定工具集或放在断点后
隐藏字符/编码差异肉眼相同但字节不同确保 UTF-8 编码一致

审计清单

每次缓存命中率为零时,检查:

  1. 系统提示中是否有动态内容? 时间戳、请求 ID、随机数
  2. 工具列表是否每次不同? 动态过滤工具会破坏缓存
  3. JSON 序列化是否确定? 使用排序后的 key
  4. SDK 版本是否一致? 不同 SDK 版本可能改变请求结构

验证缓存命中

检查 cache_read_input_tokens

response = client.messages.create(...)
print(response.usage.cache_read_input_tokens)
含义
0缓存未命中(或首次请求,或前缀太短)
> 0缓存命中,值为命中的 token 数
creation_tokens缓存写入的 token 数(首次请求)

缓存命中率计算

缓存命中率 = cache_read_input_tokens / (cache_read_input_tokens + input_tokens)

注意input_tokens 是本次请求中未命中的部分。


进阶:Task Budgets(Opus 4.7)

Opus 4.7 引入了 Task Budgets,与提示缓存结合使用:

output_config: {
task_budget: {
type: "tokens",
total: 100000 # 整个 Agent 循环的 Token 预算
}
}
  • 模型可以看到运行中的 Token 倒计时,自动调节输出长度
  • 不同于 max_tokens(硬性上限,模型不知道)
  • 最低 20,000 tokens

常见陷阱与对策

前缀太短

问题:标记缓存的前缀少于 1024 tokens,静默不缓存。

对策:确保第一个断点之前至少有 1024 tokens 的稳定内容。

动态内容污染前缀

问题:时间戳、请求 ID、随机种子等动态内容放在系统提示开头。

对策:将系统提示拆分为”固定部分 + 动态变量”,固定部分在前,动态变量放在消息中。

断点浪费

问题:在每次都变化的内容上设置断点。

对策:只对稳定内容设置断点。通常 2-3 个断点足够。

误读缓存指标

问题:只关注 cache_read_input_tokens,忽略 cache_creation_input_tokens

对策:首次请求的 cache_read 为零是正常的——此时正在写入缓存。检查后续请求是否命中。


参考来源