OpenClaw三级记忆系统实现揭秘:向量数据库+关系型数据库的混合存储方案
如果你用过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] -- 向量维度
);
这里有几个关键设计:
-
增量索引:通过 mtime和hash,只重新索引变更的文件 -
去重存储:相同文本块只存一次向量,节省空间 -
优雅降级:如果 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自己判断。系统提示词里明确写着:“如果有人说‘记住这个’,写到文件里(不要只存在内存中)。”
写入触发分两类:
-
自动写入:会话中的重要步骤、决策、异常,由Agent判断后追加进当天的Daily Log。这是append-only的,不覆盖,只追加。
-
识别写入:当系统判断某个信息“长期有用”时,写入对应的长期记忆文件。判断标准是稳定性——这个信息会在未来的多次会话里持续有价值吗?
具体分类逻辑:
-
“我以后都用深色模式” → 稳定偏好 → 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都想记住你”的时代,能有一个让你随时打开文件、看清它记住了什么的系统,本身就是一种难得的清醒。
- 点赞
- 收藏
- 关注作者
评论(0)