为AI智能体构建持久记忆系统
我们如何构建Claw-Stack记忆系统:作为session启动索引的MEMORY.md、按主题划分的Markdown文件、SQLite FTS5 + QMD向量搜索,以及关于MEMORY.md膨胀的惨痛教训。
给AI智能体提供记忆的标准建议是:使用向量数据库。存储嵌入,做相似度搜索,检索相关片段。对于查询模式是”找与这个问题相似的文档”的检索增强生成系统来说,这是好建议。对于需要记住上周二做了什么和六周前关于项目X做了什么决定的智能体来说,这不一定是正确答案。
以下是我们如何构建Claw-Stack记忆系统的,它为什么是现在这个样子,以及我们一路上学到了什么。
无状态智能体的问题
每个Claude session都是全新开始的。除非你在开头显式注入上下文,否则模型对之前session没有任何记忆。对于你只交谈一次的研究助手来说,这没问题。对于每天运行、积累关于你项目的知识、需要在数周内保持一致行为的自主智能体来说,这是一个根本性问题。
天真的解决方案是把所有东西都倒进系统提示。这在积累了几百KB上下文之前是有效的,之后会发生两件事:你开始触碰上下文限制,模型利用很长上下文开头部分的能力退化。智能体开始忽略你三个月前告诉它的事情,因为它们距离当前交互太远了。
我们需要一个具备两个特性的记忆系统:它必须是选择性的(只注入与当前session相关的内容),而且必须是人类可读的(我们需要能够审计、编辑和纠正智能体的信念)。
三层架构
记忆系统有三层:
第一层:MEMORY.md — 一个在每个session开始时加载的紧凑索引。这是一个结构化的Markdown文件,包含近期活动、活跃项目、关键联系人和基础设施备注等部分。它被刻意保持简短——系统强制执行字节大小上限——这样在有大型任务描述的session中不会消耗太多上下文预算。
第二层:按主题文件 — memory/目录中更长的Markdown文件,深入探讨特定主题。projects/claw-stack.md、contacts/key-people.md、infrastructure/servers.md。这些不会自动加载。智能体有一个read_memory工具,在需要深入了解某个主题时获取特定文件。
第三层:SQLite + QMD向量搜索 — 一个带有FTS5全文搜索和QMD(构建在SQLite之上的向量嵌入工具)索引的SQLite数据库,用于语义搜索。当智能体无法从MEMORY.md和按主题文件中回答查询时,它在所有记忆内容上运行向量搜索来找到相关片段。
为什么不用向量数据库
简短答案:对于我们的规模和访问模式,独立向量数据库的运维开销不值得。
我们选择SQLite + FTS5而非专用向量数据库的主要原因:
-
不透明性。 使用专用向量数据库,你很难在没有专门工具的情况下检验检索是否正确。Markdown文件你可以用任何文本编辑器打开。我们的SQLite数据库用任何SQLite工具都能打开,schema是我们自己写的表。
-
运维简单。 整个记忆存储就是一个
.db文件加上一个Markdown文件目录。没有需要管理的独立进程,没有格式迁移,没有数据库二进制文件与数据之间的版本兼容问题。 -
对我们的规模足够。 我们所有文件的记忆内容总共约5万词。SQLite FTS5能在毫秒内完成全文搜索。向量相似度明显优于关键词搜索的情况真实存在但足够罕见,运维开销不值得。
QMD(向量搜索层)构建在SQLite之上。嵌入使用小型量化模型在本地计算,与文本一起存储在SQLite表中。重新索引只需几秒钟。整个记忆存储就是一个.db文件加上一个Markdown文件目录。
整理流水线
记忆不会自我管理。每个session结束后,整理流水线运行:
原始session文件
→ 扫描memory/*.md(MD5哈希检查,跳过未更改的)
→ 按类别提取事实(项目更新、决策、联系人)
→ 对现有记忆去重
→ 写入更新后的按主题文件
→ 重建SQLite FTS5索引
→ 更新MEMORY.md索引
提取步骤使用LLM(Gemini为主,Claude Haiku为备用):读取session记录并以特定格式生成结构化笔记。去重步骤基于规则:如果新事实是现有条目的子字符串,跳过;如果与现有条目矛盾,标记为需要人工审核。
流水线按cron计划运行(在活跃工作期间每隔几小时运行一次),而不是在每个session后立即运行。这批量处理了处理成本,避免写入会立即被后续session覆盖的记忆文件。
MEMORY.md膨胀问题
最惨痛的教训是关于MEMORY.md的增长。
我们最初没有对MEMORY.md长度设限。整理器不断向其追加内容。六周后,MEMORY.md超过了700行。这带来了可预见的后果:session启动消耗了大部分上下文预算,然后才能加载任何实际任务内容,模型在综合一个几百行的简报的同时还要做有用的工作,明显力不从心。
解决方案是改变整理器的行为并强制执行大小上限。整理器不再直接将新事实追加到MEMORY.md,而是将它们写入按主题文件,并用指针更新MEMORY.md——一行说”请参见projects/claw-stack.md获取当前状态”,而不是将完整状态嵌入MEMORY.md。系统现在强制执行MEMORY.md的字节大小限制,防止失控增长。
这迫使我们重新思考MEMORY.md的用途。它不是智能体所知一切的摘要。它是一个session简报——在session开始时定向智能体所需的最小上下文。超出这个范围的内容按需获取。
重构之后,session启动明显变快,模型更好地利用了拥有的上下文。保持MEMORY.md真正紧凑是一种持续的纪律——我们发现严格的行数限制不如字节大小限制有效,即便如此,也需要整理器积极使用指针而非内联内容。
记忆作为人类可读的状态
这个系统背后的设计哲学是:智能体记忆应该是人类可读且人类可编辑的。这是我们刻意施加的约束。
当智能体产生错误的信念时——偶尔确实会发生——我们能在Markdown文件中找到错误条目,编辑它,修复在下一个session中生效。使用向量数据库,纠正错误信念需要知道要更新哪个嵌入,删除它,写一个新的,并可能使缓存的检索失效。使用Markdown文件,你打开文件改文本就行了。
这也让审计变得简单直接。在信任自主智能体代你做决策之前,你需要能够读取它的信念并验证其正确性。整个记忆系统就是一个Markdown文件目录。任何文本编辑器都能用。
代价是格式是固定的。我们的记忆文件遵循整理器知道如何解析和更新的特定schema。如果你想添加新的记忆类别,你需要同时更新文件schema和整理器。对于只有一个操作员的研究项目来说,这是可以接受的。对于有很多智能体和很多记忆类型的生产系统来说,你会想要更灵活的东西。
如果重来我们会怎么做
如果从头开始:
从第一天就使用更小的MEMORY.md。 我们浪费了数周清理本可以通过初始大小上限避免的膨胀。对于日常使用的助手来说,带有基于指针的条目的字节大小限制比固定行数是更好的目标。
更早区分情景记忆和语义记忆。 “周二session里发生了什么”(情景记忆)和”Claw-Stack架构是什么”(语义记忆)是不同类型的记忆,受益于不同的检索策略。我们最初把它们混在一起,后来花时间将其分离。
先构建审计工具。 维护智能体记忆系统最难的部分不是索引或检索——而是知道记忆何时出错了。我们构建审计视图(一个脚本,显示智能体关于某个主题的信念)太晚了。它应该是我们写的第一个工具。
记忆系统是Claw-Stack中我们最满意的部分之一。它是可靠运行的无聊基础设施,这正是记忆应该是的样子。