返回《Claude Code 架构设计指南》

上下文管理,五层压缩

IT周瑜

模型一次能看的上下文有硬上限,长对话必然撞墙。这套源码的应对是一个分层的压缩体系,每轮迭代开头按固定顺序过一遍,便宜的先上,贵的能不上就不上。顺序在 query.ts 里排死,工具结果预算、snip、microcompact、context collapse、autocompact。前四层都压不住,或者已经触发过的场景,还有请求过长错误的兜底压缩。

第一层工具结果预算,单条工具结果超限就地裁剪,不动别的。第二层 snip 裁掉早期历史。第三层 microcompact 最能体现这套体系的设计取向,它只清空旧的工具结果内容,白名单八个工具,读文件、跑命令、搜索、抓网页、改文件、写文件,把这些工具较早的结果替换成一句已清理占位。调用记录本身保留,模型知道读过哪些文件,只是看不到老内容了,真需要可以再读。代码里还有时间触发的变体,两次交互间隔超过阈值,顺手清一批。第四层 context collapse 把消息折叠成摘要形态的只读投影。

成本递增 工具结果预算 单条结果超限就地裁剪 snip 裁剪早期历史 microcompact 清空旧工具结果内容 白名单八个工具 context collapse 折叠为摘要的只读投影 autocompact 阈值 = 窗口 − 13000 fork 子代理写整体摘要 413 兜底 reactive 连续失败三次熔断 停止再试
图 8-1 从上到下代价递增,413 兜底挂在最外侧

第五层 autocompact 是整体摘要,触发阈值写成公式就是上下文窗口减一万三千,这一段保留量是常量。围绕它还有一组配套常量,警告线留两万,错误线留两万,手动压缩只留三千,手动压缩下手更狠是因为用户明确要腾空间。测试环境可以用环境变量把阈值改成窗口的百分比,方便复现。真要压缩时走 compactConversation,先跑 PreCompact hook 让用户脚本有机会注入指示,然后 fork 一个子代理替整段对话写摘要。摘要请求的提示词要求模型先在 analysis 标签里打草稿理清要点,再在 summary 标签里出正式摘要,草稿区从最终结果里剥掉。摘要本身也能撞上请求过长,代码的处理是截掉最老的一批消息重试,截断后的消息集要同时喂给两条路径,重试次数有上限,超过就报错。压缩完成后有一整套恢复动作,压缩前的文件状态做成附件重新注入,用过的技能、计划、工具增量清单都补回来,SessionStart hook 重跑一遍,让压缩后的模型不至于失忆。整套摘要通过一个明确的边界标记消息和旧历史分隔,标记上还带着压缩前发现过的工具名清单,第 10 章的工具搜索靠它恢复状态。

自动压缩的失败处理有真实数据背书。源码注释记着一笔账,曾有一千二百多个会话出现五十次以上连续压缩失败,最多一个会话失败三千多次,全网络每天浪费约二十五万次 API 调用。于是加了熔断,连续失败三次就停,常量名叫 MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES。教训朴素,任何自动重试都要有熔断,否则最坏情况的成本没有上界。

兜底层对付 413。请求真的过长被服务端打回,reactive compact 介入,策略更激进,连图片都换成文字占位。配合第 2 章讲过的输出超限三级恢复,升级上限、注续写消息、重试三次封顶,错误恢复的两条线就齐了。

贯穿五层有一条共同纪律,历史消息从不原地修改,每层都产出新数组替换旧的。原因还是提示缓存,缓存按前缀字节匹配,中段改一个字,后面全部作废。microcompact 的时间触发变体在改动消息后特意调了一个通知函数,告诉缓存监控下一次命中率下降是自己的操作造成的,别误报缓存断裂。