ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

对抗AI代理上下文压缩:外部记忆与init.d启动脚本工作流

对抗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 改为 5(commit 8f3a21d) - [x] 定位到证书链每次重连都会重新校验 ## 进行中 - 正在重构 sync_engine 的队列模块 ## 阻塞项 - 暂未定位证书链轮换的刷新时机,需要查看底层库的文档 ## 关键路径 / 文件 - src/network/sync_engine.go - src/network/cert_rotator.go

decisions.md记录重大决策和被否决的方案,格式为日期+背景+选择+原因+后续验证:

# DECISIONS ## 2024-XX-XX 选择指数退避而不是固定间隔重试 - 背景:弱网环境下固定间隔会让服务器过载。 - 选择:采用指数退避,上限 30 秒。 - 原因:研究表明大部分弱网恢复发生在 10 秒内,指数退避能以更低成本完成重试。 - 验证:压测 2 小时后无请求堆积。 ## 2024-XX-XX 已否决:通过切换服务端 IP 解决超时 - 原因:实测切换后问题依旧,判断瓶颈不在 DNS/接入点。 - 教训:以后再遇到超时问题,不优先考虑更换接入点。

5.2 养成“存档点”:什么时候必须更新 state.md

这套工作流能不能发挥价值,取决于你是否在正确的时间点更新状态文件。我强烈建议只在三种时机更新,否则很容易变成一份没人看的流水账:

  1. 完成一个值得提交的任务,比如修好一个 bug、跑通一个接口。
  2. 做了一个影响后续开发的技术决策,比如换框架、选数据库。
  3. 遇到卡住的问题,准备切换方案前。

我个人的习惯是每个重要 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 编码代理的工作记忆,永远不值得完全信任;真正值得信任的,是你给它留下的那三份文件。纹身会褪色,纸条会丢,但只要文件还在,它每次都能重新找到自己。”

RELATED READING

延伸阅读

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