Appearance
避免大上下文反复重建缓存
上一篇讲了缓存的原理:前缀一致才命中,变一个字节全重建。上下文越大,一次重建越昂贵——200K 前缀重建一次相当于几十轮命中的费用;1M 模式下更是灾难。本篇讲实践中最常见的"缓存杀手"和对策。
缓存杀手清单
1. 把动态内容放进了前缀
时间戳、随机数、每轮变化的状态,只要出现在系统提示或早期消息里,每一轮的前缀都不一样,缓存永远无法命中。
text
❌ system: "当前时间:2026-08-22 21:35:07。你是..." ← 每轮都变,缓存全废
✅ system: "你是..."(稳定)
✅ 时间等动态信息放到最新一条 user 消息末尾,或让模型调工具查询自己搭 Agent 时这是头号陷阱。检查方法:打印相邻两轮请求体做 diff,前缀必须逐字节一致。
2. 中途修改系统提示 / CLAUDE.md / 工具集
tools 和 system 排在最前面,改动它们等于把整条缓存链从头炸掉:
- 会话中途编辑
CLAUDE.md、/memory、更换 output style ⇒ 前缀变化; - 中途接入或断开 MCP 服务器、增删工具 ⇒
tools段变化,全量重建; - 切换模型 ⇒ 缓存按模型隔离,等于从零开始。
对策:把配置变更放在会话边界。要改 CLAUDE.md、要挂新的 MCP,先改完、/clear 或重启会话再开始干活;不要在一个 50 轮的长任务中间做这些事。
3. 修改、删除历史消息
一些自制 Agent 框架喜欢"整理"历史:把旧的工具输出截断、给旧消息瘦身、滑动窗口丢掉最早的消息。每次整理都改变了前缀 ⇒ 全量重建。
对策:
- 对话保持只追加(append-only);
- 确实需要瘦身时,一次性大幅压缩(比如把前 80% 历史换成一段摘要),而不是每轮小修小补——压缩一次付一次重建成本,之后在新前缀上继续累积命中;
- 这正是 Claude Code
/compact的做法:不到阈值不动历史,一旦压缩就是一次到位。
4. TTL 过期
默认缓存 5 分钟不访问就过期。开发时被打断 10 分钟,回来第一轮必然是全额写入。
对策:
- 高频交互期不用管,每轮命中都会刷新 TTL;
- 可预期的长间隔(批处理、低频定时任务)用 1 小时 TTL(
ttl: "1h"); - 别为了"保温"去空转请求——5 分钟一次的心跳请求本身也要花钱,算一下通常不划算。
5. 并发分叉
同一个前缀同时开多个分支请求,缓存写入尚未完成时,后到的请求会各自重建。自制多分支/并行采样系统时,先发一轮把缓存建好,再放并发。
Claude Code / Codex 用户的实用守则
官方 CLI 已自动管理断点,你的职责是别破坏前缀:
- 改配置在会话边界做:CLAUDE.md、MCP、模型切换,改完再开新会话;
- 一个任务一个会话:跑长任务别中途切模型、切风格;新任务用
/clear重开,既清上下文又从干净前缀重建缓存; - 控制单次输出物大小:一条 20 万 token 的命令输出不但吃窗口,还会把后续每轮的增量计算变贵——用
| tail、--quiet、写入文件后局部读取; - 长任务优先交给子代理:子代理的高噪音探索不进主窗口,主窗口前缀保持小而稳(见工具爆炸篇);
- 观察指标:API 用户看
cache_read_input_tokens占比,健康的长会话应在 90% 以上;Claude Code 用户可用/cost(API 计费时)和/context观察。
自建 Agent 的检查清单
text
□ tools / system / 早期消息中没有任何每轮变化的内容
□ 对话 append-only,不回头改历史
□ 断点打在稳定层级末尾(tools 末、system 末、倒数第二条消息)
□ 相邻两轮请求 diff,前缀逐字节一致
□ usage.cache_read_input_tokens > 0 且占比随轮次上升
□ 长间隔场景用 1h TTL;压缩历史一次到位而非每轮微调