ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

对抗AI编码代理上下文压缩:用init.d式外部记忆恢复失忆状态

对抗AI编码代理上下文压缩:用init.d式外部记忆恢复失忆状态

我真正意识到 AI 编码代理也会“失忆”,是在一次接近 20 万 token 的上下文压缩之后。那天我的代理正在一个长周期项目里连续工作,我对它讲过的每一轮细节都还历历在目,但它突然像被动了手术一样,再也想不起两小时前自己给出的重构方案,甚至搞错了我反复强调过的目录结构。那种感觉就是一场“脑叶切除术”:不是慢慢遗忘,而是某次压缩触发后,整段对话里的关键记忆被粗暴剪掉。后来我把代理从“靠对话记忆”改造成“靠外部文件记忆”,核心思路来自两个地方:Unix init.d 的启动恢复机制,以及电影《记忆碎片》里那套写在纸条和照片上的外置笔记系统。这篇文章就把我完整的方法、文件结构、提示词写法和验证过程都放出来,给同样在长上下文里做 AI 编码代理的人一个可复制的参考。

1. “脑叶切除术”降临的现场:20 万 token 压缩到底删掉了什么

1.1 触发压缩的那一刻,代理变成了另一个人

先还原一下现场。当时我让代理处理一个连续迭代的任务,大概涉及三个相互关联的模块,中间还穿插了两次需求调整。对话刚开始很流畅,代理能准确说出每个模块之间的依赖关系,也知道我要求“不要动公共接口”这类约束。但上下文一到 20 万 token 附近,压缩机制就被触发了。压缩之后,代理虽然还活着,却能明显感觉到它变成了另一个人:它开始频繁用猜测的方式补全变量名,对着一个已经讨论过“绝对不要用某方案”的功能又重新提了一遍方案,甚至把项目入口文件都指错了。

这其实不是代理变笨了,而是它赖以工作的上下文被压缩成了少数几条摘要。原始对话里那些具体的推论过程、用户偏好、错误修正记录,全都被合并成类似于“用户讨论了方案A和方案B,最终选择方案B”这样高度浓缩的文本。摘要不是索引,它没有保留你推理时的完整路径。你失去的不是某一个词,而是一整套“为什么这么做”的因果链。

1.2 被“切除”的信息往往是最有价值的

我把压缩后丢失的信息分成了四类,这四类正好对应了长上下文项目里最致命的东西:

丢失类型具体表现为什么难恢复
全局导航不知道项目核心模块在哪、入口在哪代码还在,但代理失去了“地图”
编码约束用户说过的命名规范、接口红线散落在历史对话里,摘要不会逐条保留
决策背景为什么选方案B而不选方案A结论可能还在,但原因已经被剪掉
当前进度正改到哪个文件、试过哪些失败路径上下文压缩优先清掉“最旧”的信息

其中最难恢复的是“决策背景”。代码文件只能展示结果,不能展示讨论过程。比如我看到代码里某个模块没有做抽象,但只有对话上下文里记录了“我们讨论过抽象,因为当前业务变化太快,强行抽象反而会锁死后续扩展”。这个信息一旦被压缩掉,代理下次看到同一段代码,很可能又提出一个已经被否决的重构方案。

1.3 为什么“把代码重新读一遍”救不回来

很多人会说,压缩就压缩吧,让代理重新扫一遍代码不就行了。我也试过,效果非常差。代码是静态的,但项目状态是动态的。代理重新读代码,能知道文件里写了什么,却不知道“我正在改哪个文件”“用户刚否掉了哪条路线”“下一个步骤应该做什么”。这些不是代码里能推出来的信息。

更麻烦的是,一次上下文的压缩往往不是只发生一次。长项目里,代理会反复逼近 token 上限,每压缩一轮,它对外部状态的感知就更模糊一层。如果你每次都想靠“重新读代码”来补救,那就等于每次都让代理从零开始探索一个熟悉但记不住的项目。这既浪费 token,也让代理的判断质量断崖式下降。

所以我的结论很直接:别再指望代理用对话记忆扛过压缩,你应该把关键状态放到上下文之外的文件系统里,让代理每一次启动都能从外部恢复。这就引出了 Unix init.d 给我的第一个启示。

2. Unix init.d 给我的第一个启示:把代理从“记性好”改造成“可重启”

2.1 init.d 到底在干什么,凭什么重启后还能恢复服务

类 Unix 系统里有一个我很早就接触,但直到前几天才真正想通的设计:init.d 启动脚本。系统启动时,内核把控制权交给 init 进程,init 再去执行/etc/init.d/下面的一系列脚本,把网络、数据库、业务服务一个个拉起来。这里最关键的一点是,开机之后的进程不需要记得“上一次运行到哪一行”,它只要按照启动脚本里定义好的步骤重新初始化,就能恢复到对外提供服务的状态。

系统不是靠“内存里的记忆”来保证永不重启的,而是靠“把状态写入磁盘、把恢复步骤固化成文件”来保证随时可以重启。对话上下文对 AI 代理来说,就像服务器里易失的内存;文件系统才是那块掉电不丢的磁盘。上下文压缩等于一次强制重启,如果你没有准备任何“启动脚本”,那重启之后自然一片空白。

2.2 把 AI 编码代理当成守护进程来设计

想通这一点之后,我给代理换了一个心智模型。以前我把它当作一个“越聊越懂我的助手”,现在我把每次对话都当作一个独立的进程实例,这个实例注定会死亡,死亡方式可能是上下文压缩、模型切换、断线重连,甚至是我自己重置对话。

那我真正需要保证的是,这个实例无论什么时候消失,下一个实例都能通过同样的启动流程恢复工作状态。这个启动流程必须包含:

  • 项目导航:知道代码在哪、入口在哪、测试命令是什么
  • 硬性原则:用户定下的约束和偏好,不能靠猜
  • 当前进度:正在做什么、做到哪、下一步是什么
  • 决策日志:关键选择为什么这么定

这些东西都应该放在文件系统里,而不是放在对话上下文里。代理每次进入新会话,先执行初始化流程读取这些文件,然后才开始干活。它记不记得上次对话不重要,重要的是文件系统里始终保留着唯一权威的状态。

2.3 对话记忆和文件记忆的边界到底怎么分

有人可能会问,那我还要不要上下文?当然要。上下文适合放“瞬时状态”,比如当前正在排查的一个小问题、刚粘贴进来的报错日志、正在讨论尚未定论的想法。这些信息本来就是临时性的,压缩掉也不可惜。

真正需要落盘的,是那种“本次对话结束后依然有价值”的信息。我用一个很简单的问题来判断:如果这个信息只能存在于当前上下文里,而我的代理明天还要继续这个项目,那它是不是必须被写进文件?只要答案是“是”,我就立刻写。比如某个接口规则、某个被否决的方案、某个即将开始的任务,这些都不是临场讨论,而是项目长期的“开机配置”。

一旦你按这个边界来管理,上下文压缩就不再是灾难,而只是例行重启。这也是为什么我在真实项目里开始像管理服务器一样管理代理:每台“代理服务器”都有一套 init.d,每次上线先跑启动脚本。

3. 《记忆碎片》式外部笔记:让失忆后的代理有路可走

3.1 记忆碎片里的核心设定,正是失忆代理的写照

电影《记忆碎片》讲的是一个无法形成新长期记忆的人,靠拍立得照片、手写纸条甚至纹身来给自己留下线索。他每次醒来都不知道自己前一秒做过什么,但通过外部记录,他可以继续追踪那件对自己非常重要的事。代理被上下文压缩之后,基因上是同一个模型,但短期记忆已经被切掉了。它和我都非常需要一个“外部档案”,让没有记忆的个体也能沿着笔记继续执行任务。

电影里的主角不敢完全相信自己的大脑,因为记忆会被篡改。代理也一样:压缩后的摘要不可靠,它可能把“讨论过但否掉了的方案”当成可选方案,把“绝对不动的模块”当成普通模块。因此外部笔记必须比对话摘要更权威。它的存在不是为了备份语气,而是为了在任何时刻都提供一份可以被重新读取的真相。

3.2 笔记四要素:导航、原则、进展、决策

我给每份外部笔记拆成四个文件,对应四类必须持久化的信息。

首先是导航文件。它负责告诉一个完全失忆的代理:项目是什么、入口文件在哪、核心模块在哪、测试怎么跑。很多代理失忆后的第一反应是“从头扫描整个仓库”,这非常浪费 token。导航文件能让它在一秒内恢复方向感。

其次是原则文件。它记录那些绝对不能违反的规则,比如命名风格、接口红线、框架约束等。这些规则通常是在对话里多次强调才形成的,一旦压缩被切掉,代理很容易重新踩进旧坑里。原则文件写得越短越好,让代理每次启动都能读一遍,成本极低,回报极高。

第三是进展文件。它记录当前进行到哪一步。这个文件必须非常具体,不能只写“正在重构”,而要写“正在重构 src/core/scheduler.ts 的取消任务逻辑,已完成状态机改造,未开始队列去重”。这样即便是完全陌生的新会话,读完也能直接接手。

第四是决策日志。它记录关键选择的原因。我在项目里会专门留一个清单,每条决策包括“当时的问题、可选方案、为什么选了现在这个”。这个文件解决的是代理重复提出已被否决方案的问题。如果代理知道“方案A已经被验证过性能不足,详见决策日志第3条”,就不会再浪费一堆 token 去走回头路。

3.3 让代理形成“先写笔记再动手”的肌肉记忆

笔记文件不可能靠人肉手工维护,必须让代理自己养成更新习惯。我在提示词里明确要求:每次完成一个里程碑、每次遇到需要做技术选择时,先更新对应的笔记文件,再继续进行接下来的任务。

我还试过在项目提示词里写这样的规则:“当你准备改动某个核心文件之前,必须先在progress.md里写清你要改什么、为什么改。如果你发现一个方案被否定,立即追加到decision-log.md。”这样做的效果比我想象中好,代理在行动前会暂停一下,把状态固化到磁盘,而不是只在对话里留下痕迹。

说实话,让代理主动持续更新笔记并不容易。它本质上是在对抗模型“直接输出”的惯性。所以我会把“更新笔记”本身当成一个子任务,甚至会在提示词里写出更新的格式,让代理像填表一样执行。一旦这个习惯建立起来,笔记文件就变成了项目里最可靠的技术文档。

4. 落地实现:一套 init.d 加 memo 双通道的代理恢复机制

4.1 我的.agent目录长什么样

最终我在项目根目录下建了一套非常简单但很管用的目录结构:

.agent/ ├── init.d/ │ ├── 01-navigation.md │ ├── 02-principles.md │ ├── 03-progress.md │ └── 04-decision-log.md ├── hooks/ │ └── pre-edit.sh ├── run-init.sh └── session.ctx

init.d里的四个 markdown 文件就是上面说的四类外部记忆。run-init.sh是启动入口,它会按顺序读取并输出所有笔记文件。hooks/pre-edit.sh是我用来强制刷新进度的小钩子,它可以和编辑器或工作流结合,在代理修改关键文件前把当前任务重新登记一次。session.ctx记录当前会话的标识和时间,用于切换会话时快速识别状态是否过期。

run-init.sh本身极其简单,核心逻辑就是把笔记文件打出来:

#!/usr/bin/env bash cd "$(git rev-parse --show-toplevel 2>/dev/null || true)" for f in .agent/init.d/*.md; do echo "===== $(basename "$f") =====" cat "$f" echo done

它做的事情和 Unix init.d 一模一样:不关心你之前状态如何,只负责把预置脚本执行一遍,让系统恢复到已知状态。代理不需要任何魔法记忆,只需要在每次新任务开始时先跑一遍这个脚本。

4.2 一次“冷启动”的完整流程

我把代理从失忆状态恢复到可工作状态的过程,总结成下面几步:

  1. 开启新的代理会话,或者检测到上下文刚被压缩过。
  2. 在会话里输入初始化指令,让代理读取run-init.sh的输出内容。
  3. 代理根据导航、原则、进展、决策四个文件,简述自己对当前状态的理解。
  4. 确认理解无误后,直接开始推进进展文件里写的“下一步”。
  5. 工作过程中,如果完成任务或做出决策,立即回写03-progress.md和04-decision-log.md。
  6. 下次无论因为什么原因开启新会话,再次重复第 2 步。

这个流程里最关键的是第 2 步。很多人在新会话里直接丢给代理一个任务,代理根本不会主动去读笔记。我最后是把“无论如何都要先跑初始化”写进了项目级提示词里,才彻底解决这个问题。

4.3 关键提示词模板,我实际在用的版本

为了方便你直接抄,我把初始化指令贴在下面。注意这不是废话,它决定了代理会不会真正依赖外部笔记:

初始化指令:请先运行 bash .agent/run-init.sh,读取其中所有笔记内容。 不要复述这些文件,也不要向我确认“是否明白”。 请直接告诉我三件事:本项目的核心目标、当前进度、下一步要做的事。 之后所有操作都以 .agent/init.d/03-progress.md 里的进展为准。 如果遇到新的技术选择,完成阶段后先把结论追加到 04-decision-log.md。

为什么要强调“不要复述”?因为复述等于把笔记内容又重新加载进上下文,白白消耗 token,还让代理觉得自己已经“记住”了,实际上只是多念了一遍。而明确要求它输出“核心目标、当前进度、下一步”这三件事,是为了验证代理真的读懂了笔记,而不是只做了文本扫描。

在文件更新上,我用的提示词同样很具体:

当你完成任务清单中的某一项时,先在 03-progress.md 里把该项标记为已完成, 然后写清楚“下一步任务”和“当前风险”。如果某方案经过验证被否决, 必须把否决原因写入 04-decision-log.md,否则视为本次操作无效。

这是一条强约束,能避免“活干完了但文件没更新”的偷懒行为。

4.4 这套组合为什么能抵御上下文压缩

道理说穿了非常简单。上下文压缩只能删除对话历史,不能删除项目文件。你越是把关键知识塞进对话里,就越容易在压缩时被连根拔起。反过来,你把知识写成磁盘文件,并且让代理养成“干活前先读文件、干完活先更新文件”的习惯,那对话历史就只是临时缓冲区,不再是唯一真相。

每经历一次压缩,代理都可以通过重新运行run-init.sh回到同样的状态。这就像是给服务器做了一次故障恢复演练:虽然进程被杀掉了,但只要启动脚本还在,它就能重新拉起全部服务。这套机制的防丢能力,取决于笔记文件的完整性和新鲜度,而不是模型本身的记忆能力。

5. 实测验证与藏在背后的几个坑

5.1 我如何故意制造一次压缩事故

为了验证这套系统不是心理安慰,我做了一次比较硬核的压力测试。测试分成三个阶段:

第一阶段,让代理在本项目的笔记指引下完成一个中等难度的模块改造,同时确保进展文件和决策日志都更新到位。

第二阶段,故意把一段超长日志文件丢给代理并要求它做总结,人为把对话上下文推高,直到触发压缩。这一步我没有办法精确控制压缩时机,但通过塞入大量无价值的文本,可以很快逼近 token 上限,压缩通常会在这个过程里被触发。

第三阶段,开启一个全新的代理会话,只给出一条提示:“请执行初始化指令,然后继续开发”。然后观察它是否能准确说出当前进度、下一步任务以及之前定下的约束。

5.2 恢复效果对照:有笔记和没笔记是两个世界

我对比了有笔记基线和没有笔记基线的差距:

验证项没有笔记基线有笔记基线
项目入口和核心模块重新扫描仓库才能找到,误差大秒级恢复,路径准确
编码约束大概率遗忘,重新踩坑原则文件明确列出,不会违反
当前进度完全不知道从哪继续进展文件直接指向下一步
被否决的技术方案可能再次提出决策日志里有一条明确否决记录

实际测下来,只要笔记更新得及时,新会话里的代理可以做到“像没有失忆一样”继续干活。尤其是decision-log.md,它解决了我最头疼的重复决策问题。新代理不再像以前那样对每个方案都重新评估一遍,而是先看日志里有没有现成结论,这既防止走回头路,也显著节省了上下文和推理时间。

5.3 有些状态确实无法做到完全免疫

当然,我不建议你把这套机制神化成“绝对免疫”。有些状态天生存在于上下文和当前环境中,无法完全持久化。

比如视觉状态。代理有时会基于某个输出内容进行视觉上的观察,上下文压缩后它不会记得自己刚才盯着哪个区域。这类信息我只能妥协,用一句话写进进展文件,例如“我最后在查看 src/core/scheduler.ts 的第 120 行附近”,能把损失降到很低,但不可能完美还原。

比如临时讨论里的灵感和猜测。如果一次对话里你随口说了一个想法,但代理没有把它写进决策日志,那这个想法就会彻底消失。压缩之后你再问它,它也只会给出模糊的回复。所以我说这套体系针对的是“可文档化的状态”,而不是所有人的临场思维。

5.4 实操中我踩过的几个坑,以及最后的处理办法

这套体系并不是第一天就完全好用的。我开始时踩了不少坑,最值得说的是下面这几个。

第一个坑是笔记越写越长。最初的navigation.md写了三四十行,结果代理每次启动读到一半就开始泛泛复述,反而不如短文件有效。后来我强制要求每个文件控制在 50 行以内,只保留“读完之后能继续干活”的最低必要信息。过长的内容会被拆到专项文档里,不在初始化路径上。

第二个坑是只更新进度,不更新决策。代理往往愿意在完成任务后更新 progress,但不会自觉去写“为什么放弃了方案A”。我发现必须用提示词把这件事变成硬性任务,否则决策日志会一直停留在最初那几条。后来我把“否决方案必须写日志”当作完成任务的提交标准,这个问题才解决。

第三个坑是笔记文件也可能过期。老状态留在 progress 里会严重误导代理,它可能会继续做一件已经做完的事。我的处理方式是要求代理在开始新任务前先看“当前任务状态”,如果发现笔记和实际代码不一致,第一件事不是动手,而是先把 progress 修订正确。这个“先矫正再执行”的过程反而让我发现了很多本来会被掩盖的沟通误差。

还有一个非常现实的坑:新会话初始化之后,如果代理没有强制被要求“以笔记为准”,它还是会倾向于依赖自己刚看到的代码和对话。项目级提示词里加上“所有操作以.agent/init.d/03-progress.md为准”之后,这个问题的发生频率才降下来。

经过这一轮调整,我现在面对上下文压缩已经不怎么慌了。我甚至开始主动把每一次新会话当作一次“服务器重启”来设计,确保即使整个过程从头再来,代理也能在几分钟内进入工作状态。这套组合拳的收益远超我最初预期:不只是防止失忆,它顺带让代理的决策一致性、token 利用率和项目文档质量都提升了。

最后再多说一句我个人的感受:别等到上下文已经 20 万 token 了,才开始想“万一被压缩怎么办”。现在就把.agent/init.d建起来,养成“每个重要结论都落盘”的习惯,你会发现哪怕下次真的又来一场“脑叶切除术”,你的代理也能稳稳地回到原来的位置上。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进