Claude Code 频繁 Compacting?发布次日我根治了 2 个痛点
v0.5.0 发布次日就出了 v0.5.1——不是翻车救火,是我最喜欢的一类迭代:疼了就根治,一天闭环,一步门禁没省——两个痛点,各根治一次。
先给没读过前篇的读者一句话背景:CCM(Claude Code Model Manager)是我用 Rust 写的单二进制「本地模型路由控制平面」——把 Claude Code 指向任意第三方模型网关(z.ai、DeepSeek 等),中间加装路由、熔断、观测与运行时切换。立项原因第 75 篇讲过,不展开。
时间线先摆出来,全部出自仓库 docs/V0.5_PLAN.md 的记录:
| 日期 | 事件 |
|---|---|
| 10-10 | v0.5.0 tag + Release |
| 10-10 | greenlight「ccm 侧根治」,落地 commit 2d39dae——起因:直连 glm 长会话在 1M 窗口 ~15% 处被提前压缩 |
| 10-11 | 问了一句 --resume 支不支持 → greenlight「实现吧,然后注意修改文档」(commit 84e4d2d) |
| 10-11 | v0.5.1 发布:tag 前 docs 审计 7 缺陷全是文档(修进 tag commit 05ab15c),CI 双 runner 绿,产物 + LF checksums 重下验证 |
痛点一:1M 的窗口,15% 就开始 Compacting
症状与机理
我用 ccm run glm 挂直连长会话(glm-5.3 走 z.ai,真实上下文窗口 1M,z.ai 文档口径),Claude Code 的界面开始频繁蹦 Compacting conversation。压缩点在哪?V0.5_PLAN §11 的落地记录写得很直白:会话在真实 1M 窗口的 ~15% 处就被自动压缩(单例实测,约)。
150K 上下就动手压缩,反推 Claude Code 把 glm-5.3 当成了 200K 档的陌生模型在管。「按假定小窗口提前压缩」的行为,是文档记录在案的。
200K 这个数也不是凭空推断——社区口径约 200K,官方文档对 pinned 模型场景还明文记载了 200K 默认(仓库 USAGE 5.3 记录);只是官方没覆盖 unpinned 的第三方 id,那一档属经验值【从业者判断】。
机理一句话:Claude Code 不认识第三方模型 id,解析不了就按一个保守的假定小窗口做 proactive auto-compaction(README / V0.5_PLAN §11 口径)。它不是坏了,它在保护你——只是保护的参数配错了对象。
钥匙其实早就存在:CLAUDE_CODE_MAX_CONTEXT_TOKENS。§11 记录的适用条件——对无法解析的 id 直接生效(claude code v2.1.193+);对它认识的 claude-* id 无效,除非叠加 DISABLE_COMPACT。以官方文档为准。
192 个测试全绿的 v0.5.0 对此一无所知,这不怪测试:机理藏在别人的客户端行为里,仓库里没有任何测试能表达它——v0.5 做 count_tokens 时就确认过这条边界(the repo cannot test client behavior,V0.5_PLAN §3.3)。发布前靠 spike 照亮过一次(17 次重试风暴,见第 81 篇),这次轮到真实使用。
为什么不打补丁
最省事的补丁人人都会:README 加一节,教用户每次启动前手动 export 一个 CLAUDE_CODE_MAX_CONTEXT_TOKENS=1000000。
为什么不做?因为这个方案把「模型的属性」错放成了「shell 的属性」。你换个终端、换台机器、换个模型,这个 env 就错位了——而窗口明明长在模型身上。
最讽刺的是,ccm 自己都不允许犯这个错:代理模式至今拒绝注入任何窗口,理由记录在 §11——运行时 ccm switch 会让启动时刻注入的窗口过期,launch-time 的窗口配不上 runtime 的模型。我凭什么把一个自己都不肯犯的错,包装成用户手册教出去?
所以根治的形态很自然:窗口成为模型声明的一等字段,直连启动时由 ccm 注入。当时的决定,计划记录的 greenlight 原话只有一句:「ccm 侧根治」。
落地:一个字段,连同它的全部边界
[models.glm]
provider = "zai"
model_id = "glm-5.3"
context_window = 1000000
# 等价的 CLI 写法
ccm add model glm --provider zai --model-id glm-5.3 --context-window 1000000
值的纪律:>= 1,0 在 parse/load/add 三层被拒;CLI 上限钉在 i64::MAX(TOML 整数天花板)——能 parse 却存不下的值在 flag 处就报错,不留到 config save 才炸。
主干一行,边界六条——缺了边界的修复会在别的模式里复发。以下出处均为 V0.5_PLAN §11 与 README:
- 直连启动注入
CLAUDE_CODE_MAX_CONTEXT_TOKENS;代理永不注入(理由如上) - starter 配置 glm = 1M(z.ai 文档口径的能力事实);
claude-*不声明,Claude Code 原生认识它们的窗口 ccm discover注册不带窗口——网关的 /v1/models 里没有这份数据,和 pricing 一样走「手填事实」政策,绝不猜ccm doctor三态诊断:已声明 → ✓;未声明的第三方 current →!提示;openai-compatible(仅代理可用,注入承诺不适用)→ 整行不出现- doctor 还会检出 Claude Code 自己 settings 里冲突的同名 env——但只在「已声明且可直连」时查,不越界报警
/_ccm/models声明了才带字段,没声明 key 直接缺席(wire shape 有测试钉住)
对抗验证走了三轮。第一轮被中途杀掉——验证 agent 无视 review-only 指令改了工作树,两处未授权编辑回滚后对着全量 diff 审过才放行(完整教训在第 82 篇《让 Agent 审 Agent》),此后固化为全只读 Explore agent + git-diff 篡改封条。
抢救出的 findings 硬化五条边;第二轮(封条完整,专审这个特性)12 confirmed / 3 plausible / 5 refuted,再硬化六条——最硬的一条:doctor 的窗口诊断连缺 provider 的模型都会报警,可那种启动在注入发生前就中止,诊断注定扑空;现在 provider 查找排在窗口检查之前,这道闸收敛成有测试钉住的纯谓词。
第三轮是发布前的 docs 审计(7 条,见时间线)。
痛点二:--resume 传不进去
从一个问题到一行决定
10 月 11 日,我盯着 ccm run 问了个朴素问题:claude --resume <session-id> 做得到吗?做不到——ccm 把 claude 子进程包起来启动,参数通道压根没留。
V0.5_PLAN §11 记录的 greenlight 原话:「实现吧,然后注意修改文档」。
包一层进程的隐藏成本:宿主每多一个参数,封装层就欠一次门。
设计:把 -- 之后的世界原样交出去
ccm run [flags] [target] -- <claude args...>
# 例如续一个会话(<session-id> 换成你的会话 id):
ccm run glm -- --resume <session-id>
实现核心是 clap 的 #[arg(last = true)]:-- 之后的一切被原样捕获,追加到 claude 之后,直连与代理两种模式都生效。
三个细节。其一,-- 之后的内容永远不填 target 位置参数——clap 见到 -- 就跳到最后一个 positional,所以 ccm run -- --resume <id> 启动的是持久化默认目标,不会把 --resume 误当模型名。
其二,我最在意的一条回归钉:ccm run glm --proxy 必须仍是代理模式。尾部捕获一旦写贪,这条命令会被静默翻转成直连启动——测试专门钉住这个行为,防未来的重构悄悄吃掉它。
其三,clippy 强制把代理会话参数收拢成一个 Option<(&str, &str)>——两个值要么都在、要么都不在。这原本是两个 Option 之间的君子协定,现在成了编译期事实。
Windows 护栏:大声拒绝,好过猜一种转义
Windows 上 ccm 经 cmd /c 启动 claude,& | < > ^ " 和换行符就是现成的注入面(其他平台直接 exec 无 shell,没这个问题)。
策略不是转义,是拒绝:含元字符的透传参数在任何副作用之前点名报错、exit 1。计划记录的政策原话:"loud rejection beats guessing an escaping strategy"——拒绝得大声,好过猜一种转义策略。
两个细节,都是对抗验证逼出来的:
- 「任何副作用之前」动真格:校验被提到 proxy pre-switch 之前——pre-switch 会持久化写 clients.toml,先切再报错就留下脏状态
- 元字符集合最初漏了换行符:裸换行在 cmd 里就是命令分隔符,而标准库不会替它加引号
%PAIR% 变量展开是记录在案的 caveat,不拦——拦它就得猜 cmd 的展开语义,又绕回猜转义的老路。
验证抓到了什么
痛点二另起一轮对抗验证(全只读 agent + 封条完整)15 findings,去重后 7 confirmed,另 6+2 条被有据驳回。最值钱的一条:launcher 的 argv 测试在 Linux CI 上会 panic——get_args() 不含程序名,Windows 上碰巧无害。
修法不是放宽断言,是在 WSL2 上实跑,把两个平台各自的完整 argv 钉进测试。
又是「真实环境 × 交界处」:门禁全绿拦不住的,都藏在两块钢板的焊缝里。
E2E 冒烟(Windows,V0.5_PLAN §11 记录的三种结局):
-- --version 透传 → 打印 claude 版本,exit 0
含 cmd 元字符的参数 → 点名拒绝,exit 1
claude 参数忘在 -- 之前 → clap 分隔符提示,exit 2
评论区预答:三个一定会来的反驳
反驳一:出尔反尔? v0.5 计划白纸黑字写着不把 context window 持久化进 Model schema——这条 non-goal 确实在,V0.5_PLAN §4 的判据原文:"point-in-time facts go stale in config"——时点事实写进配置会过期。但它针对的是 probe 自动探测的数据;这次落地的是手填声明,用户对自己选的模型负责,与 pricing 表同一政策。
计划 §4 同时记下了推翻本身:"reversed post-release as a hand-entered declaration"。可以推翻,推翻必须留痕——这是「文档即契约」的另一半。
反驳二:一天一个版本,是不是 v0.5.0 没测好就发了?
恰恰相反,v0.5.1 一步门禁没省——时间线最后一行就是清单:docs 审计、双平台门禁、CI 双 runner 绿(run 38105354005)、checksums、重下验证(V0.5_PLAN §2)。两轮发布审计(v0.5.0 8 条 confirmed、去重 5 个缺陷;v0.5.1 7 条)缺陷全是文档——代码有测试兜底,文档只能靠审计。
至于「没测好」——这类交界处问题只有真实使用照得亮。发布不是测试的终点判决,是探测的起点。攒一个月发个大版本?那期间每一次 Compacting 都在提醒我痛点还活着。
反驳三:一行环境变量就能解决,何必这么重?
能解决一次启动,解决不了错位——窗口是模型的属性,不是 shell 的。完整论证见上文「为什么不打补丁」一节。
写进 Claude Code 自己的 settings.json?那份 env 是静态的,ccm switch 运行时换模型它就悄悄过期——这正是代理模式拒绝 launch-time 注入的同一条理由。注入必须跟着「当前直连的那个模型」走,所以它的家在模型声明里;doctor 会检出 settings 里的同名冲突,抓的就是这份静态 env 和声明打架的时刻。
方法论:疼了就根治的三要素
两次迭代共用一个骨架,拆开是三条:
- 定位机理,不停在症状。 频繁 Compacting 是症状;「客户端按假定小窗口管理陌生 id」才是机理。对着症状修(教用户调低期望、忍一忍)是安慰剂。
- 修在正确的层。 窗口是模型的属性 → 进模型声明;不进用户手册,不进 shell env,不进 FAQ。
--resume同理:正确的家是原样透传通道,不是一张越攒越长的参数对照表。判断口诀:这个知识应该住在哪个数据结构里? - 边界与主干同落。 代理不注入、doctor 三态、discover 不猜数、
claude-*免声明——只修 happy path 的根治是半个根治,另外半个会在别的模式里复发。
打补丁和根治的差别,一张表说清:
| 打补丁 | 根治 | |
|---|---|---|
| 知识住在哪 | 用户手册 / FAQ | [models.<name>] 一等字段 |
| 出错谁发现 | 用户搜 issue | ccm doctor 启动即诊 |
| 换模型 / 换终端 | env 错位 | 声明跟着模型走 |
| 下一个用户 | 重新踩一遍坑 | 无感 |
ccm 的价值全在「我的配置 × 别人的客户端」这条缝上——缝上的行为要么根治、要么把边界写成声明,唯独不能糊。
现在就能做的事
在用 Claude Code 接第三方网关的读者,30 秒自检:
# 装了 ccm 的:先跑体检,看窗口诊断行
ccm doctor
# ✓ 已声明 / ! 第三方模型未声明 / 整行不出现(openai 兼容)
# 再验透传通道
ccm run glm -- --version
# 打印 claude 版本且 exit 0,通道就是通的
没用 ccm 的,记一件事就够:频繁 Compacting conversation 而网关明明是大窗口,先查 Claude Code 认不认识你的模型 id——CLAUDE_CODE_MAX_CONTEXT_TOKENS 对陌生 id 直接生效。
两个特性的完整落地记录在仓库 docs/V0.5_PLAN.md §11;ccm 本体在 github.com/Thneoly/ccm(MIT,206 tests,Windows + Linux 双平台验证)。《CCM 造记》第 83 篇,下一篇见。