缓存点设计
Agent 系统作者:如何设计一个 agent 以取得高命中率 —— 先讲 block 模型,再讲八条放置 4 个断点的设计动作,最后讲压缩设计对缓存的后果。与压缩页互为配套
目标直说
决定一个长运行 agent 要花多少钱的,是一个数字:cache 命中率。按 Anthropic 定价,命中 = 未缓存读的 10%,写入 = 未缓存读的 125%(5 分钟 TTL)或 200%(1 小时 TTL)。Agent 里每个 token 最终只会落在三个桶里之一:
每 token 成本 = 0.10 × p(hit)
+ 1.25 × p(write, 5m) // 1h TTL 时为 2.00
+ 1.00 × p(uncached)
一个 30 步的 ReAct 任务,命中率 0.90 的成本大约是命中率 0.0 的 1/7。这一页所有工程目标只有一个 —— 把 p(hit) 推到尽可能接近 1,并且在对话增长、压缩触发时保持在那。
这是一本设计 playbook。第 1 节是心智模型(事实层,对齐 Anthropic 官方文档)。第 2 节是完整的八条动作 playbook —— 怎么放置 4 个断点。第 3 节讲为什么其中三条(动作 4、5、6)是压缩决策 —— 压缩设计对缓存的后果。第 4 节是 Zapvol 的实现。第 5 节覆盖 single-agent happy path 之外的场景(子 agent、模型切换、开发迭代、什么情况别用 cache)。第 6–8 节是诊断(失效模式、度量、检查清单)。
1. 一页讲完心智模型
一次 cache 命中要两种连续性同时成立(同一个断点上):
- 字节续上 —— 从请求起点到断点为止的每个字节都与之前某条已写入的 cache 条目完全一致。
- 空间续上 —— 那条老条目写入时的位置在当前断点往前 20 个 block 位置以内(窗口把断点本身算作这 20 个位置里的第一个)。超出窗口就停止搜索,更早的写入对 cache 不可见。
任一续不上就是一次冷写。把 4 个断点想成锚链上的 4 颗锚:字节稳定 = 锚钉在岩石上的牢固程度;20-位置的回溯 = 相邻两颗锚之间的最大链长。下面 playbook 的每一条动作都在守住两种连续性中的一种或两种。
被 hash 的是什么 —— block 模型
Anthropic 把请求序列化为一条 block 流:
tool[0], tool[1], … │ sys[0], sys[1], … │ msg[0].content[0], …, msg[N].content[L]
一个 block
是一个工具定义、一个系统 text 块、或一条 message 里 content 数组的一项(text、tool_use、tool_result、image、document、thinking)。每个 block 在这条线性流里都有一个位置。
cache_control
放在某个 block 上意味着:“在这个位置把到这个 block 为止的每个字节 hash 一次;命中就从 cache 读;没命中就回溯最多 19 个更早位置找之前的写入;两条路径最终都会在这个断点写一条新条目供后续请求读(前缀低于模型下限时静默跳过)“。每请求最多 4 个断点。你无法 cache 半块 block、也无法强制中间位置写入。hash 按字节敏感
—— 语义相同但 JSON 键顺序不同的两个 block 哈希值就不同,这也是 Swift /
Go 这类会在 JSON 序列化时随机化键顺序的 runtime 会默默打坏 cache 的原因。Anthropic 不会替你规范化 JSON。
匹配怎么跑
- 每个断点处 hash 完整前缀(从请求起点到这个断点为止),查。
- 没命中 → 向前走最多 20 个位置(断点本身算第一个),每个位置都查”之前有哪次请求在这里标过
cache_control并成功写入了”。窗口内都找不到就停止搜索,整段前缀按冷写处理。 - 首个命中胜出。命中点之后的 block 重新处理;当前断点会以自己的前缀 hash 作为新条目写入。
cache 索引的是前缀,不是单个 block。一条 cache 条目覆盖的是”从请求起点到被标记 block 为止的每一个字节”,不是被标记的那个 block 本身。这就是”任何更早 block 的字节变了、其后每个断点都废”的原因 —— 它们都是同一段前缀的不同长度的 hash。
Anthropic 的原话:“It is looking for prior writes, not for stable content.”
翻译:回溯只能找到”历史上有人在这里标过 cache_control
并成功写入”的位置。只是”历史请求里出现过但从没被标过”的 block 对 cache 是不可见的 ——
它们压根不在 cache 的索引里。
具体例子(对应 Anthropic 文档里的场景):
- 第 1 轮 —— 10 个 block,断点在 block 10 → 写入
E1 = hash(0..10)。 - 第 2 轮 —— 15 个 block,断点在 block 15 →
hash(0..15)miss;回溯向前走,在位置 10 找到E1(在窗口内)。block 11–15 重新处理;写入E2 = hash(0..15)。 - 第 3 轮 —— 35 个 block,断点在 block 35 →
hash(0..35)miss;回溯检查 20 个位置(block 35 到 16)都找不到。E2(位置 15)正好在窗口外一格 → 完全 MISS、全段重写。
第 3 轮的这次 MISS 正是 slot 2(压缩边界)要解决的失败模式 —— 在尾部更近处多放一个显式断点,每一轮回溯都能落到一个已缓存的条目上。
失效级联
因为 hash 覆盖到断点为止的全部字节,任何更早 block 的任何一个字节变,其后每一个断点都废。级联:
| 改动 | 废掉 |
|---|---|
| 工具定义字节(任何) | tools + system + messages |
| 系统 text 字节(任何) | system + messages |
| Web search 开关 | system + messages |
| Citations 开关 | system + messages |
速度设置(speed: "fast" 与标准之间切换) | system + messages |
tool_choice 参数 | 仅 messages |
| Extended-thinking 参数(开关 / 预算) | 仅 messages |
| 任何位置增减图片 | 仅 messages |
| 非-tool-result 的用户内容(开 thinking) | 清掉之前的 thinking blocks |
最小可缓存前缀
断点处的前缀低于模型下限时,cache 写入会被静默跳过:
| 模型 | 最小(token) |
|---|---|
| Claude Opus 4.7 / 4.6 / 4.5 | 4,096 |
| Claude Haiku 4.5 | 4,096 |
| Claude Sonnet 4.6 | 2,048 |
| Claude Sonnet 4.5 / 4 | 1,024 |
| Claude Opus 4.1 / 4 | 1,024 |
这就是全部模型。下面的所有内容都是围绕它做工程。
2. Playbook —— 八条按成本影响排序的动作
每条 = “做什么” + “为什么提升命中率” + “什么情况下会失手”。从上往下应用;下游动作假设上游已就位。
动作 1 —— 一个断点覆盖 tools + system 头部(槽位 1)
做:在最后一个 system text 块上放 cache_control。
因为 cache 覆盖”从请求起点到断点 block 为止”的所有内容(包括断点所在那一块本身),这一个标记同时吃下了整个工具列表和完整系统提示。这在多数 agent 上占每请求 token 数的 30–80%、从第二轮起是字节级稳定的。把它从每轮的 1.0× 变成 0.1× 是最大的单项降本。
失手条件:系统提示或工具列表跨轮变化 —— 见动作 2 / 3。
动作 2 —— 工具定义跨轮字节级一致
做:每 session 算一次过滤后的工具列表;用确定性 JSON 键顺序序列化工具;避免工具描述里出现随时间变化的文本。
Tools 是第一缓存级。任何一个字节变都会级联到每一级。两轮之间工具按不同顺序排列(哪怕是”按名字字母序” vs “按定义序”)就会把四个断点一次废光。工具描述里含”当前时间是 14:32”每轮就是一次爆表。
常见的隐藏非确定性:工具构造器里 Map/Set 的迭代顺序、运行时权限过滤、Intl.DateTimeFormat 的本地化格式。
动作 3 —— 每个 block 都做确定性序列化
做:从工具定义、系统 text、tool_result 输出里拔掉时间戳、request ID、随机 nonce、不稳定的 map 迭代顺序。
两个”语义相同”但”字节不同”的 block 对 cache 而言是两个 block。确定性序列化是动作 1 周围的篱笆 —— 让它在 session 与 session 之间都成立。
可变字节常见潜入处:错误对象的字符串化、日期的 JSON 化、在输出 payload 里 log 当前时间的工具包装、摘要器 prompt 里带
{{now}}。
动作 4 —— 用压缩边界锚定 mid-prefix(槽位 2)
做:放第二个断点,位置要选”压缩 epoch 内跨轮字节稳定”的那个。用压缩边界 —— 最后一个被摘要的消息下标,由压缩器发布。
只有槽位 1 在长对话上不够用。从槽位 1 被写起算新增超过 20 个 block 位置后,20-block 回溯够不到槽位 1 了,第 N+1 轮就要为整段头部付一次冷写。槽位 2 通过给回溯提供一个更近的落点来堵这个洞。
天真中点(Math.floor(length / 2))每一轮漂一格 —— 每次漂就是一次 miss。压缩边界则在同一个 epoch 的每一步都字节级一致。Zapvol 里这通过
extraBreakpointAt 接入。为什么这个位置是 唯一 稳定的 mid-prefix 锚点 —— 而不是”刚好方便的一个”
—— 在第 3 节展开。
失手条件:压缩改写了边界之前的内容 —— 见动作 5。
动作 5 —— 压缩在不可变头部之后追加式操作
做:设计压缩器让它永远不改槽位 1 断点之前的任何 block、只把整轮片段替换成 summary block(在槽位 2 位置之后)。
改写更早任何 block 的压缩(哪怕只是往系统提示里加一句备注)是 100% cache-miss 操作 —— 再聪明的断点放置也救不回来。不变式是:头部不动、尾部可被改写。每次这样的改写 = 一个 cache epoch(一次贵写 + 多次便宜读)。
动作 6 —— 压缩粒度为整轮 / 整块
做:把整轮替换成一个 summary block;对 tool_result 按固定长度整块截断,每次相同 block 被重新序列化都截到那个长度;永远不编辑 block 内部的字段。
子块改动击穿 cache 粒度。一个为了去噪声而裁剪 tool_result
中段的压缩器会改该 block 的字节,进而改其后每个前缀 hash,却只省下几 KB。优选:
- 替换,不编辑。“清空的” tool_result 变成一个新 stub block;原 block 整个消失。
- 固定截断长度。“4 KB”是稳定数字;“方便时就截”不是。
- 轮次边界,不是消息边界。一段被压缩的轮 = 一个 summary block;半个被压的轮 = cache 陷阱。
动作 7 —— 标记最后一条用户消息与最后一条消息(槽位 3、4)
做:在最后一条用户消息上放断点(与 tail 不同时)和最后一条消息上放断点。
工具密集的内循环里,用户问题保持不变,assistant 在累积 tool_use 与
tool_result。没有槽位 3,一条长工具链超过 20
block 时回溯丢掉槽位 2。槽位 4 是前瞻的槽位:现在在 tail 写入 → 下一轮对这段精确前缀命中。
失手条件:最后一条消息里含时间戳或每请求 ID —— 见动作 3。
动作 8 —— TTL 与压缩节拍匹配
| TTL | 写倍率 | 适用 |
|---|---|---|
| 5 分钟 | 1.25× | 内循环工具迭代、交互式 chat、dev/test |
| 1 小时 | 2.00× | 长运行 agent 作业、带空档的 HITL session、batch |
决策规则:TTL 内期望复用次数 × 0.9 > 写溢价。收支平衡点:5m 约 0.28 次、1h 约 1.11 次。
允许混合 TTL,但更长 TTL 必须出现在更短 TTL 之前(请求顺序上)。典型混合:槽位 1–2 上 1h(慢动的头和 mid)+ 槽位 3–4 上 5m(快动的尾)。
失手条件:5m TTL + 每 20 分钟压一次 → cache 在能被复用前就过期 → 压缩后每一步都冷写。
3. 压缩对缓存的影响
上面八条动作里,三条(动作 4、5、6)是纯粹的压缩决策,第四条(动作 8,TTL)与压缩节拍直接挂钩。这不是巧合 —— 压缩是唯一一个会改写 block 流本身的 context-engineering 操作。其他所有技术 —— 记忆、JIT 加载、子 agent 隔离、prompt 设计 —— 都保持既有 block 字节稳定。一旦改写 block,你要么把 cache epoch 往前延伸,要么把它炸掉。
压缩设计的四个维度每一个都直接牵动 cache:
| 压缩维度 | 对 cache 的作用 |
|---|---|
| 边界 | 增长型对话里,唯一一个能给 slot 2 作锚点的字节稳定位置 |
| 范围(改写到哪) | 碰到头部就是 4 个槽位在下一轮同时失效 |
| 粒度(整块 vs 子块) | 子块编辑破坏字节续上;整块替换才能保住 |
| 节拍(多久触发一次) | 决定每次写入能被后续多少次读取摊销 |
压缩边界是其他任何东西都提供不了的锚点
压缩器在 epoch 内的每一步都发布”最后一个已摘要消息的下标”。这个下标是增长型对话里唯一一个 epoch 内前缀字节一致的位置。它是 slot 2 唯一能坐下去还能反复命中的地方。
没有压缩,slot 2 就无处可放。Math.floor(length / 2)
每一轮漂。“每 15 个 block 放一个”也每一轮漂。消息尾部不存在另一个 epoch 稳定的位置。压缩在创造锚点、缓存在消费锚点。
动到头部就是灾难
压缩一旦改写 slot 1 断点之前的任何 block —— 比如未来新加一个”session 中途从记忆里刷新系统提示”的 feature —— slot 1 的前缀 hash 会变,下一轮四个断点同时失效。没有局部恢复。不变式简单绝对:头部不可变,尾部可改写。违反它的代价是每一轮都要冷写整段前缀,直到下一次 session 边界才算完。
粒度决定字节续上
尾部改写有两种做法 —— 整轮替换成一个 summary block,或者编辑既有 block 里的字段去噪声。第二种在摘要 token 上更便宜,但它会打坏那个 block 的字节 hash,连锁改变其后每一个前缀 hash。保留下来的 block 必须在 epoch 的每一步里字节级一致;唯一能保证这一点的办法是永远不原地编辑 —— 整块替换、固定长度截断、字段绝不动。
节拍决定摊销
每次压缩事件 = 一个新的 cache epoch:一次对新前缀的贵写,加上后续多次便宜读。每 3 轮一压意味着频繁付写税;按任务边界压则把同一次写摊到 20+ 次后续读上。TTL 窗口内读写比,就是节拍决定实际在控制的那个量。节拍选得好,这个比例稳定在 ~10 以上,压缩几乎免费;选得差,压缩本身就成为主要成本项。
财务收支平衡点(来自动作 8)比 ~10 的运营目标低得多:5m TTL 约 0.28 次复用/写、1h TTL 约 1.11 次。“收支平衡”和”~10”之间的差就是你对付方差的余量 —— 有的写根本没被复用(session 结束、用户放弃)、TTL 在下一次压缩前就过期、前缀因非确定性而变脏。设计时瞄运营目标,不是瞄收支平衡 —— 后者是悬崖,不是平台。
具体影响
“按 cache 设计的压缩” vs “不考虑 cache 的压缩”之间的差距不是渐进的 —— 是二元且巨大的。以 30 步 tool-heavy agent 在 Opus 4.7 上为例:
- 压缩器在 session 中途重写系统提示,或原地编辑 tool_result 字节 → 命中率 ≈ 0.2
- 压缩器追加式、整轮替换、边界发布为 slot 2 → 命中率 ≈ 0.9
- 同样的任务、同样的工具、同样的对话长度 —— 总成本大致相差 5 倍,完全由压缩器怎么碰 block 流决定
这就是上面八条里三条(动作 4、5、6)给了压缩、第四条(动作 8,TTL)与压缩节拍挂钩的原因 —— agent harness 里再没有哪一块”每行代码改动能撬动的缓存收益”比压缩大。每个压缩决策同时是一个缓存决策,在设计层面不可分。
4. Zapvol 的实现
packages/backend/src/agent/model.ts 用两个函数实现 playbook:
createCachedInstructions(instructions, model)—— 用cache_control包住系统提示,隐式吃下其前所有工具定义。这是动作 1。applyCacheControl(messages, model, { extraBreakpointAt })—— 在最后一条消息上标(槽位 4)、在最后一条用户消息上标(若与上一个不同)(槽位 3)、在 mid-prefix 锚点上标(槽位 2)。从压缩器拿extraBreakpointAt(动作 4);只在压缩未触发且对话超过 20 条时回退到Math.floor(length / 2)。
单位说明:Anthropic 的回溯窗口按 block 计(20 个 block / 断点);上面 Zapvol 的回退阈值按 消息
计(20 条消息)。一条消息通常含 2–5 个 block(text + tool_use + tool_result),所以”20 条消息”是 Anthropic”20
blocks”规则的保守代理 —— 到这个阈值触发时,尾部实际上已经远超 20
block 了。用更紧的代理会更早触发,代价是短对话里提前吃掉一个断点。
调用方(agent-round.ts)把压缩 epoch 的 compactedPrefixEnd
穿到 cache 层,让动作 4、5、6 协同:压缩器生产稳定边界,cache 层在该精确位置锚住槽位 2,被压缩过的前缀在 epoch 的每一步里字节级一致。
遥测:applyCacheControl emit cache.breakpoints_placed,字段
messagesCount、compactedPrefixEnd、extraBreakpointUsed、placedAt、lastRole。运维 dashboard 把这份 shape 作为命中率主数据源查
—— 不要在不同时更新 dashboard 的情况下改它。
自动缓存 —— 以及 Zapvol 为什么不用
Anthropic 提供了”自动缓存”模式:在请求顶层(不挂在任何具体 block 上)设
cache_control,系统会在每一轮自动把断点放在最后一个可缓存的 block 上,随对话增长自动往后推。每一轮写入新 tail、之前的轮次通过 20-位置回溯从 cache 读取。
这本质上等于”由 API 替你做了 slot 4”。对 chat 式 agent(每轮追加 1–2 块)单独就够 —— 回溯永远能够到上一轮的写入,而且头部(tools + system)小到全量重读也不心疼。
对 tool-heavy agent 不够。一次 ReAct step 能追加 5–10 块(tool_use + tool_result + assistant text)。两三步后 20-位置窗口就够不到头部,每个请求都要为整段 tools + system 付冷写。修复方式一定需要显式的 slot 1(头部)和 slot 2(mid-prefix 锚在压缩边界)—— 到这一步,slot 4 顺手显式标一下成本几乎为零。
所以 Zapvol 用的是全显式四槽(createCachedInstructions +
applyCacheControl),没有在请求顶层开自动缓存。四个槽全部由我们调度、不交给 API。
如果未来开启顶层自动缓存,这些 edge case 必须记住:
- 自动缓存会占用 4 个槽位之一。
- 最后一块已经有显式
cache_control且 TTL 相同 → 自动缓存 no-op。 - 最后一块已经有显式
cache_control但 TTL 不同 → API 返回 400。 - 已有 4 个显式断点 → 自动缓存无槽可用、API 返回 400。
- 最后一块不是 eligible cache target → 自动缓存向前找最近的 eligible block;都找不到就静默跳过。
5. Single-agent happy path 之外
上面的 playbook 和 Zapvol 实现都假设了”单 agent、单一模型、提示稳定”的主路径。实际部署会撞上四个让缓存故事发生变化的分支。
子 agent 各自拥有独立的缓存链
注册的子 agent(browser、write-todos、task 子任务等)作为独立的 API 请求序列运行,每个都有自己的
applyCacheControl 栈。子 agent 的对话不继承父的消息历史(CLAUDE.md 称之为 “sub-agent
isolation”),所以父和子有各自独立的前缀 hash —— 缓存在父子之间永远不会流动。后果:
- 每个子 agent 在一个 session 里首次被调用时,都要为自己的槽位 1 付一次冷写;同一个子 agent 在 TTL 内的后续调用可以读缓存。
- 命中率 dashboard 按 agent 类型分段 —— 把父 agent 和子 agent 的读混在一个平均里,会掩盖哪一条链实际上是健康的。
- 不要尝试设计父子共享缓存。独立请求、独立 hash,就是这样。
中途切换模型会重置缓存
Anthropic 的缓存按 (model, prefix) 键入。中途切换模型(BYOK 换 key、用户降级、A/B 测试)意味着:
- 切换后的第一个请求:
cache_creation_input_tokens > 0, cache_read_input_tokens = 0。这是正常现象,不是回归。 - 不同模型的最小 token 门槛不同。从 Opus(4,096)切到 Sonnet 4.6(2,048)能让一些原本不可缓存的提示变得可缓存;反向也成立。
- 冷启动断言(第 8 节)需要在每个模型切换边界都触发,而不仅是 session 开始。如果测试套件切换模型,要相应地接入”第 1 轮行为”的预期。
开发迭代会不断打脏缓存
在活跃开发系统提示、工具描述或压缩格式期间,每一次编辑 = 新的前缀 hash = 每次请求都是冷写。代价:
- 打脏的前缀 1.25×,命中率 ≈ 0,直到提示稳定下来。
- 建议:重度迭代期,要么把开发期成本接受为”便宜的信号”,要么在 dev flag 下暂时不走
createCachedInstructions。提示稳定后再打开,通过第 8 节冷启动检查验证第 2 轮有读。 - 不要在 prod 里意外留着”关闭缓存”的 dev 配置 —— 用
NODE_ENV或显式 flag gate 住。
什么情况别用缓存
有些工作负载开了缓存反而更贵。这些场景跳过缓存:
- 一次性 / 每请求独一无二的提示(对用户独特文本做分类、一次性分析):每个请求都是冷写、永远读不到 —— 纯 1.25× 税。
- 头部里混进了每请求方差大的内容(时间戳、每请求 ID、工具定义里的用户特定数据):写总是冷的,哪怕”意图”是稳定的。先修方差(动作 3)再开 cache,不要反过来。
- 低于最小 token 下限的提示(第 1 节表格):写会被静默跳过、
cache_control成空操作。只有当提示会被跨请求复用时才值得垫长度。 - 扇出批处理(1 万个并行独立请求、没有复用):缓存是纯负担。
门槛规则(出自动作 8):TTL 内预期命中次数 × 0.9 > 写溢价。低于收支平衡点(5m 约 0.28 次、1h 约 1.11 次),别开。
6. 失效模式(反向 playbook)
每一次命中率回归都能归到某条动作被违反。拿这份表当诊断索引:
| 症状 | 违反了哪条 | 修复 |
|---|---|---|
第 2 轮 cache_read_input_tokens = 0 | 动作 1 或 3 | 验证断点就位;审计 tool + system 的字节 |
| 对话增长超过 20 个 block 后命中率崩 | 动作 4 | 把槽位 2 锚在压缩边界上,让回溯还能够到头部 |
| 压缩后立刻命中率崩 | 动作 5 或 6 | 检查压缩是否”追加式 + 整块” |
| 每轮命中率都在 0.2–0.4 | 动作 2 或 3 | 猎杀 tool / message 序列化里的非确定性 |
第 1 轮 cache_creation_input_tokens = 0 | 前缀低于下限 | 垫系统提示或接受代价 |
| 压缩后每步都完全 miss | 动作 8 | TTL 太短、与压缩节拍不匹配 |
| 用户发任务中段 text 后命中率崩 | Thinking | 工作流允许时把中段输入走 tool_result |
命中率随 tool_choice 剧烈波动 | 动作 2 | Session 开始定死 tool_choice |
7. 度量
命中率 —— 头牌指标
hit_ratio = cache_read_input_tokens
/ (cache_read_input_tokens + cache_creation_input_tokens + input_tokens)
良好行为长任务的目标:第 3 轮后 > 0.85、持续多轮 session > 0.90。低于 0.5 是 bug 而不是折衷。
cache.breakpoints_placed 事件
值得建的 dashboard:
- 断点数量直方图 —— 应集中在 3 或 4。众数在 1 意味着只缓存了系统提示。
extraBreakpointUsed随任务年龄的比例 —— 任务越过压缩阈值后应升到接近 1。- 相邻
placedAt间距分布 —— 相邻标记应保持 < 20 block。
冷启动验证
# 第 1 轮
assert response.usage.cache_creation_input_tokens > 0 # 动作 1 生效
assert response.usage.cache_read_input_tokens == 0
# 第 2 轮
assert response.usage.cache_read_input_tokens > 0 # 动作 2/3 稳住了
第 2 轮读为零 → 前缀跨轮变了 —— 回上面的失效模式表逐行过。
8. 上线检查清单
- 动作 1 ——
cache_control在最后一个 system 块;槽位 1 缓存 tools + system。 - 动作 2 —— 工具列表每 session 算一次;JSON 键顺序固定。
- 动作 3 —— 任何被 cache 的 block 里都没有时间戳、request ID、不稳定 map 迭代。
- 动作 4 —— 槽位 2 锚定在压缩边界上(或压缩未触发时的 length-midpoint 回退)。
- 动作 5 —— 压缩器不改动槽位 1 之前的任何 block。
- 动作 6 —— 压缩替换整轮 / 整块;截断长度固定。
- 动作 7 —— 最后一条用户消息与最后一条消息各带一个断点。
- 动作 8 —— TTL 与压缩节拍匹配(紧循环 5m、长运行 1h)。
- 遥测就位 ——
cache.breakpoints_placedemit、cache_read_input_tokens捕获、命中率进 dashboard。 - 前两轮已验证 —— 第 1 轮写入、第 2 轮读取。
延伸阅读
- ← 上下文压缩 —— 那个稳定的轮次边界就是动作 4 的基础。
- Context Management —— 注意力预算;命中率是它的成本维度。
- Memory Design —— 窗口外生活的东西不进 cache epoch、不能让它失效。
- Operations / Dashboards ——
cache.breakpoints_placed的落点。
Sources
- Prompt caching —— Anthropic, Claude API docs(block 模型、20-block 回溯、失效规则、定价的一手参考)
- Effective Context Engineering for AI Agents —— Anthropic, 2026(缓存感知的压缩指引)
packages/backend/src/agent/model.ts—— Zapvol 的applyCacheControl/createCachedInstructionspackages/backend/src/agent/agent-round.ts—— 把压缩边界穿到 cache 层的调用点