资讯动态

AI编码代理抗压缩记忆系统:用init.d思路解决长对话失忆

发布时间:2026/10/22 10:27:52 来源:尧图企业网站定制
1. 当上下文被压缩代理为什么会“失忆”做过 AI 编码代理的人大概都经历过这种崩溃你花了两小时跟代理把需求聊清楚它理解了项目结构、记住了命名约定、甚至记住了你讨厌它用某个库。然后对话一长上下文窗口顶到上限系统自动做了一次压缩。压缩完之后代理像被做了脑叶切除术一样突然开始用你三令五申禁止的写法把之前定好的接口签名改得面目全非甚至忘了自己刚才在改哪个文件。这不是模型变笨了是上下文压缩把它的“工作记忆”洗掉了。压缩算法通常保留最近几轮对话和系统提示中间那些“我们约定过”的内容被摘要成一句话细节全丢。对于聊天场景这没问题但对于一个要连续改十几个文件的编码代理这等于把施工图纸烧了只留一句“盖个房子”。我后来用一个很土的办法解决了这个问题把 Unix 的 init.d 启动脚本思路和电影《记忆碎片》里主角靠纹身和纸条维持记忆的机制移植到代理的记忆管理上。核心就一句话——别指望上下文窗口记住任何东西把所有关键状态落到磁盘上每次开工前重新加载。这套方案让我的代理在 20 万 token 的压缩风暴里活了下来压缩前后行为完全一致。这篇文章我会把这套机制的设计思路、目录结构、脚本实现、踩坑记录全部摊开讲。适合正在做 AI 编码代理、或者被长对话失忆问题折磨的开发者。不需要你懂内核开发但需要你愿意接受一个反直觉的前提代理的记忆不该存在对话里。2. 核心设计思路把记忆从上下文里搬出来2.1 为什么“加大窗口”是条死路很多人第一反应是换更大的上下文窗口。我试过从 8K 换到 32K 再换到 128K问题只是被推迟了没有消失。原因有三层。第一层是成本。窗口越大每轮推理的 token 消耗越高一个改十几次文件的编码任务账单能翻好几倍。第二层是注意力稀释。窗口里塞的东西越多模型对中间内容的注意力越弱这就是常说的“lost in the middle”你精心写的约定放在中段模型根本看不见。第三层最致命——压缩依然会发生。哪怕窗口有 20 万 token长任务照样能顶满系统照样会触发摘要压缩你的记忆照样被洗。所以真正的解法不是“记住更多”而是“不依赖记住”。这就像 Unix 系统重启进程内存全丢但系统照样能恢复到可用状态因为关键配置写在/etc里启动脚本按顺序读一遍就重建了运行环境。代理也应该这样。2.2 init.d 给我们的三个启发Unix 的 init.d 机制看起来古老但它解决的是一个和代理记忆一模一样的问题如何在无状态的启动过程中可靠地重建一个有状态的服务。它有三个设计值得抄。第一约定优于配置的目录结构。/etc/init.d/下每个脚本就是一个服务脚本名就是服务名不需要额外的注册表。代理的记忆也应该这样一个文件对应一类记忆文件名自解释。第二幂等的启动语义。init 脚本可以反复执行start已经启动的服务不会重复启动。代理加载记忆也必须是幂等的加载十次和加载一次结果一样否则重试就会污染状态。第三显式的依赖顺序。init 脚本有运行级别和依赖声明保证 A 在 B 之前启动。代理的记忆加载也有顺序先加载项目全局约定再加载当前任务状态最后加载临时草稿顺序错了就会互相覆盖。2.3 《记忆碎片》教我们的“外部化记忆”电影里主角有顺行性遗忘每隔几分钟就忘光但他靠三样东西维持任务连续性纹身永久事实、宝丽来照片带注释的快照、纸条临时待办。这三样对应到代理记忆系统里就是三个层级的持久化文件。纹身层项目级的硬约束比如“这个仓库用 tabs 不用 spaces”“所有 API 返回统一包装成{code, data, msg}”。这些几乎不变写在memory/tattoo.md。照片层任务级的快照比如“当前正在重构user_service已完成 3/7 个函数接口签名已定稿为 XXX”。写在memory/snapshot.md每个任务阶段更新一次。纸条层临时待办和草稿比如“下一步要跑测试”“这个 bug 怀疑在缓存层”。写在memory/notes.md随时可丢。关键在于这三层文件在每次代理开工前被强制读取压缩发生多少次都不影响。上下文压缩洗掉的是对话历史洗不掉磁盘文件。代理每次“醒来”第一件事就是读这三个文件把自己重新拼装成压缩前的样子。提示这套机制的本质是把易失的上下文记忆转换成非易失的磁盘状态。压缩只影响前者后者永远安全。3. 目录结构与文件规范让记忆可读可写可审计3.1 整体目录布局我在项目根目录下建了一个.agent/隐藏目录所有记忆文件都放里面。为什么不放系统临时目录因为记忆必须跟着项目走换台机器、换个代理实例只要目录在记忆就在。结构如下.agent/ ├── memory/ │ ├── tattoo.md # 永久约束几乎不改 │ ├── snapshot.md # 任务快照阶段更新 │ └── notes.md # 临时草稿随时可丢 ├── hooks/ │ ├── pre_task.sh # 任务开始前加载记忆 │ ├── post_edit.sh # 每次编辑后更新快照 │ └── on_compact.sh # 检测到压缩后强制重载 ├── log/ │ └── memory.log # 记忆读写审计日志 └── config.yaml # 记忆系统配置这个布局的灵感直接来自 init.dmemory/相当于/etc/init.d/hooks/相当于运行级别脚本log/相当于/var/log/。每个目录职责单一不混用。3.2 三个记忆文件该写什么tattoo.md 是宪法不是日记。它只放那些“代理如果违反就必须回滚”的硬约束。我实测下来这个文件超过 200 行就开始失效因为模型对超长约束的遵守率会下降。所以写法要极度精炼用祈使句一条一行。# 项目硬约束 - 缩进用 4 空格禁止 tab - 所有对外接口返回 {code, data, msg} - 禁止引入新的第三方依赖除非 tattoo 里显式批准 - 数据库查询必须走 repository 层禁止在 service 里裸写 SQL - 提交信息格式type(scope): subjectsnapshot.md 是施工进度板。它记录“当前任务进行到哪了”必须包含任务目标、已完成项、进行中项、待办项、关键决策。这个文件是压缩后恢复的核心因为压缩最容易丢的就是“我们刚才在干嘛”。# 当前任务快照 ## 目标 重构 user_service把同步调用改成异步 ## 已完成 - [x] 接口签名定稿async def get_user(uid: int) - User - [x] repository 层已改完 3 个方法 ## 进行中 - [ ] service 层 get_user 改造当前卡在异常处理 ## 关键决策 - 异常统一抛 UserNotFound不返回 None - 缓存层暂不动下一阶段处理notes.md 是草稿纸。它允许混乱允许过期代理可以随手写“怀疑 bug 在 cache key 拼接”“下次试试用 dataclass”。这个文件的价值在于降低代理写记忆的心理门槛不用每次都想“这条够不够格进 snapshot”。3.3 文件更新的触发时机记忆文件不能只在任务结束时写那样压缩发生在中途就白搭。我的触发策略是三个时机任务开始前pre_task.sh读取三个文件注入到系统提示最前面。每次文件编辑后post_edit.sh检查 snapshot 是否需要更新如果本次编辑改变了任务进度就追加一行。检测到压缩后on_compact.sh是兜底一旦发现上下文长度骤降或摘要标记出现立即重新读取三个文件并注入。这里有个细节注入位置比注入内容更重要。我试过把记忆放在对话中段压缩后模型基本无视放在系统提示最开头遵守率明显提升。原因是模型对开头和结尾的注意力最强记忆必须抢开头。注意不要把所有记忆都塞进系统提示那样每轮都消耗大量 token。正确做法是只注入 tattoo 和 snapshot 的摘要版完整版放在磁盘代理需要时自己读。4. 核心脚本实现从加载到审计的完整链路4.1 pre_task.sh开工前的记忆重建这个脚本是整套机制的心脏。它做四件事检查记忆文件是否存在、读取内容、生成注入块、写审计日志。我用 bash 写因为不依赖任何运行时任何环境都能跑。#!/usr/bin/env bash set -euo pipefail AGENT_DIR.agent MEM_DIR$AGENT_DIR/memory LOG_FILE$AGENT_DIR/log/memory.log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $1 $LOG_FILE } # 确保目录存在幂等 mkdir -p $MEM_DIR $AGENT_DIR/log # 三个记忆文件缺失则创建空文件 for f in tattoo snapshot notes; do touch $MEM_DIR/$f.md done # 生成注入块带明确分隔符 { echo AGENT MEMORY BEGIN echo ## 硬约束必须遵守 cat $MEM_DIR/tattoo.md echo echo ## 当前任务快照 cat $MEM_DIR/snapshot.md echo echo ## 临时笔记可能过期仅供参考 cat $MEM_DIR/notes.md echo AGENT MEMORY END } $AGENT_DIR/injected_context.txt log pre_task: 记忆已加载tattoo$(wc -l $MEM_DIR/tattoo.md)行 snapshot$(wc -l $MEM_DIR/snapshot.md)行 cat $AGENT_DIR/injected_context.txt这个脚本的关键设计是幂等。反复执行不会重复追加内容因为每次都是覆盖写injected_context.txt。这直接抄了 init 脚本的语义start一个已启动的服务是安全的。4.2 post_edit.sh编辑后的快照更新每次代理改完文件这个脚本被调用。它不自动改 snapshot而是提示代理自己判断是否需要更新。为什么不让脚本自动写因为脚本不知道这次编辑是否改变了任务进度只有代理知道。脚本的职责是提供模板和检查。#!/usr/bin/env bash set -euo pipefail AGENT_DIR.agent SNAPSHOT$AGENT_DIR/memory/snapshot.md LOG_FILE$AGENT_DIR/log/memory.log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $1 $LOG_FILE } # 检查 snapshot 最后修改时间超过 10 分钟提醒更新 if [ -f $SNAPSHOT ]; then last_mod$(stat -c %Y $SNAPSHOT 2/dev/null || stat -f %m $SNAPSHOT) now$(date %s) diff$(( (now - last_mod) / 60 )) if [ $diff -gt 10 ]; then log post_edit: snapshot 已 $diff 分钟未更新建议刷新 echo 提醒snapshot 已 $diff 分钟未更新如果任务进度有变化请更新 .agent/memory/snapshot.md fi fi这个“超时提醒”机制是我踩坑后加的。早期版本没有它代理连续改十几个文件都不更新快照结果压缩一来进度全丢。加了提醒后代理会主动在关键节点刷新 snapshot。4.3 on_compact.sh压缩后的强制重载这是兜底脚本也是最难写对的一个。难点在于如何检测压缩发生了。不同平台的压缩信号不一样有的给摘要标记有的直接截断历史。我的策略是双保险检测 token 数骤降 检测摘要关键词。#!/usr/bin/env bash set -euo pipefail AGENT_DIR.agent LOG_FILE$AGENT_DIR/log/memory.log LAST_LEN_FILE$AGENT_DIR/log/last_context_len log() { echo [$(date %Y-%m-%d %H:%M:%S)] $1 $LOG_FILE } # 从环境变量读取当前上下文长度由代理框架注入 CURRENT_LEN${AGENT_CONTEXT_LEN:-0} if [ -f $LAST_LEN_FILE ]; then LAST_LEN$(cat $LAST_LEN_FILE) # 长度骤降超过 40% 视为压缩 if [ $LAST_LEN -gt 0 ] [ $CURRENT_LEN -lt $(( LAST_LEN * 60 / 100 )) ]; then log on_compact: 检测到上下文骤降 $LAST_LEN - $CURRENT_LEN触发重载 bash $AGENT_DIR/hooks/pre_task.sh /dev/null echo 检测到上下文压缩已重新加载记忆。请以 .agent/memory/ 下的内容为准。 fi fi echo $CURRENT_LEN $LAST_LEN_FILE这个脚本的 40% 阈值是实测调出来的。设太低会误报设太高会漏报。40% 能覆盖大多数压缩场景又不会因为正常对话波动而误触发。4.4 审计日志让记忆可追溯memory.log记录每次记忆读写格式统一。它的价值在排查问题时体现当代理行为异常你可以翻日志看它最后一次加载记忆是什么时候、加载了什么版本。[2025-01-15 10:23:01] pre_task: 记忆已加载tattoo12行 snapshot18行 [2025-01-15 10:35:44] post_edit: snapshot 已 12 分钟未更新建议刷新 [2025-01-15 10:41:02] on_compact: 检测到上下文骤降 185000 - 42000触发重载 [2025-01-15 10:41:03] pre_task: 记忆已加载tattoo12行 snapshot21行看到没压缩发生在 10:41:02重载在 10:41:03中间只隔一秒。代理几乎无感地恢复了记忆。5. 实操过程从零搭起一套抗压缩记忆系统5.1 第一步初始化目录和文件在项目根目录执行一条命令建好骨架mkdir -p .agent/{memory,hooks,log} touch .agent/memory/{tattoo,snapshot,notes}.md touch .agent/log/memory.log然后把前面三个脚本分别写入hooks/下加执行权限chmod x .agent/hooks/*.sh这一步没什么技术含量但目录名和文件名必须固定因为脚本里是硬编码路径。我试过做成可配置的结果配置本身也需要记忆绕回去了。约定优于配置在这里是真理。5.2 第二步填充 tattoo.md打开.agent/memory/tattoo.md把项目的硬约束一条条写进去。判断标准很简单如果代理违反这条你会不会想回滚会就写进去不会就别写免得稀释注意力。我一般从这几个维度收集代码风格缩进、命名、架构约束分层、依赖方向、接口约定返回格式、错误码、流程规范提交信息、分支命名。每个维度控制在 3-5 条总共不超过 20 条。5.3 第三步接入代理框架的钩子不同代理框架的钩子机制不一样但思路一致在任务开始、文件编辑、上下文变化三个时机调用对应脚本。以常见的几种框架为例框架类型任务开始钩子编辑后钩子压缩检测命令行代理启动参数文件监听轮询上下文长度IDE 插件onSessionStartonFileSaveonContextChange自研框架手动调用手动调用手动调用如果是自研框架最省事的做法是在系统提示拼装函数里第一行就调用pre_task.sh并把输出拼进去。这样每次拼提示都自动加载记忆不需要额外的钩子管理。5.4 第四步验证抗压缩效果搭好之后必须验证。我的验证方法是人为制造压缩让代理做一个长任务中途手动清空对话历史模拟压缩看它能否继续。具体步骤给代理一个多步骤任务比如“重构三个模块的日志调用”。做到一半手动清空对话只保留系统提示。观察代理下一步动作是否符合 snapshot 里的进度。如果代理能准确说出“我正在改第二个模块第一个已完成”说明记忆系统生效。如果它一脸茫然检查injected_context.txt是否被正确注入。提示验证时一定要用真实的长任务短任务触发不了压缩测不出问题。我一般用“重构 5 个以上文件”的任务来测。5.5 第五步调优注入内容跑通之后开始调优。核心指标是注入 token 数和遵守率的平衡。注入太少代理记不住注入太多每轮成本高还稀释注意力。我的调优经验是tattoo 全量注入它短且关键snapshot 只注入“目标 进行中 关键决策”三段已完成和待办可以省略notes 只注入最近 5 条。这样注入量能控制在 500 token 以内遵守率还很高。6. 常见问题与排查技巧实录6.1 代理无视记忆照旧犯错这是最常见的问题。排查顺序先看注入位置再看注入内容最后看模型本身。注入位置记忆必须在系统提示的最开头不能放在中段或结尾。我试过放在结尾遵守率直接掉一半。注入内容检查是否有矛盾。比如 tattoo 说“禁止用 tab”snapshot 里却写“缩进用 tab”代理会困惑。记忆文件之间必须一致。模型本身有些模型对系统提示的遵守率天生就低这时候只能靠更频繁的提醒或者在每次用户消息前再注入一次摘要。6.2 压缩检测漏报on_compact.sh的阈值检测不是万能的。有些平台压缩时不改变上下文长度只是替换内容。这种情况我的兜底方案是定时重载不管有没有检测到压缩每 20 轮对话强制重载一次记忆。代价是多消耗一点 token但换来的是稳定。6.3 snapshot 更新太频繁导致噪音早期我让代理每次编辑都更新 snapshot结果文件里全是“改了第 3 行”“删了个空行”这种噪音。后来改成只在任务阶段变化时更新判断标准是“这次编辑是否改变了任务的整体进度”。改一个函数内部实现不算改完一个函数算。6.4 记忆文件冲突多个代理实例同时操作同一个项目时记忆文件会冲突。我的解法是加文件锁在pre_task.sh和post_edit.sh里用flock包一层。虽然简单但能避免大部分并发写坏文件的情况。exec 200$AGENT_DIR/log/.lock flock -n 200 || { echo 另一个代理正在操作稍后重试; exit 1; }6.5 常见问题速查表现象可能原因排查动作代理完全无视记忆注入位置错误检查是否在系统提示开头压缩后行为突变压缩检测未触发查看 memory.log 有无重载记录记忆内容自相矛盾文件间不一致对比 tattoo 和 snapshotsnapshot 全是噪音更新过于频繁改为阶段更新多实例写坏文件无并发控制加 flock 文件锁注入 token 过高全量注入改为摘要注入6.6 几个我踩过的坑坑一把记忆写在对话里。最早我让代理“记住这个约定”结果压缩一来全没了。记忆必须落盘这是铁律。坑二snapshot 写得太细。我一度把每个函数的改动都写进 snapshot结果文件几百行代理读都读不完。snapshot 是进度板不是 diff 记录。坑三忘了幂等。早期pre_task.sh用追加写重试几次后注入块重复了十几遍token 爆炸。改成覆盖写就好了。坑四检测阈值拍脑袋。40% 这个数字是试了七八次才定下来的一开始设 20% 误报不断设 60% 又漏报。阈值必须实测。7. 进阶玩法让记忆系统自我进化7.1 记忆的自动归档跑久了之后snapshot 会积累大量已完成任务。我的做法是任务完成后归档把 snapshot 内容移到memory/archive/下按日期命名snapshot 清空准备下一个任务。归档文件不注入只在需要追溯时手动查。archive_snapshot() { local date_str$(date %Y%m%d_%H%M%S) mv .agent/memory/snapshot.md .agent/memory/archive/snapshot_$date_str.md touch .agent/memory/snapshot.md }7.2 从 notes 里提炼 tattoonotes 里经常出现重复的临时约定比如代理反复写“记得用 async”。当某条笔记出现超过 3 次就该考虑把它提升为 tattoo。我写了个简单的统计脚本grep -o 记得[^。]* .agent/memory/notes.md | sort | uniq -c | sort -rn | head -5出现频率高的手动确认后写进 tattoo。这样记忆系统会随着项目推进越来越懂你的偏好。7.3 跨项目的记忆共享有些约束是跨项目的比如“我讨厌用某个库”。这种可以放在用户级目录~/.agent/memory/tattoo.md项目级 tattoo 加载时先读用户级再读项目级后者覆盖前者。这样换项目不用重写通用约束。7.4 记忆版本化把.agent/memory/纳入 git 管理每次记忆变更都有 commit 记录。好处是能回滚——如果某次 snapshot 更新写错了导致代理跑偏直接git checkout回上一个版本。我实测下来这个习惯救过我好几次。8. 一些个人体会这套机制我用了大半年最大的感受是AI 代理的可靠性不取决于模型多强而取决于你给它搭的外部脚手架多稳。模型会忘、会漂、会被压缩洗脑但磁盘文件不会。把记忆外部化等于给代理装了一个不会失忆的大脑外挂。另一个体会是简单胜过聪明。我一开始想搞复杂的向量检索、语义压缩后来发现最土的文件读写反而最可靠。init.d 那套东西几十年没变不是因为它先进是因为它把“可靠重建状态”这件事做到了极致。代理记忆管理也该这样别追求花哨追求每次都能正确恢复。最后分享一个小技巧定期手动检查injected_context.txt。这个文件是代理实际看到的记忆不是你以为它看到的。我踩过好几次坑都是因为注入内容和预期不符。养成每周看一眼的习惯能提前发现很多问题。这套方案后续还能扩展比如把记忆文件做成结构化 JSON 方便程序处理或者接入外部数据库做多机同步。但核心思路不变记忆在磁盘不在上下文加载靠脚本不靠模型自觉。把这两条守住20 万 token 的压缩风暴也就是一阵风而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑