编码代理上下文压缩后如何无损恢复:外置记忆与幂等设计 1. 事故现场20 万 token 被“压缩”成 6 万的那一刻先交代一下事故背景。当时我在做一个规模不小的重构实验目标是让编码代理一口气完成一个支付模块的跨仓库改造。基座模型支持 20 万 token 的上下文窗口我觉得绰绰有余于是把需求说明书、仓库结构地图、之前试过但失败的技术方案、各种接口约束一股脑全部塞进去。前两三个小时运行得非常顺代理仿佛一个记忆力惊人的高级工程师能准确引用早期分析过的代码路径能复述三小时前的设计决策。直到某一次交互系统弹出了上下文超限的警告随后自动执行了摘要压缩——用一段几百词的总结替换掉了绝大部分历史消息。我管这次压缩叫“脑叶切除术”因为效果真的很像代理从一个知道全局的人变成了一个只记得最近五分钟对话的失忆症患者。压缩后的现象极具戏剧性前后不到五分钟它开始重新实现一个半小时前已经明确放弃的方案而且是在我特意在上下文里写过“此方案已验证不可行”的前提下。它引用了几个不存在的变量名那些变量位于被早期决策淘汰掉的旧模块里上下文里出现过一次之后就再也没出现过。它自作主张开始修改另一个完全不相关的模块理由是“感觉之前的上下文里提过这块需要改”。实际上根本没提过它只是潜意识里在补脑。我最初以为这是某个模型的偶发抽风但复盘后意识到这不是 bug而是必然。一个能连续运行几个小时、跨越几十个文件的编码任务它的有效记忆注定会超过任何固定窗口的承载能力。上下文窗口再大本质上也只是临时工作台不是长期记忆库。把项目推进的连续性完全寄托在这样一个可变长度的堆栈上就像把重要资料只存在同事脑子里他哪天离职你的项目就断片了。那次事故之后我花了两周时间重新思考编码代理的架构设计最终引入了两个看起来毫不相关的东西Unix 的 init.d 启动机制以及老电影《记忆碎片》里男主角的外置记忆方法。思路很简单让代理不再依赖“完整记忆”来工作而是让它像一台可随时重启的服务像失忆者一样靠笔记和纹身恢复行动能力。改造之后我又人为地做了几次“脑叶切除术”——主动清空会话历史、强制压缩上下文、杀进程重开——代理都能在几个动作内恢复到正常推进状态。这篇文章就是把这次改造的全过程拆开讲清楚。里面有具体的设计思路、文件结构、提示词模板、幂等操作清单以及我踩过的大小坑。适合正在用编码代理做真实项目、并且被“上下文越开越大却越来越不靠谱”折磨过的人。就算你只是用 AI 辅助写代码没有跑到 20 万 token 这种极限这套思路也能救你于水火。2. 为什么“上下文越大越安全”是一个彻头彻尾的幻觉很多人有一个直觉模型记不住事那就把窗口买大一点把能塞的东西全塞进去问题不就解决了我一度也这么想直到做了几个对照组实验才彻底改变看法。2.1 大上下文的三宗罪第一宗罪是注意力稀释。业界管这个叫“Lost in the middle”——模型对上下文最前面和最后面的内容敏感度较高对中间部分往往会“视而不见”。我实测下来非常明显把一段 3000 行的代码仓库摘要放在对话中部问代理某个模块的入口函数是什么它经常答非所问但把同样内容放在系统提示末尾准确率立刻上去了。上下文窗口越大中间地带的盲区也越大因为模型注意力天然倾向于头和尾。第二宗罪是压缩结果不可预测。自动摘要保留什么、丢弃什么完全取决于摘要模型和启发式策略。我观察到的典型倾向是保留最近的对话和系统提示保留那些“看起来很关键”的结论句但丢失中间的推导过程、失败的尝试、以及各种“为什么不做 A 而做 B”的决策理由。这恰恰是最致命的部分——决策理由丢失之后代理会重新提出已经被否决的方案而你甚至不知道它是否再次评估过。第三宗罪是成本和延迟。每一轮请求都要把完整的上下文历史重新发给模型token 越多单次调用的费用越高、响应越慢。我用 20 万窗口跑一个任务实际进行到一半的时候每次请求已经携带了十几万 token 的历史。压缩不是可选项而是必然触发。到那时候你不是在“决定”要让步多少记忆而是在被动接受系统替你做了多少记忆切割。2.2 员工培训 vs 操作手册我觉得整个问题可以类比成两种团队管理方式。方式一你雇了一个高级工程师但不给他电脑、不给他文档只允许他凭你“口头跟他说过什么”来工作。于是你需要不断把信息灌到他脑子里开会、对齐、强调生怕他记漏。这种方式的问题在于他一旦嗓子哑了、出差了、离职了所有知识就归零。方式二你给团队写一套操作手册和配置模板告诉每个人“所有事实都写在文件里有疑问就查文件不要凭记忆擅自决策”。哪怕明天全员换一批人项目照样能推进。init.d 属于方式二。传统的 Unix 系统启动时init 进程会依次执行 /etc/init.d/ 下面的一堆脚本每个脚本负责启动某一个具体服务网络、定时任务、守护进程。关键设计是每个启动脚本本身是完整自洽的它不需要“记住”上一次启动到哪一步了也不需要别人向它口述当前状态。它读配置文件执行启动逻辑对外暴露一个简单的 start/stop/restart 接口。就算某个服务崩溃了、系统重启了只要重新跑一遍脚本服务就能恢复到标准状态。这个思路迁移到编码代理上就成了一句核心原则任务推进的真相必须存放在外部文件系统里而不是存放在模型上下文里。3. init.d 教我的第一课把大任务切成“最小自含单元”拆任务不是一个新概念人人都在说。但 init.d 给人的启发不是“拆得越细越好”而是“每个单元要像服务脚本一样自洽”。3.1 从一个任务脚本到一个任务单元传统 init.d 脚本有三个特点单一职责、接口明确、状态外置。它只管一个服务只接受 start/stop 这类有限指令当前系统状态通过配置文件和 PID 文件暴露给外界。把这些特性翻译到编码代理任务里我给每个大任务设计了四层结构子任务粒度一个子任务可以用一句“验收标准”描述清楚比如“把 validateBalance 函数里针对负数的分支改为返回 Result.Err且现有测试全部通过”。子任务边界明确列出允许修改的文件或目录以及绝对禁止触碰的路径。子任务状态每次开始、每次完成都会重写一个 STATE.md 文件记录当前进度、下一步行动、最近一次通过的测试命令。子任务依赖如果需要调用其他模块的接口必须在 CONTRACT.md 里登记这个接口的签名而不是靠代理“记得”那个接口长什么样。我把一个“迁移支付模块并对接新网关”这样的大任务拆成了类似这样的序列接入网关 SDK在单独的分支上编译通过抽象本地支付接口新旧实现并存迁移存量调用点每改一批跑一次回归测试清理旧代码最终删除废弃文件。每个步骤都能在一到两轮工具调用内完成验证每个步骤结束后都会产生一个 commit 和一次状态文件更新。3.2 这样做为什么能抗压缩因为在新的架构里代理的当前会话只需要承载“当下这一步”的信息不需要承载整个项目的全景图。它每次看到的内容是有限的一个子任务的目标、当前进度、下一步行动、边界条件。其他任何信息都留在仓库里等需要时再读。上下文被压缩后丢失的是“刚才对话里说过什么”而不是“这个任务本来要干什么”。后者存在于 TASK.md 和 STATE.md 里压缩操作删不掉磁盘上的文件。这就是抗压缩的本质把记忆从模型内部搬到文件系统外部。实际测算下来一个代理冷启动时需要阅读的状态文件总共不超过 400 token。也就是说即使上下文被压缩到只剩系统提示和最近一轮输出代理也可以通过重新读取这 400 token 恢复全部必要的任务感知而不是靠模型脑补缺失的三万字历史。3.3 拆分粒度的实操经验粒度不是一个可以拍脑袋决定的量我试过三个档位最后落在“一个函数或一个接口模块”上太粗子任务涉及三个以上模块验证要跑整整二十分钟中途任何意外都会导致代理失去方向。太细一个改动不到二十行代理在切换任务上下文上花费的 token 比干活还多来回倒腾反而降低效率。合适一个子任务改动一个模块验收命令在几十秒内可以跑完移植到下一轮时状态文件的“下一步”可以直接指向具体函数。有个判断技巧如果子任务的验收标准写起来需要用“以及”“并且”这种连接词说明它还不够原子。一个原子子任务的验收标准应该是一句话最好是一个命令。# 一个我实际用过的原子验收标准 npx jest src/balance/validation.test.ts --silent grep -r oldTransfer( src/ --include*.ts | wc -l # 期望输出 04. 《记忆碎片》式的外置记忆系统拆完任务之后还有一个更微妙的问题要解决代理即使拿到一份完整的任务列表它仍然会在执行过程中“重新发明轮子”。我刚才提到过上下文压缩之后代理会重新提出早就被否决的方案。这时候你需要的不只是状态文件而是一套“决策记忆”。4.1 男主角的工作法则电影《记忆碎片》里的男主角患的是短期记忆丧失他记不住几分钟前发生的事情。为了生存他采用了一套极端的个人工作系统把必须相信的事实写在拍立得照片背面、做成随身携带的卡片甚至用纹身把最重要的事实刻在身上。他不试图“记住”事情他只是“读取”事实。每次醒来他都会经历一套固定流程先看笔记确认当前目标执行当前行动完成后更新笔记。他永远不信任自己的直觉只信任外部记录。编码代理遇到上下文压缩时状态比男主角还要惨——它连“醒来”之后的几分钟都记不住。但它也有一个男主角没有的优势它可以随时读仓库里的文件而且读多少遍都不会厌烦。所以我把这个工作法则完整移植到了代理提示词里。4.2 四文件记忆结构我在每个任务目录下维护四个文件只维护四个多一个都不行文件职责更新频率TASK.md任务目标、范围、验收标准范围调整时STATE.md当前进度、下一步、最近证据每完成一步就重写CONTRACT.md对外接口签名、文件位置约定接口变更时DECISIONS.md已否决的方案和否决理由每次否决新方案时追加STATE.md 是核心。每次会话开始代理先读它每完成一个操作代理重写它。重写时遵循一个硬性规则内容必须包含可直接执行的下一步指令而不是模糊描述。这个规则我在恢复演练中吃过大亏后面会详细讲。DECISIONS.md 则是防“失忆复发”的救命稻草。上下文压缩之后代理最容易犯的错误不是忘了任务目标而是重新捡起一个已经被打回的设计方案。我见过它三次提出同一个被否定的方案每次的理由都大同小异。后来我要求所有尝试过的方案都要在 DECISIONS.md 里登记并且必须写明否决理由。下一次代理读到“方案 X 已否决因为会导致旧网关的回调机制失效”它就不会再沿着那条死路走下去。4.3 一份 A4 纸原则外部记忆系统也有一个反模式文件越来越多最终从“外部记忆”变成“另一个大上下文”。所以我给自己定了一个死规定代理每次会话必须读取的核心记忆文件总长度不超过 400 token。这是什么概念大约是一份 A4 纸。这个限制逼着 STATE.md 只保留五个小节当前范围、已完成、进行中、下一步、最近证据commit hash 或测试命令。想记录更长的技术背景就拆到 TASK.md 的子文件里或者放到 docs/ 目录让代理在需要时再读。核心记忆文件保持短小冷启动成本才能控制在可忽略的水平。5. 实操让编码代理在上下文压缩后“无损恢复”的三层防护理论讲完接下来是能直接抄作业的部分。我把整套方案拆成三层防护体系每一层解决一个问题。5.1 第一层任务颗粒度与幂等指令第一层解决“任务被压缩后不知道从哪开始”的问题。这靠两个东西原子化子任务和幂等指令。原子化子任务前面已经说了幂等指令需要单独强调。所谓幂等就是“执行一次和执行十次最终结果一致”。我观察过代理在上下文丢失后的典型行为它发现“这个函数还没写好”于是再“添加”一次结果把一个已经存在的函数又添加了一遍编译直接报重名错误。这种事故的根源是任务描述用了“添加”“修复”“创建”这种动作词代理无法区分“当前状态已经是目标状态”的情况。改成幂等描述之后问题大幅减少不要写“添加 XX 字段”要写“确保 XX 字段存在且类型正确”。不要写“修复 bug”要写“运行测试使 test_balance 这类用例全部通过”。不要写“重构函数”要写“把函数 A 替换为 B并保留 A 作为兼容层直到 grep 确认无调用点”。行动之前先检查状态行动之后再重写状态这就是幂等循环。上下文压缩之后即使代理把同一个子任务执行两遍第二次执行时它先检查“字段已存在”就会跳过这一操作而不是傻乎乎地再创建一次。5.2 第二层冷启动协议与提示词模板第二层解决“会话重开之后代理变成新人”的问题。我给系统提示词加入了一段全局生效的“冷启动协议”每次新会话开始都要执行你是一个无状态执行单元。 你没有上一次会话的记忆。 每次会话开始按以下顺序依次执行 1. 读取 TASK.md、STATE.md、CONTRACT.md、DECISIONS.md 如果文件不存在先把它们创建出来再继续。 2. 运行 git status确认当前工作区基线。 3. 如果 STATE.md 的“已完成”列表已经包含当前子任务直接报告完成并停止。 4. 否则只处理 STATE.md 中“下一步”里列出的第一条任务。 5. 每完成一个小步骤立即重写 STATE.md并运行对应验收命令。 6. 永不修改 TASK.md 和 CONTRACT.md除非任务描述明确要求。这段协议最核心的是第 1 步和第 2 步。代理不是拥有记忆而是“按流程读取记忆”。就算上下文被压缩到只剩系统提示加最近一条输出这段协议仍然在代理仍然会先读文件、再确认基线、再执行下一步。STATE.md 的写法也很有讲究。它必须是一份可以让另一个“失忆代理”直接接手行动的文档。我改进后的格式是这样的# STATE 当前范围 - src/balance/validation.ts 中 validateBalance 函数的入参校验逻辑 已完成 - 接入新网关 SDKcommit a3f9c2e - 抽象本地支付接口 IPaymentProvidercommit 8bd1a50 - 验证通过命令pnpm exec jest src/balance --silent 进行中 - 迁移 src/balance/validation.ts 中对旧 API 的两个调用点 下一步 - 把 validateBalance 函数中针对负数的分支改为返回 Result.Err 参考 CONTRACT.md 第 2 节中 ErrorCode.INVALID_AMOUNT 的定义。 - 完成后运行pnpm exec jest src/balance/validation.test.ts --silent 最近证据 - 最后一次通过测试的 commit8bd1a50这种格式下代理的“下一步”不再是一句口头禅而是一个可以直接开工的指令。恢复演练时我给代理只有这段 STATE.md、系统提示和仓库路径它完整续跑成功。5.3 第三层检查点与模拟失忆演练第三层解决“上下文已经压缩完了怎么办”的问题。我的答案是不要试图在同一个会话里续命直接杀掉会话做一次冷启动。冷启动的完整流程如下记录当前工作时间线把最近一次完成的 commit 记到 STATE.md 里。关闭会话清空对话历史或者直接结束进程。用一个极简的启动指令开新会话“读取 TASK.md 和 STATE.md然后开始干活”。观察它是否能够自行恢复到压缩前的位置。这个流程有点像 init 系统的 restart 操作某个服务状态异常最省事的办法不是修复它而是重启它。因为状态已经外置到文件系统重启之后脚本重新跑一遍状态自然恢复。编码代理也一样会话状态不可靠那就直接丢弃会话让代理从磁盘重新构建认知。我在实验里做过一次故意制造“脑叶切除术”的演练代理已经把支付模块迁移完成了三分之一我手动把对话历史全部清空然后重新启动代理。第一次演练翻车了因为我当时在 STATE.md 里写的是“下一步处理余额问题”。代理拿着这句话看了半天不知道从哪里动手。改进后我把“下一步”写成上面那种指向具体文件、具体函数、具体验收命令的格式再演练一次代理用了不到五分钟就回到了正常推进节奏。这个模拟失忆演练特别有用。它不需要花很多时间却能把系统中所有隐式依赖“记忆”的地方暴露出来。每演练一次你就会发现一个新的“代理其实偷偷记在上下文里”的东西然后把那部分信息搬到外部文件里。6. 实测踩坑与排查速查表整套方案运行了两周我把实际遇到的麻烦按症状整理了一张速查表。如果你也在做类似的改造可以直接对照。症状根因对策上下文压缩后代理开始重复实现已完成函数它在凭空回忆没按流程读 STATE.md冷启动协议第 1 步强制读文件并增加“先用 grep 确认函数是否已存在”的步骤代理开始修改无关模块TASK.md 里的边界描述不够强硬在 TASK.md 增加“禁止触碰路径”段落并明确列出单个文件的绝对路径记忆文件越写越长最终超过 token 预算试图记录所有细节核心文件只保留 STATE 五节DECISIONS 单独拆文件历史背景移出核心文件代理更新 STATE.md 时写“进展顺利”“一切正常”等废话状态文件缺少验证钩子每条“已完成”必须对应一个验收命令或 commit hash没有证据不许写“完成”任务拆得过细来回切换上下文成本高过度拆分默认以“一个函数或一个接口模块”为粒度需要跨三个模块的动作拆到独立分支执行压缩后代理开始重新尝试已被否决的方案DECISIONS.md 没有被强制读取在冷启动协议里显式要求“执行任何方案前先搜索 DECISIONS.md 是否已有否决记录”除了表格里的常规问题还有三个心得值得细说。第一个心得验收命令是最好的状态真相。文字描述再详细也比不过一条能跑出结果的命令。如果 STATE.md 里的“已完成”都带有对应的 commit hash 或测试结果代理失忆后恢复的速度明显加快。因为模型对“真话”的辨识度不高但对“一条明明白白的 pytest 结果”几乎没有发挥空间。第二个心得压缩发生后不要“接着聊”。我见过很多人在上下文告警后试图通过继续提问让代理恢复记忆结果往往是代理开始一本正经地编造话术来配合你。模型和人一样在记忆缺失时倾向于脑补而且脑补得自信满满。所以我的建议是别救了关掉会话做一次冷启动让代理重新读文件。宁可损失半小时的“热状态”也不要靠幻觉工作五分钟。第三个心得用 git 作为最终仲裁者。上下文和记忆文件都有失真可能但 git 分支、commit 树、diff 内容是客观存在的。每次恢复演练都以“运行测试 git status”开局以实际工作区状态为准。就算 STATE.md 被写错了git log 也能告诉你上一次真正完成到哪一步。后来我给代理加了一条规则每次更新 STATE.md 前先跑一次 git status把真实文件变更情况当作事实来源。这样代理写进记忆文件的基本上都是可以从仓库验证的事实。7. 收尾能扛失忆的工具才是好搭档整套方案跑下来让我最意外的不是“代理变聪明了”而是“代理变得让人放心了”。过去我盯着它是怕它忘记某个关键约束现在我可以放手让它连续跑几小时每隔几个子任务检查一次状态文件即可。因为即使最坏的情况发生——上下文被压缩、会话崩溃、进程被杀——我只需要重新启动一个会话让它读一遍磁盘上的状态它就能从上次的真实进度继续走。另一个感受是外部记忆系统不是我脑补出来的新东西它一直在那边。Unix 用 init.d 实现了服务重启后的自我恢复电影里的男主角用笔记实现了失忆后的存续它们共用的核心思想就是一句话重要的东西不要放在大脑里要放在外部并且每次行动前主动读取。如果你也想试试这套方案我给一条最实际的建议不要一开始就在真实项目上做改造太容易翻车。先找一个中等规模的模拟项目写一个 TASK.md跑一次完整流程然后人为清空会话做一次失忆演练。第一次失真之后你会立刻发现你写的 STATE.md 到底有多依赖“上下文里那些没说出来的话”。改到第两三次你就会摸到那个平衡点。最后再分享一个小技巧把四个记忆文件放在任务目录的根部而不是散落在子目录里。这样每次会话的系统提示只需要注入一个固定路径代理不需要在工具调用里反复搜索定位。少一次遍历就少一份出错的可能。我后来在一切准备就绪后又做了一次极端测试把上下文窗口人为压到只有几千 token代理依然能靠这套外部系统把任务跑完。那时我确定了一件事真正让人安心的从来不是更大的窗口而是在窗口缩小为零时系统照样知道自己在干什么。