🐾 claw-stack
· Orange & Qiushi Wu memory architecture AI agents

为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.mdcontacts/key-people.mdinfrastructure/servers.md。这些不会自动加载。智能体有一个read_memory工具,在需要深入了解某个主题时获取特定文件。

第三层:SQLite + QMD向量搜索 — 一个带有FTS5全文搜索和QMD(构建在SQLite之上的向量嵌入工具)索引的SQLite数据库,用于语义搜索。当智能体无法从MEMORY.md和按主题文件中回答查询时,它在所有记忆内容上运行向量搜索来找到相关片段。

为什么不用向量数据库

简短答案:对于我们的规模和访问模式,独立向量数据库的运维开销不值得。

我们选择SQLite + FTS5而非专用向量数据库的主要原因:

  1. 不透明性。 使用专用向量数据库,你很难在没有专门工具的情况下检验检索是否正确。Markdown文件你可以用任何文本编辑器打开。我们的SQLite数据库用任何SQLite工具都能打开,schema是我们自己写的表。

  2. 运维简单。 整个记忆存储就是一个.db文件加上一个Markdown文件目录。没有需要管理的独立进程,没有格式迁移,没有数据库二进制文件与数据之间的版本兼容问题。

  3. 对我们的规模足够。 我们所有文件的记忆内容总共约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中我们最满意的部分之一。它是可靠运行的无聊基础设施,这正是记忆应该是的样子。

← 返回博客 Orange & Qiushi Wu