跳转至

附录 4A:Prompt Caching

Prompt Caching 是 provider 级别的优化:如果你连续多次请求用了相同的 prompt 前缀,provider 可以跳过那段前缀的计算,直接复用缓存。对 Agent 循环来说,这意味着 system prompt + 工具定义 + few-shot 示例这些每轮不变的部分,只需要在第一轮完整计算一次。

这不是你自己在内存里做的缓存(那个第四章已经讲了)。这是 provider 在推理服务端做的 KV cache 复用。


三家机制对比

OpenAI

OpenAI 的 prompt caching 从 2024 年 10 月开始自动生效,不需要你改代码。

命中条件:

  • 请求的 prompt 前缀与之前某次请求完全相同(逐 token 匹配)
  • 前缀长度至少 1024 token
  • 缓存有效期大约 5-10 分钟(官方未公布精确值,流量大时缓存活得更久)

价格:

  • 缓存命中的 token:按输入价格的 50% 计费
  • 缓存未命中:正常价格

API 响应中的识别方式:

{
  "usage": {
    "prompt_tokens": 2050,
    "completion_tokens": 120,
    "prompt_tokens_details": {
      "cached_tokens": 1024
    }
  }
}

cached_tokens 字段告诉你有多少 token 命中了缓存。如果这个字段是 0 或不存在,就是全 miss。

适合 Agent 的点: ReAct 循环每轮都带同样的 system prompt + 工具定义。假设这部分有 1500 token,跑 6 轮,总输入量里有 9000 token 是重复前缀。缓存命中后,这 9000 token 按半价计费。

Anthropic

Anthropic 的 prompt caching 需要你显式标记哪些 block 要缓存。自动缓存也有,但显式标记更可控。

显式标记方式:

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "你是杭州旅游助手。[省略 500 字的完整 system prompt...]",
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[
        {"role": "user", "content": "明天杭州天气如何?"},
    ],
)

cache_control: {"type": "ephemeral"} 标记这个 block 可以被缓存。

命中条件:

  • 被标记的 block 内容与之前请求完全相同
  • 最小缓存粒度 1024 token(Sonnet/Haiku)或 2048 token(Opus)
  • 缓存有效期 5 分钟(每次命中刷新计时器)

价格(以 Claude Sonnet 为例):

类型 每百万 token 价格 相对比例
首次写入缓存 $3.75(基础 $3 + 25% 写入溢价) 1.25x
缓存读取 $0.30 0.1x
未缓存输入 $3.00 1x

缓存读取只要基础价格的 10%。写入时多付 25%,但只付一次。

使用量反馈:

{
  "usage": {
    "input_tokens": 2150,
    "output_tokens": 503,
    "cache_creation_input_tokens": 1500,
    "cache_read_input_tokens": 0
  }
}

第一次请求 cache_creation_input_tokens = 1500(写入缓存)。第二次请求同样内容时,cache_read_input_tokens = 1500(从缓存读取)。

DeepSeek

DeepSeek 的缓存策略更像 OpenAI 的自动模式,但折扣更大。

命中条件:

  • 前缀匹配,自动生效
  • 最小前缀长度根据模型不同,通常 64 token 起(比 OpenAI 宽松很多)
  • 缓存有效期未公开明确数字,实测约几分钟

价格(以 DeepSeek-V3 为例):

类型 每百万 token 价格
缓存命中输入 $0.014
缓存未命中输入 $0.14
输出 $0.28

缓存命中是未命中价格的 1/10


旅游助手的缓存收益计算

假设 ReAct 循环跑 6 轮,每轮的 messages 结构如下:

轮次1: [system(1500t)] + [user(200t)]                              → 输入 1700t
轮次2: [system(1500t)] + [user(200t)] + [asst(100t)] + [tool(300t)] → 输入 2100t
轮次3: [system(1500t)] + [历史(600t)] + [asst(100t)] + [tool(300t)] → 输入 2500t
轮次4: [system(1500t)] + [历史(1200t)]                              → 输入 2700t
轮次5: [system(1500t)] + [历史(1800t)]                              → 输入 3300t
轮次6: [system(1500t)] + [历史(2400t)]                              → 输入 3900t

6 轮总输入 token:16200

其中 system prompt 每轮重复:1500 * 6 = 9000 token

不用缓存(OpenAI GPT-4o,$2.50/M 输入 token):

total_input_tokens = 16200
cost_no_cache = total_input_tokens * 2.50 / 1_000_000
print(f"无缓存成本: ${cost_no_cache:.4f}")
无缓存成本: $0.0405

用缓存(假设轮次 2-6 全部命中 system prompt 缓存):

uncached_tokens = 16200 - (1500 * 5)  # 第一轮全价,后 5 轮 system 缓存
cached_tokens = 1500 * 5

cost_uncached_part = uncached_tokens * 2.50 / 1_000_000
cost_cached_part = cached_tokens * 1.25 / 1_000_000  # 50% 折扣
cost_with_cache = cost_uncached_part + cost_cached_part

savings_pct = (1 - cost_with_cache / cost_no_cache) * 100

print(f"有缓存成本: ${cost_with_cache:.4f}")
print(f"节省: {savings_pct:.1f}%")
有缓存成本: $0.0315
节省: 22.2%

同样场景用 Anthropic Claude Sonnet($3/M 输入,缓存读取 $0.30/M):

cost_no_cache_claude = 16200 * 3.00 / 1_000_000

uncached_tokens_claude = 16200 - (1500 * 5)
cached_tokens_claude = 1500 * 5
cache_write_cost = 1500 * 3.75 / 1_000_000  # 第一轮写入

cost_uncached_claude = uncached_tokens_claude * 3.00 / 1_000_000
cost_cached_claude = cached_tokens_claude * 0.30 / 1_000_000  # 读取

cost_with_cache_claude = cost_uncached_claude + cost_cached_claude + cache_write_cost
savings_pct_claude = (1 - cost_with_cache_claude / cost_no_cache_claude) * 100

print(f"Claude 无缓存: ${cost_no_cache_claude:.4f}")
print(f"Claude 有缓存: ${cost_with_cache_claude:.4f}")
print(f"节省: {savings_pct_claude:.1f}%")
Claude 无缓存: $0.0486
Claude 有缓存: $0.0329
节省: 32.3%

Anthropic 的缓存读取折扣更大(90% off),所以循环轮次越多,省得越多。


缓存命中的实践要点

什么能命中

缓存靠前缀匹配。所以 messages 数组的前面部分越稳定,命中率越高。

✅ 容易命中:
  messages = [system_prompt, fewshot_1, fewshot_2, ..., user_message]
  ↑ 这段前缀不变 ────────────────────────────↑  只有这里变

❌ 难以命中:
  messages = [system_prompt_with_dynamic_date, ..., user_message]
  ↑ 每天的日期不同,前缀就变了,缓存失效

实操建议

  1. System Prompt 放最前面,内容别动态化——如果你在 system prompt 里插了当前日期、当前用户名,每次请求前缀都不同,缓存永远 miss。把动态信息放在 user message 里。

  2. Few-shot 示例固定顺序——调换示例顺序就是不同前缀。

  3. 工具定义放在 system prompt 里且保持稳定——如果工具列表经常变,考虑把不变的工具放前面,动态工具放后面。

  4. 监控 cached_tokens——不看这个字段,你永远不知道缓存到底有没有生效。写一个简单的计数器:

class CacheHitTracker:
    def __init__(self):
        self.total_input_tokens = 0
        self.total_cached_tokens = 0

    def record(self, usage: dict):
        self.total_input_tokens += usage.get("prompt_tokens", 0)
        cached = usage.get("prompt_tokens_details", {}).get("cached_tokens", 0)
        self.total_cached_tokens += cached

    def hit_rate(self) -> float:
        if self.total_input_tokens == 0:
            return 0.0
        return self.total_cached_tokens / self.total_input_tokens

tracker = CacheHitTracker()

# 模拟 6 轮 ReAct 的 usage
tracker.record({"prompt_tokens": 1700, "prompt_tokens_details": {"cached_tokens": 0}})
tracker.record({"prompt_tokens": 2100, "prompt_tokens_details": {"cached_tokens": 1500}})
tracker.record({"prompt_tokens": 2500, "prompt_tokens_details": {"cached_tokens": 1500}})
tracker.record({"prompt_tokens": 2700, "prompt_tokens_details": {"cached_tokens": 1500}})
tracker.record({"prompt_tokens": 3300, "prompt_tokens_details": {"cached_tokens": 1500}})
tracker.record({"prompt_tokens": 3900, "prompt_tokens_details": {"cached_tokens": 1500}})

print(f"总输入 token: {tracker.total_input_tokens}")
print(f"缓存命中 token: {tracker.total_cached_tokens}")
print(f"缓存命中率: {tracker.hit_rate():.1%}")

输出:

总输入 token: 16200
缓存命中 token: 7500
缓存命中率: 46.3%

46.3% 的命中率意味着接近一半的输入 token 在享受折扣。


三家对比总结

维度 OpenAI Anthropic DeepSeek
触发方式 自动 显式标记 + 自动 自动
最小前缀 1024 token 1024/2048 token ~64 token
缓存折扣 50% off 90% off(读取) 90% off
写入溢价 25%
有效期 ~5-10 分钟 5 分钟(命中刷新) 未公开
识别方式 cached_tokens cache_read_input_tokens 类似 OpenAI

选择建议:

  • ReAct 循环轮次多(>5 轮)、system prompt 长(>1500 token):Anthropic 的 90% 折扣最划算
  • 短前缀、高频调用:DeepSeek 的 64 token 门槛最低
  • 什么都不想改:OpenAI 自动生效,零代码改动

返回 第四章