讲讲这个,顺便讲一下 claude sdk 的 prompt cache 的 checkpoint。mcp 会大量动态加载 tools,工程上如何解决前缀缓存问题的。
回复 群友 3892: 先回答你引用的那条消息:缓存写入确实要加钱。5 分钟 TTL 写入是基础输入价的 1.25 倍,1 小时 TTL 是 2 倍;读回来才是便宜的,标准是 0.1 倍,最新的 Fable 5.1 / Mythos 5.1 是 0.025 倍,Opus 5.5 / Sonnet 5.5 是 0.05 倍。所以“开了缓存反而更贵”是真事:前缀只要命中率低,你就是在每轮支付 25% 的溢价,却一次都没有读回来。 一、四个 checkpoint 分别是什么 这指的是 Anthropic Messages API 中最多可以放置 4 个 cache_control 断点(顶层自动缓存会占用其中一个名额)。断点本身不收费;收费是按“该断点后面的前缀被写入或被读取”来计算的。前缀的渲染顺序固定为 tools → system → messages,命中是字节级前缀匹配,序列化顺序、JSON key 顺序、一个时间戳都会导致整个后半段缓存失效。 实际放置的位置分为三层:最后一个 tool 定义上放一个,用于缓存整个工具前缀;system 最后一块放一个,用于缓存系统提示;messages 里最近一轮完成处放一个,用于持续追加对话。如果单轮里工具调用特别密集,也要补一个中间断点,因为命中检查只会向后找 20 个 block,超过 20 个 block 就找不到上次写的记录了,会静默 miss。 另外有几点容易忽略:有最低长度门槛,低于门槛时根本不会缓存,也不报错,比如 Opus 4.6 / 4.5 是 4096 token,Sonnet 系列是 1024;TTL 从请求开始计算,不是从响应结束算,流式回 4 分钟,那下一轮就只剩 1 分钟可用;多个请求并发时,第一个请求没开始返回,后面的都读不到,只能预热(max_tokens=0 发一发)。 二、MCP 动态加载 tools 与缓存冲突的解决方式 问题的根源是 tools 位于前缀最前面,工具集一旦变动(增删或改顺序),tools、system、messages 全都会被作废。MCP 客户端又经常在中途 connect 新 server,直接把定义追加进 tools,每次都是一次全量重写。 工程上有几种做法,按推荐程度排: 一、用官方的 tool search + defer_loading。所有工具定义照常在 tools 数组里发一遍,但把不常用的标上 defer_loading: true,让它们不进入 system 前缀,只在对话里作为 tool_reference 块被展开。前缀字节不动,缓存就一直命中。限制是每个请求最多 个延迟工具,至少保留 1 个非延迟(要放断点),而且 defer_loading 和 cache_control 不能标在同一个工具上,API 会直接返回 400。默认建议 3~5 个高频工具常驻,剩下的按需搜索。 二、会话中途加工具,但不要碰 tools 数组。用 tool_addition / tool_removal 块放在对话中间的 system 消息里(inline-tools beta),或者干脆把操作员指令作为 role: system 消息追加进 messages,而不是修改顶层 system。这两条都能保住已经缓存的前缀。 三、自己实现一层固定工具集。对外只暴露少数几个通用工具(比如 search_tools、call_tool),真正的 MCP 工具藏在后面,由模型通过一次调用把 schema 取回来放进 tool_result。缺点是多一跳,好处是前缀完全可预测,换任何模型都成立。在官方方案出现之前,这基本是标准做法。 四、连固定工具集都不行的话,做客户端侧聚合和稳定化:MCP server 的工具先经过 gateway 聚合、排序规范化(按名字排序、去掉描述里的版本号和动态字段),会话开始时一次性拉全量并冻结,中途不再变动;真要变更,通过追加对话内容而不是改 tools。 另外容易忽略的影响因素:切换模型会清空整个缓存(缓存是模型级作用域)。一个任务中途切到便宜模型跑,就等于作废全部缓存,所以子任务要单独发请求,不要切换主循环的模型。thinking 参数变化在多数模型上也会导致 messages 缓存失效。 判断方式很简单:读 usage 里的 cache_read_input_tokens,如果连续几轮一直是 0,就说明有静默失效因素,逐字节 diff 两个本该一致的请求,看第一个分歧点在哪里。