
一个重构做到一半,Claude Code 的额度用完了,于是在同一个目录里换成 Codex 接着干。Codex 启动后的第一句话通常是:请介绍一下这个项目。
这时候被忘掉的东西其实有三样,而且它们完全不是一回事。第一样是刚才那半小时的对话,试过哪些方案、哪条路走死了、还差哪个函数没改。第二样是这个仓库本身的构造,WAL 是怎么轮转的、鉴权中间件挂在哪一层。第三样是任务队列,哪件事做完了、哪件事被另一件事挡着、谁已经在做了。
大多数所谓 agent 记忆产品,把这三样东西塞进同一个容器里,然后统称为「记忆」。容器通常是一个向量库。结果就是三件事都没真正解决:交接时拿不到关于「刚才」的准确记录,查代码时捞回一堆语义相近但没用的片段,多个 agent 并行时不知道谁在做哪件事。
这几天的 GitHub 趋势里恰好凑齐了三种不同的解法,值得对照着看。它们做的其实是三个不同的问题。
一、会话交接:一份有主的记录,比一段摘要有用
ai-memory 处理的是第一种遗忘。它的设计里有两个决定,直接决定了它在并发场景下的表现。
第一个是交接被定义成协议,而不是摘要。每次交接有明确的 owner,并且只能被领取一次。这个细节听起来像工程洁癖,实际上是多人多机器共用一份记忆时的核心问题:如果交接只是一段自然语言摘要,两个 agent 同时读到它,就会同时开工,然后互相覆盖。加上所有权和一次性领取,交接才变成可以并发使用的东西。
第二个是它不把记忆存在数据库里。源文件是一个 git 版本管理的 markdown wiki,SQLite 里的 FTS5、实体、图邻居、可选向量都只是派生索引,删掉可以重建。这条选择带来几个直接后果:可以用 grep 查、可以用 Obsidian 打开、可以手工改、可以 rsync 到别处。作者在 README 里写得很直白:没有需要维护的向量库,没有锁进二进制块里的内容。
默认路径不需要任何 API 调用。捕获、检索、交接全靠 hooks 和全文索引,没有 key 也能跑。加 LLM 是可选项,用来把一天的观察整理成可读的页面。
它还有一个我认为最容易被忽略的机制:记忆会按时间衰减,而用过的记忆衰减得更慢。打开页面、搜索命中、从链接点进去,都算使用。冷掉的会话笔记不会被直接丢掉,而是压缩成耐久的事实,比如文件路径、错误码、做过的决定。整套动作默认关着,因为重写和删除内容风险太高,得先过一轮召回率评测才敢开。
这句话背后的判断很清醒:自动压缩记忆是危险操作,可以设计,但不该默认打开。
二、代码知识:检索粒度决定了成本
第二种遗忘是「知识不在上下文里」。传统做法是 grep,agent 拿到的是文件名和行号,然后自己把整个文件读进上下文。这中间的成本经常被低估。
xerj
的做法是把任意目录自动索引成可查询的数据集,代码通过 tree-sitter 保留符号和行号,查询返回的是 passage 加 file:line,不是一份文件清单。它自己做的对照实验里,三个 arm 各跑 16 次,都是真实的 claude -p token 计数:
| 方式 | 输出 token | 成本 | 解出 |
|---|---|---|---|
| 纯靠模型记忆 | 260,916 | $11.18 | 11/16 |
| grep 驱动 | 26,477 | $3.27 | 16/16 |
| xerj 检索 | 9,982 | $1.58 | 16/16 |
比 grep 少 2.7 倍输出 token,同样的解出率。还有一个数字更说明问题:它测到过单个语料上 grep 路线要读进 1.06M 输入 token 的情况。
grep 的毛病不在于慢。它告诉你该去看哪个文件,但恢复上下文的活儿还得 agent 自己干,而这活儿是按文件大小计费的。passage 级检索把这一步删掉了。
但这份数据有个前提,作者自己写在了案例研究里:收益取决于模型有没有背过。对私有代码、内部系统、冷门库、以及训练截止之后出现的东西,优势明显;对热门公开库,效果中性甚至有害,因为模型本来就记得,检索反而多绕一圈。看到「少 2.7 倍 token」就去部署,多半会在自己的主力框架上失望。
顺带一提,xerj 默认的 embedder 是词法的、离线的。神经嵌入要另外下 90MB 左右才启用。也就是说,在它看来向量搜索是加分项,不是地基。这和很多人对「AI 记忆」的直觉是反的。
同一层问题还有另一种取舍。engram 是一个 Go 二进制加一个 SQLite 文件,通过 MCP 暴露二十多个工具,支持 14 种 agent 的接入。它不引入新服务、不需要 Node 或 Docker,记忆默认只留在本机,需要跨机器时用 git 导出分块。如果你的诉求是「别为了记忆再养一套基础设施」,这个方向更省事。
三、任务状态:它是图,不是清单
第三种遗忘最容易被当成小事,就是「干到哪了」。
大多数人的做法是在仓库里放一个 markdown 清单,上面写几条待办。这个做法在单 agent 单分支下能用,一旦并行就开始烂:两个 agent 同时勾同一行,git 冲突;四十个任务里哪些能开工、哪些被挡着,只能靠人读;做完的任务越堆越多,清单本身变成上下文负担。
beads 把这一层当成正经的分布式系统来做,底层是 Dolt,一个带版本控制的 SQL 数据库,支持单元格级合并。
- 任务 ID 用内容哈希,形如
bd-a1b2,多分支并行创建不会撞号。 bd ready只列出没有未完成前置依赖的任务,谁能开工是算出来的,不是人读出来的。bd update <id> --claim是原子领取,把负责人和状态一次改掉。- 关闭的任务会被压缩,README 里管这个叫语义化的记忆衰减,目的是别让历史任务撑爆上下文。
这里的关键差别是数据形状。任务状态的核心结构是「谁挡着谁」,是一张有向图;记忆事实的核心结构是「关于什么」,是一堆可检索的文本。用 markdown 存图,问题不在格式难看,在于没有合并语义,也没有依赖推理。同理,用向量库存待办,也查不出「现在能干什么」。
四、把窗口开大一点,救不了这件事
有一种流行说法:上下文窗口都上百万了,记忆问题会自然消失。
Chroma 在 2025 年 7 月那份 context rot 报告里测了 18 个模型,包括当时的 GPT-4.1、Claude 4、Gemini 2.5 和 Qwen3。结论是模型对上下文的利用并不均匀,输入越长,表现越不稳定,而且这种下降是非均匀的,不同任务、不同干扰类型差别很大。一个反直觉的发现是:同样的 haystack,打乱顺序后的表现反而好于逻辑结构完整的版本。
2026 年那篇关于长程搜索的论文(arXiv 2606.29718)补了另一半:模型经常在上下文远没用完的时候就放弃,或者给出一个不确定的错误答案,作者把这个现象叫提前终止。也就是说,瓶颈不是窗口大小,是模型在窗口里怎么工作。
窗口变大只解决「能不能放进去」,不解决「放进去之后还认不认得」。
五、几个别急着忽略的代价
能重建比能检索更重要。 ai-memory 把 markdown 当源、索引当派生物,这个顺序一旦反过来(只有向量库,没有原始文本),你就失去了审计、手工修正和迁移的能力。判断一个记忆方案,先问一句:索引删了能不能重建。
自动压缩需要评测兜底。 衰减、去重、合并冷笔记,这些都会改写内容。ai-memory 把这套默认关掉、保留 git 版本链和 restore-page,beads 用 Dolt 的分支和合并来兜底,思路是一样的:宁可慢一点,也不要不可逆。
这层东西现在全挂在别人的钩子上。 上面几个项目都靠生命周期 hooks 或 MCP 接进 Claude Code、Codex、Cursor 这些 harness。规范一变,或者官方把同类功能收进主线,第三方就得跟着改。今天趋势榜上这一整片项目都在同一个风险里。
并发问题比检索问题更早暴露。 一个人用一个 agent 的时候,记忆系统只要查得准就行。一旦两个 agent 同时跑、或者两个人共用一个服务器,所有权、租约、一次性领取这些机制就变成刚需。很多记忆工具还没走到这一步。
结语
把「agent 的记忆」当成一个功能来做,最后多半会做成一个向量库加一层摘要。把它拆成三个问题来做,结构就清楚了:会话交接要有主、可领取;代码知识要按 passage 建可重建的索引;任务状态要存成能算依赖、能合并的图。
三种遗忘对应三套结构。共同点只有一个:源数据得是你自己能读、能改、能备份的文本,索引随时可以扔掉重来。这个标准比任何 benchmark 都实用。
参考来源
- ai-memory: Long-term memory for AI coding agents
- xerj: local AI search, autoindex ,含 reference coding 案例研究
- engram: persistent memory for AI coding agents
- beads: distributed graph issue tracker for AI agents
- Context Rot: How Increasing Input Tokens Impacts LLM Performance, Chroma
- Diagnosing and Mitigating Context Rot in Long-horizon Search, arXiv 2606.29718