news 2026/10/14 6:54:00

AI编码代理上下文压缩自救:外部记忆+init.d式恢复机制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI编码代理上下文压缩自救:外部记忆+init.d式恢复机制实战

我最近完成了一个累计消耗 20 万 token 的重构任务,主角不是人类团队的代码评审,而是一名 AI 编码代理。任务中途,它经历了一次上下文压缩——平台为了节省窗口,把早期会话中大量的代码发现过程、关键决策和变量命名约定全部浓缩成了几行摘要。那种感觉就像代理被做了一场脑叶切除术:语法能力还在,推理能力还在,但它忘了我们前十个小时刚刚确认过的那些事。这篇文章就聊聊我是怎么用 Unix init.d 式的启动恢复机制和《记忆碎片》式的外部记忆法,让编码代理对上下文压缩免疫的。

先说个背景。如今主流的 AI 编码代理已经能在一个足够大的上下文窗口里连续工作十几个小时,期间不断探索代码库、修改文件、运行测试。问题在于,无论窗口标称多大,总会触顶。一旦触顶,工具就会启用压缩或裁剪策略,早期对话里的代码路径、命名约定、已经否决的方案,全都会被"摘要化"甚至直接丢弃。我用过不少补救办法,比如让代理把重要信息贴在对话里,但效果都很差。后来我把思路换了一下:既然对话记忆不可靠,那就不要依赖对话记忆。我把关键信息全部迁到文件系统里,再仿照 Unix init.d 的方式在每次会话启动时自动装载,最终成功扛过了三轮压缩。这篇文章会把整套方案、实际操作和踩坑记录完整写出来,适合所有正在用编码代理做大型重构、长任务开发,并且受够了"代理突然失忆"的开发者参考。

1. 先搞清楚敌人是谁:上下文压缩是怎么把 20 万 token 弄丢的

我强烈建议你先花十分钟理解上下文压缩的内部机制,不然后面所有技巧都是在打黑枪。编码代理本质上是在一个有限窗口里"看世界",窗口里装着系统提示、工具返回的代码片段、测试输出和你之前的全部对话。随着任务推进,窗口会不断膨胀,直到超过限制。这时代理平台会启动压缩流程,把较早的历史进行归并、概括,腾出空间给新内容。

1.1 压缩发生前的典型征兆

压缩通常不会提前通知你,但会有一些肉眼可辨的征兆。我第一次踩坑时,代理在连续工作了几个小时之后突然开始重复提问:"这个模块的入口函数是什么来着?"、"我们之前确认过要用哪种日志格式吗?"——这两个问题在五个小时前早就讨论过好几次。更明显的是,它开始引用不存在的文件路径,或者把之前刚刚否决过的方案重新提出来。如果你发现代理开始频繁使用"根据我的理解"、"我可能记错了"这类句式,说明它的长期记忆已经出现了空洞。

还有一类征兆更隐蔽:代理开始"过度解释"。因为它记不住之前已经约定的内容,所以每次都会重新推演一遍前置逻辑,导致输出变长、变慢。如果你观察到同样一个决策被反复论证,而且论证方式每次都不同,那基本可以确定上下文已经发生了压缩。

1.2 为什么"别碰长任务"不是好建议

很多人会劝你:把任务拆小,别用编码代理跑太长会话。这话有道理,但不是所有任务都能拆干净。跨系统重构、技术债盘点、批量迁移,这些任务天然需要一个很大的探索上下文:你前期读过的每一处代码调用关系,可能都会影响后期某个修改点。如果你把任务拆到十几个短会话里,反而会丢失全局视角,代理甚至会重复完成已经做过的工作,或者做出和之前决策冲突的改动。

所以问题的核心不是"怎么避免长会话",而是"怎么让长会话在压缩之后还能维持状态"。正视这件事之后,我才开始设计外部记忆方案。

2. 记忆碎片式外部记忆:把大脑从对话里搬到文件系统里

《记忆碎片》讲的是一个无法形成新记忆的人,靠着便条、照片和纹身来维持生活。我每次想到上下文压缩,都会觉得 AI 编码代理的处境和主角极其相似——它的"短期记忆"就是上下文窗口,窗口一压缩就等于短暂失忆。主角的解法是什么?把所有关键信息写到身体外部。编码代理的解法也一样:把所有关键信息写到项目文件里,而不是留在对话历史中。

2.1 编码代理的两类记忆:易失性与持久性

我习惯把代理的信息来源分成两类。第一类是易失性记忆,也就是对话历史、临时工具输出,这类信息在压缩时会丢失或变形。第二类是持久性记忆,也就是文件系统里的代码、文档、配置文件,这类信息即使你把会话关掉再重开,它依然存在。我要做的,就是尽量把关键的状态信息从易失性记忆转移到持久性记忆。

也就是说什么呢?当你发现代理"记得"某个决策时,不要去依赖这个"记得",你要立刻把它写进一个文件。代理的对话记忆就像白板上的粉笔字,随时会被擦掉;文件系统就像一个永久笔记本,只要 git 仓库还在,内容就丢不了。

2.2 记忆库文件结构:五个文件管理完整任务状态

我的做法很简单,在项目根目录建一个memory/目录,里面放五个固定文件,每个文件有明确的职责边界。我不推荐只有一个大文件,因为大文件读起来费 token,而且代理容易"看了后面忘了前面"。

文件名职责更新时机
GOAL.md任务目标、验收标准、范围边界任务开始前确定,中途如变更就需要改
DECISIONS.md关键决策、为什么选它、否定了什么方案每次做出架构或方案决策时
PROGRESS.md当前进度、已完成的模块、正在进行的事每个子任务结束时
MAP.md代码地图、关键文件路径、入口函数位置探索新代码区域时
GOTCHAS.md已知陷阱、命名约定、特殊规则每次踩坑或发现隐藏规则时

PROGRESS.md是这个体系的核心,它相当于压缩后的"记忆锚点"。我会规定代理每次完成一个子任务后必须更新它,写清楚"现在改到哪了、下一步打算做什么、有哪些待验证的假设"。MAP.md也很重要,它省去了压缩后重新探索代码库的成本——代理不用再重新翻一遍项目结构,直接读地图就行。

2.3 什么时候必须写记忆

我给自己和代理定了几条铁规矩,违反任何一条都会直接导致后续流程混乱。

第一,完成一个可验证的子任务后必须写。比如"把订单查询接口抽出来了,测试通过"——这种状态必须立刻记录。第二,做出一个"这有两种方案,我选了 A"级别的决策时必须写,并且要在DECISIONS.md里写清楚"为什么不是 B",这一点相当关键,否则压缩后代理可能会觉得"B 好像更好"然后推翻你的决策。第三,发现任何不明显的规则时必须写,比如"该项目的数据库连接必须走代理类,不能直接 new 连接",这类潜规则是代理最需要记住、但最容易被压缩掉的东西。

3. init.d 式启动装载:每次开工之前让代理先"服药"

光有记忆文件还不够。如果你每次新开一个会话,都要手工把五个文件内容一条条贴给代理,那效率太低,而且贴的时候还可能漏掉某个文件。这里我参考的是 Unix 世界的 init.d 机制。

3.1 从 Unix init.d 借鉴的核心思想

Unix 系统开机时,init 进程会扫描 init.d 目录下的脚本,把系统服务一个一个拉起来,从而让机器恢复到上次关机的状态。我受到启发:为什么不让编码代理也拥有一个"开机自启"机制?每次打开新会话时,自动把memory/目录下的关键内容装载进上下文,这样代理一开工就知道自己是谁、任务是什么、决策有哪些、进度到哪了。

这个机制带来的最大好处是:会话不再是状态的容器,只是一个执行环境。状态全部存在文件里,你随时可以把会话关掉、重开、甚至换一台机器,代理的状态都不会丢失。

3.2 落地实现:一个恢复脚本和一段固定提示词

我先写了一个 bash 脚本,每次启动编码代理前自动组装提示词。脚本逻辑非常简单,但极其有效。第一个版本是这样的:

#!/usr/bin/env bash # revive.sh —— 编码代理恢复脚本,模拟 init.d 开机自启 set -euo pipefail MEMORY_DIR="memory" AGENT_CMD="your-agent-cli" # 替换成你自己的编码代理命令 if [ -d "$MEMORY_DIR" ]; then PROMPT="阅读以下记忆文件,并先输出一份 200 字以内的任务状态摘要: ===GOAL=== $(cat "$MEMORY_DIR/GOAL.md") ===DECISIONS=== $(cat "$MEMORY_DIR/DECISIONS.md") ===PROGRESS=== $(cat "$MEMORY_DIR/PROGRESS.md") ===MAP=== $(cat "$MEMORY_DIR/MAP.md") ===GOTCHAS=== $(cat "$MEMORY_DIR/GOTCHAS.md") 请先复述:1. 当前目标 2. 已完成进度 3. 接下来第一步。然后等我的指令。" else echo "警告: 未找到 memory 目录,将不带记忆启动" PROMPT="新任务,开始探索项目结构。" fi exec $AGENT_CMD -p "$PROMPT"

这里的关键设计有两处。第一,提示词里明确要求代理"先输出 200 字以内的任务状态摘要"——这个动作是为了强迫它真正读取文件并理解内容,而不是假装读了之后直接问你要指令。第二,脚本放在项目根目录,并且我给它起了一个明确的别名,这样每次开脑暴式的新会话都非常方便。

我用的是 bash 的$(cat ...)直接内联文件内容,好处是零依赖。如果你的记忆文件很大,可以考虑换成文件路径让代理自己去读,但前提是它拥有读取本地文件的能力。我之所以选择内联,是因为这种方式最保险,能确保内容真正进入上下文,不依赖代理的工具调用纪律。

3.3 在系统层面固化启动逻辑

如果你和我一样平时用 shell 别名,可以把启动逻辑直接固化到~/.bashrc或~/.zshrc里。我加了两条别名:

alias rev='bash ./revive.sh' # 带记忆启动,适用于老任务 alias fresh='your-agent-cli' # 不带记忆启动,适用于全新任务

我把fresh和rev分开,就是为了避免每次启动新任务时都被迫读一堆旧记忆。实际使用中,rev是我最常用的命令,尤其是在隔了一夜、第二天继续做同一个任务的时候。早上起来,我只需要打开终端执行rev,代理就会自动进入工作状态,不需要我把昨天讨论的细节重新讲一遍,那种体验真的很舒服。

4. 实战记录:一次 20 万 token 重构任务的幸存实录

理论说完了,来一个真实的项目记录。为了保证可复现性,我会尽量把操作细节和对话样张写清楚。

4.1 任务背景与初始记忆构建

这个项目是一个跨平台的后端系统,任务是把一个庞大的单体服务拆出独立的调度模块。代码量大约 30 万行,语言栈是 Python。我没有直接让代理开始改代码,而是花了约 3000 token 的对话量,先走完记忆构建阶段。

我在开始时这样说:"先不要写任何代码。请你用两轮交互完成三件事:第一,定位调度模块的核心入口和依赖关系;第二,总结当前拆分需要遵循的架构约束;第三,把结果按 GOAL、MAP、DECISIONS、GOTCHAS 的格式写入 memory 目录。"

这个初始阶段我是刻意让代理探索代码库,但不让任何结论只停留在对话里。探索完成后,我检查了几个关键文件的内容,确认 MAP 和 GOTCHAS 不是空话,而是具体到函数名和路径。

有个细节很值钱:我在 GOAL 里明确写了"拆分后不得修改调度模块的对外 API 签名",这条约束在后期压缩后成了我最强的护身符。因为就算代理忘了很多探索细节,它只要读到这一条,就不会做出破坏 API 的改动。

4.2 分阶段执行与记忆写入节奏

任务被我切成了五个阶段:接口梳理、依赖解耦、数据迁移、模块抽取、回归测试。每个阶段开始前,我会让代理先读一遍PROGRESS.md,阶段结束后再让代理以"总结、决策、下一步"三段式更新一次记忆。

以依赖解耦阶段为例,一天的会话累积了大约 6 万 token,中间发生过一次压缩。压缩后的代理出现了一个典型症状:它重新建议我使用消息队列来解耦两个内部模块,但这个问题我们在早期已经讨论过,当时认为引入消息队列会显著增加运维成本,最终否决了。如果没有DECISIONS.md,我估计又要花四五千 token 重新论证一遍。但那次我直接打开文件给代理看:

## 已否决方案:引入独立消息队列 - 理由:初期阶段单机同步调用已经足够,引入队列会增加部署节点和故障排查成本 - 替代方案:接口抽象 + 异步任务表 - 如果未来流量增长到 X 阈值,可重新评估

代理读完这句后,非常干脆地道歉并说"我明白了,继续按原来的接口抽象方案进行"。整个纠正过程只消耗了不到 200 token。如果没有这份文件,这个纠正可能要消耗好几千 token,而且代理还不一定信服。

4.3 压缩后的恢复操作

到了第四阶段,累计 token 已经接近 20 万,期间经历了三次压缩。最严重的一次发生在模块抽取阶段,那次压缩后代理的表现几乎是"断片"的:它记不住我们约定的目标服务目录结构,也记不住中间层接口的命名规则,甚至准备重新从单体里复制一遍代码。

我当时的操作就三步。第一步,退出当前会话,不试图在压缩后的残破上下文里继续沟通。第二步,执行rev命令重新启动代理,装载全部记忆文件。第三步,要求代理先输出 100 字内的状态摘要,我确认摘要准确后再下达指令。

第三步非常关键。如果代理输出的摘要和实际事实不符,说明记忆文件本身有歧义或者它读错了,这时候绝对不能往下走,需要先修正记忆文件。我曾经历过一次摘要错误,后来发现是PROGRESS.md里"已完成"和"进行中"写岔了,导致代理的整个恢复报告完全偏离。修正文件后重新装载,恢复质量立刻提升。

恢复后的代理表现相当惊人:它在没有旧对话的情况下,准确说出了我们要抽取的模块边界、已通过的测试数量、以及下一个待拆解的函数清单。这证明外部记忆方案是可行的,只要状态在文件里,代理的"灵魂"就在。

5. 常见问题与排查技巧实录

用了这套方案几个月,踩了不少坑,也总结了一些非常有价值的排查经验。我把几个最高频的问题列出来,都附上我验证过的解决方式。

5.1 代理对记忆文件"视而不见"

症状是:你让它读记忆文件,它嘴上说"好的",但实际行为表明它根本没读,还是自顾自地按错误方式推进。我遇到过一次,代理的回复里引用了 GOAL.md 里根本没有的内容,显然是幻觉。

解决办法是引入"复述验证"。不要只让代理"阅读"记忆,要让它在动手前先输出一段摘要,并且要求摘要里包含具体的函数名、文件名或验收数字。如果它输出的摘要含糊不清,比如"此项目目标是改进系统",那基本可以断定它没有有效读取,我会重新装载。实测中,"要求输出具体文件名和路径"比"要求输出总体理解"有效得多。

5.2 记忆文件膨胀成第二个上下文

我用了一段时间后,DECISIONS.md涨到了接近 2 万 token。这时候每次启动都要消耗不少上下文去装载,反而拖慢速度。后来我做了分层处理:每个记忆文件头顶放一个"摘要区",只保留最重要、最不可妥协的内容,细节放到文件下方的"详情区"。

启动脚本只装载摘要区,当代理需要深入某个具体决策时,再去读详情区文件。这样做之后,每次启动的记忆装载成本降到了 1500 token 以内,但关键信息一条没丢。这个分层思路其实和代码架构里的"最外层只暴露接口"是一样的道理。

5.3 代理不更新记忆文件

如果你只是要求代理"在合适的时候更新记忆",它基本不会主动去做。我的解决方案是把记忆更新设计成一个必须执行的钩子,而不是一个可选的善意行为。我在每个子任务的提示词里明确要求:"完成子任务后,必须更新 PROGRESS.md 的『已完成』与『下一步』两节,否则视作任务未完成。"

在编码代理的配置里,我还为此加了自动化工具调用规则,让它可以自动读取、追加、修改memory/目录下的 markdown 文件。这个改动极大提升了更新频率。你可以把记忆文件想象成数据库:只有每次都写,崩溃恢复才有东西可读。

5.4 记忆存储混乱以及多会话并行

在多个任务并行时,我出现过一次记忆文件互相污染的惨案:两个不同方向的探索性工作,写了同一个DECISIONS.md,导致代理分不清当前沿用哪套方案。后来我给每个任务线加了独立前缀,比如memory/feature-A/和memory/refactor-B/,互不干扰。如果你只是在单个项目里做单线程任务,可以不搞这么复杂,但只要你开始并行推进两个特性,独立目录几乎是必需的。

在切换任务线时,我的建议是执行rev时指定任务线名称,否则很容易出现"这次其实是在做任务线 B,却加载了任务线 A 的记忆"这种事故。有一个更稳妥的做法是,直接在GOAL.md首行写明任务线标识,脚本里加一步检测,确认装载的是当前需要的记忆。

5.5 压缩之后代理开始编造"记忆"

最危险的情况之一,就是压缩后的代理不仅忘事,还会为了面子编造不存在的进度。它会告诉你"该模块已经重构完成",实际上代码根本没改。原因是早期对话里有某些"计划要改"的行为,被压缩后概括成"已改完"。

这提醒我意识到一个铁律:外部记忆文件里的每一条"已完成",都必须是可验证的,最好附上测试结果或 git 提交号。比如写"已抽取用户服务(提交 a3f9e2、单测通过)"而不是"用户服务已经抽取好了"。后续验证时,直接查提交记录比问代理本身可靠得多。老实说,这个做法也逼着我养成了更规范的工作习惯,现在哪怕是纯人类协作,我也会要求每项重大完成必须有提交证据。

6. 手把手教你搭建自己的"上下文压缩免疫系统"

如果你准备把这套方案用到自己的项目里,我给你一个干净的落地步骤,照做就行,整个过程只需要十分钟。

6.1 初始化记忆库目录

在项目根目录执行几条命令:

mkdir -p memory touch memory/GOAL.md memory/DECISIONS.md memory/PROGRESS.md memory/MAP.md memory/GOTCHAS.md

然后往GOAL.md里写一段明确的任务目标。我建议用这样的模板开头:

# 任务目标(GOAL) - 项目背景:一句话描述 - 最终成果:可验收的输出是什么? - 约束条件:禁止做什么? - 范围边界:哪些模块不碰?

注意,约束条件和范围边界一定要写"不做什么",这比"要做什么"更重要。AI 编码代理在压缩后最容易犯的错不是不做事,而是过度做事——它会主动去重构那些你根本没打算碰的代码。

6.2 修改启动脚本模板

把上面那个revive.sh脚本保存到项目根目录,给执行权限:

chmod +x revive.sh

然后给 shell 加上别名,确保rev命令在任意项目目录下都能快速启动。有一个小细节可以提一下:如果项目目录变了,脚本里的MEMORY_DIR="memory"用的是相对路径,所以只要确保你在项目根目录执行rev就可以了。

6.3 约定记忆更新规则

在项目根目录建一个AGENT_RULES.md,或者在启动提示词里直接说明,让代理遵守以下纪律:

  1. 每次开始新会话前,必须先阅读记忆库并输出摘要。
  2. 每个子任务完成后,更新PROGRESS.md,格式是"已完成 + 验证方式"。
  3. 每个架构决策,写入DECISIONS.md,格式是"决策 + 理由 + 否决方案"。
  4. 发现任何坑,写入GOTCHAS.md,格式是"现象 + 原因 + 规避方式"。
  5. 在记忆与代码产生冲突时,以代码实际行为为准,并同步修改记忆文件。

这些纪律其实就是把人类团队里的文档规范和提交规范,移植到了编码代理身上。效果是,代理的行为越接近一个有职业素养的工程师,而不是一个只会写代码的执行器。我接触过的项目里,"会写记忆"的代理效率明显更高,而且很少出现你来我往的重复沟通。

6.4 定期做"记忆巡检"

每两三个小时,我会专门花一轮对话检查记忆库的更新质量。让代理列出它最近更新的条目,我来判断这些条目是否有价值。如果发现大量废话,比如"讨论了性能问题"这种没有细节的记录,我会要求它重写。这个巡检动作看起来简单,但实际是防止记忆库腐化的关键。没有巡检的记忆库,用不了几天就会变成一坨既占 token 又没用的垃圾。

巡检还有一个附带好处:你能及时发现代理当前的理解与你预期出现了偏移。如果它在DECISIONS.md里记录了一个你完全没印象的决策,说明你们俩的信息已经不对称了,这时候及时纠错比等项目做歪再回头处理轻松得多。

7. 这套方案的边界与后续扩展

最后聊点实在的。这套方案并不是万能的,它也有自己的边界。如果你的编码代理没有文件读写工具,那"外部记忆"就无从谈起,你只能退回到手工贴文本的模式。另外,如果记忆文件本身写得很烂,那么装载多少次都没用——输入的是垃圾,输出的也只会是垃圾。

但我个人实际操作下来的体会是,这套方案的性价比极高:它不依赖某个特定 AI 平台的功能,不依赖特定的模型,只需要一个能读写文件的编码代理和一个愿意花十分钟建立目录结构的你。我踩过几次坑之后,最大的感悟是:不要和上下文压缩正面对抗,那是模型架构决定的物理限制,你赢不了。你能做的,是让记忆的载体从对话转移到文件系统,再借一套"启动自恢复"的机制让每次开工都站在前一次的肩膀上。这个思路,放在任何一代编码代理上,都不会过时。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:53:46

API网关和反向代理区别在哪?MAI Gateway统一治理方案深度解析

“API 网关和反向代理区别在哪”——表面看是产品对比,深层看是两个时代的入口范式差异。本文走"统一治理方案深度解析"视角,从命名、演进、架构三层切入,再讲 MAI Gateway 如何用三层治理栈把两类入口收敛为同一层。 第一层&#…

作者头像 李华
网站建设 2026/10/14 6:53:41

CSS伪类与伪元素完全指南:状态与虚拟元素的区别、优先级和实战

写CSS伪类和伪元素这块内容,我其实挺有感触的。我记得自己刚接触前端的时候,对着:hover和::before这两个东西完全分不清,总觉得它们长得差不多,用起来好像都能往页面上加点东西,直到后来被一个::before配合content属性…

作者头像 李华
网站建设 2026/10/14 6:53:41

AnyPS5串流实战:从采集编码到外网低延迟全链路解析

1. 从“AnyPS5”这个标题说起:它到底想解决什么问题第一次看到“AnyPS5”这个标题,我脑子里蹦出来的第一个念头是:这大概率又是一个围绕“跨平台、跨设备、跨环境”做文章的项目。为什么这么说?因为“Any”这个前缀在技术圈里几乎…

作者头像 李华
网站建设 2026/10/14 6:53:39

数据库课程设计实战:可运行的全栈电商订单系统

简介:本资源是高校《数据库应用》课程设计的完整实践项目包,面向计算机专业本科生及Web开发初学者,聚焦教室管理系统的数据库建模、前后端开发与部署全流程。项目基于关系型数据库(含.mdf主数据文件与.ldf日志文件)与J…

作者头像 李华
网站建设 2026/10/14 6:53:39

AnyPS5自研跨平台远程串流方案:延迟优化与踩坑实战

客厅里那台次世代主机买了大半年,我真正坐在它面前玩的时间,可能还没刷系统设置的时间多。工作一忙,下班只想窝在床上用平板,或者出差时打开轻薄本——主机却永远孤零零地躺在电视柜上。这个念头催生了我折腾 AnyPS5 这个小项目的…

作者头像 李华
网站建设 2026/10/14 6:53:39

Spring整合MyBatis核心原理与常见坑位解析

做过几年 Java 后端的人,多半都跟 MyBatis 打过交道。尤其是手头维护着几个老项目,或者要在一个非 Spring Boot 的传统 Spring 工程里硬塞一套持久层框架时,就会发现网上讲“Spring 整合 MyBatis”的文章很多,但大多是贴一段 XML …

作者头像 李华