# 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" } },
];
2
3
tools 也在最后一个工具上打断点,因为缓存是“到此为止的前缀”:
apiTools[last] = {
...apiTools[last],
cache_control: { type: "ephemeral" },
};
2
3
4
流式响应结束后,从 final usage 读取缓存字段:
cacheRead: (final.usage as any)?.cache_read_input_tokens ?? 0,
step60/utils/cost.ts 把缓存读按低价计算:
cacheRead * pin * CostTracker.CACHE_READ_MULT
再由 QueryEngine.ts 把缓存数据挂到 round 事件:
yield {
type: "round",
cacheRead: result.cacheRead,
saved: cost.stats().saved,
};
2
3
4
5
数据流:
system/tools 打 cache_control
→ API 服务端缓存稳定前缀
→ final.usage 返回 cache_read_input_tokens
→ CostTracker 低价计费 + 计算 saved
→ round 事件展示缓存命中与省钱
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 五层深合并。