编码 Agent 的一次会话里,绝大部分 token 是重复的:同一套 system prompt、同一份工具定义、不断增长的消息历史,每一轮请求都要原样再发一遍。如果每次都让模型从头处理这些前缀,既慢又贵。Prompt caching(前缀缓存)就是把这部分稳定前缀在服务端缓存住,命中时直接复用,省下重新处理的时间和费用。对长会话的编码 Agent 来说,这几乎是性价比最高的一个优化点——但前提是你得理解它的脾气。

缓存是「前缀匹配」,一字节都不能差

最关键的一条认知:缓存命中是基于前缀匹配的。系统从请求的最开头逐字节比对,一旦遇到和缓存内容不一致的字节,从那个位置往后的全部内容缓存全部失效,只能重新处理。

这意味着缓存对「内容的顺序和稳定性」极其敏感。请求在送入模型前的渲染顺序大致是:

1
tools  →  system  →  messages

所以一个铁律是:稳定的内容放前面,易变(volatile)的内容放后面

  • 冻结不变的 system prompt、确定下来的工具列表 → 放在最前。
  • 时间戳、每请求的随机 ID、用户这一轮新问的问题 → 放在最后一个 cache 断点之后。

如果你把一个 now() 时间戳塞进了 system prompt 的开头,那么恭喜,整个请求的缓存每一轮都失效——因为第一个字节区域每次都在变,后面再稳定也救不回来。

1
2
3
4
5
6
# 反例:system 里有每次都变的时间戳,前缀缓存永远 miss
system = f"当前时间 {datetime.now()}。你是一个编码助手……"

# 正例:稳定内容在前,volatile 的时间放到 messages 末尾
system = "你是一个编码助手……" # 冻结
messages.append({"role": "user", "content": f"[ts={now}] {question}"})

哪些操作会悄悄炸掉整段缓存

除了显式的 volatile 内容,还有几类「隐形失效源」专坑自搓 harness 的人:

  1. 切换模型。 缓存是绑模型的。主循环这一轮用 A 模型、下一轮用 B 模型,A 攒下的缓存对 B 完全无效。
  2. 改动工具定义。 工具在渲染顺序里排最前,任何一个工具 schema 的字段变化、甚至 JSON 字段顺序变化,都会让其后(包括 system 和全部 messages)的缓存崩盘。
  3. 未排序的 JSON。 工具定义或结构化内容如果序列化时字段顺序不稳定,前缀就时好时坏,缓存命中率会莫名其妙地低。

针对这些,有两个对策很实用:

  • 用 subagent 跑需要换模型的子任务。 如果某个子任务想用更便宜的小模型,不要在主循环里直接切模型把主缓存毁掉——而是 fork 一个 subagent 单独去跑那个模型,主循环始终维持单模型,缓存连续性不被打断。
  • 工具 schema 用「追加」而非「替换」。 当 Agent 需要更多工具时(比如 tool search 按需加载新工具),把新工具追加到列表末尾,而不是重排或替换已有的工具定义。这样前面那段稳定的工具前缀仍然命中,只有新增部分需要处理。

怎么确认缓存真的命中了

不要凭感觉,要看数。API 返回的 usage 里有一个字段:

1
2
resp = client.messages.create(...)
print(resp.usage.cache_read_input_tokens) # 这一轮从缓存读了多少 token

cache_read_input_tokens 告诉你这轮请求有多少输入 token 是从缓存命中读取的(而非重新处理)。在一个跑了多轮的长会话里,这个值应该随历史增长而显著大于 0。

如果它恒为 0,几乎可以断定存在静默失效源。 按上面的清单挨个排查:

  • system prompt 里是不是藏了 now() / 每请求 ID?
  • 工具定义序列化时 JSON 字段顺序稳不稳定(有没有排序)?
  • 主循环是不是在偷偷换模型?
  • cache 断点是不是设在了 volatile 内容之前,导致断点后的变化字节把缓存拖垮?

cache_read_input_tokens 当成一个常驻的健康指标盯着,命中率掉下来你能第一时间发现。

一个长会话的理想布局

把上面的原则拼起来,一个对缓存友好的编码 Agent 请求大致长这样:

1
2
3
4
5
[ tools     ]  冻结/只追加,字段排序稳定        ← 缓存前缀
[ system ] 冻结,无时间戳无随机 ID ← 缓存前缀
[ cache 断点 ]
[ messages ] 历史尽量稳定增长
[ 本轮新增 ] 时间戳、新问题等 volatile 内容 ← 断点之后

只要保证断点之前一切稳定、断点之后才放变化量,再配合单模型主循环和工具追加策略,长会话的缓存命中率就能稳稳维持在高位。

值得强调的是,缓存优化和上一篇讲的上下文管理是互相牵制的:context editing 的剪枝、compaction 的总结都会改写消息历史,而历史一旦在中段被改动,那个位置之后的缓存就会失效。所以两者要协同设计——尽量让剪枝和压缩作用在靠后的边界上、或者在一次压缩后接受随之而来的一次性 miss,而不是让历史中段频繁抖动。把「何时清理」和「缓存断点设在哪」放在一起规划,才能既不爆窗口、又保住命中率。

小结

Prompt caching 的全部要义就一句话:稳定的放前面、易变的放后面,盯住 cache_read_input_tokens 别让它归零——做到这点,编码 Agent 的长会话才能真正又快又省。