前言 OpenClaw作为一个能够自动执行任务的AI Agent,记住每次聊天的关键信息或者偏好等,这正是依赖于它的Memory模块,这也是每一个智能Agent核心的模块之一,下面就来学习一下OpenClaw的Memory模块是怎么设计与实现的。
记忆文件位置 首先查看一下我们安装好的OpenClaw上Memory是怎么存储的,我们来到~/.openclaw/workspace目录下可以看到很多md后缀的文件
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 注入时也变得方便了。
同时设计为文本文件也有几个优点(我猜)
方便阅读和管理:我们人类可以【一眼丁真】,也可以随意修改删除,同时还可以用GIT等工具对记忆进行管理。
可迁移:可以直接将MEMROY文件复制到另一个机器上,纯文本文件啥系统都支持的,不依赖特定服务,没有环境困扰。
方便搜索:MEMORY本身可以无限存储,存储时不在意长度。搜索、拆分、上下文注入的时候再去处理即可,存储层和索引层分开了,解耦了。
方便模型:记忆本身也是由模型生成的,模型擅长生成MD格式的文件,容错性也高,避免模型生成类似JSON等强格式的数据时抽风,导致记忆损坏。
硬要说缺点的话,在我看来就是
查询需要额外的组件来实现,例如额外的embedding索引维护
并发写入同一个文件冲突
文件太多太大,影响IO
搜索 OpenClaw默认是采用了混合搜索的机制
语义向量搜索
BM25关键词匹配
向量0.7 + 关键词0.3排序后取出TOPN的记忆片段
向量搜索 先来回忆和学习一下什么是语义向量搜索,通俗一点介绍就是
想象你在逛一家巨大的书店 。
1. 传统的关键词搜索(Keyword Search): 你想要找关于“如何处理失恋”的书。 如果你告诉图书管理员:“我要找《如何处理失恋》”,他会在系统里搜这几个字。
如果有一本书叫《走出失恋阴影》,虽然内容对口,但因为书名没有完全匹配“如何处理”这几个字,大概率搜不到 。
这就是传统搜索的痛点:必须字面匹配,不懂语义。
2. 向量搜索(Vector Search): 现在的图书管理员(AI)非常聪明。他把每一本书的内容都“读”了一遍,然后把书按照内容的相似度 放在书架上。
讲“爱情”和“失恋”的书放在一起。
讲“物理”和“宇宙”的书放在一起。
讲“苹果(水果)”和“香蕉”的放在一起,把讲“苹果(手机)”和“华为”的放在另一个角落。
当你问:“我心里很难受,刚分手怎么办?”(注意:你没有提“失恋”二字)。 管理员立刻把你带到“情感治愈区”,拿起那本《走出失恋阴影》给你。 这就是向量搜索 :不看字面,只看意思(语义) 。它通过计算“你的问题”和“书的内容”在意思上 有多接近,来给你答案。
现在各大模型服务商都有提供向量接口,不用我们自己下载开源模型在本地部署了,下面来试一下语义向量的效果,可以看到下图的请求,就是将文字转换为1536 维的数据(具体看模型有多少维)。
小知识 理论上来说维度越大,效果越好,但存在很大的边际效应递减的问题,代价挺大的,例如从 1536 维增加到 3072 维,效果可能只提升了 1%,但你的成本却翻倍了。对于绝大多数企业应用(RAG、知识库、客服),768 维 或 1024 维 是目前的黄金平衡点 。
可以去MTEB Leaderboard 网站查看模型评测的榜单,中文的检索向量模型通常就是下面这三个系列里选择的
bge-m3、bge-large-zh
Qwen系列
E5系列
在了解向量搜索的基本知识后,就可以看下OpenClaw的源码是怎么实现搜索逻辑了
将仓库clone下来后,查看【src/memory/embeddings.ts】文件,这个文件就是向量搜索的核心代码文件之一
逻辑如下:
Local (如果本地模型文件存在) ↓ 失败
OpenAI ↓ 失败 (缺 API key)
Gemini ↓ 失败 (缺 API key)
Voyage ↓ 失败 (缺 API key)
返回 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) < 阈值被拒 │ └────────────────────────────────────┘