如果你的 LLM 调用账单正在失控,或者你发现缓存命中率长期徘徊在 20% 以下,这篇文章就是为你准备的。基于 ProjectDiscovery 的真实案例——三个月内将缓存命中率从 7% 提升至 84%,累计节省 59% 的 LLM 成本——我将拆解背后的工程决策与实现细节,帮你把 AI 推理成本真正降下来。

一、缓存为何失效?从 KV Cache 的底层逻辑说起

很多团队把 Prompt 缓存当作黑盒,只关心“命中率”这个数字,却不理解其内部机制。在 Transformer 架构中,每个 token 都会生成一对 Key-Value 张量,用于自注意力计算。这是神经网络推理中最重的计算之一,也是延迟和成本的主要来源。

KV Cache 的核心思想很直接:如果下一次请求的前缀部分与上次完全相同,就可以直接复用已算好的 KV 张量,跳过重复计算。复用的前提是字节级别的精确匹配——不是语义相似,而是每个字符、每个空格都必须一致。一个多余的空格、一个不同的时间戳,都会导致缓存完全失效。

这意味着,任何动态内容一旦混入 prompt 前缀,就会让整个缓存失效。理解了这一点,后续所有优化策略都变得顺理成章。

二、两大主流 Provider 的缓存机制对比

不同的 AI 服务商对 Prompt 缓存的实现方式截然不同,选择正确的策略前,必须了解它们的差异。

Anthropic Claude:手动标记,精确控制

Claude 要求开发者显式地在内容块上添加 cache_control 字段,来标记可缓存的片段。具体实现如下:

import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是一个代码审查专家...",
"cache_control": {"type": "ephemeral"}
}
],
messages=[
{"role": "user", "content": "帮我审查这段代码"}
]
)
usage = response.usage
print(f"写入缓存: {usage.cache_creation_input_tokens}")
print(f"从缓存读取: {usage.cache_read_input_tokens}")

关键约束包括:最小缓存单元为 1024 token,最多支持 4 个断点,TTL 最短 5 分钟(频繁访问可延长至 1 小时)。其定价逻辑如下:

类型价格(每百万 token)
标准输入$3.00
缓存写入(首次)$3.75(+25%)
缓存读取(命中)$0.30(-90%)

从成本角度看,Break-even 点在 1.4 次命中——只要同一个前缀被使用两次以上,缓存就是划算的。这为高频调用场景提供了显著的成本优势。

OpenAI:全自动模式,零代码改动

OpenAI 则采用完全自动化的方式:任何超过 1024 token 的 prompt 前缀都会自动进入缓存,无需开发者干预。其 API 调用示例如下:

from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一个代码审查专家...(很长的系统提示)"},
{"role": "user", "content": "审查这段代码..."}
]
)
usage = response.usage
cached = usage.prompt_tokens_details.cached_tokens
total_prompt = usage.prompt_tokens
hit_rate = cached / total_prompt if total_prompt > 0 else 0
print(f"缓存命中率: {hit_rate:.1%}")

缓存按 128 token 粒度命中,TTL 约为 5-10 分钟。价格方面,命中部分享受 50% 折扣,且无写入溢价。对于快速迭代的团队,OpenAI 的自动模式显然更省心。

三、Prompt 结构即缓存架构:五个最贵的习惯

无论使用哪家 Provider,核心原则只有一句话:越静态的内容越靠前,越动态的内容越靠后。下面是一个理想的 prompt 结构模板:

┌─────────────────────────────────────┐
│  System Prompt(所有请求共享)        │  ← 最稳定,最靠前,打缓存断点
├─────────────────────────────────────┤
│  工具定义(工具集固定时)              │  ← 次稳定,打缓存断点
├─────────────────────────────────────┤
│  检索文档 / 上下文资料                │  ← 对话内共享
├─────────────────────────────────────┤
│  对话历史(随对话增长)               │  ← 动态增长
├─────────────────────────────────────┤
│  当前用户消息                        │  ← 每次不同,最靠后
└─────────────────────────────────────┘

但在实际工程中,以下五个习惯会悄悄摧毁你的缓存命中率:

  1. 在 system prompt 中注入时间戳——这会让前缀每次变化,直接导致缓存 miss。示例:
# ❌ 每次前缀都不同,永远 miss
system = f"当前时间:{datetime.now()}。你是一个助手..."
# ✅ 时间放到 user message
system = "你是一个助手..."
user = f"[当前时间:{datetime.now()}]\n用户问题:{question}"
  1. 注入用户 ID 或请求 ID——这些动态值会破坏前缀的稳定性。正确做法是放在 user message 中。
  2. 随机化 few-shot examples 顺序——固定顺序,按质量排序后保持不变,避免每次生成新的排列。
  3. 每次动态生成工具定义——建议使用 json.dumps 序列化后缓存字符串,直接复用,而不是每次重新生成。
  4. 在 system prompt 开头放用户配置——静态部分前置,用户配置后置,确保前缀稳定。

⚠️ 这些习惯看似微小,但累积起来可能让你的缓存命中率暴跌至 20% 以下。

四、Agent 系统的三断点架构:从 20% 到 84% 的飞跃

ProjectDiscovery 的核心突破在于引入了“三断点”架构,将 prompt 分为三个缓存区域:

import json
def build_agent_messages_v2(system_prompt, tool_definitions,
conversation_history, working_memory, user_message):
"""三断点架构:最大化缓存命中率"""
# 断点1:静态系统提示(最稳定,TTL ~1小时)
system = [
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"}
}
]
# 断点3:工具定义(工具集不变时极稳定)
tool_defs_text = json.dumps(tool_definitions, ensure_ascii=False, sort_keys=True)
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": f"<tools>\n{tool_defs_text}\n</tools>",
"cache_control": {"type": "ephemeral"}  # 断点3
},
{"type": "text", "text": "[对话开始]"}
]
},
{"role": "assistant", "content": "好的,我准备好了。"}
]
# 对话历史(断点2 = 最近N轮)
if conversation_history:
messages.extend(conversation_history[:-1])
last_hist = conversation_history[-1].copy()
if isinstance(last_hist.get("content"), str):
last_hist["content"] = [{
"type": "text",
"text": last_hist["content"],
"cache_control": {"type": "ephemeral"}  # 断点2
}]
messages.append(last_hist)
# 工作内存 + 当前用户消息(动态,不打断点,放最后)
current_content = ""
if working_memory:
current_content += f"<working_memory>\n{working_memory}\n</working_memory>\n\n"
current_content += user_message
messages.append({"role": "user", "content": current_content})
return system, messages

其中最关键的是 Relocation Trick:将工作内存从 system prompt 末尾移到 user message 末尾。工作内存每步都在变化,如果放在 system prompt 尾部,会导致整个 20K token 的系统提示缓存每步失效。移到 user message 之后,system prompt 缓存就能稳定命中。

仅此一个改动,命中率从 <20% 直接跃升至约 74%。后续通过优化工具定义和 few-shot 顺序,最终稳定在 84%。

五、并发陷阱与冷启动:不可忽视的工程细节

在分布式环境中,并发请求可能导致缓存竞争问题。例如,多个实例同时写入同一个缓存键,可能造成数据不一致。解决方案是采用分布式锁或原子操作:

T=0ms: 请求A → 缓存 miss,开始写入
T=2ms: 请求B → 缓存还在写,miss
T=5ms: 请求C → 命中!

此外,服务启动时的冷启动问题同样重要。建议在服务启动时预热缓存,提前发送一次相同的请求,确保后续请求能命中:

async def warm_up_cache(client, system_prompt, tool_definitions):
response = await client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1,
system=[{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"}
}],
messages=[{"role": "user", "content": "ping"}]
)
print(f"预热完成,写入 {response.usage.cache_creation_input_tokens} token")

冷启动优化对于高频服务尤其关键,可以显著降低首请求延迟。

六、监控指标体系:让命中率成为产品指标

要持续优化缓存,必须建立完善的监控体系。核心指标包括:缓存命中 token 数、缓存写入 token 数、命中率、TTL 分布、以及缓存失效原因分析。推荐使用以下监控代码模板:

from dataclasses import dataclass
@dataclass
class CacheMetrics:
cache_creation_tokens: int = 0
cache_read_tokens: int = 0
regular_input_tokens: int = 0
request_count: int = 0
def record(self, usage):
self.request_count += 1
self.cache_creation_tokens += getattr(usage, 'cache_creation_input_tokens', 0)
self.cache_read_tokens += getattr(usage, 'cache_read_input_tokens', 0)
self.regular_input_tokens += getattr(usage, 'input_tokens', 0)
@property
def hit_rate(self) -> float:
total = self.cache_creation_tokens + self.cache_read_tokens + self.regular_input_tokens
return self.cache_read_tokens / total if total > 0 else 0
def report(self):
print(f"命中率: {self.hit_rate:.1%}")
if self.request_count > 100 and self.hit_rate < 0.5:
print("⚠️  命中率低于 50%,建议检查 prompt 结构")
elif self.hit_rate >= 0.7:
print("✅ 命中率健康(>70%)")

目标基线:生产系统命中率应大于 70%。如果低于此值,说明 prompt 结构或动态内容管理仍有优化空间。

实践总结与自检清单

优化 Prompt 缓存并非一次性工作,而是一个持续迭代的工程过程。以下三条核心原则值得牢记:

  • 静态内容前置:越稳定的内容越靠前,确保前缀可缓存。
  • 动态内容后置:越易变的内容越靠后,避免破坏缓存。
  • 持续监控命中率:把它当作产品指标,定期审查和优化。

最后,附上一份 5 分钟自检清单,帮助你快速定位问题:

  • System prompt 里有没有时间戳或用户 ID?
  • 工具定义是不是每次动态生成?
  • System prompt 超过 1024 token 了吗?
  • 有没有监控缓存命中 token?
  • Agent 系统里工作内存放在 prefix 中间还是末尾?

参考资料

  1. How We Cut LLM Costs by 59% With Prompt Caching — ProjectDiscovery, 2026
  2. Prompt Caching Infrastructure — Introl, 2026
  3. Prompt Caching 201 — OpenAI, 2026

[AFFILIATE_SLOT_1] 如果你正在寻找高效的 LLM 成本管理工具,不妨参考一些成熟的 APM 平台,它们能帮助你自动化监控和优化缓存策略。

在 AI 和深度学习领域,Prompt 缓存优化是自然语言处理工程落地中常被忽视的环节。通过本文的方法,你不仅能显著降低 LLM 调用成本,还能提升系统响应速度。记住:缓存不是银弹,但正确的工程实践足以让成本砍半。

[AFFILIATE_SLOT_2] 想要更深入的学习资源?推荐关注相关技术社区,获取最新的 Prompt 工程最佳实践。