OpenClaw三级记忆系统实现揭秘:向量数据库+关系型数据库的混合存储方案

举报
霍格沃兹测试开发学社 发表于 2026/03/10 10:30:25 2026/03/10
【摘要】 如果你用过OpenClaw,一定被它的“记忆力”震撼过——几个月前随口提过的偏好,下次聊天它还记得;上周讨论过的项目细节,这周接着聊它能无缝衔接。这种能力不是魔法,是一套精心设计的三级记忆系统在背后支撑。今天我们就从源码层面,彻底拆解这套架构:它怎么存、怎么查、怎么在“记得住”和“不烧钱”之间找到平衡。一、为什么需要三级记忆?在OpenClaw之前,AI助手的记忆通常只有一层:会话上下文。你...
如果你用过OpenClaw,一定被它的“记忆力”震撼过——几个月前随口提过的偏好,下次聊天它还记得;上周讨论过的项目细节,这周接着聊它能无缝衔接。

这种能力不是魔法,是一套精心设计的三级记忆系统在背后支撑。今天我们就从源码层面,彻底拆解这套架构:它怎么存、怎么查、怎么在“记得住”和“不烧钱”之间找到平衡。

一、为什么需要三级记忆?

在OpenClaw之前,AI助手的记忆通常只有一层:会话上下文。你今天跟它聊完,明天它就不认识你了。OpenClaw的设计者想得很明白:人的记忆是分层的,AI也应该这样

打开OpenClaw的工作区,你会看到这样的结构:

~/.openclaw/workspace/
├── MEMORY.md              # 长期记忆:偏好、决策、持久事实
├── memory/
│   ├── 2026-03-05.md      # 今日日志(短期记忆)
│   ├── 2026-03-04.md      # 昨日日志
│   └── ...                # 历史日志
├── sessions/              # 会话存档(近端记忆)
├── USER.md                # 用户身份
└── SOUL.md                # Agent人格设定

这套结构对应着三个层次:

  • 短期记忆(Daily Log):每天一个append-only的日志文件,记录当天发生的事。新会话启动时,系统会自动加载“今天+昨天”的日志,让Agent拥有最近48小时的连续感。

  • 近端记忆(Sessions):完整的会话存档。当对话太长被压缩时,关键信息会被冲刷到这里。

  • 长期记忆(MEMORY.md):经过筛选的持久知识——你的技术栈偏好、项目决策、常用工具链。这些信息会在每次私聊时自动加载。

这套分层设计的核心思想是:模型不需要知道所有事,只需要知道此刻最相关的事

二、存储层:SQLite + 向量,关系型与非结构化的联姻

但光有Markdown文件不够——要在大堆文本里快速找到相关内容,必须建索引。这就是SQLite登场的时刻。

每个Agent对应一个独立的SQLite数据库,位于~/.openclaw/memory/{agentId}.sqlite。看表结构就知道设计者的用心:

-- 核心表:记录文件元数据
CREATETABLE files (
    idINTEGER PRIMARY KEY,
    pathTEXTUNIQUE,
    mtime INTEGER,           -- 修改时间,用于增量索引
    sizeINTEGER,
    hashTEXT                 -- 内容哈希,去重用
);

-- 核心表:存储文本块
CREATETABLE chunks (
    idINTEGER PRIMARY KEY,
    file_id INTEGER,
    start_line INTEGER,
    end_line INTEGER,
    textTEXT,
    hashTEXTUNIQUE,         -- 文本哈希,跨文件去重
    embedding TEXT            -- JSON序列化的向量
);

-- 虚拟表:全文搜索(FTS5)
CREATEVIRTUALTABLE chunks_fts USING fts5(
    text,                     -- 索引的文本字段
    content=chunks            -- 关联到chunks表
);

-- 虚拟表:向量搜索(sqlite-vec)
CREATEVIRTUALTABLE chunks_vec USING vec0(
    embedding float[1536]      -- 向量维度
);

这里有几个关键设计:

  1. 增量索引:通过mtimehash,只重新索引变更的文件
  2. 去重存储:相同文本块只存一次向量,节省空间
  3. 优雅降级:如果sqlite-vec扩展没装上,系统会回退到JS暴力计算

特别值得一提的是这个降级策略。代码里是这样实现的:

async function searchMemory(queryVector, limit = 5{
try {
    // 快速路径:用sqlite-vec在数据库内计算余弦距离
    returnawait db.all(`
      SELECT c.text, vec_distance_cosine(v.embedding, ?) AS dist
      FROM chunks_vec v
      JOIN chunks c ON c.id = v.id
      ORDER BY dist ASC LIMIT ?
    `
, [queryVector, limit]);
  } catch (err) {
    // 回退路径:全量加载到内存暴力计算
    const allChunks = await db.all("SELECT id, text, embedding FROM chunks");
    return allChunks
      .map(chunk => ({
        ...chunk,
        dist: cosineSimilarity(queryVector, JSON.parse(chunk.embedding))
      }))
      .sort((a, b) => a.dist - b.dist)
      .slice(0, limit);
  }
}

这套机制保证了:无论环境如何,记忆系统永远可用——只是快慢的区别。

三、检索层:BM25 + 向量,两种思维的交织

有了存储,下一步是怎么查。OpenClaw的核心检索工具叫memory_search,它实现的是混合检索

为什么需要混合?因为纯向量检索有盲区——它懂语义但不懂精确匹配。你搜“Mac Studio网关主机”能找到“运行网关的那台机器”,但搜环境变量名“DB_PASSWORD”可能抓瞎。反之,BM25擅长精确匹配但不懂同义替换。

OpenClaw的做法是:让两者打架,然后加权平均

// 混合检索的核心逻辑(简化版)
asyncfunction hybridSearch(query, options = {}) {
const vecWeight = 0.7;  // 向量权重
const bm25Weight = 0.3// BM25权重

// 分别检索
const vectorResults = await vectorSearch(query);
const bm25Results = await bm25Search(query);

// 合并结果集(取并集)
const allChunkIds = newSet([
    ...vectorResults.map(r => r.id),
    ...bm25Results.map(r => r.id)
  ]);

// 计算综合得分
const finalResults = [];
for (const id of allChunkIds) {
    const vecScore = vectorResults.find(r => r.id === id)?.score || 0;
    const bm25Score = bm25Results.find(r => r.id === id)?.score || 0;
    
    // 归一化BM25分数(越小越好转成越大越好)
    const normalizedBm25 = 1 / (1 + bm25Score);
    
    finalResults.push({
      id,
      score: vecWeight * vecScore + bm25Weight * normalizedBm25
    });
  }

return finalResults.sort((a, b) => b.score - a.score).slice(0, options.limit);
}

这套算法的关键在于:用并集而非交集。只要向量或BM25任一方法认为某块内容相关,它就有机会进入候选池,最后通过加权得分决定谁胜出。

检索到相关块后,如果需要完整上下文,Agent可以调用memory_get工具,根据文件路径和行号范围精确读取内容。

四、写入策略:Agent自己决定该记什么

比检索更难的是写入——什么东西值得记?什么东西不值得?

OpenClaw的原则很激进:由Agent自己判断。系统提示词里明确写着:“如果有人说‘记住这个’,写到文件里(不要只存在内存中)。”

写入触发分两类:

  1. 自动写入:会话中的重要步骤、决策、异常,由Agent判断后追加进当天的Daily Log。这是append-only的,不覆盖,只追加。

  2. 识别写入:当系统判断某个信息“长期有用”时,写入对应的长期记忆文件。判断标准是稳定性——这个信息会在未来的多次会话里持续有价值吗?

具体分类逻辑:

  • “我以后都用深色模式” → 稳定偏好 → preferences.md
  • 解决了一个复杂问题 → 可复用经验 → learnings.md
  • 开始一个新项目 → 项目状态 → projects.md
  • 提到一个重要的人 → 人物信息 → contacts.md

还有一个很妙的机制叫预压缩记忆冲刷。当会话token数逼近上下文窗口上限时(比如Claude的200K用了176K),系统会主动提示Agent:“快把重要东西写到磁盘上,不然等会儿被压缩了就没了”。这个设计解决了大模型的致命伤——静默遗忘。

五、这套方案的优缺点,我摊牌了

用了这么久,OpenClaw的记忆系统确实牛,但槽点也不少。咱们客观聊聊。

优点

  • 零运维:不用装Postgres,不用配Docker,一个SQLite文件搞定
  • 数据私有:全在本地,不上云
  • 可审计:所有记忆都是Markdown,打开就能看,用户知道Agent记住了什么
  • 增量索引:只处理变更文件,效率高
  • 优雅降级:向量库挂了还有BM25,BM25挂了还有纯文本

缺点

  • token消耗偏高:LinkedIn的实战报告说OpenClaw是“token-hungry”,记忆系统是主要原因
  • 向量检索不懂关系:能找到“Alice”和“auth团队”,但推不出“Alice负责auth”
  • 维护成本随规模线性增长:文件一多,索引更新、重嵌入都得自己操心
  • 高门槛:虽然零运维,但得懂文件结构、会配环境变量,小白上手并不容易

社区已经在尝试改进。有人用Mem0做自动捕获,有人用QMD做更精准的检索,还有人引入知识图谱解决关系推理。但这些改进也带来了新的复杂度——没有银弹。

六、实战:怎么用好这套记忆系统

最后给几个实战建议,都是踩坑换来的经验。

1. 定期做记忆“体检”

长期记忆文件会随着时间膨胀。建议定期(比如每月)手动过一遍MEMORY.md,删掉过时的、合并重复的。这活不能全指望AI。

2. 教会Agent分类

在系统提示词里加几句引导,告诉Agent什么该记、记哪里。比如:

当用户表达稳定偏好时(“我喜欢”“我习惯”),写入preferences.md
当用户开始新项目时(“我要做一个”),写入projects.md
当用户完成复杂任务时(“解决了”),写入learnings.md

3. 善用Heartbeat做记忆维护

OpenClaw的Heartbeat机制可以定期执行记忆维护任务。比如每天凌晨跑一次:

openclaw cron add --name "记忆维护" \
  --cron "0 3 * * *" \
  --system-event "运行记忆整理:合并相似项,删除低价值项,生成摘要"

4. 查询时显式指定范围

调用memory_search时,可以通过scope参数限定搜索范围(比如只搜learnings.md),能显著提升召回准确率,节省token。


OpenClaw的记忆系统给我最大的启发是:AI的记忆不应该是黑盒。用Markdown存真相,用SQLite建索引,用BM25+向量做检索——这套组合拳既保证了功能,又让一切透明可控。

当然,它远非完美。但在这个“所有Agent都想记住你”的时代,能有一个让你随时打开文件、看清它记住了什么的系统,本身就是一种难得的清醒。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。