
1. 从一次上下文崩溃说起为什么长会话代理总会“失忆”做过 AI 编码代理的人大概都经历过这种崩溃瞬间你花了两个小时跟代理把一套构建脚本调通中间它读了十几个文件、跑了几轮测试、修了三个边界条件结果上下文一压缩它突然开始重新问你“这个项目用什么语言写的”。更离谱的是它会把之前已经确认过的接口签名改回去把刚删掉的死代码又加回来仿佛前面那两小时是一场集体幻觉。这个项目标题里提到的“20 万 token 的脑叶切除术”说的就是这件事。当会话上下文逼近模型窗口上限时系统会触发压缩机制把早期对话摘要化甚至直接丢弃。对聊天机器人来说这没什么但对一个正在维护代码状态的编码代理来说这等于把它的工作记忆切掉一大块。标题里用《记忆碎片》来类比非常准确——那部电影的主角只能靠纹身、纸条和宝丽来照片维持连续性而我们要做的就是给代理准备一套属于它自己的“纹身和纸条”。我这次折腾的核心目标很明确让一个长时间运行的编码代理在经历多轮上下文压缩之后依然能准确知道“我是谁、我在哪个项目、我改过什么、下一步该干什么”。整套方案借用了 Unixinit.d的思路——用一组幂等的、可重复执行的启动脚本在每次会话恢复时把关键状态重新注入。下面我把这套东西从设计到落地完整拆一遍包括我踩过的坑和最后稳定下来的配置。2. 整体设计思路把代理的“人格”和“记忆”拆开管理2.1 为什么不能只靠系统提示词硬扛最直觉的做法是把所有关键信息塞进系统提示词指望它永远不被压缩掉。我一开始也是这么干的结果发现两个问题。第一系统提示词本身也会占用窗口你塞得越多留给实际工作的空间越小压缩触发得越早。第二很多代理框架在压缩时对系统提示词和对话历史是一视同仁的它并不会因为“这段很重要”就特殊对待。所以正确的思路不是“防止压缩”而是“压缩之后能快速恢复”。这就像服务器重启你不可能阻止它重启但你可以保证它重启后能自动拉起所有服务。init.d的价值就在这里它定义了一套标准的、幂等的启动流程不管机器重启多少次跑一遍脚本状态就回来了。2.2 三层记忆结构的设计我把代理需要维持的状态拆成三层分别用不同机制承载。第一层是不变的身份层包括项目根路径、技术栈、代码规范、禁止事项。这部分内容极少变动我把它写成一个独立的identity.md每次会话启动时通过脚本读取并注入。它的特点是短、稳定、必须每次都在。第二层是可变的进度层记录当前任务、已完成步骤、待办事项、已知阻塞。这部分我用一个结构化的progress.json来存代理每完成一个关键节点就更新它。压缩发生后脚本会把这个文件重新读进来转成一段紧凑的摘要注入。第三层是原始证据层也就是完整的操作日志、文件变更记录、测试输出。这部分体量最大不可能全量注入我的做法是只保留最近 N 条更早的用脚本生成索引代理需要时再按需读取。提示三层结构的核心原则是“注入成本从低到高、恢复优先级从高到低”。身份层永远注入进度层每次恢复都注入证据层按需拉取。2.3 为什么选择脚本注入而不是框架内置机制现在不少代理框架都提供了某种“记忆”功能但我实测下来更倾向于自己用脚本控制。原因有三个。一是可控性我知道每一轮到底注入了什么、占了多少 token。二是可移植性脚本方案不绑定特定框架换一个代理实现照样能用。三是可调试性出问题的时候我可以直接看脚本输出而不是去猜框架内部做了什么。这套思路本质上就是把代理当成一个需要被“引导启动”的服务而不是一个需要被“喂养上下文”的聊天对象。视角一换很多设计就顺了。3. 核心细节拆解init.d 式状态恢复的四个关键环节3.1 身份锚点让代理每次醒来都知道自己是谁身份锚点是我踩坑最多的地方。最初我写了一大段项目介绍结果发现代理经常忽略其中的关键约束。后来我改成极简的键值对格式效果立刻好转。原因很简单结构化、短、无歧义的内容更容易被模型稳定遵循。我的identity.md大概长这样project_root: /workspace/myapp language: python 3.11 framework: fastapi test_command: pytest -x lint_command: ruff check . forbidden: 不要修改 migrations 目录下的历史文件 convention: 所有新接口必须有类型注解注意这里的forbidden和convention是自然语言但非常短。我试过写成长段落代理的遵循率明显下降。短句、命令式、一条一个约束这是我从多次失败里总结出来的经验。注入时机也很关键。我是在每次会话恢复的第一个动作就注入而不是等代理开始工作后再补。因为压缩往往发生在长会话的中段如果恢复后代理已经基于残缺记忆开始行动再注入就晚了。3.2 进度快照用结构化数据替代自然语言摘要进度层我坚持用 JSON 而不是自然语言。自然语言摘要看起来友好但有两个致命问题一是每次压缩后重新生成的摘要会有信息漂移二是代理很难精确判断“这一步到底做没做”。我的progress.json结构如下{ current_task: 为订单模块添加幂等重试, completed: [ 定位到 order_service.py 的 create_order 方法, 确认现有重试逻辑在 network 异常下会重复下单 ], in_progress: 编写幂等键生成逻辑, blocked: [], next_steps: [ 在 create_order 入口加幂等键校验, 补充并发场景的单元测试 ], last_updated: session-42 }这个文件由代理自己在关键节点更新我给它定的规则是每完成一个completed项、每改变一次in_progress、每发现一个blocked都必须立刻写回。写回动作本身很便宜但它是整个恢复机制的基石。注意last_updated字段我用来做版本校验。如果恢复时发现这个字段比预期旧说明代理可能漏写了脚本会提示我人工介入。3.3 证据索引让代理按需“回忆”而不是全量背诵证据层是最容易失控的部分。我一开始把完整日志全存下来结果恢复时一注入就爆窗口。后来改成索引加按需读取。具体做法是每次代理完成一个较大的操作单元脚本自动生成一条索引记录包含时间、操作类型、涉及文件、一句话结果。完整日志存在磁盘上索引里只放路径。代理需要细节时通过一个recall工具按索引路径读取。索引记录大概长这样{ id: op-017, type: file_edit, target: order_service.py, summary: 在 create_order 中插入幂等键校验分支, log_path: /logs/op-017.json, timestamp: session-42-step-8 }这样恢复时只需要注入最近 10 到 20 条索引token 占用极小但代理能清楚知道“我最近干了什么、细节去哪找”。3.4 恢复脚本幂等是唯一的要求把上面三层串起来的就是恢复脚本。它的逻辑非常简单读身份、读进度、读最近索引、拼成一段注入文本、交给代理。关键在于它必须幂等——跑一次和跑十次结果一样不会重复追加、不会污染状态。我用一个简单的 shell 脚本实现核心逻辑如下#!/bin/bash # restore_context.sh IDENTITY$(cat /workspace/state/identity.md) PROGRESS$(cat /workspace/state/progress.json) RECENT$(tail -n 20 /workspace/state/index.jsonl) cat EOF [SESSION RESTORE] $IDENTITY [CURRENT PROGRESS] $PROGRESS [RECENT OPERATIONS] $RECENT EOF这个脚本的输出会被代理框架作为恢复消息注入。我特意用了明确的段落标记方便代理区分不同层的信息。提示脚本里不要做任何有副作用的操作比如修改文件、调用外部接口。恢复脚本只读不写这是保证幂等的前提。4. 实操落地从零搭一套抗压缩的代理工作流4.1 目录结构与环境准备先说目录结构这套东西不复杂但位置要固定否则脚本找不到。/workspace/ state/ identity.md progress.json index.jsonl logs/ op-001.json op-002.json ... scripts/ restore_context.sh append_index.sh update_progress.shstate放三层记忆logs放完整证据scripts放工具脚本。我建议把整个state和logs目录纳入版本控制这样即使环境重建记忆也不会丢。环境准备上没什么特殊依赖一个 POSIX shell 加jq就够了。jq用来在脚本里安全地读写 JSON比手写字符串拼接可靠得多。4.2 身份文件的编写要点身份文件我建议控制在 200 token 以内。超过这个量压缩后重新注入的收益就开始下降。写法上遵循三个原则用键值对、用命令式短句、一条约束一行。我见过有人把整个 README 塞进身份文件结果代理反而抓不住重点。身份文件不是文档是约束清单。它回答的是“我是谁、我不能做什么”而不是“这个项目有多牛”。4.3 进度更新的触发时机进度更新最容易出问题的地方是“什么时候写”。写太频繁会打断代理工作流写太少又会在压缩时丢状态。我最后定下来的触发点是三个完成一个明确的子任务、切换当前任务、遇到阻塞。具体实现上我给代理定义了一个update_progress工具它接受completed、in_progress、blocked、next_steps四个字段内部用jq合并到progress.json。代理只需要在关键节点调用一次不需要自己拼 JSON。#!/bin/bash # update_progress.sh jq --arg task $1 --arg done $2 \ .current_task $task | .completed [$done] \ /workspace/state/progress.json /tmp/progress.json \ mv /tmp/progress.json /workspace/state/progress.json这个脚本用临时文件加mv的方式写入避免写入过程中被读取导致文件损坏。这是个小细节但在并发场景下很重要。4.4 索引追加与日志落盘索引追加我用一个类似的脚本每次代理完成一个操作单元就调用一次。日志落盘则是代理框架自己的职责我只需要保证日志路径和索引里的log_path一致。这里有个经验索引的summary字段一定要由代理自己写不要用脚本自动生成。因为代理知道这次操作的真实意图而脚本只能看到文件变更。我试过自动生成摘要结果恢复时代理经常误解自己之前干了什么。4.5 恢复流程的完整串联把所有环节串起来一次完整的恢复流程是这样的会话启动或压缩触发后框架调用restore_context.sh。脚本读取身份、进度、最近索引拼成恢复消息。恢复消息作为第一条消息注入代理。代理基于恢复消息确认当前状态继续工作。工作过程中代理在关键节点调用update_progress.sh和append_index.sh。整个流程没有任何魔法全是普通脚本和文件。但正是这种朴素让它在各种框架下都能跑起来。5. 常见问题与排查技巧实录5.1 恢复后代理仍然“失忆”怎么办这是最常见的问题。排查顺序我建议从下往上先看恢复脚本输出是否完整再看注入消息是否真的到达代理最后看代理是否遵循了注入内容。我遇到过一次脚本输出正常但代理完全无视。后来发现是注入消息被放在了对话历史的最前面而压缩机制把它当成旧消息又压了一遍。解决办法是把恢复消息标记为高优先级或者在框架层面配置为不参与压缩。不同框架配置方式不同但思路是一样的恢复消息必须享受特殊待遇。5.2 进度文件被写坏或字段缺失JSON 写坏通常是因为并发写入或者写入中断。我的应对是两点一是所有写入都用临时文件加mv二是恢复脚本里加一层校验字段缺失时用默认值兜底。jq .completed // [] | .in_progress // unknown \ /workspace/state/progress.json这行jq保证即使字段缺失脚本也不会崩而是给出一个安全的默认值。代理看到unknown会主动询问而不是基于错误状态继续。5.3 索引膨胀导致恢复变慢索引文件是追加写的时间长了会很大。我的做法是定期归档保留最近 100 条在主索引更早的移到index.archive.jsonl。恢复脚本只读主索引需要历史时再按需查归档。归档脚本很简单用head和tail切分即可。关键是归档动作要幂等重复跑不会丢数据。5.4 代理不主动更新进度有些代理比较“懒”你不催它就不写。我的解决办法是在身份文件里明确写一条约束“每完成一个子任务必须调用 update_progress”。同时在恢复消息里加一句提醒。实测下来明确约束加恢复提醒遵循率能到九成以上。如果还是不行可以在框架层面做强制代理每执行 N 次工具调用框架自动触发一次进度快照。这是兜底方案不优雅但有效。5.5 常见问题速查表现象可能原因排查动作解决方式恢复后代理重复已完成工作进度文件未更新检查 progress.json 的 completed 字段补写进度加强更新约束恢复消息被压缩注入位置或优先级不对查看框架压缩日志配置恢复消息不参与压缩进度文件损坏并发写入或中断检查文件是否为合法 JSON改用临时文件加 mv 写入索引过大恢复慢未归档历史索引查看 index.jsonl 行数定期归档只保留最近 100 条代理忽略身份约束身份文件过长或格式松散检查 identity.md 长度精简为键值对短句提示这张表我建议贴在项目 README 里出问题时按行排查比临时想快得多。6. 几个让我少走弯路的实操心得第一恢复机制要尽早搭不要等出问题再补。我一开始觉得“代理挺聪明的应该不会忘”结果第一次长会话就翻车。后来把恢复脚本前置到项目初始化阶段后面再没出过大问题。第二注入内容宁短勿长。我试过把完整进度历史全注入结果代理反而抓不住重点。现在只注入当前任务和最近几步效果稳定得多。信息密度比信息总量重要。第三所有状态文件都要能人工阅读和编辑。我用 JSON 和 Markdown 而不是二进制格式就是为了出问题时能直接打开看、直接改。代理的记忆应该是透明的不是黑盒。第四恢复脚本要能独立运行。我把它设计成不依赖代理框架、不依赖网络、不依赖任何外部服务。这样即使框架升级、环境迁移恢复逻辑照样能用。这种解耦在长期维护里价值极大。这套东西我用了几个月中间经历过好几次 20 万 token 级别的压缩代理的状态连续性基本没再出过问题。它不复杂但每一处设计都是从实际翻车里抠出来的。如果你也在做长会话代理建议从身份锚点和进度快照这两个最小可用部分开始跑通了再往上加索引和归档。先让它不失忆再让它记得好。