AI编码代理上下文压缩自救指南:外部记忆与init.d式编排实战 我用 AI 编码代理处理那种动辄几万行、牵一发动全身的真实项目迟早会撞上一堵墙上下文窗口确实很大容得下二十万 token 的对话历史但窗口不是硬盘。它在某个时刻会自动做一次“压缩”我把这个动作叫作脑叶切除术。压缩之后代理对项目的记忆变成一段含糊的总结而那段总结往往把你最在乎的约束给切掉了——不是删除文件是它真的“想不起来”。这篇文章想分享的是我围绕这个痛点搭起来的一套完整方案参考《记忆碎片》里主角 Leonard 的外部记忆策略再借用 Unix 生态里 init.d 的启动编排思路让 AI 编码代理对上下文压缩产生“免疫力”。我会把设计缘由、具体文件模板、脚本实现、踩过的坑全写出来适合那些已经在长会话里吃过亏、或者正准备让代理长期值守项目的开发者阅读。如果你还没遇到压缩事故那说明你的会话还不够长——早晚会遇到的。1. 那次失忆事故上下文压缩是怎么把代理变成“另一个代理”的先说一次让我决定彻底改变工作方式的翻车经历。当时我在重构一个核心服务的响应解析模块会话已经持续了很久中间积累了大量的 diff、测试输出、调试日志上下文明明还有余量但代理框架提示接近上限需要整理历史。我没太在意让它自动压缩。压缩之后代理继续干活表面上看一切正常结果几个小时后我发现它把旧的兼容解析层整个删掉了还附带了一条自信满满的提交信息“旧解析逻辑已无引用清理冗余代码”。问题在于从压缩后的摘要来看这个删除确实合理。摘要里只写了“重构解析模块”没写“必须保留旧字段兼容层”更没写“三个月前线上事故就是因为直接放弃旧格式”。我花了一整天才发现并回滚那一版 diff 有几万行。这件事让我彻底想明白代理在压缩后得到的不是完整档案而是简历——高度凝练、立场乐观、专挑能讲通的事情讲。1.1 压缩那一刻到底发生了什么上下文压缩的本质是有损摘要。它不是为了让你“无损恢复”而是为了在有限窗口里腾出空间继续干活。实际操作中有三类信息最容易在摘要里被丢掉信息类型原始上下文中的样子压缩后的常见结果否定性约束“不要删除 v1 字段”完全消失只留下“重构解析器”量化边界“最多允许 3 个并发任务”“控制并发”数字没了决策理由“不继承基类是因为线上要热更新”“使用独立实现”理由蒸发我见过最离谱的一次代理把“用户上传的文件名必须做安全过滤”理解成“要过滤文件名中的特殊字符以支持 Unicode”然后主动把安全过滤函数替换成字符串清洗函数。表面上看它更“懂”了实际上是把一道安全防线拆了。这类事故的共同源头就是理由丢失——约束还在也许能被摘要保留一层皮但只要“为什么存在”丢了代理在权衡取舍时就会倒向常规模式。1.2 为什么上下文越大这类事故反而越频繁这是个反直觉的事实窗口越大长会话里积累的“环境噪音”就越多。工具调用记录、往返的 JSON、测试输出、终端日志、diff 片段这些内容一口气能吃掉大半窗口真正的任务记忆只占一小部分。等到窗口快满时压缩算法面对的不是“该记的都记了”而是“一堆杂讯里夹着几条关键约束”。压缩又偏偏不会发生在你任务收尾的时候而是发生在最需要集中精力的中段。越长的会话中间状态越多摘要的失真就越严重。所以真正的问题不是“窗口再大一点会不会解决”而是“你愿不愿意接受一个有损记忆的协作者”。我的回答是不愿意所以开始设计外部记忆系统。2. 抄《记忆碎片》的作业把记忆从“脑内”搬到“体外”《记忆碎片》的主角 Leonard 有一种顺行性遗忘症无法形成新的长期记忆。他没有跟自己的短期记忆硬刚而是把所有线索写在小纸条上拍成拍立得照片甚至纹在身上。他设计了一套流程每天早上醒来按顺序读取纸条按纸条上的指令行动再把新发现写进新的纸条。这套系统的关键是记忆不依赖大脑而依赖外部载体。这个思路几乎可以原封不动搬到 AI 编码代理身上。LLM 的上下文窗口本质上是工作记忆一旦被压缩相当于 Leonard 睡了一觉。与其赌压缩算法保留什么不如让它每次醒来都有一套可以“重读案卷”的外部系统。上下文是短期记忆文件系统是纸条和纹身会话启动指令是他每天给自己下达的行动计划。我第一次把这个类比讲给团队里的开发者听时对方立刻明白了为什么我在项目里放了一堆看起来多余的 Markdown 文件。2.1 我设计的四类“记忆卡片”我在项目里建了一个.memory/目录里面的文件按数字编号顺序就是加载顺序。命名方式直接参考了 init.d 脚本的编号思想系统启动时按 S01、S02、S03 的顺序执行编号即顺序。01_agenda.md当前状态。记录“我现在在哪、要往哪去”包括正在进行的任务、当前阶段、已知的阻塞项。02_constraints.md硬约束。只能用陈述句写死每条必须带“为什么”。比如“不删除 legacy 解析分支线上仍有老客户端删除会导致兼容性事故”。03_decisions.md决策日志。每条包含日期、上下文、决定、理由。这能防止代理用“常规最佳实践”覆盖你深思熟虑过的特殊设计。04_next_actions.md下一步行动清单。要具体到能指引代理在没有人类在场时继续推进而不是压缩后重新发明任务。这四个文件加起来就是代理的“外部记忆”。每次会话开始代理先读这四个文件每完成一个里程碑人再更新这四个文件。记忆不依赖刚才聊了什么而是依赖磁盘上写了什么。2.2 写记忆卡片的三个铁律第一个铁律每条记忆 事实 理由 触发条件。只有事实没有理由压缩后代理会觉得它不重要只有理由没有事实代理没法落地执行。举个例子我会这样写一条约束## 硬性约束响应解析模块必须保留 v1 字段兼容 - 事实v1 响应中的 timeout 字段、legacy_error_code 字段必须继续输出 - 理由线上仍有旧客户端依赖这两个字段移除曾在 2024-11 引发生产事故 - 触发条件修改 parser 层、序列化器、接口返回结构时都必须回读本条第二个铁律尽量避免模糊词。“注意安全”“尽量兼容”“合理优化”这类词等于没写。要把“尽量兼容”改成“必须以旧格式写出 timeout 字段顺序按 v1 文档”。代理不是不听话而是模糊指令给了它太多自由发挥空间。第三个铁律卡片总量保持在 200 行以内。超过这个量代理在读取时就开始选择性阅读约束漏读率会明显上升。这就像给 Leonard 的纸条太多他反而不知道该信哪张。如果卡片超过 200 行说明项目状态本身已经失控先做清理再继续干活。3. init.d 式启动编排让代理每次醒来都先读案卷init.d 是类 Unix 系统里经典的 SysV init 启动机制。系统开机时/etc/init.d下的脚本会按编号和前缀依次执行K 开头的是停止服务S 开头的是启动服务S01先跑底层依赖S99再拉起上层应用。整个系统的状态能在重启后自动恢复靠的不是人脑记住每个服务上次在跑什么而是这套按顺序执行的启动脚本。AI 编码代理的新会话就是一次系统重启。上下文被压缩后代理处于失忆状态需要一套启动脚本来恢复工作状态。所以我设计了两个启动层一个是项目内的启动说明文件一个是代理系统级配置里的强制加载指令。二者是冗余关系缺一个都不够稳。3.1 用脚本自动打包“启动上下文”我在项目根目录放了一个build_context_pack.sh它把.memory/下的四个记忆卡片、当前 git 状态合并成一个context_pack.md再生成一个 md5 校验值。代理只需读这一个文件而不是分散的四个文件能显著减少“漏读一个”的概率。#!/usr/bin/env bash # build_context_pack.sh set -euo pipefail MEMO_DIR${1:-.memory} OUT_FILE${2:-context_pack.md} { echo # Context Pack - $(date -u %FT%TZ) echo for f in 01_agenda.md 02_constraints.md 03_decisions.md 04_next_actions.md; do if [[ -f $MEMO_DIR/$f ]]; then echo ## $f cat $MEMO_DIR/$f echo fi done echo ## git status git status --short 2/dev/null || true } $OUT_FILE md5sum $OUT_FILE $OUT_FILE.md5 echo context pack written: $OUT_FILE为什么用脚本而不是手动复制一是减少遗漏二是保持格式一致三是可以挂进 git hook。每次准备提交前自动跑一遍确保context_pack.md永远和记忆卡片同步。我一般还会把这个脚本的执行结果输出到终端让代理能直接看到“context pack written”这样的反馈。3.2 加载指令怎么写才不会被代理忽略有了context_pack.md还不够关键是怎么让代理每次会话开始都必须读它。我的做法是在项目根目录放一个START_HERE.md里面只有强制指令同时在代理的系统级指令文件里保留同样一段话。系统级指令的优先级更高因为项目文件本身可能被压缩忽略而系统级配置相对更受重视。## READ ME FIRST 在开始任何工作之前你必须执行 1. 读取项目根目录下 context_pack.md 2. 对比其 md5 与 context_pack.md5 是否一致 3. 在回复开头列出你识别到的 3 条硬约束 4. 若你的计划与这些约束冲突停下并向用户说明冲突点让代理复述硬约束是我在实践中发现最有效的“输出即验证”手段。如果代理列出的约束和卡片内容对不上说明读取失败或摘要失真立即中止任务。复述出来的内容也会进入新的短期记忆相当于给代理做了一次定向强化比默认“它已经看懂了”可靠得多。4. 冗余设计三条防线保证关键约束传递到执行点启动包能解决会话开始时的失忆问题但压缩随时可能发生启动指令本身也可能被自动摘要吞掉。还有一个更隐蔽的问题上下文太大时代理读到后面的代码可能已经忘了前面读过的约束。所以我把硬约束放到三个位置启动包里、源码注释里、任务清单校验点里。三层互相独立任意两层失效还有一层能把代理拦住。4.1 代码锚点让约束出现在“即将犯错”的地方在易被误删、误改的模块顶部或接口声明处我加上锚点注释用词非常显眼。代理要修改目标文件必然先读取文件内容锚点注释就会进入它的即时上下文。即使外部记忆全丢这个锚点还在。# AI-ANCHOR: DO NOT REMOVE legacy fields from parse_v1() # Reason: old clients still send format v1; removing breaks production. # Last verified: 2025-06-01 def parse_v1(data): # 保留timeout, legacy_error_code ...锚点注释里写“Last verified”日期是有意为之。当代理发现当前日期离验证日期很远可以主动提示这条约束可能已过期需要人工确认。这能避免一种反向问题代码维护了两年旧约束已经不再适用但代理因为锚点注释而永远不敢动它。《记忆碎片》里 Leonard 的困境就是这样他无法判断纸条是否被篡改。日期和理由就是给代理提供判断依据。4.2 任务校验点每完成一个子任务强制回读约束锚点能拦住在单个文件里的错误修改但拦不住跨文件的系统性破坏。所以我额外维护一个tasks.md任务清单规则是每个子任务完成后代理必须勾选任务、复述与该任务相关的两条约束、并运行对应的测试或构建命令。## Task: 迁移响应解析器 - [x] 阅读 context_pack 中的 02_constraints.md - [ ] 完成 parser 迁移 - [ ] 运行 pytest tests/test_legacy_compat.py - [ ] 复述约束不删除 v1 字段保持 timeout 字段兼容这背后的逻辑是把校验动作从“记忆里找”变成“输出里查”。测试失败时即使代理以为自己记得清清楚楚工具输出也会把它拉回现实。我在上面标注了“必须运行 pytest”是因为避免代理只勾选不复述。勾选这个动作本身廉价复述和跑测试才是有意义的验证。4.3 冗余也有代价别忘了处理“过期”三层冗余系统运行久了最烦人的不是失效而是自相矛盾。某条约束在启动包里已经更新了但源码锚点注释还写着旧版本。代理同时读到两个矛盾指令时处理方式不确定——有时候它会选最新的有时候会按更显眼的执行。我的解决办法是让每条记忆都带日期并且定期做记忆回收。每次完成一个可合并的改动我会花十分钟更新四张卡片每周五做一次清理删除已失效的决策检查锚点注释里的 “Last verified” 是否需要滚动。只有主动维护冗余才不会变成干扰。一个从不清理的外部记忆系统最终会变成 Leonard 那一堆互相矛盾的纸条比没有记忆更危险。5. 实测表现、翻车教训和最终的工程结论这套系统用了大半年帮我撑过了不少真实场景也暴露了不少问题。先说有效的一面再说为什么它照样会翻车。5.1 这套系统帮我撑过的几个真实场景第一个是跨天多会话。前一天和代理聊到一半第二天直接开新会话加载context_pack后它立刻接上了进度完全不需要我重新复述需求。第二个是自动压缩发生时压缩后代理虽然丢失了中间推理细节但读完卡片仍能继续执行至少不会去碰被标为硬约束的东西。第三个是无人值守的批量重构代理改到危险模块时会被锚点注释拦下转过来问我而不是闷头继续改这正是我要的效果。我最看重的其实是第二个场景。自动压缩是没法完全禁止的但有了启动包压缩就不等于失忆。它只是一次“重启”重启之后代理还会去读案卷。5.2 仍然翻车的地方和我的对策翻车第一类记忆卡片超过 200 行后代理开始选择性阅读。它不会完整读每一行而是扫标题和加粗部分。被压缩后更严重可能只记得卡片的大致目录。解决办法不是写更多条目而是拆分和精简把不重要的内容移出记忆卡片放进普通文档。翻车第二类强制读取指令被代理的“工具调用优先策略”压过。有一次代理没有读启动包直接开始分析代码我后来发现是项目文件里那条指令被会话中段的工具输出淹没了。我把强制读取指令同时放进了系统级配置文件名也改成了更显眼的START_HERE.md放在项目根目录。冗余在这里又一次救了场。翻车第三类多代理并发协作时会写坏记忆文件。两个代理同时更新04_next_actions.md互相覆盖。解决方式是引入简单的文件锁比如用mkdir -p .memory/lock作为互斥信号写操作只允许一个代理串行执行。不搞复杂分布式锁因为记忆文件的写入频率本来就不高。翻车第四类过期记忆误导代理。某条约束已经不再适用但代理看到“硬约束”标签就严格遵循导致动作变得保守甚至拒绝合理重构。所以我要求每条卡片和锚点都带日期一旦发现约束与当前代码事实矛盾优先向用户报告而不是继续执行。这正是 Leonard 做不到的——他无法识别纸条是否失效但我们的代理至少能通过代码现状来校验。5.3 有一点比所有记忆系统更重要及时开新会话最后分享一个让我受益最多的习惯与其等 20 万 token 窗口走到自动压缩不如在关键里程碑主动“重启”。保存卡片、新开会话、加载启动包这一套动作通常花不到十分钟但能避免一次灾难性的有损压缩。自动压缩是应急机制主动重启才是日常操作。说到底这套方法并不复杂外部记忆文件负责保存状态init.d 式启动编排负责恢复状态代码锚点和任务校验点负责在执行点兜底最后靠维护纪律保证记忆不腐烂。本质上我把“希望代理记得什么”从概率问题变成了工程问题。用这种思路就算上下文再被压缩代码该在哪还在哪约束该在哪还在哪——这句话就是我搭这套系统最想留下的结论。