Others

想确认空闲压缩有没有命中缓存,结果查出是 MCP 服务器在悄悄弄丢缓存

2806 字约 10 分钟

摘要空闲压缩插件跑起来之后,我想知道压缩那一次请求到底读到了多少缓存,却发现哪里都查不到。我改了插件把它记下来,先后怀疑缓存时长设置和中转站,最后查到是 MCP 服务器连接的先后顺序,还顺手统计了 8000 多条请求里每一次丢缓存的原因。

上一篇写了我给 Claude Code 配的空闲自动压缩。压缩确实发生了,但省没省钱,要看压缩那一次请求自己有没有读到缓存。压缩要把整段历史发出去,如果它读到的是缓存,大约按十分之一的价格算,如果读不到,就是全价重新处理几十万 token,不但没省钱,还多花了一次。

所以我想知道的是:压缩请求的 cache_read_input_tokens 是不是接近压缩前的上下文大小。

哪里都查不到

助手先找了所有可能记录这个数字的地方。

  • Claude Code 的会话记录里,压缩后只多两条。一条是 compact_boundary,记压缩前后的上下文大小,另一条是摘要本身。生成摘要的那次请求不算一条回复,它的用量没有落盘。
  • cc-switch 读的就是会话记录,所以它的请求列表里也没有这两次压缩。助手去它的数据库里核对过,压缩发生的时间段是空的。
  • Orca 的会话库里也只有每一轮的上下文统计。
  • 中转站我进不去后台,能用 API key 查的接口只有按天、按模型的汇总,拆不出单条请求。

压缩前最后一条请求的缓存是命中的,这说明空闲开始时缓存还在,但 25 分钟之后还在不在,就是查不到的那一部分。

改插件,把压缩请求的用量记下来

助手在插件接口的类型声明里找到了线索:session.compact 事件带着一个 usage 字段,就是压缩自己那次模型请求的用量,输入、输出、缓存读、缓存写四项齐全。idle-compact 本来就是靠这个事件拿到压缩前后的数字。

直接把用量写进会话记录不可行。插件接口没有往会话记录里追加内容的办法,自己改 jsonl 可能弄坏恢复会话,cc-switch 还会把它当成一条真实请求重复计费。

我让助手把 idle-compact fork 了一份,在 custom 分支上加了两个提交:

  • 在 Orca 这类没有状态栏的会话里不再刷状态栏,之前我本地打过一个补丁,这次顺便正式化。
  • 每次主会话压缩都往 ~/.claude/idle-compact/compactions.jsonl 追加一行,记录触发方式(插件触发记为 idle,我手动敲的是 manual)、空闲了多久、压缩前后的上下文大小,以及压缩请求的四项用量。

12 个测试通过,类型检查和插件校验也通过。冒烟测试时,我开了个临时会话,回复一句后马上压缩,压缩前的上下文是 29,520 token,压缩请求读到 24,252,写了 5,265。压缩确实复用了正常对话的缓存。

但压缩经常读不到缓存

接着测一个大一点的会话,上下文约 7 万 token,压缩紧跟在上一条请求后面,结果是这样:

压缩请求的缓存时长设置缓存读缓存写
subagentPromptCacheTtl: "5m"(我原来的设置)23,061,只有系统提示50,574,对话部分全部重写
临时改成 "1h",和主对话一致68,4615,017

压缩请求不算主对话,它的缓存时长走的是 subagentPromptCacheTtl,我设的是 5 分钟,主对话是 1 小时。助手据此下了结论,通过这个中转站,两种时长的缓存互相读不到,建议把它也改成 1 小时。

我当时有个疑问:压缩完上下文都没了,压缩请求自己写进缓存有什么用?助手解释说,要的不是让压缩请求去写,而是让它读到主对话已经写好的缓存。读到了,几十万 token 按读取价算,读不到就是全价加上写入一份以后没人用的缓存。我同意了,把设置改成了 "1h"。

改完之后,助手开了个新会话验证,压缩请求这回却一点缓存都没读到。

6 组对照,推翻了自己

助手于是做了对照实验,在 1h 和 5m 两种设置之间交替,跑了 6 组。结果是压缩能不能命中缓存,和设成 1h 还是 5m 没有关系,设成 5m 的那一组也命中了。

前面“两档时长不一致”的结论,是只靠一次碰巧的对照下的,不成立。我让助手把设置改回了 "5m",并且把改回之后的配置和改之前的备份逐项比对,内容完全一致。

那真正的原因是什么?助手把命中和不命中的会话摆在一起看,发现分界在于第一条请求发出的时候,MCP 服务器有没有连上。

  • 会话启动后第一条请求发得太早,MCP 服务器(我装的是文档查询工具 context7)还没连上,主对话的开头就少了约 2.2k token 的 MCP 说明。
  • 等到压缩的时候 MCP 已经连上,压缩请求的开头多了这一段,开头对不上,后面整段对话都读不到缓存。
  • 如果第一条请求发出时 MCP 已经连上,两边的开头一致,压缩就能读到几乎全部缓存。

14 个测试会话里有 13 个符合这个规律。

为什么开头多 2.2k token,整段缓存都读不到

提示缓存是按前缀匹配的。请求从头开始逐段比对,只要中间有一个地方不一样,它后面的全部内容都不能复用。把一小段内容插在最前面,等于整个会话都换了一个新的前缀。

对我平时的使用没有影响。助手去查了我常用的两个 Orca 会话,第一条请求之前 MCP 都已经连上了,所以只要缓存没过期,压缩就能命中。会碰上的主要是它自己测试用的 claude -p,它一启动就立刻发请求,等不到 MCP 连上。

追问:之前空闲不到一小时就丢缓存,是不是也是它

这让我想起上一篇里那几次空闲才 34、38 分钟缓存就整段失效。我问助手,会不会也是 context7 造成的。

助手这回把那几次逐条展开。空闲 33.9 分钟和 38.5 分钟的两次,缓存读取都是 0,连系统提示都没读到,前后都能对上 context7 的断线重连。空闲 45.9 分钟那次读到了系统前缀,更像是下面说的中转站那一类。

为什么一次断线重连就会让整段缓存作废?我用的是自定义的接口地址,官方文档说这种情况下工具通常没法延迟按需加载,会话开始时就是完整加载的。远程的 MCP 服务器空闲一段时间会断开,一断一连,工具列表就变了,缓存的前缀跟着变,整段缓存都作废。

context7 在 95 个会话里只有 6 个用过,一共调用 18 次。它每个会话开头多占约 2.2k token,对几十万 token 的会话不到 1%,开销可以忽略,但它造成的缓存丢失一点都不便宜。

统计了一遍所有丢缓存的原因

助手顺势把 10 月 8 日以来所有主对话的请求统计了一遍:共 8,258 条,其中丢掉 2 万 token 以上缓存的有 38 次,一共重写了约 1,140 万 token。

原因次数重写量能不能避免
空闲超过 1 小时(含升级后恢复会话)14390 万正常过期
中转站:空闲 5 到 60 分钟、什么都没变,只剩约 2.6 万的系统前缀8280 万我这边没法解决
context7 断线重连3180 万关掉它
会话中途改 /effort493 万大会话里别中途改
会话中途换 /model345 万同上
中转站:间隔不到 5 分钟、无变化355 万没法解决
上下文突然变小,原因不明255 万不确定
恰好在改全局 CLAUDE.md 的时候141 万官方说改它不影响缓存,大概率是巧合

其他插件和 MCP,包括 agent-recall、claude-hud、frontend-design 和 idle-compact 本身,都没有出现在任何一次丢缓存里。只有 context7 是远程连接,会断线。

几条结论:

  • context7 关掉。它是能直接消除的最大一项,仅 10 月 13 日一晚就造成约 180 万 token 的重写。我用 claude plugin disable 关了它,插件还装着,想用随时能启用。
  • /effort 和 /model 在会话开始时定好。助手查到官方的说法是某些模型改 effort 可以保留缓存,但我这个自定义中转上,一改就整段重写。
  • 中转站那两项占了不少,只能接受,或者换一家缓存更稳定的。比如有一次只空闲了 18 分钟,81.3 万的上下文只读到 2.6 万,重写了 78.7 万。这也意味着,哪怕压缩插件的时间设得再准,偶尔也会撞上缓存已经没了的时候。

被它自己纠正了三次

回头看这次排查,助手有几个结论后来都被改写了。

第一次是把标签当成事实,说我的缓存只有 5 分钟。第二次是只靠一次碰巧的对照,就说两档缓存时长不一致导致压缩读不到。第三次最有意思,它帮我调 ttlMinutes 的时候顺口说“中转站的缓存在空闲 34 到 56 分钟后就整段没命中过好几次”,我问它这个判断是怎么得出的,它承认这句话是直接搬了记忆文件里的记录,没有重新核对。重新用会话记录算了一遍,“56 分钟”那次根本找不到,其余的数字也改了,38.5 分钟那次后来还查出是 context7 重连,不是中转站。

追问“你是怎么得出的”很有用

助手写进记忆文件的东西,和它刚算出来的东西一样,都可能是错的,区别只在于前者看起来更像事实。每次它给出一个具体的数字和结论,我都值得问一句依据是什么,能不能再用数据复核一遍。

没解决的部分

  • 中转站有时在 5 到 24 分钟就丢掉对话缓存,看不出规律,原因只是推测,没有证实。
  • 有 2 次上下文突然少了 8.1 万和 5.3 万,同时缓存失效,会话记录里没有任何清理痕迹,我没办法确认是什么,就不硬套原因了。
  • 以上都是我自己一台机器、一个中转站、2.1.29x 版本上的数据。MCP 先后连接的那条规律对任何接 MCP 的人应该都适用,中转站的那部分则不能推广。

要点就一条:提示缓存吃的是前缀,开头的任何变化,包括一个 MCP 服务器晚连上几秒,都会让后面的整段对话重新计费。想省钱,先把开头的东西稳住。

评论