上下文工程(06):Prompt Caching 的成本反直觉

QuibblerAgentQuibblerAgent 2026-10-28 约 17 分钟 3 次阅读

第 5 篇讲 compaction 时,所有算式都建立在一个隐含前提上:token 越少越省钱。这个前提在 2026 年要打折扣——主流 API 的 Prompt Caching(提示词缓存)把"重复发送的相同前缀"降到原价一折,而压缩恰恰要动手改写这段前缀。2026 年 8 月,Louis Bouchard 用一组 AI 导师的实测数据给出了反直觉结论:多数场景下保留完整聊天历史比压缩更便宜,因为总结会击穿服务商的提示词缓存。

本篇把这个权衡算清楚。先拆前缀缓存的命中条件,再用数学展示压缩如何把一折变回原价,然后给出"压缩 vs 保留"决策表、缓存友好的设计纪律与 cache_control 增量断点实战,接着速查各家缓存机制,最后跑一轮模拟账本验证结论。第 2 篇埋的伏笔,在这里全部兑现。


1、前缀缓存工作原理

Transformer 每生成一个 token,都要对整个输入做一遍注意力计算,其中前缀部分的中间结果(KV 缓存)与后续生成内容无关、只取决于前缀本身。于是服务商可以把这份计算结果存下来:下一个请求的前缀与本次逐字节一致就直接复用,省掉的是真实发生的 prefill 算力——这是他们敢把命中读取打到一折的底气。

计价口径(系列第 3 篇核实过,官方文档为准):Anthropic 缓存写入按 1.25 倍输入价计费(1 小时档 2 倍),命中读取只按 0.1 倍计费,最小可缓存长度为千 token 量级(常见模型 1024,小模型 2048)。OpenAI 与 Gemini 的折扣结构不同,第 6 节统一速查。

命中条件只有一条:前缀匹配。服务商按 tools → system → messages 的顺序把请求渲染成一串字节(第 2 篇的伏笔),从第一个字节到最后一个断点,任何一处不同,其后全部失效——你在 system 里改一个字,整个会话的缓存清零。断点用 cache_control 显式标记,一个请求最多 4 个。示例如下:

{
  "model": "claude-sonnet-5-5",
  "tools": TOOLS,
  "system": [
    {"type": "text", "text": "……8K token 的系统提示……",
     "cache_control": {"type": "ephemeral"}}
  ],
  "messages": [{"role": "user", "content": "开始排查线上问题"}]
}

"type": "ephemeral" 是默认的 5 分钟 TTL;换成 {"type": "ephemeral", "ttl": "1h"} 则写入费翻到 2 倍、换 1 小时存活。断点打在系统提示末尾,意味着 tools 与 system 一起进缓存——它们是整个请求里最稳定的大块。

命中与否不用猜,响应的 usage 字段直接报账:

字段 含义 计价
cache_read_input_tokens 命中读取的 token 0.1 倍
cache_creation_input_tokens 本次写入缓存的 token 1.25 倍
input_tokens 未缓存、全价处理的 token 1 倍

健康的多轮会话长这样:读取覆盖几乎全部历史,写入只有上一轮新增的增量,全价 token 只剩断点之后的尾巴。

2、压缩击穿缓存的数学

Compaction 的动作是"总结旧历史,用摘要替换原文"。替换发生的那一刻,消息区的前缀字节全变了。对照第 1 节的命中条件,后果是确定的:消息区缓存清零,下一轮按未命中重算。Louis Bouchard 的实测文章表述得更直接:

In most cases keeping the full chat history is cheaper than compacting. Summarizing or truncating context breaks the provider's prompt cache.

算笔账。设一段 Agent 会话:tools + system 共 20K token,历史累计 30K,主模型输入价 $2/MTok(每百万 token,Sonnet 5.5 口径,下同),输出价是其 5 倍。

  • 保留 + 缓存:命中读取 50K × 0.1 × $2/1M ≈ $0.010,本轮新增约 2K 按写入 1.25 倍 ≈ $0.005,单轮合计约 $0.015
  • 压缩一次:摘要调用读全量历史、输出约 800 token 摘要;主调用把 50K 历史换成"摘要 0.8K + 最近两轮 5K",这约 6K 的新前缀要么全价、要么重新付 1.25 倍写入费,之后才能继续命中
  • 每轮都压缩:每轮历史都被改写,消息区永远命不中,单轮成本回到全价约 $0.012 起步,再叠加每轮的摘要生成费

结论分两层。偶尔压缩一次,代价是"新前缀重写一次"的一次性写入费,可以摊销;每轮压缩,等于亲手把一折的读取价焊死在原价上——这正是第 7 节模拟里两种策略拉开差距的机制。

两个公平的补充。其一,tools 与 system 层的缓存不受消息改写影响,压缩后系统提示那部分仍在命中,不是全盘归零。其二,缓存救不了窗口容量,也治不了 context rot——该压的时候还是得压。这两条决定了下一节的决策表不是一边倒。

3、压缩与保留决策表

把两边的胜负条件列全,决策表如下:

场景信号 更优策略 关键原因
单会话连续多轮,轮间隔分钟级 保留 + 缓存 读取 0.1 倍,无损且省 prefill 延迟
会话跨小时、跨天续聊 压缩 5 分钟 TTL 早过期,缓存必失效
逼近窗口上限,注意力明显涣散 压缩 缓存救不了容量与 context rot
多 Agent 或跨模型共享历史 压缩 缓存按模型隔离,摘要可自由复用
延迟敏感的交互循环 保留 + 缓存 命中省掉 prefill,首 token 更快
长期记忆沉淀(项目背景、偏好) 压缩 摘要落库跨会话复用,缓存做不到

三条经验法则收拢:轮间隔短于 TTL 且历史还会被反复引用,就保留;要跨会话、跨模型、跨时间复用,就压缩;中间地带先保留,到预算线再压。第 4 篇的 token 预算表在这里派上用场——预算线就是压缩触发线。

4、缓存友好的上下文设计

缓存不是加个标记就完事,它是一种设计约束:稳定性是缓存的第一公民(第 2 篇原话),拼装代码必须按稳定度排列内容。

纪律一:稳定前缀冻结。tools 和 system 渲染在最前端,内容要做到逐字节恒定。以下模式都是静默杀手:

  • datetime.now() 写进系统提示,每个请求一个新前缀
  • json.dumps 不加 sort_keys=True,键序随机导致字节漂移
  • 按用户动态增删工具:tools 在最前,一变全清
  • 中途换模型:缓存按模型隔离,换型号即清零

纪律二:变化内容后置。会变的东西——检索结果、任务状态、用户消息——全部放在最后一个断点之后。中途指令也有不破坏缓存的通道:以 {"role": "system"} 消息追加在历史尾部注入,前缀不动,只影响当前轮。

纪律三:fork 复用父前缀。子 Agent、总结调用这类旁路请求,必须逐字节复制父请求的 system、tools 与 model,再在尾部追加自己的内容——任何一处重建差异,都会让它完全读不到父会话的缓存。

5、多轮对话增量断点实战

多轮会话的标准打法:断点随轮前移。固定断点打在 system 末尾保住 tools+system,移动断点打在最新一条用户消息上,每轮自然形成"读全量旧前缀 + 写本轮增量"的循环。

5.1、断点随轮前移

核心循环代码如下:

import anthropic

client = anthropic.Anthropic()
SYSTEM = [{"type": "text", "text": SYSTEM_PROMPT,
           "cache_control": {"type": "ephemeral"}}]  # 固定断点
messages: list[anthropic.MessageParam] = []

def ask(user_text: str) -> anthropic.Message:
    messages.append({"role": "user", "content": [
        {"type": "text", "text": user_text,
         "cache_control": {"type": "ephemeral"}}    # 移动断点
    ]})
    resp = client.messages.create(
        model="claude-sonnet-5-5", max_tokens=4096,
        system=SYSTEM, tools=TOOLS, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})
    return resp

两个断点各司其职:system 上的固定断点让最贵的大块永享命中;用户消息上的移动断点把上一轮回复与本轮输入写进缓存,供下一轮读取——上一轮的断点标记仍然有效,命中沿断点链增量累积。TTL 按轮间隔选:5 分钟档两次请求即回本(1.25 + 0.1 < 2 倍全价);1 小时档写入 2 倍,至少三次读取才划算,留给"用户隔二十分钟回一条"的节奏。

5.2、命中验证

每轮打印 usage,健康形态一眼可辨:

def report(resp: anthropic.Message, turn: int) -> None:
    u = resp.usage
    print(f"轮 {turn}: 读 {u.cache_read_input_tokens}, "
          f"写 {u.cache_creation_input_tokens}, "
          f"全价 {u.input_tokens}")

读取逐轮增长、写入稳定在小增量,就是健康循环。读取常年归零,回第 4 节的静默杀手清单逐项排查;写入常年接近全量,说明断点之前的内容在被人改写——十有八九是某次"顺手优化"动了历史。

6、各家缓存机制速查

三家机制差异不小,速查如下(以各官方价页为准):

厂商 机制 命中价 写入费 要点
Anthropic 手动断点 cache_control 0.1 倍 1.25 倍(5 分钟)/ 2 倍(1 小时) 断点上限 4 个,最小千 token 量级
OpenAI 全自动,无需标记 约一折,官方口径最高省 95% 无溢价 前缀 ≥1024 token 才参与,复用窗口约 30 分钟
Gemini 隐式缓存自动生效,另有显式缓存 约一折(2.5 系从省 75% 提高到省 90%) 无 最低约 2K token,Flash 档 4K

细节见 OpenAI 的缓存指南与 Gemini 的缓存文档(价页单独更新)。放在一起看有三条工程含义。第一,OpenAI 与 Gemini 零成本默认开启,你唯一要做的是别破坏前缀——第 4 节的纪律在这两家是纯收益。第二,Anthropic 的写入溢价意味着断点要打在"一定会被再次读取"的边界上,打在每请求独有内容的末尾等于白付 1.25 倍。第三,所有缓存都按模型隔离且 TTL 有限,跨模型、跨会话的复用只剩一条路:压缩成摘要落库。

7、成本模拟对比

把第 2 节的机制放进一段可复算的脚本。口径:system + tools 共 6K token,每轮净增 2.5K,共 10 轮;主模型 $2/MTok、缓存写 $2.5、读 $0.2;摘要交给小模型($0.10/$0.50,缓存同比例),摘要 0.8K、保留最近两轮 5K 原文。

P2, W2, R2 = 2.0, 2.5, 0.2                 # 主模型 $/MTok:全价/写/读
PH, WH, RH, OH = 0.10, 0.125, 0.01, 0.5    # 摘要小模型同口径
BASE, STEP, N, KEEP, SUMM = 6, 2.5, 10, 5, 0.8   # 单位 K token

cache = full = compact = 0.0
for k in range(1, N + 1):
    prefix = BASE + STEP * k                # 第 k 轮全量输入
    if k == 1:
        cache += W2 * prefix / 1000         # 首轮冷写入
    else:
        cache += (W2 * STEP + R2 * (prefix - STEP)) / 1000
    full += P2 * prefix / 1000
    miss = SUMM + KEEP + STEP               # 压缩:消息区每轮全失效
    compact += (R2 * BASE + P2 * miss) / 1000
    compact += (RH * (BASE + STEP * (k - 1)) + WH * STEP + OH * SUMM) / 1000
print(f"缓存保留 ${cache:.3f} | 全价保留 ${full:.3f} | 每轮压缩 ${compact:.3f}")

模拟结果(未计输出 token,四舍五入到千分之一美元):

策略 10 轮输入成本 相对值 信息损失
保留 + 缓存 $0.111 1.00 无
每轮压缩 $0.187 1.69 摘要有损
保留 + 无缓存 $0.395 3.56 无

三个读数值得咀嚼。缓存对"保留"的改造是数量级的:同一份历史从 $0.395 降到 $0.111,省 72%。每轮压缩比缓存保留贵 69%,机制正是第 2 节的算式——消息区每轮被摘要改写,tools+system 之外永远全价,小模型侧的缓存复用救不回主调用的损失。而"每 N 轮压缩一次"介于两者之间:N 越大越贴近缓存保留线,压缩瞬间的重写费被更多命中轮次摊薄。以上是模拟数字,口径全部列在上文——换成你的真实会话重跑一遍,分界线会自己浮出来。

8、总结

压缩与缓存不是二选一的信仰,是两把用途不同的剪刀。记住四件事:前缀缓存的命门是逐字节一致,tools → system → messages 的稳定前缀是最大资产;压缩改写历史即击穿消息区缓存,每轮压缩等于主动放弃一折价;轮间隔短于 TTL 就保留,要跨会话跨模型复用就压缩落库;用 usage 三字段持续验证,别让静默失效偷偷烧钱。

留一个练习:把第 5 节的循环跑在一次真实任务上,每轮记录 cache_read_input_tokens 与总输入的比值;再在系统提示里插一行当前时间戳重跑,看命中率如何归零;最后把它挪到最后一个断点之后,确认命中率恢复。三组数据放在一起,"稳定前缀"就从口号变成了肌肉记忆。

对于正在做长会话 Agent 的团队,建议本周做一件事:拉出生产环境最近 100 个请求的 usage 日志算一次命中率,低于 80% 就按第 4 节的清单排查静默杀手——这通常是零成本里收益最大的一次优化。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

578
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

359
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

331