# Step 60: Prompt Caching

一句话导读:把每轮都重复发送的 system prompt 和 tools 定义标成可缓存前缀,让后端复用已处理过的 token,第二轮开始明显省钱。


# 一、这一步做了什么(What)

step60 给 API 调用加上 prompt caching:把稳定不变的 system prompt 和 tools schema 打上 cache_control 断点,并在 usage 里读取 cache_read_input_tokens / cache_creation_input_tokens。同时改造成本统计,让每轮输出能显示“缓存命中多少 token、累计省下多少钱”。

这一步不是“本地把 prompt 存起来”,而是利用模型服务端的前缀缓存能力:客户端仍发送完整请求,服务端发现前缀命中后用低价缓存读替代重新处理。

# 二、面试官视角:为什么要做?(Why)

面试题:对话历史本来就要每轮发给模型,为什么还要专门做 prompt caching?

因为 agent 的请求有一大块是“稳定前缀”:系统提示、工具定义、工具说明、长期记忆。它们每轮都一样,却会被反复计入输入 token。如果工具很多,tools schema 本身就可能几千 token;对话越长,每轮重复成本越高。

不做缓存 做缓存
每轮重新处理 system + tools 稳定前缀从缓存读
成本随工具数线性重复 重复部分按低价读
用户看不见缓存收益 round 事件显示 cacheRead/saved

一句话:prompt caching 优化的是 agent 最常见的浪费:每轮重复喂同一大段上下文。

# 三、原理:它是怎么工作的(How)

核心改动在 step60/utils/api.ts。system prompt 从字符串变成内容块,末尾打断点:

const systemBlocks: any[] = [
  { type: "text", text: sysText, cache_control: { type: "ephemeral" } },
];
1
2
3

tools 也在最后一个工具上打断点,因为缓存是“到此为止的前缀”:

apiTools[last] = {
  ...apiTools[last],
  cache_control: { type: "ephemeral" },
};
1
2
3
4

流式响应结束后,从 final usage 读取缓存字段:

cacheRead: (final.usage as any)?.cache_read_input_tokens ?? 0,
1

step60/utils/cost.ts 把缓存读按低价计算:

cacheRead * pin * CostTracker.CACHE_READ_MULT
1

再由 QueryEngine.ts 把缓存数据挂到 round 事件:

yield {
  type: "round",
  cacheRead: result.cacheRead,
  saved: cost.stats().saved,
};
1
2
3
4
5

数据流:

system/tools 打 cache_control
  → API 服务端缓存稳定前缀
  → final.usage 返回 cache_read_input_tokens
  → CostTracker 低价计费 + 计算 saved
  → round 事件展示缓存命中与省钱
1
2
3
4
5

# 四、深入追问(面试常见 follow-up)

Q:为什么缓存 system + tools,而不是缓存用户消息?
A:system + tools 最稳定、体积大、每轮都重复,是收益最高且失效最少的前缀。用户消息变化频繁,缓存命中不稳定。

Q:把 cache_control 放到最后一个 tool 上是什么意思?
A:缓存断点标记的是“前缀到这里为止”。最后一个工具之前包含整个工具表,所以一次断点覆盖全部 tools schema。

Q:DeepSeek 自动缓存,为什么还要加 cache_control?
A:README 实测说明 DeepSeek 接受标记但主要靠自动缓存。保留 cache_control 是为了对齐 Anthropic 语义,也让代码结构和真实 Claude Code 的缓存断点一致。

Q:缓存读为什么还要计费?
A:服务端仍要从缓存取前缀并参与推理,只是比重新处理便宜。真实 Anthropic 还有 cache write / cache read / TTL 的分档价格。

# 五、踩坑 / 设计权衡

缓存不是越多越好。断点太碎会增加失效和写缓存成本;断点太少又覆盖不到大块稳定内容。教学版只放 system + tools 两个收益最明确的位置,避免过早做复杂的断点优化。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
system/tools 打 cache_control 同样缓存稳定前缀
读取 cache_read_input_tokens 同款 usage 字段
缓存读 0.1x、写 1.25x 简化计费 真实按模型、TTL、读写类型精确计费
round 事件展示 cacheRead/saved 真实成本状态会随 session 恢复和聚合

# 七、一句话总结

Prompt caching 的本质是把 agent 每轮重复发送的稳定前缀变成低价缓存读:system 和 tools 是第一优先级,usage 字段是唯一可信数据源,成本统计必须缓存感知。

# 下一节预告

下一篇是 step61 多层设置系统合并:从“一个配置文件”升级成 user/project/local/cli/enterprise 五层深合并。