对抗AI代理上下文压缩:外部记忆与init.d启动脚本工作流
你的代理刚被做了一次脑叶切除术它还没意识到发生了什么正礼貌地问你“我们刚才在做什么”这不是段子是我用 AI 编码代理开发某跨平台系统的第三天真实遭遇的一幕。20 万 token 的上下文窗口听起来很大但当一个会话里塞满了代码片段、报错堆栈、文件结构和几十轮修改讨论后工具开始执行上下文压缩。压缩之后代理对几分钟前的细节一无所知像极了电影里那个每隔几分钟就失去记忆的主角。这个场景有多痛经历过的人都懂。更要命的是如果你只是把需求重新粘贴一遍它依然会丢三落四。我试过各种“补救”手段踩了一堆坑之后才慢慢总结出一套让代理对上下文压缩“免疫”的工作流。核心思路就两样东西Unix 里那个古老的 init.d 启动脚本机制和一部老电影《记忆碎片》里主角用的外部记忆系统。这篇文章把我的完整方案、文件模板、启动词和踩过的坑都写出来给同样被上下文压缩折磨的工程师一个能直接抄的作业。1. “脑叶切除术”发生那一刻上下文压缩到底拿走了什么1.1 上下文窗口不是硬盘是一块会被擦写的草稿板先搞清楚一件事大语言模型的上下文窗口本质上是一块临时工作区。模型每次生成新的 token 时都需要重新“过一遍”上下文里的全部内容就像是老师上课要重新看一眼黑板上的所有板书才能往下写。这块黑板越大老师每节课要花的精力就越多反应也会变慢。所以当你把对话拉得很长时工具为了控制延迟和成本会在接近上限时进行上下文压缩。压缩的动作通常是这样的工具把“原始对话记录”交给模型让它生成一份精简的摘要然后把摘要当作新的上下文基础继续对话。表面上代理还保留着整体认知比如“我们在开发一个跨平台系统”“系统里有网络层问题”但具体到代码、参数、步骤全部被概括掉了。这就像你烧掉一本厚厚的笔记本然后凭记忆口述一份纪要纪要再准确也丢掉了原稿里那些细微的、未成形的念头。1.2 一次真实的失忆事故记录我用一个具体案例说明。当时在开发一个模拟项目做到第三天时会话累计到了接近 20 万 token主要在排查一个偶发超时问题。压缩发生后的代理表现如下它知道项目名称、整体架构、当前大目标是解决网络层问题。它不知道我已经把超时重试参数从 3 次改成 5 次并通过日志验证过效果。它不知道我已经排查到证书轮换逻辑并且怀疑是证书链的问题。它不知道我们刚刚决定放弃某种重试策略因为它会导致请求堆积。我让它继续修超时问题结果它兴高采烈地把那个已经被否决的方案又实现了一遍。那一刻我感觉自己不是在和一个助手合作而是在照顾一个每隔五分钟就失忆的病人。更讽刺的是压缩前代理还主动跟我说“我已经把这些改动记在心里了”但压缩机制根本不给它“心里”的位置。1.3 压缩真正拿走的是“状态”不是“信息”我把压缩前后的损失清单列成了表格方便看清楚。丢失内容压缩前压缩后项目背景完整大致保留当前正在修改的文件明确经常忘已完成的操作明确模糊技术决策和理由明确只剩结论被否决的方案明确完全丢失容易重走老路未验证的假设明确丢失这里的关键认知是上下文压缩拿走的不是“信息”而是“状态”。信息可以从代码仓库、文档里重新读出来但状态——包括你已经走了多远、下一步该踩哪块石头、哪条路是死路——只存在于那段被压缩掉的对话里。你所有“重新喂一遍”的努力本质上都是想把这部分状态从外部灌回代理的临时工作区。问题在于这个工作区本身仍然是易失的。你把丢失的内容粘回去没过多久又会满又会触发下一次压缩然后又丢一次。这是一个注定追不上的循环。2. 三个“重新喂回去”的救法为什么全军覆没我属于那种不信邪的人。第一次失忆后我尝试了所有直觉想到的补救方式结果就是花了好几天一次次地在同一个地方跌倒。2.1 重贴需求你给了目标没给状态机第一个想法最简单把原始需求、最新代码、相关文件全贴回去让代理重新读一遍。看起来没毛病但这就像你给一个失忆的人看完成拼图的完整图案却不告诉他哪几块已经拼好了、哪一块刚拼错又拆下来了。他当然知道最终要拼成什么样可他不知道当前进度只能从零开始摸索。代价是巨大的。重新贴回去的代码和需求每一段都是 token。本来上下文就已经满了你这一贴等于又往一块满着的水杯里强行倒水十几轮对话后大概率再次触发压缩。结果就是代理在“失忆-重读-再失忆”的循环里反复横跳永远无法深入任何一个问题。这种撞墙式的体验让我意识到问题不在“喂什么”而在“喂给谁”。只要上下文还是一个随时会被清空的临时白板你往里写什么都是沙上建塔。2.2 靠 Git 日志抢救只能看到代码看不到原因第二个想法是把希望寄托在版本控制上。我那时候想代码改动都提交了让代理读 Git 历史和 diff 不就行了结果更麻烦。Git 确实记录了代码层面的变化比如哪一行改成了什么但它完全不记录背后的“为什么”。例如某个依赖被移除是因为它和另一个库冲突导致启动崩溃某个函数被拆分是因为循环依赖。这些决策过程只存在于对话里压缩后代理面对 Git 历史就像侦探面对一堆证据碎片能拼出大概经过却拼不出推理链条。更要命的是让代理从 Git 历史反推业务逻辑会消耗大量 token。它需要读很多次 commit、对比很多个版本然后再构建一个“可能正确”的上下文。整个过程又长又容易跑偏最后往往得出一个表面上合理、实际与当事人意图完全不符的结论。2.3 压缩前手动整理要点人肉记忆同步必漏必偏第三个想法稍微靠谱一点在上下文快满的时候我自己整理一份要点让代理照着继续。坚持了一段时间发现这活儿比亲自写代码还累。你得回顾这几十轮对话里哪些才是关键的哪些是路过的问题和临时方案还要区分“已完成”“进行中”“下一步”的边界。整理出来的东西天然带有主观顺序我重视的地方写得多代理重视的可能没写。而且人肉整理有个致命问题你不可能每一轮对话结束后都去同步一次。开发到了专注的劲头上谁会记得去更新“记忆文件”等触发压缩的那一刻再补已经晚了。更扎心的是你辛辛苦苦整理的摘要本质上仍然是给人看的自然语言代理拿过来也要重新理解一遍理解过程中照样会丢细节。这个方案临时救急可以但它不是架构只是止痛药。3. 《记忆碎片》的启示把记忆放到大脑之外还要写成指令在被压缩机制折磨到几乎想把电脑扔出窗外的时候我想起了一部电影《记忆碎片》。这部电影的主角没法形成新的长期记忆每隔几分钟就会忘掉自己身在何处、正在干什么。但他依靠三样东西维持行动身上的纹身、口袋里随时掏出来的照片以及手写的纸条。靠这些东西他愣是能追查真相、完成一系列复杂的行动。3.1 主角记录的不仅是事实更是“下一步操作”反复回想这个情节我发现一个容易被忽略的细节主角的记录里写的不是“我是谁”“我为什么来这里”而是“找这个人”“去那个地点”“别相信某某”。换句话说他记录的不是静态信息而是操作指令。这些指令的作用不是让他回忆过去而是让他即使在失忆状态下也能立刻知道该做什么、不该做什么。这个细节给了我极大的启发。我们的 AI 编码代理每次触发上下文压缩就等于一次“失忆”。我们需要的不是让它事后去理解一份详细的会议纪要而是留一套“失忆后也能执行”的外部指令。这才是真正能救命的东西。你给代理的文件如果只是描述历史它还是要花时间理解、推断如果你把文件写成“当前状态下一步动作”的格式它睁开眼睛就能直接按图索骥。3.2 把电影道具翻译成代理文件我把这套外部记忆系统拆成了三类文件一一对应电影里的道具电影道具对应文件作用照片、纸条brief.md失忆后的初始化引导告诉代理它是谁、项目是什么、接到任务后第一步干什么纹身state.md当前状态快照已完成、进行中、待办、阻塞点相当于刻在身上的进度条笔记本decisions.md决策日志记录“为什么这样做”和“哪些路是死路”避免重复踩坑这三个文件的共同原则是“外部可观察”。意思就是代理每次需要知道什么都是打开文件直接读而不是依赖记忆里“之前聊过”的隐式信息。压缩机制可以清空代理的对话历史但它清空不了磁盘上的文件。只要文件还在代理就能重新建立工作状态。在使用这套系统前我还特意验证了一个担忧代理会不会因为这些文件的存在而不再主动理解代码实测结果是不会因为文件里写的是“当前进度和方向”不是一个完整的替代品。真正复杂的技术细节代理仍然需要去读代码。文件的作用是给它一张地图不是替它把路都走完。4. Unix init.d 的设计哲学恢复不是靠记忆是靠脚本有了外部记忆文件下一步问题来了压缩发生后代理怎么知道要去读这些文件如果每次都靠我手动说一句“去读一下 state.md”那工作流还是不完整。真正让我把这事彻底想通的是 Unix 里一个快被时代淹没的旧机制init.d。4.1 内存会丢脚本不会System V init 是 Unix/Linux 经典的启动机制。系统开机后init进程会根据运行级别去/etc/init.d/目录下依次执行一串脚本把 SSH、网络、数据库等服务全部拉起来。这套设计最妙的地方在于它完全不指望关机前进程的状态还残留在内存里。内存是易失的断电就清空所以它把所有服务怎么启动、依赖什么顺序、需要哪些配置全部写成磁盘上的脚本。系统重启后任何服务都能通过执行脚本恢复到可用状态。这个思想放在 AI 编码代理身上简直是一比一复刻。上下文窗口就是易失的内存压缩就是断电。项目文件、记忆文件就是持久化的磁盘。代理每次被压缩相当于一次“系统重启”。如果我们有一个类似 init 脚本的东西让代理在重启后自动执行固定的恢复序列它就能像服务一样重新站起来。关键点恢复动作必须写死在脚本里不能让代理自由发挥否则它会在慌乱中随机翻找信息浪费大量 token。4.2 为代理写一份“启动引导词”我把这个 init 脚本的思想落实为一段“启动引导词”。每次上下文压缩发生或者我主动开启新会话我都会把下面这段文本粘给代理你在一个长期项目中工作。上下文压缩可能已导致记忆丢失 现在请按顺序执行以下恢复流程 1. 读取项目根目录下的 .agent-memory/brief.md了解项目的一句话目标和你的工作原则 2. 读取 .agent-memory/state.md获取当前阶段、已完成事项、进行中任务、阻塞项和下一步 3. 读取 .agent-memory/decisions.md了解已经做过的关键决策和已被否决的方案避免重复尝试 4. 运行 git status 和 git log --oneline -10核对状态文件与实际代码是否一致 5. 如果状态文件与代码事实冲突以代码事实为准并在回复开头标注不一致的地方 6. 最后用不超过 5 句话向我复述你的“当前行动计划”并等我确认后再动手。这段话就是代理的 init 脚本。它把恢复路径写死让代理压缩后的第一反应不是“漫无目的地重新探索整个代码库”而是“按固定顺序读取记忆文件、核对实际状态、回来跟人对齐”。实测下来这个流程把恢复成本从过去的几千 token 压缩到几百 token而且还减少了代理自作主张的风险。4.3 init 脚本之外的“运行级别”概念也有用init.d 还有一个概念让我受用不同的运行级别启动不同的服务。对应到我的工作流里是给不同场景准备不同的引导词。比如“深度开发模式”的引导词要求代理读取所有文件、核对全部状态而“快速修 bug 模式”的引导词只要求读取 state.md 的前三行直接进入问题区域。这样既保留了恢复能力又不至于每次重启都把整个项目的历史倒进上下文。这套分场景引导词尤其适合那些上下文窗口不大、想省 token 的用户。你想解决一个很小的问题没必要让代理把整个决策日志都读一遍。把“运行级别”的思想引入进来之后整个工作方案立刻变得轻量很多。5. 实操落地一套完整的压缩免疫工作流把前面所有想法整合成一个可以在任何项目里直接复制的目录结构和文件模板。我用一个虚构的模拟项目举例你可以在真实项目中直接套用。5.1 目录布局与三个文件模板在项目根目录下新建一个.agent-memory文件夹专门放这些记忆文件。项目根目录/ ├── .agent-memory/ │ ├── brief.md # 启动引导相当于 init 脚本 │ ├── state.md # 状态快照相当于纹身 │ └── decisions.md # 决策日志相当于笔记本 ├── src/ ├── docs/ └── tests/brief.md的写法很简单把上一节的“启动引导词”放进去再加上项目的一句话目标和一个关于“人不希望代理做什么”的负面清单。# BRIEF ## 项目一句话目标 这是一个跨平台客户端的数据同步模块目标是保证弱网环境下消息可靠到达。 ## 对代理的核心要求 - 本文件是工作状态的唯一来源压缩后必须按顺序恢复。 - state.md 与代码事实冲突时以代码事实为准并标注冲突。 - 没有完成“当前行动计划”确认前不要修改任何代码。 - 不要重复尝试 decisions.md 中标记为“已否决”的方案。state.md是核心中的核心我采用表格结构让状态一目了然# STATE 快照最后更新2024-XX-XX ## 下一步最优先 - [ ] 在 sync_engine.go 中验证证书轮换逻辑确认是否能解决偶发超时 ## 已完成 - [x] 超时重试参数从 3 改为 5commit 8f3a21d - [x] 定位到证书链每次重连都会重新校验 ## 进行中 - 正在重构 sync_engine 的队列模块 ## 阻塞项 - 暂未定位证书链轮换的刷新时机需要查看底层库的文档 ## 关键路径 / 文件 - src/network/sync_engine.go - src/network/cert_rotator.godecisions.md记录重大决策和被否决的方案格式为日期背景选择原因后续验证# DECISIONS ## 2024-XX-XX 选择指数退避而不是固定间隔重试 - 背景弱网环境下固定间隔会让服务器过载。 - 选择采用指数退避上限 30 秒。 - 原因研究表明大部分弱网恢复发生在 10 秒内指数退避能以更低成本完成重试。 - 验证压测 2 小时后无请求堆积。 ## 2024-XX-XX 已否决通过切换服务端 IP 解决超时 - 原因实测切换后问题依旧判断瓶颈不在 DNS/接入点。 - 教训以后再遇到超时问题不优先考虑更换接入点。5.2 养成“存档点”什么时候必须更新 state.md这套工作流能不能发挥价值取决于你是否在正确的时间点更新状态文件。我强烈建议只在三种时机更新否则很容易变成一份没人看的流水账完成一个值得提交的任务比如修好一个 bug、跑通一个接口。做了一个影响后续开发的技术决策比如换框架、选数据库。遇到卡住的问题准备切换方案前。我个人的习惯是每个重要 commit 之前先让代理把 state.md 更新到当前最新状态再提交代码。这样 Git 提交和记忆文件就是同步的相当于每个版本节点都有存档。5.3 压缩前强制“现场快照”如果你预感上下文快满了不要傻等着工具自动压缩主动让它先写快照。我喜欢对代理说这样一句话“在进入下一次上下文压缩之前把 state.md 更新到最新状态列出下一步要做的第一件事。完成后再回复我我要确认你已经存档。”这个动作虽然只花两分钟却能让压缩发生后的恢复过程顺滑很多。你可以把这句话写进 brief.md 的负面清单里让代理每次在上下文接近上限时都能自觉执行。5.4 把记忆文件纳入 Git但不纳入编译目标我在实践中把整个.agent-memory目录提交到了 Git 仓库这样记忆文件会跟随分支、伴随历史任何一次 checkout 都能还原当时的记忆。要注意的是如果你有多个长期并行任务记忆文件也会跟着分支冲突所以最好的做法是每个大的功能分支维护自己的一套.agent-memory合并回主干时再统一归档。别把它当成代码一样频繁 rebase它是给人看、给代理看的坐标不是代码逻辑本身。6. 把话说透这套方案能挡什么挡不住什么做了大半年的小白鼠我对这套“压缩免疫工作流”的边界条件有了很清醒的认识。它确实救了我很多次但它不是万能的。把话说透对你决定是否入坑很重要。6.1 它能救回来的三类场景长会话触发压缩后的继续开发。这套是目前体感最明显的。以前压缩后代理要花好几分钟找回状态甚至把代码改错现在压缩后只需贴一次启动引导词它就能在几十秒内恢复并且快速定位到下一步。跨天、跨会话的续接。第二天早上重新打开代理我不知道昨天的上下文还能不能用以前只能从新会话开始重新解释背景。现在直接贴启动引导词让代理读文件基本上一分钟就能进入状态。人员交接。某一次我临时把项目交给同事继续对方花五分钟读完三个记忆文件就大概明白了进度和注意事项比之前靠聊天记录交接效率高得多。6.2 它挡不住的东西必须诚实告诉你高度隐式的上下文仍然会丢。比如某个函数里局部变量的命名习惯为什么是这样某次讨论中说“先不加缓存等数据量大了再考虑”这种只在对话里一闪而过的微小决策不太可能被写进决策日志压缩后可能永远找不回来。记忆文件本身可能被污染。代理在更新 state.md 的时候也会写错比如把“尚未完成”写成“已完成”。你要是不抽查就会带着错误的状态继续开发比没有记忆文件还危险。所以每隔几次压缩我都会自己扫一眼 state.md 的关键行确认没有异常。如果你不养成立刻更新的习惯这套方案就是空壳。这事极其反人性做得最专注的时候最不想停下来更新状态文件。我后来摸索出一个办法把“更新记忆文件”也作为一个待办交给代理让它在每次完成子任务后先更新文件再做下一个任务相当于把它变成了流程的一部分而不只是我意志的延伸。6.3 让记忆文件保持“新鲜”的三个技巧最后分享一些长期使用下来非常有效的小技巧把“下一步”放在 state.md 的第一行表格里确保代理一打开文件就能看到而不是埋在一大段文字里。控制 decisions.md 的内容质量。只记录有结论的决策和带来教训的失败过程性的纠结不要写这样文件体积会一直保持稳定代理读取成本也低。每次 Git 提交时同步检查记忆文件的更新相当于让版本控制成为记忆系统的存档触发器。我个人在这套工作流里收获最大的其实不是“省了多少 token”而是心态变了。以前我害怕上下文压缩总觉得那是不可控的天灾现在我把它当成一次预期的重启。压缩发生后我会很平静地打开新会话粘贴启动引导词让代理像一台服务一样重新拉起自己。你阻止不了系统断电但你可以保证断电之后它能凭脚本找回自己的位置。这套方案改造后的第一个项目收官时我对同事说过一句话“AI 编码代理的工作记忆永远不值得完全信任真正值得信任的是你给它留下的那三份文件。纹身会褪色纸条会丢但只要文件还在它每次都能重新找到自己。”