news 2026/10/28 5:31:39

AI编码代理上下文压缩后如何接得上:基于430条记录的长期记忆层设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI编码代理上下文压缩后如何接得上:基于430条记录的长期记忆层设计

1. 这个实验到底在折腾什么

先把这个标题拆开看。上下文压缩,指的是 AI 编码代理在长会话里,当对话历史、代码文件、工具调用结果累积到超出模型上下文窗口时,系统会自动把旧内容做摘要、截断或丢弃,腾出空间给新内容。AI 编码代理,就是像 Codex 这类能自己读文件、改代码、跑命令、看报错的自动化编程助手。问题就出在这两者的交界处:压缩一发生,代理就"失忆"了,之前读过的文件路径、改过的函数名、跑通的命令,全没了,于是它开始重复劳动、改错文件、甚至把已经修好的 bug 又改回去。

这个实验用 10 天时间、430 条公开记录,专门盯着"压缩之后代理怎么接得上"这件事。所谓"接得上",就是压缩发生后,代理还能不能延续之前的任务状态,而不是从零开始重新摸索。适合谁看?如果你在用 Codex 这类 CLI 编码代理跑长任务,或者你在自己搭 agent 框架、被上下文窗口卡过脖子,这篇就是给你写的。我踩过的坑基本都在下面了。

先说结论方向:单纯靠"压缩得更好"解决不了问题,真正管用的是在压缩之外维护一份独立于对话历史的长期记忆,让代理每次都能从这份记忆里把关键状态捞回来。这个思路在社区里常被叫做 long_mem 或者 engram 一类的记忆层设计。下面我把整个实验的设计、实现、踩坑和排查过程完整拆一遍。

2. 为什么压缩之后代理会"断片"

2.1 上下文窗口的硬约束和压缩的必然性

任何模型的上下文窗口都是有限的。你让代理去重构一个中型项目,它要读的文件、要看的报错、要跑的测试输出,加起来轻松超过窗口上限。这时候系统只有几条路:要么截断最早的对话,要么把中间一段做摘要,要么按重要性丢弃。不管哪条路,本质都是信息有损。

我实测下来,最坑的不是"丢了多少 token",而是丢的东西恰好是任务状态。比如代理在第 3 轮读到了src/utils/date.ts里有个formatDate函数,第 40 轮它要改这个函数,但中间压缩把第 3 轮的内容摘要成了一句"读取了若干工具文件"。摘要里没有函数名、没有路径,代理就只能重新去搜,搜的时候又可能搜到同名的另一个文件,直接改错地方。

2.2 代理"断片"的三种典型表现

我把 430 条记录里代理出问题的模式归了三类,这个分类后面排查问题很好用:

表现类型具体症状根因
重复劳动反复读同一个文件、反复跑同一条命令压缩丢了"已做过"的记录
状态错乱改错文件、用错变量名、回退已修复的 bug压缩丢了"当前进度"和"关键标识"
目标漂移忘了原始需求,开始做无关的事压缩丢了"任务目标"和"约束条件"

这三类里,状态错乱最危险,因为它会直接破坏代码。重复劳动只是浪费 token,目标漂移你还能靠人工拉回来,但状态错乱可能让你花半小时 review 才发现代理把对的改成了错的。

2.3 为什么"压缩得更聪明"是条死路

一开始我也想过,能不能让压缩算法更聪明,只保留重要内容。试了两天就放弃了。原因是:你无法在压缩的那一刻预知未来哪条信息会被用到。第 3 轮读到的那个函数名,在第 40 轮才用得上,压缩发生在第 20 轮,它怎么知道要留?

这就像你整理书桌,把"暂时用不到"的东西收进箱子,结果三天后要找的东西正好在箱子里。压缩算法再聪明,也是在信息不完整的情况下做决策。所以正确的方向不是"压缩得更好",而是把关键状态从对话历史里剥离出来,单独存一份,压缩对话历史时不动它。这就是 long_mem / engram 这类记忆层的核心思想。

3. 记忆层怎么设计才接得上

3.1 记忆层要存什么:三类关键状态

不是所有东西都值得进记忆层,存太多等于没存。我最后收敛到三类:

  • 任务锚点:原始需求、验收标准、硬约束(比如"不许改数据库 schema")。这类东西一旦确定就不该变,压缩多少次都得留着。
  • 实体索引:代理操作过的文件路径、函数名、类名、关键变量。这是防止"改错文件"的核心。
  • 进度快照:当前做到哪一步、哪些已验证通过、哪些还待办。

这三类里,实体索引是实验里收益最大的。因为代理 80% 的"断片"事故都跟"找不到之前那个东西"有关。把实体索引单独存一份,代理每次压缩后先读这份索引,基本就能接上。

3.2 记忆层的存储结构设计

我用的是最简单的键值 + 结构化条目混合方案,不搞复杂向量库,因为实测下来向量检索在"精确找回某个函数名"这种场景上不如直接结构化查询稳。结构大概长这样:

{ "task_anchor": { "goal": "给用户模块加软删除", "constraints": ["不改 schema", "兼容旧数据"], "acceptance": "旧数据查询不受影响" }, "entities": [ {"type": "file", "path": "src/user/service.ts", "note": "核心逻辑在这"}, {"type": "function", "name": "softDeleteUser", "file": "src/user/service.ts"}, {"type": "table", "name": "users", "note": "加了 deleted_at 字段"} ], "progress": { "done": ["schema 迁移", "service 层实现"], "verified": ["单元测试通过"], "todo": ["controller 层接入", "补集成测试"] } }

这个结构的好处是代理可以精确查询,比如"我之前改过哪个文件",直接读 entities 里 type=file 的条目,不用做语义检索。语义检索留给"我大概想找跟日期相关的逻辑"这种模糊需求。

3.3 记忆的写入时机:别等压缩才写

这是实验里最关键的一个设计决策。我一开始是"压缩发生时,顺便把要保留的写进记忆层"。结果发现压缩那一刻代理已经在丢信息了,写进去的东西本身就不全。

后来改成边做边写:代理每完成一个原子操作(读了一个文件、改了一个函数、跑通一条命令),就立刻往记忆层追加一条。这样压缩发生时,记忆层早就存好了,压缩爱怎么压怎么压,不影响记忆层。这个改动让"接得上"的成功率从大概六成提到了九成以上。

提示:写入要幂等。同一个文件被读三次,记忆层里应该还是一条(或更新 note),不是三条。否则记忆层自己会膨胀成新的"上下文窗口"。

4. 实操:把记忆层接进编码代理

4.1 整体流程和关键节点

整个链路是这样的:代理执行操作 → 操作结果写入记忆层 → 上下文接近上限 → 触发压缩 → 压缩后代理先读记忆层 → 继续任务。关键节点有两个:写入点和恢复点。

写入点我放在工具调用的返回处理里。代理每次调用"读文件""改文件""跑命令"这类工具,返回结果先过一遍记忆层写入逻辑,再进对话历史。恢复点放在压缩之后的第一条系统消息里,强制代理先读记忆层再干活。

4.2 写入逻辑的实现要点

写入逻辑的核心是从工具调用结果里抽取实体。读文件就抽路径,改文件就抽路径 + 改动的函数/类名,跑命令就抽命令本身和结果状态。抽取不用太复杂,正则 + 简单 AST 就够,别上大模型,太慢。

def extract_entities(tool_name, args, result): entities = [] if tool_name == "read_file": entities.append({"type": "file", "path": args["path"]}) elif tool_name == "edit_file": entities.append({"type": "file", "path": args["path"]}) # 从 diff 里抽函数名 for fn in parse_changed_functions(result["diff"]): entities.append({"type": "function", "name": fn, "file": args["path"]}) elif tool_name == "run_command": entities.append({"type": "command", "cmd": args["cmd"], "status": "ok" if result["exit"] == 0 else "fail"}) return entities

这里有个坑:edit_file 的 diff 解析别写太复杂。我一开始想精确解析出改了哪几行、哪个 AST 节点,结果各种边界情况处理到崩溃。后来退一步,只抽"这个文件被改过"和"diff 里出现的函数名",够用了。记忆层是辅助,不是精确账本。

4.3 恢复逻辑:压缩后怎么让代理"想起来"

恢复逻辑比写入更讲究。压缩后不能简单把整个记忆层塞回上下文,那样等于没压缩。我的做法是按当前任务阶段做筛选:

  1. 任务锚点永远全量注入,这部分很小,几十个 token。
  2. 实体索引按"最近操作过"和"与当前 todo 相关"两个维度筛,只注入 top N。
  3. 进度快照只注入 done 和 todo,verified 的细节可以省。
def build_recovery_context(memory, current_todo, top_n=15): ctx = {"task_anchor": memory["task_anchor"]} # 实体按最近使用 + 相关性排序 entities = rank_entities(memory["entities"], current_todo) ctx["entities"] = entities[:top_n] ctx["progress"] = { "done": memory["progress"]["done"], "todo": memory["progress"]["todo"] } return ctx

实测下来 top_n 取 15 左右比较平衡,太少接不上,太多又挤占上下文。这个值跟你的模型窗口大小有关,窗口大可以放宽到 25。

4.4 一个完整的恢复消息长什么样

压缩后注入给代理的系统消息大概是这样,我直接贴一段脱敏后的真实记录:

[记忆恢复] 你正在执行的任务:给用户模块加软删除。 硬约束:不改 schema,兼容旧数据。 你已操作过的关键文件: - src/user/service.ts(核心逻辑,已改) - src/user/controller.ts(待接入) 已完成的步骤:schema 迁移、service 层实现、单元测试通过。 待办:controller 层接入、补集成测试。 请基于以上状态继续,不要重复已完成的工作。

这条消息大概 200 token,但能让代理瞬间回到状态。对比没有这条消息的情况,代理平均要多花 3 到 5 轮才能重新摸清状况,而且经常摸错。

5. 430 条记录里踩出来的坑

5.1 记忆层自己膨胀的问题

前面提过写入要幂等,但实际做的时候还是踩了坑。代理有时候会用不同的路径写法指向同一个文件,比如./src/user/service.ts和src/user/service.ts,甚至绝对路径。如果不做归一化,记忆层里同一个文件会有好几条,恢复时注入一堆重复内容。

解决办法是写入前做路径归一化:统一转成相对于项目根目录的路径,去掉./,解析软链接。这个处理放在写入逻辑最前面,一行代码的事,但不做就是灾难。

5.2 代理"不信任"记忆层

这个坑很隐蔽。有几次代理明明收到了记忆恢复消息,但它还是重新去读了一遍文件。原因是它在对话历史里找不到"我读过这个文件"的证据,就本能地重新确认。这不算错,但浪费 token。

我的处理是在恢复消息里加一句明确的指令:"以上状态已由系统确认,无需重新验证。"加了这句之后,重复读取明显减少。代理对系统消息的信任度高于对自身记忆的信任度,这个特性要利用起来。

5.3 压缩触发时机和记忆写入的竞态

有几次出现"记忆层里没有某条记录,但代理明明做过这个操作"。排查发现是压缩和写入的竞态:代理刚发起一个操作,压缩恰好在这时触发,操作结果还没写进记忆层就被压缩截断了。

解决办法是把写入做成同步阻塞:工具调用返回后,先同步写完记忆层,再允许压缩检查。这会稍微增加一点延迟,但换来的是记忆的完整性。异步写入看着优雅,实际会丢数据。

5.4 常见问题速查表

问题现象可能原因排查方向解决
压缩后代理重复读文件恢复消息没注入实体索引检查 build_recovery_context 输出确认 entities 非空且 top_n 够大
代理改错文件实体索引里路径不唯一查记忆层是否有重复路径加路径归一化
记忆层越来越大写入不幂等统计 entities 条目数增长写入前按 key 去重
恢复后代理仍迷茫任务锚点没注入检查 task_anchor 字段锚点必须全量注入
记忆层缺记录写入与压缩竞态看日志时间戳写入改同步阻塞

5.5 几条不那么显然的经验

第一,记忆层不要存代码内容本身。存路径和函数名就够了,代码让代理自己去读。存代码内容会让记忆层迅速膨胀,而且代码一改就过期,反而误导代理。

第二,恢复消息要放在对话历史的最前面,不是最后面。放最后面代理容易忽略,放最前面它每次生成都会看到。这个细节我调了两天才发现。

第三,给记忆层加个"最后更新时间"。代理有时候会基于过期状态干活,比如记忆层说"controller 待接入",但其实上一轮已经接了。加时间戳后,代理会优先信任更新的记录。

6. 这套方案还能怎么扩展

实验做完,我觉得这套记忆层思路不止能用在编码代理上。任何长任务型的 agent,只要涉及上下文压缩,都能套这个模式:把任务锚点、实体索引、进度快照从对话历史里剥离出来,独立维护,压缩后按需恢复。

具体到编码场景,还能往下做的有几个方向。一是跨会话记忆,现在记忆层是单次会话内的,如果代理重启,记忆就没了。把记忆层持久化到磁盘,下次会话能接着用,这对长期项目很有价值。二是记忆的自动过期,有些实体操作完就再也不用了,一直留着占地方,可以按"多久没被引用"做淘汰。三是多代理共享记忆,如果同时跑几个代理分工干活,共享一份记忆层能避免它们互相踩脚。

不过这几点我还没在 430 条记录里充分验证,属于下一步想试的方向。就目前这套单会话方案,10 天跑下来,代理在长任务里的"断片"率从最初的接近四成降到了不到一成,剩下的主要是模型自身能力边界导致的问题,跟记忆层无关了。

最后分享一个我调参时的小技巧:恢复消息的 top_n 不要拍脑袋定,用记录反推。把 430 条记录里"恢复后仍需重新读取"的案例挑出来,看当时注入了多少实体、缺了哪类,就能算出你这个项目场景下的合理值。我这边算下来是 15,但你的项目文件多、依赖复杂的话,可能要 20 到 25。这个值跟项目规模强相关,没有通用答案。

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

Java在AI开发中的优势与JBoltAI框架实战

1. Java在AI领域的独特优势解析作为一门拥有28年历史的编程语言,Java在人工智能领域展现出令人惊讶的适应性。我最初接触AI开发时也疑惑:Python不是更主流吗?但经过多个项目的实战验证,发现Java在以下场景具有不可替代性&#xff…

作者头像 李华
网站建设 2026/10/28 5:30:37

impeccable用法全解析:从词源到实战,掌握这个零差错高级词汇

1. “impeccable”不是“完美”的翻版:一个自带“零差错”气场的词我第一次注意到这个词,是在一份简历上。候选人在自我评价那一栏写了一句“I have impeccable attention to detail”。作为长期做英文内容审校和稿件改写的人,我一眼看到这个…

作者头像 李华
网站建设 2026/10/28 5:29:37

Codex云端开发环境实战:AI编程与云IDE工作流全解析

1. 从本地终端到云端工作台:Codex 这次到底动了谁的奶酪第一次看到 Codex 云端开发环境上线的消息,我正蹲在本地终端里跟一个依赖冲突死磕。那台笔记本风扇狂转,编译到一半内存爆掉,我盯着满屏红色报错,突然意识到一件…

作者头像 李华
网站建设 2026/10/28 5:29:37

使用FFmpeg批量获取MP3文件元数据的技术指南

1. 项目概述在日常音频处理工作中,我们经常需要批量获取某个目录下所有MP3文件的详细信息。作为一款强大的多媒体处理工具,FFmpeg能够高效完成这项任务。本文将详细介绍如何通过FFmpeg命令行工具,快速查看指定目录下所有MP3音频文件的元数据和…

作者头像 李华
网站建设 2026/10/28 5:29:37

编程范式进化:从代码实现到系统设计的思维转变

1. 从代码到思考:编程范式的进化最近在开发者社区看到Karpathy那句"我已经不写代码了,你还在打字?"的发言,让我不禁思考:当AI领域的顶尖专家开始减少直接编码,这意味着什么?作为从业十…

作者头像 李华
网站建设 2026/10/28 5:29:36

云端开发环境如何重塑编码工作流:Codex 实操与 AI 辅助编码边界

1. 云端开发环境到底改变了什么1.1 从“装环境”到“开箱即用”的转变我最早接触 Codex 这类云端开发环境的时候,第一反应是:这不就是把 IDE 搬到浏览器里吗?但真正用了一段时间之后才发现,它改变的根本不是工具形态,而…

作者头像 李华