Others

Claude Code 隔夜回来的第一条消息特别费额度,我给它配了空闲自动压缩

3185 字约 11 分钟

摘要长任务跑完我去睡觉,早上回来上下文快满了,提示缓存早已过期,第一条消息要把整段上下文按未命中重新计费。我找到官方悄悄上线的空闲压缩和几个社区插件,选定 idle-compact,装上之后却一次都没触发,前后调了几轮才真正生效。

我经常让 Claude Code 跑一个很长的任务,然后去睡觉,第二天早上再回来看。这种时候上下文通常已经快满了,提示缓存早就过期,我发出的第一条消息要把整段上下文当作没缓存过的内容重新处理一遍,额度消耗很大。

我的想法很朴素:任务跑完之后,能不能自动压缩一次上下文?

先弄清楚缓存怎么计费

助手先查了官方的提示缓存文档,几条结论决定了后面所有的做法。

  • 缓存还热的时候做 /compact 很便宜。压缩请求要把整段历史发给模型,这些内容能按缓存读取的价格算,大约是基础输入价的十分之一,主要成本只是生成摘要。
  • 缓存过期之后再做 /compact,反而是最贵的。同样是整段历史,这时全部要按未命中的价格重新处理,还会被写进一份以后没人读的缓存。
  • 压缩完上下文变短,之后每一轮都便宜。

所以压缩必须赶在缓存过期之前做,不能等我早上起来再手动敲。

官方没有“任务跑完自动压缩”的开关

助手确认了两件做不到的事。hook 没有触发 /compact 的办法,Stop hook 只能阻止停止并给 Claude 一段指令,执行不了内置命令。Claude 自己也不能调用 /compact。

能用的只有一个笨办法:在长任务的提示词末尾加一句,让它完成后把结果、改动清单、未解决问题和下一步写进 NOTES.md。早上想继续就直接发消息,想做新事就 /clear,让它去读 NOTES.md,几乎零成本。这个办法最稳,但要我每次都记得写。我想要一个自动的。

官方其实悄悄上线了空闲压缩

助手上网搜了一圈,在 Claude Code 的 issue 里找到了线索:新版本里有一个文档没写的功能,缓存过期之前会自动压缩一次。

  • 2.1.286 起悄悄上线,默认开启,没有 release note。
  • 只在 1 小时缓存下生效,空闲约 54 分钟触发,上下文低于 200k token 的会话会跳过。
  • 由服务端开关控制,客户端设置里只有关闭它的 idleCompaction: false,没有“开启”这个操作。

我追问有没有人明确说过怎么开启,答案是没有,官方人员在 issue 里只提到会加一个设置,用来关掉它。

在我的机器上它多半不会生效。我的 Claude Code 通过第三方中转站访问,还设了 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1,服务端开关可能根本传不到客户端。助手把本机所有会话记录扫了一遍,从来没有出现过 Compacted while idle 这条提示。这条路对我走不通。

这一节来自社区读代码的结论

触发条件和设置名都是 issue 评论里的人读了程序后推断的,不是官方文档。换一个版本可能就变了。

社区有一批插件,关键看我的缓存到底几分钟

助手把社区里能找到的几类插件都看了一遍。

做法代表限制
空闲到点压缩一次,不发额外请求idle-compact固定按 1 小时计时,不检测缓存时长
同上,但从记录里检测缓存时长claude-cache检测到 5 分钟缓存就跳过自动压缩
过期前定时发廉价请求保温一批 cache-keepalive电脑睡眠就停,有的检测到中转站就不工作
保温若干次后再压缩cc-cache-warmer必须用 cmux 终端
保温后压缩一次claude-cache-keeper按 5 分钟缓存设计,会连续发好几次保温请求

选哪个,取决于我的缓存到底是 5 分钟还是 1 小时。

助手最先看的是会话记录里的 cache_creation 字段。几乎所有写入都记在 ephemeral_5m 下,10 月 1 日以后 5 分钟档约 4500 万 token,1 小时档只有 24 万。它据此断定我的缓存只有 5 分钟,推荐了专为 5 分钟设计的 keeper 插件。

我觉得不对,因为我设过 promptCacheTtl: "1h",于是追问了一句:真的只有 5 分钟吗?

助手重新做了一次测量,这次不看标签,看行为。它找出所有两次请求间隔超过 5 分钟、上下文大于 2 万 token 的情况,看后一次请求读到了多少缓存。

时间 / 版本间隔结果
10-02、10-03 / 2.1.2856 到 25 分钟全部未命中,整段重写
10-08 起 / 2.1.293 到 2.1.2955 到 39 分钟绝大多数命中,最长一次 38.7 分钟,上下文 22 万
10-08 起60.9、63.3、63.5 分钟未命中

2.1.285 那几天确实是 5 分钟,10 月 8 日以后基本就是 1 小时了。命中的边界落在 39 到 60 分钟之间,吻合 1 小时缓存,只是 40 到 60 分钟之间没有干净的样本,所以“恰好 1 小时”只能算推断。

标签不等于行为

cache_creation 里的 ephemeral_5m 是中转站返回的字段,不一定真实。判断缓存活了多久,要看间隔之后下一次请求的 cache_read,而不是看这个标签。

选定 idle-compact

缓存是 1 小时,之前被排除的两个插件又可以用了。我让助手把 idle-compact 和 claude-cache 的源码拉下来对比,只读不运行。

  • idle-compact 一共 149 行,不联网,不读写文件,不执行外部命令。
  • claude-cache 626 行,同样不联网,但阈值只能填占上下文窗口的百分比,不能填绝对 token 数。
  • 我平时是在 Orca 的聊天界面里用 Claude Code,Orca 是一个同时管理多个 AI 编码会话的桌面应用。这种会话里直接调用压缩接口会被拒绝,idle-compact 的源码专门处理了这一点,改用 /compact 命令。claude-cache 没有处理。

所以选了 idle-compact。它的行为按源码确认是这样的:

  • 从主对话最后一次请求开始计时,子代理的请求不算。
  • 空闲满 ttlMinutes - marginMinutes,并且上下文不小于 minContextTokens,就压缩一次。
  • 如果已经空闲超过 ttlMinutes,比如电脑睡了一觉,就不压缩,因为那时缓存已经过期,压缩要付全价。
  • 每段空闲最多压缩一次,失败最多重试 3 次。

装之前,助手先把 settings.json 备份了一份,装的是 0.2.1 版,源码与审过的逐字节相同。配置我定的是 ttlMinutes 60,marginMinutes 5,minContextTokens 先定的 5 万,一分钟后我又改成了 50 万。

装好之后,一次都没触发

第二天我发现一直没有看到压缩,问助手是不是没起作用。它把安装之后的会话记录翻了一遍,插件其实一直在运行,只是门槛太高,从来没有同时满足过条件。

空闲时长当时的上下文有没有被压缩
56 分钟4.7 万没有,不到 50 万
276 分钟5.5 万没有,不到 50 万
585 分钟5.1 万没有,不到 50 万

安装之后所有空闲超过 50 分钟的情况,上下文都只有 5 万左右。真正涨到七八十万的会话,我一直在频繁对话,没有一次闲到 55 分钟。安装后出现的三次压缩,都发生在上一条回复之后 3 到 6 分钟,是我自己手动敲的 /compact。

我给自己设了一个永远触发不了的门槛。

同时助手还发现,缓存并不总能撑到 55 分钟。有几次空闲不到一小时,缓存就整段失效了,比如空闲 34 分钟、上下文 60 万,缓存读取为 0,整段重写。原因后面单独写,先说结论:靠近 55 分钟才压缩,压缩时缓存可能已经没了。

于是我把 ttlMinutes 改成 30,minContextTokens 改成 30 万,也就是空闲 25 分钟、上下文超过 30 万就压缩。当天晚上,它在两个会话里各触发了一次。

上一轮结束压缩间隔上下文
17:35:4618:00:26约 24.7 分钟37.8 万降到 0.7 万
18:43:4319:08:39约 24.9 分钟55.9 万降到 1.5 万

这个结论是推出来的。Orca 里的会话是由插件把 /compact 塞进命令队列,记录里写的是 trigger: manual,和我手动压缩看起来一样。能区分开的只有时间:我手动压缩总是在上一条回复后几分钟之内,这两次都是空闲 25 分钟左右,正好对上 30 减 5。

睡一晚,它还是没触发

结果那天夜里,我睡觉的几个会话又全都没压缩。中间我为了测缓存把 ttlMinutes 改过几次,最后停在 60,也就是空闲 55 分钟才压缩。

助手这回去翻了 Orca 的会话库,发现问题不在插件。Orca 的聊天模式每 5 分钟巡检一次,一个会话如果 30 分钟没有动静,没有正在跑的回合,没有后台任务,没有待回答的问题,Orca 就会把它的 Claude 进程停掉,等我下次发消息再拉起来。这个 30 分钟是写死在 Orca 代码里的,设置里没有对应的开关。进程一停,插件也跟着停了,等不到第 55 分钟。

会话最后一轮结束Orca 关闭插件原定压缩
74 万上下文03:1903:49约 04:14
24 万上下文22:4923:19不到 30 万,本来不压缩
34 万上下文22:49约 23:20约 23:44

所以在 Orca 里,压缩最晚要在空闲约 28 分钟时开始,压缩本身还要 1 到 2.5 分钟。ttlMinutes 回到 30,空闲 25 分钟压缩,正好能赶在被停掉之前做完。

代价是空闲 25 分钟到 1 小时之间回来的人,看到的已经是压缩后的摘要,细节会丢一些。我权衡之后觉得隔夜整段重写是最贵的情况,值得这个代价。

一个意外:压缩让后台任务省了一次整段重写

有一个会话是例外,它在 04:13 正常压缩了,空闲 53 分钟,上下文 82.6 万。

这个会话在 03:14 启动了一个后台开发服务器,03:20 一条请求撞上 524 超时,这一轮就结束了。因为后台还有任务在跑,Orca 认为它还有事,没有关掉进程,所以插件能走到压缩那一步。

05:14,开发服务器到了后台命令的 2 小时上限被停掉,Claude Code 给模型发了一条完成通知。这条通知开启了新的一轮,模型读着摘要接着写代码,之后又被 16 次后台通知唤醒,自己干了三个多小时。

那时离上一次请求已经将近 2 小时,缓存早就过期。如果没有压缩,第一条请求要把 82.6 万整段重新写入缓存,实际只写了 5.6 万。

顺手把自动压缩也提前了

助手还提醒我 Claude Code 有一个 autoCompactWindow 设置。我用的模型上下文窗口是 100 万,默认要涨到约 96.7 万才自动压缩。上下文越大,每一条请求都要把整段上下文按缓存读取价再付一遍,也更容易被卡住。我把它设成了 80 万,开新会话用 /context 看,预留的缓冲是 3.3 万,占 80 万的 4.1%,说明已经生效,实际触发点约 76.7 万。

已经开着的会话可能还按旧值算,要立刻生效可以在会话里运行 /autocompact 800k。

最后的配置

设置值含义
promptCacheTtl"1h"主对话用 1 小时缓存
autoCompactWindow800000涨到约 77 万自动压缩
idle-compact ttlMinutes30配合 Orca 的 30 分钟回收
idle-compact marginMinutes5空闲 25 分钟压缩
idle-compact minContextTokens300000不到 30 万的会话不处理
适用边界

这一套是针对我自己的环境调出来的,包括第三方中转站、Orca 和 2.1.29x 版本的 Claude Code。官方的空闲压缩行为、中转站的缓存时长、Orca 的回收时间都可能变。电脑也必须在任务结束后保持唤醒至少 25 分钟,睡眠之后计时器会停。

压缩是有损的,长任务的结尾仍然建议写一份 NOTES.md 做保险。另外这只是确认了“压缩发生了”,没有确认“压缩那一次请求命中了缓存”。后者我是另外查的,下一篇写。

评论