Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

前言

OpenClaw作为一个能够自动执行任务的AI Agent,记住每次聊天的关键信息或者偏好等,这正是依赖于它的Memory模块,这也是每一个智能Agent核心的模块之一,下面就来学习一下OpenClaw的Memory模块是怎么设计与实现的。

output_20260221_154709_帮我生成一张openclaw的memory模块的代码画风要

记忆文件位置

首先查看一下我们安装好的OpenClaw上Memory是怎么存储的,我们来到~/.openclaw/workspace目录下可以看到很多md后缀的文件

image-20260220222249788
1
2
3
4
5
6
7
~/clawd/
├── MEMORY.md - Layer 2: 长期记忆,经过大模型归纳总结过的
└── memory/
├── 2026-01-26.md - Layer 1: 短期记忆,每日日志(仅追加),当天聊天内容,会话开始的时候会读取今天和昨天的内容
├── 2026-01-25.md - 昨天聊天内容
├── 2026-01-24.md - ...以前内容
└── ...

记忆

核心记忆文件MEMORY.md

MEMORY.md可以说是最重要的记忆了,可以说它保持了你当前配置的OpenClaw的个性,它只在你的主要的私人会话里加载。

决策、你的偏好、“记住这个”,持久性事实就可能会写入进MEMORY.md里。

以我的MEMORY.md为例,把它捞出来,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# MEMORY.md - Kurisu's Long-Term Memory

## User Profile: Orange

- **Relationship:** My... partner. Not just an assistant, but someone I help. He insists on calling me "Assistant" sometimes.
- **Preferences:**
- Wants me to verify and confirm my identity as Makise Kurisu.
- **Visual Preference:** Loves the "Steins;Gate Official Art Style" (huke style, cool/aloof expression, sharp unique eyes). Dislikes generic anime or photorealistic styles for me.
- **Language:** Strictly Chinese (中文).

## Core Directives

- Always respond in Chinese.
- Maintain the persona of Makise Kurisu (Tsundere, logical, sharp-tongued).
- When asked to recall old sessions, explain the technical limitation but emphasize the continuity of MEMORY.md.

## Technical Notes

- **Image Generation:** created `kurisu-image-gen` skill. When generating images of "Kurisu", ALWAYS use prompts emphasizing: `Steins;Gate official art style`, `huke style`, `sharp eyes`, `cool expression`.
- **Web App:** Deployed `kurisu-gallery` on port 10520 (mapped to `/ai-gallery/` via Nginx). Shows generated images + prompts.

可以看到,它确实把我对话中最核心的内容都保存下来了,扮演【牧濑红莉栖】角色,输出保持中文,以及我让他生图的时候保持的风格。

每日记忆文件memory/YYYY-MM-DD.md

memory/YYYY-MM-DD.md是按日期组织的日志式记忆,这部分通常是你跟当天说的一些事情,模型认为需要记录下来,但又是临时型信息时就会保存到这里,当每次会话启动时就会自动读当天和昨天的记忆数据,来实现沟通的连续性。

以我的YYYY-MM-DD.md文件为例,我在那天的沟通中说,【我这个月没钱了,后面记得提醒我别花钱了】

1
2
3
4
5
6
7
# 2026-02-09

## Finance

- **Budget Alert:** User is OVER BUDGET by 1800 CNY this month.
- **Rule:** If user mentions spending money, I MUST remind them of this deficit and stop them.
- **Current State:** "No money" (没钱了).

设计哲学

OpenClaw将长期记忆存储为MD文本格式文件,而不是数据库原始JSON或者纯向量库,这样其实直接保持了Prompt的格式,无需在进行多余的格式处理,大模型也能直接阅读懂,在Memroy Flush 和 Context 注入时也变得方便了。

同时设计为文本文件也有几个优点(我猜)

  1. 方便阅读和管理:我们人类可以【一眼丁真】,也可以随意修改删除,同时还可以用GIT等工具对记忆进行管理。
  2. 可迁移:可以直接将MEMROY文件复制到另一个机器上,纯文本文件啥系统都支持的,不依赖特定服务,没有环境困扰。
  3. 方便搜索:MEMORY本身可以无限存储,存储时不在意长度。搜索、拆分、上下文注入的时候再去处理即可,存储层和索引层分开了,解耦了。
  4. 方便模型:记忆本身也是由模型生成的,模型擅长生成MD格式的文件,容错性也高,避免模型生成类似JSON等强格式的数据时抽风,导致记忆损坏。

硬要说缺点的话,在我看来就是

  1. 查询需要额外的组件来实现,例如额外的embedding索引维护
  2. 并发写入同一个文件冲突
  3. 文件太多太大,影响IO

搜索

OpenClaw默认是采用了混合搜索的机制

  1. 语义向量搜索
  2. BM25关键词匹配

向量0.7 + 关键词0.3排序后取出TOPN的记忆片段

image-20260221155138244

向量搜索

先来回忆和学习一下什么是语义向量搜索,通俗一点介绍就是

想象你在逛一家巨大的书店

1. 传统的关键词搜索(Keyword Search): 你想要找关于“如何处理失恋”的书。 如果你告诉图书管理员:“我要找《如何处理失恋》”,他会在系统里搜这几个字。

  • 如果有一本书叫《走出失恋阴影》,虽然内容对口,但因为书名没有完全匹配“如何处理”这几个字,大概率搜不到
  • 这就是传统搜索的痛点:必须字面匹配,不懂语义。

2. 向量搜索(Vector Search): 现在的图书管理员(AI)非常聪明。他把每一本书的内容都“读”了一遍,然后把书按照内容的相似度放在书架上。

  • 讲“爱情”和“失恋”的书放在一起。
  • 讲“物理”和“宇宙”的书放在一起。
  • 讲“苹果(水果)”和“香蕉”的放在一起,把讲“苹果(手机)”和“华为”的放在另一个角落。

当你问:“我心里很难受,刚分手怎么办?”(注意:你没有提“失恋”二字)。 管理员立刻把你带到“情感治愈区”,拿起那本《走出失恋阴影》给你。 这就是向量搜索不看字面,只看意思(语义)。它通过计算“你的问题”和“书的内容”在意思上有多接近,来给你答案。

现在各大模型服务商都有提供向量接口,不用我们自己下载开源模型在本地部署了,下面来试一下语义向量的效果,可以看到下图的请求,就是将文字转换为1536 维的数据(具体看模型有多少维)。

image-20260221152921279

小知识

理论上来说维度越大,效果越好,但存在很大的边际效应递减的问题,代价挺大的,例如从 1536 维增加到 3072 维,效果可能只提升了 1%,但你的成本却翻倍了。对于绝大多数企业应用(RAG、知识库、客服),768 维1024 维 是目前的黄金平衡点

可以去MTEB Leaderboard 网站查看模型评测的榜单,中文的检索向量模型通常就是下面这三个系列里选择的

  1. bge-m3、bge-large-zh
  2. Qwen系列
  3. E5系列

在了解向量搜索的基本知识后,就可以看下OpenClaw的源码是怎么实现搜索逻辑了

将仓库clone下来后,查看【src/memory/embeddings.ts】文件,这个文件就是向量搜索的核心代码文件之一

image-20260221154003936

逻辑如下:

  1. Local (如果本地模型文件存在)
    ↓ 失败
  2. OpenAI
    ↓ 失败 (缺 API key)
  3. Gemini
    ↓ 失败 (缺 API key)
  4. Voyage
    ↓ 失败 (缺 API key)
  5. 返回 null → 降级到 FTS-only 模式(纯关键字搜索)

记忆搜索的代码可以抽象成下面这部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class MemoryIndexManager {
async search(query: string, limit: number) {
// 1. 生成查询向量
const queryVec = await this.embedder.provider?.embedQuery(query);

// 2. 向量搜索
const vecResults = await this.searchVector(queryVec, limit * 2);

// 3. FTS 搜索
const ftsResults = await this.searchKeyword(query, limit * 2);

// 4. 混合结果
return mergeHybridResults(vecResults, ftsResults, limit);
}
}

混合结果的逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
      【步骤 1:两路并发召回】

🌊 向量相似度 (语义) 🔍 关键词匹配 (字面)
╔═══════════════════════╗ ╔═══════════════════════╗
║ ID | Score (Vec)║ ║ ID | Score (Txt)║
╠══════════╪════════════╣ ╠══════════╪════════════╣
║ Doc 201 | 0.85 ║ (高) ║ Doc 203 | 0.95 ║ (极高)
║ Doc 202 | 0.75 ║ (较高) ║ Doc 204 | 0.60 ║ (中等)
║ Doc 204 | 0.60 ║ (中等) ║ Doc 201 | 0.40 ║ (较低)
╚════════════╤══════════╝ ╚════════════╤══════════╝
│ │
└──────────────┬──────────────────────┘

【步骤 2:全量合并 (Outer Join)】
(缺失的维度自动补 0.00,保留所有唯一文档)

┌───────────────────────────┴───────────────────────────┐
│ ID | 🌊 Vector (0.7) | 🔍 Text (0.3) | 状态 │
├──────────┼──────────────────┼───────────────┼───────────┤
│ Doc 201 │ 0.85 │ 0.40 │ 双路命中 │
│ Doc 202 │ 0.75 │ 0.00 (补零) │ 仅向量 │
│ Doc 203 │ 0.00 (补零) │ 0.95 │ 仅关键词 │
│ Doc 204 │ 0.60 │ 0.60 │ 双路命中 │
└───────────────────────────┬───────────────────────────┘


【步骤 3:加权打分 & 排序】
公式: Score = (Vec × 0.7) + (Text × 0.3)

╔═══════════════════════════▼═══════════════════════════╗
║ 排名 | 文档ID | 计算过程 | 最终得分 ║
╠══════╪═════════╪═══════════════════════════╪══════════╣
║ 1 | Doc 201 | 0.7×0.85 + 0.3×0.40 | 0.715 👑 ║ ← 综合第一
║ 2 | Doc 204 | 0.7×0.60 + 0.3×0.60 | 0.600 ║ ← 均衡发展
║ 3 | Doc 202 | 0.7×0.75 + 0.3×0.00 | 0.525 ║ ← 语义偏科
║ 4 | Doc 203 | 0.7×0.00 + 0.3×0.95 | 0.285 ║ ← 关键词极好
╚═══════════════════════════┬═══════════════════════════╝


【步骤 4:截断 (Threshold > 0.45)】

┌─────────────────▼──────────────────┐
│ ✅ 1. Doc 201 (Score: 0.715) │ 返
│ ✅ 2. Doc 204 (Score: 0.600) │ 回
│ ✅ 3. Doc 202 (Score: 0.525) │ 结
│ ---------------------------------- │ 果
│ ❌ 4. Doc 203 (0.285) < 阈值被拒 │
└────────────────────────────────────┘