前阵子我碰到了个特别崩溃的场景给一个AI编码代理排了一整段连续开发任务它读源码、跑测试、写改动上下文一路涨到接近20万token。正当我以为一切都按计划推进时模型侧一次上下文压缩直接把代理打回原形——它开始反复修同一个文件、自相矛盾地改接口、甚至打算去创建已经被我删掉的模块。那一刻真像给代理做了一场“脑叶切除术”。如果只是偶尔炸一次也就算了问题是这种“失忆”对于长周期任务几乎是必然事件。我自己踩过几次坑之后慢慢琢磨出一套能让编码代理对上下文压缩免疫的土办法核心就两条参考Unix init.d那种“进程重启但状态自恢复”的思路再借一层《记忆碎片》里主角靠外部便签活着的思路。这套东西不依赖任何特定工具纯粹是工作区组织和提示词设计层面的对策。无论你用的是哪种模型、哪种Agent框架都可以直接套用。这篇文章会从上下文压缩的成因讲起然后给出我实际在用的工作区状态文件方案、便签协议、恢复流程最后附上排查经验和一批“什么时候根本不需要做免疫”的判断标准。内容偏实践适合正在用编码代理处理多文件重构、跨模块改造、或者长周期维护任务的开发者。1. 背景20万token的“脑叶切除术”是怎么发生的1.1 上下文膨胀与压缩的真实触发点先说清楚20万token是怎么堆出来的。很多人以为AI编码代理的上下文主要来自用户对话其实不是。编码代理这类工具的上下文消耗大头通常有三块代理为了理解项目会主动读取源文件一个上千行的模块读进来就是好几千token。要做跨模块重构时它可能一口气读十几个文件瞬间就是3-5万token出去了。工具调用结果也会全部留在对话历史里。跑一次测试stdout、stderr、exit code少则几百token多则几千token。如果测试带日志输出一次就能把上下文推高几千。模型自身每轮输出也是算进后续上下文的。编码代理的每个操作都要先生成计划和代码一次完整回复可能有2000-4000token一轮轮累积下来非常可观。因此一个执行了30-40轮工具的连续任务轻松就能把上下文推到15-20万token。当模型上下文窗口逼近上限时服务端通常会采取两类处理一是直接把最早的对话记录截断掉这部分信息彻底消失二是把历史对话做摘要压缩但摘要本身会损失大量细节尤其是具体文件路径、具体变量名这类关键信息。截断后的后果就是代理明明在第三轮已经决定采用方案B并完成了初步实现到了压扁之后它却只记得自己在“重构数据访问层”然后按方案A的旧思路继续输出代码。轻则重复劳动重则直接生成引用不存在的接口、覆盖已完成的改动。1.2 为什么这对编码代理来说比想象中更致命对普通问答来说上下文被压缩只是回答质量下降。但编码代理是带状态的执行器它在每一轮里做的决定都建立在前几十轮的事实之上。压缩等于突然抽走决策链条的下半部分代理的行为就会从“连贯执行”退化成“每个动作随机排列”。我在实践中见过最典型的情况是代理处理一个数据库迁移任务前20轮已经完成了新表的创建脚本并修改了三个DAO文件。压缩发生后它继续“完善迁移”但不是去检查现有脚本是否完整而是重新生成了一份同名脚本覆盖掉原有的、经过多轮修正的版本。而且它自始至终没意识到覆盖有问题因为在它的上下文里“曾经有这份脚本”这个事实已经不存在了。编码任务的另一个特点是纠错成本高。问答答错了重新问一句就行编码代理如果在一个被压缩后的状态下胡写五百行代码你光靠肉眼review都能盯半天。所以对编码代理来说“压缩免疫”不是一个锦上添花的优化而是长任务能不能真正跑完的前提条件。2. 破局思路代理不应该“记住”而应该“外置记忆”2.1 Unix init.d留给我的启示每次启动都是一次冷启动如果你接触过传统Unix服务应该对init.d目录不陌生。System V init把一个个服务的启停脚本放在 /etc/init.d 下系统启动时按运行级别依次执行这些脚本把服务拉起来。这里有个容易被忽略的细节init.d脚本本身只负责start、stop、restart这些动作它不会替服务“记住”业务状态。真正让一个服务在重启后还能继续干活的是服务自己。它启动时去读自己的配置文件、数据目录、上次持久化的中间结果然后把自己恢复到“可以继续工作”的状态。防火墙程序重启后仍然知道该放行哪些端口因为它读了iptables的规则文件Web服务重启后仍然知道自己的域名配置因为它读了Nginx的conf文件。整个系统之所以能在崩溃后自愈靠的不是某个进程的大脑而是“外部状态文件 启动时主动加载”这套机制。AI编码代理遇到的上下文压缩本质上也类似模型侧就是一次“进程重启”对话历史就是易失的RAM压缩一发生RAM里的东西就没了。代理如果还想继续干活就得像init.d拉起的守护进程一样主动去读磁盘上的“配置和数据文件”把自己恢复到上一个检查点。指望上下文里残留的那点摘要是不够的得靠工作区里真实存在的文件。2.2 “记忆碎片”教会我的便签协议《记忆碎片》这部电影我反复看了好几遍主角因为无法形成新的长期记忆只能靠便签纸、照片和纹身来维持对世界的追踪。他每过几分钟就会“失忆”但靠着外部记录他仍然能一步步推进自己追查的目标甚至能完成相当复杂的多步骤行动。其实每个AI编码代理都是电影里的主角模型本身没有长期记忆每一轮对话对它来说都是“新的一天”上下文压缩只是把这个“新的一天”出现的时间点提前了而已。所以没必要把压缩当作无解灾难只需要像主角给自己留便签一样给代理设计一套随时可读的外部记录协议。我刚开始理解这一层的时候很激动但很快发现一个更关键的问题便签不是写得越多越好。主角的便签之所以有效是因为每张都回答了一个具体问题。对我设计的代理来说便签必须回答这几个问题我们现在在哪个任务上我们已经完成了哪些关键步骤下一步要做哪个具体动作绝对不能碰哪些已经否决的选项这四件事写清楚压缩之后代理就能靠便签精确回血。没写清楚便签再多也只是让代理在错误方向上更笃定地狂奔。3. 实战给AI编码代理做上下文压缩免疫3.1 第一层像init.d一样设计工作区状态文件具体落地时我会在每个被代理管理的项目根目录下放一个隐藏目录比如 .agent-state/里面用固定文件名维护状态。这套结构模仿的是init.d的“服务启动时要加载的状态”.agent-state/ ├── TARGET.md # 最顶层目标声明“我到底在做什么” ├── PROGRESS.md # 轻量进度只写入已完成和下一步 ├── CONTEXT.md # 当前任务现场代理每次被压缩后优先读这个 ├── NOTES/ # 按主题拆分的长期笔记 └── tasks/ └── 01-dao-refactor.md # 子任务级别便签每个文件的用途必须单一。TARGET.md 不写细节只写项目级目标目标会被反复阅读所以永远控制在30行以内。PROGRESS.md 是任务清单每完成一个关键节点就更新一次格式就三块已完成列表、进行中的事、待办列表。CONTEXT.md 是压缩后恢复的入口里面除了指向其他状态文件还会抄录最重要的当前决策和禁止事项它的理想状态是“上下文被压扁后代理只用读这一个文件就能重新接上线”。写入时机上我要求代理在两种情况发生时必须更新文件第一完成了一个可独立验证的修改比如“DAO层三个文件重构完成且测试通过”第二发现原计划需要调整时比如“方案B不可行改方案C”。这样做的好处是状态文件始终反映最近一次真实进展而不是代理想象中的进展。3.2 第二层Memento式便签协议状态文件是基础但真正让我觉得“免疫了”的是给代理设计了一套更严格的便签模板。这套模板直接作为一种固定格式写进代理的系统提示词里要求它每执行几个动作后就把关键结论ullback成一则极简便签。我的模板长这样STATUS: IN_PROGRESS或 DONE / BLOCKED GOAL: 完成数据访问层从JDBC到MyBatis的迁移 COMPLETED: [x] 新增 mybatis-config.xml [x] UserDao 迁移完成单元测试通过 NEXT: 迁移 OrderDao先读现有实现再动工 STOP: - 不要删除旧的 JDBC 实现迁移完成并验证后再手工清理 - 不要改动 UserDao它已经通过测试 TRUTH: - OrderDao 当前位于 src/main/java/.../OrderDao.java - 新版本接口不再支持 setConnection() REF: src/main/resources/mybatis-config.xml注意STOP这一块。《记忆碎片》式的表达里“不要做什么”和“下一步做什么”同样重要。我在实践中发现上下文被压缩后代理最危险的行为不是停滞而是把已经否决的方案重新当成新鲜想法执行。把“停止项”写进便签相当于给代理装了一个护栏。这则便签不需要很长原则是必须能被模型在一个阅读步骤里消化掉所以整体压缩在10-20行以内。每隔几次工具调用就更新一次更新不是重写全文而是只替换发生变化的字段。这样代理即使在压缩发生的瞬间失去了上下文也能在恢复后第一时间读到自己最近的状态。3.3 压缩发生时的“第一响应动作”即使有状态文件和便签代理第一次遭遇压缩时仍然可能懵住因为它刚经历了一次“失忆”根本不知道自己失忆了。它会继续按照残留的最后一点上下文乱写而不是主动去读状态文件。所以恢复机制必须前置到系统提示词里。我会在给代理的全局指令里加入这样一条硬规则如果你发现自己对上下文中的任务进度不再清晰或者认为某些前置步骤缺失请不要猜测不要继续修改代码。先执行“状态恢复动作”读取 .agent-state/TARGET.md、PROGRESS.md、CONTEXT.md然后输出一份“恢复确认清单”列出现在处于哪个阶段、下一步计划做什么并等待人类确认或直接按清单继续。这一步非常关键。因为许多代理框架在上下文被截断后不会遇到任何显式报错它只是安静地继续生成。如果不加这个“先读后做”规则状态文件设计得再好也可能不被使用。与此同时我自己也会用脚本做一道保险。项目里放一个 load-workspace.sh把状态文件拼成一篇bootstrap摘要供代理在压缩后一次性读取#!/usr/bin/env bash # 用法在代理被压缩后要求它运行此脚本并阅读输出 cat .agent-state/TARGET.md echo --- cat .agent-state/CONTEXT.md echo --- cat .agent-state/PROGRESS.md echo --- ls -t .agent-state/tasks/ | head -5与其让代理自己去翻一整个目录不如由脚本给它一份高度浓缩的“当前现场”。这就像init.d脚本里的status动作一样让任何新接手的进程都能快速弄清状况。4. 实操记录一次被压缩后的快速回血4.1 事故现场一个重构到一半的跨平台项目我拿一次真实踩坑经历来复盘。当时我让一个编码代理处理模拟项目X的跨平台数据访问层重构任务是把它从一套老旧的JDBC直连方式迁移到ORM框架涉及十几个数据访问对象并且要求不能破坏已有接口。任务大概执行了四十多分钟代理已经完成了UserDao和ProductDao两个模块的迁移测试也通过了。问题出在我一时手滑调整了模型侧的上下文策略代理的下一次请求直接触发了压缩20万token的上下文被截断到只剩很少一部分。恢复后的代理表现非常典型它开始打算重新实现UserDao因为它的上下文里“UserDao已经迁移完成”这条事实已经丢失了。我还观察到它准备新建一个已经存在的Mapper文件并且在代码里引用了一个已经被移除的旧工具类。我立刻中止了它的动作没有让它继续盲目生成。4.2 恢复流程冷启动而不是热修复我的恢复动作分五步走完全是照着init.d“重启后自检状态”的思路来的。第一步停止生成隔离现场。我用CtrlC中断代理不让它在失忆状态下继续产生新的改动。第二步检查工作区脏状态。我手动跑了两个命令git status --short git diff --stat这两条命令可以在几十秒内判断代理在压缩前后的实际改动范围避免它只是在上下文中“以为自己改了”而实际上没有落盘。第三步运行 load-workspace.sh让代理重新读取状态文件。这里我不让代理自己随便翻文件而是让它只读脚本生成的bootstrap摘要先把目标、进度、禁止项重新装进它的上下文。第四步执行一致性核对。让代理把自己刚读完的状态和磁盘上真实存在的文件做一次对比输出“已完成清单”的验证结果。这一步很重要因为它能发现上下文被压缩前剩下的一些隐性问题比如代理之前改了文件却忘了更新PROGRESS.md或者PROGRESS.md写了完成但实际代码不完整。第五步重新接续任务。恢复后的代理被明确告知不需要重新设计整个迁移方案只需要继续处理还没有完成的OrderDao迁移。这相当于把一个已经启动的守护进程杀了一次然后重新拉起但它通过读取状态文件直接回到了原来的业务现场。整个恢复过程大概用了三分钟。对比第一次遇到压缩时我原地修修补补折腾半小时还没把代理拉回状态这三分钟让我彻底确认了这套方案可行。4.3 恢复过程中的额外发现这次事故让我注意到一个经常被忽略的点代理在上下文被压缩前往往已经产生了一些“思维垃圾”——它在某一轮生成的备选方案、临时变量名、甚至废弃不用的导入语句虽然没有被执行但残留在工作区文件里。当代理恢复后如果带着这些垃圾继续往下写很容易把新代码和旧草稿混在一起。所以我在恢复流程里额外加了一个动作恢复后强制做一次代码体检重点查未使用的导入、重复定义的工具方法、以及引用了旧接口名的残留调用。这一步不需要多复杂的工具grep就能快速完成grep -rn OldJdbcUtil src/ || true如果发现残留引用就让代理先清理干净再继续下一个任务。这样恢复后的代理不仅在“任务方向”上对了在“代码卫生”上也比压缩前更干净有时甚至是一次变相的轻量化。5. 常见问题与排查技巧实录5.1 上下文压缩后的典型“失智”症状速查表我在多轮实践中整理了一份症状与对策对照表适合遇到压缩时快速定位问题。常见症状深层原因恢复动作反复修改同一个已完成模块上下文丢失了“已完成”事实重读PROGRESS.md标记已完成项为冻结状态突然扔掉原方案开始新方案历史摘要把决策过程压没了在STOP字段里写明已否决方案恢复时不讨论旧方案引用已删除的接口或工具类代码事实和上下文不一致先跑grep/编译拿到真实错误清单再改代码重复创建同名文件或模块不知道文件系统现状恢复后先输出目标目录的文件树再决定是否新建生成代码时旧逻辑与新逻辑混用上下文残留草稿被当成事实清理未提交的杂乱改动只保留经过验证的版本这张表格不是让你逐条对号入座也不是说每次压缩只会触发一种症状。实际上常见的情况是一个代理同时表现出两三种症状所以第4节里的“停止生成 - 读状态文件 - 一致性检查”流程无论症状如何都建议整套执行。5.2 排查“外置记忆”是否真的在起作用状态文件做了却不检查等于没做。我后来养成一种习惯每隔一段时间就做一次“空对话恢复测试”。方法是直接开一个新会话不粘贴任何旧对话只把一个状态文件的内容贴给模型让它基于这些信息回答“当前任务是什么下一步要做什么哪些事情不能做”。如果模型能给出准确答案说明你的外置记忆质量合格。如果模型答得模糊甚至把STOP里的“不要删除JDBC实现”理解成“下一步删除JDBC实现”那么问题通常出在状态文件本身写得不够清晰或者信息组织方式与模型的理解方式不匹配。我调整过几次模板发现最有效的写法是短句、明确动词、带具体路径。比如不要写“继续迁移数据访问层”而要写“将OrderDao.java从JDBC直连改为Mapper接口实现Mapper文件位于 src/main/resources/mapper/OrderMapper.xml”。模型对具体路径和具体文件的记忆远比对抽象动作的记忆可靠。另外还要定期检查状态文件的真实性。如果PROGRESS.md声称完成了某个模块但实际代码还没编译通过这个状态文件就会变成另一个幻觉源头让代理在错误的“已完成”基础上继续构建。所以我在每次完成关键里程碑后都会跑一次构建用构建结果去校正状态文件。5.3 我踩过的几个坑第一个坑是状态文件越写越大最后变成一颗新炸弹。最初我让代理把所有技术细节都写进CONTEXT.md结果文件涨到几百行本身就被模型上下文截断完全失去“快速回血”的意义。后来我把CONTEXT.md严格控制在30行以内详细内容下沉到NOTES目录只让CONTEXT.md保存指向。第二个坑是允许多个代理并行更新同一个状态文件。有一次我开了两个代理会话分担两个模块的迁移它们同时去写PROGRESS.md把对方的进度覆盖了。后来我改用固定规则每个子任务有独立的tasks/xx.md总进度文件只允许“当前活跃代理”写入否则就加锁标志。我的做法是在PROGRESS.md顶部留一行ACTIVE_AGENT: session-123另一个代理看到ACTIVE不属于自己就不写总进度。第三个坑是恢复时保留了旧对话残留。有一次我让代理在同一个会话里做恢复虽然它读了状态文件但旧上下文里那些已经被否决的片段仍然影响着输出。后来我坚持“空环境恢复”的原则恢复动作必须从新会话开始或者至少清空之前的工具输出让代理只带着bootstrap摘要重新出发。6. 有些时候真的不用做免疫6.1 短任务和一次性任务上下文压缩免疫不是银弹也不是所有编码代理任务都需要这套基础设施。如果任务能在二十分钟内完成或者上下文消耗低于模型窗口的50%那大概率不会触发压缩。这种情况下给工作区加状态文件纯属增加成本和认知负担让代理直接对话反而更高效。我自己的判断标准是这次任务是否需要代理在多个文件之间来回修改并且每一轮修改都依赖于前几轮的产出。如果不需要那这个任务就是短任务直接让代理一口气做完即可最多在结束时人工review一遍。比如生成一个独立脚本、修复一个单元测试、把一段代码从一种写法改成另一种写法这些都不需要外置记忆。6.2 与其做免疫不如做“分阶段化疗”比给代理设计恢复机制更省心的方案是从源头限制每个会话的上下文膨胀。把一个大需求拆成多个独立的小批次每个批次只负责一个子任务上下文小、干净、用完即弃。这相当于把“长时间运行的大进程”拆成“多个短生命周期的任务”从根本上降低压缩发生的概率。我通常会写一个简单的调度脚本把大任务切成三个阶段step1() { # 阶段1让代理生成迁移清单输出到 tasks/plan.md # 上下文只包含需求文档和源文件清单完事即结束 } step2() { # 阶段2读取tasks/plan.md让代理逐个处理清单里的文件 # 每个文件单独启动一个代理会话避免十几个文件的历史全部累积 } step3() { # 阶段3汇总验证跑编译和测试把差异反馈给一个新的代理会话 }这种“分阶段化疗”的思路本质上是在压缩发生之前就把记忆切换到外部。每个阶段之间传递的就是一个小文件下个阶段用空上下文启动也能正常工作。很多时候你会发现根本不需要“恢复”机制因为每个代理会话都足够短根本活不到被压缩的那一步。不过要注意分阶段只适合可以清晰切分的任务。如果任务要求某个代理必须全程掌握全局状态比如“同时调整A模块并让B模块适配A的新接口”硬拆分反而会丢失跨模块的一致性。这时候还是老老实实做状态文件和便签协议更稳。6.3 混合策略按失忆成本做决策我在落地时常用“失忆成本”来判断用哪套方案如果代理失忆后的修复时间不超过十分钟就什么都不做如果修复时间可能超过三十分钟就给工作区加状态文件如果任务跨度超过一天或者涉及多个文件深度耦合就默认开启完整的状态文件加便签协议并且把大任务切成阶段配合恢复流程一起使用。这套混合策略看起来朴素但在实践里非常稳。它既避免了高射炮打蚊子也避免了大任务裸奔到最后被一次压缩全部掀翻的惨案。我自己在项目里的习惯是给编码代理的工作区里长期保留一套状态文件不管这次任务用不用得到多一个目录总比多一次灾难好。真正要权衡的只是状态文件要不要更新得那么勤——短任务就让代理少写几笔长任务就严格按协议维护。最后分享一个让我受益最多的细节不要把“状态文件”当成代理的额外负担去逼它每天写作文而是把它当成“给下一位代理的交接手册”。每一次代理因为压缩失忆或者因为会话关闭重新启动下一棒能不能接上全靠这份手册写得是否真实、简洁、指向清晰。我甚至会把这句话写进代理系统提示词“你每次更新状态文件时都假设下一位接手的同事完全不认识你也完全没看过当前对话。”用这种心态去写状态文件才真正具备让代理对上下文压缩免疫的力量。
阅读完成 · 觉得有帮助?