OpenClaw
← 总览
01
memory · pluggable backends

记忆系统总览

OpenClaw 的记忆系统不是「套壳向量库」,而是一套可插拔的记忆后端。三个内置后端——memory-lancedb、memory-wiki、active-memory——共用一份 MemorySearchManager 契约,分别满足语义搜索、结构化 vault、即时会话状态三类需求。

OpenClaw 把「记忆」拆成三层而不是一个大向量库,是因为这三类检索问题本质不同:"和这句话最像的旧记忆是什么"(语义相似)、"这个主题/这个人持久积累了什么"(结构化 vault)、"当前这轮对话里刚刚发生了什么"(即时上下文)。把它们塞进一个后端只会互相拖累,所以 OpenClaw 让三者各跑各的,统一在 memory-host-sdk 的接口之下。

三个后端都是 extensions/memory-* 下的独立插件,按 openclaw.plugin.json 声明式注册,可以单独启用/停用,也可以被第三方后端替换。记忆的自动沉淀挂在 agent 生命周期钩子 agent_end 上——一轮对话结束就把值得记的东西抽出来、嵌入、入库,全程不阻塞 agent 主流程。

memory-lancedb

语义搜索

向量嵌入检索。把文本 embed 成向量存进 LanceDB,按 L2 距离找最近邻,回答"和当前语境最像的旧记忆"。

memory-wiki

结构化 vault

Markdown vault + 全文检索。原子写入 .md 文件,可被 Obsidian / git 直接读,回答"持久积累了什么"。

active-memory

即时会话

按会话滑动窗口的活跃上下文,由 SessionMemoryState 三增量门控抽取,回答"这轮刚发生了什么"。

本地优先:三种后端的存储都落在用户自己的机器上——LanceDB 文件、Markdown vault、会话状态都不出本机。「一个助手、一套记忆」靠的是 agentId + sessionKey 这把跨平台锚点,而不是云端账户。
02
contract · the shared interface

memory-host-sdk 契约

源码:packages/memory-host-sdk。所有记忆后端实现同一个 MemorySearchManager 接口——宿主只认接口,不关心后端是向量、vault 还是会话窗口。

packages/memory-host-sdk · MemorySearchManager
export interface MemorySearchManager {
  search(query: string, opts: MemorySearchOptions): Promise<MemoryHit[]>;
  readFile(ref: string): Promise<string>;          // 把命中条目展开成原文
  status(): Promise<MemoryBackendStatus>;          // 后端健康/容量上报
  probeEmbeddingAvailability(): Promise<boolean>;  // 嵌入端点可用?
  probeVectorAvailability(): Promise<boolean>;     // 向量存储可用?
}

五个方法把记忆后端的能力面收敛到最小:search() 做检索、readFile() 把命中条目展开回原文、status() 上报后端状态,两个 probe*() 探针让宿主在运行时探测嵌入与向量存储是否可用——这正是降级逻辑的支点:探针返回 false 时,宿主自动把记忆退回到纯全文检索,而不是抛错中断 agent。

search / readFile

检索 + 展开

search() 返回命中列表,readFile() 把某条命中一路展开成完整原文——"先摘要后展开"的两段式读取。

status()

状态上报

后端把自己的健康度、容量、就绪状态报给宿主,用于诊断与 UI 展示。

probe*()

能力探针

两个探针把"嵌入是否可用"与"向量库是否可用"分开探测,让降级可以分级——可全文不可向量时仍能跑。

为什么是接口而不是实现:把记忆抽象成 MemorySearchManager 之后,"换记忆后端"就是换一个插件——LanceDB / wiki / active 三选一甚至并存,第三方也能实现同一接口接进来。宿主代码对此完全无感。
03
lancedb · vector recall

memory-lancedb · 向量嵌入检索

源码:extensions/memory-lancedb/index.ts。把记忆文本嵌入成向量存进 LanceDB,按 L2 距离找最近邻,相似度用 1 / (1 + d) 归一化。自动沉淀挂在 agent_end 钩子上。

extensions/memory-lancedb/index.ts — agent_end 自动沉淀 + 判重
// agent_end 钩子:一轮对话结束,抽取用户消息并尝试沉淀
const vector = await embeddings.embed(text);
const existing = await db.search(vector, 1, 0.95);  // 0.95 相似 = 重复
if (existing.length > 0) return;                  // 判重命中 → 跳过,不重复入库
await db.store({ text, vector, importance: 0.7, category });

// embedding 端点挂了 → probeEmbeddingAvailability=false
// → 退回纯全文检索,沉淀暂停,不抛错、不阻塞 agent

沉淀流程:agent_end 触发 → 抽取本轮用户消息 → embeddings.embed() 嵌入成向量 → 用 0.95 相似阈值查一次库做判重 → 命中即跳过(避免把"早上好/谢谢"这类高度重复的内容反复入库),未命中才 db.store() 落盘,带上 importance 与 category。

检索侧:LanceDB 返回 L2 距离 d,OpenClaw 用 1 / (1 + d) 把它转成 0–1 的相似度分数——距离越近分数越高,方便和其他后端的命中分数统一排序。

表 01 · memory-lancedb 关键常量与字段
项值 / 公式作用
判重阈值0.95db.search(vector, 1, 0.95) 命中即视为重复,跳过入库
相似度1 / (1 + d)把 L2 距离 d 归一化为 0–1 分数
importance0.7新记忆的默认重要度权重
沉淀时机agent_end对话结束钩子,异步沉淀不阻塞主流程
embed → search → store

三步沉淀

嵌入、判重、入库三步走,把可确定的部分(哈希、查重、落盘)做稳,把 LLM/嵌入这类能容忍失败的重活放在能降级的位置。

probeEmbeddingAvailability

嵌入不可用即降级

探针返回 false 时记忆退回纯全文检索,新记忆沉淀暂停;不抛错、不阻塞 agent,等端点恢复再续。

04
wiki · the markdown vault

memory-wiki · Markdown vault

源码:extensions/memory-wiki。把记忆持久化成 Markdown vault——YAML frontmatter + 正文,通过 content_store 原子写入(temp → fsync → rename),配全文检索,命中后注入 prompt 段。

content_store — 原子写入(崩溃安全)
// 原子写入:先写临时文件、fsync、再 rename
async function writeAtomic(path: string, body: string) {
  const tmp = `${path}.tmp`;
  await fs.writeFile(tmp, body);
  await fs.fsync(tmp);            // 落盘后再换名
  await fs.rename(tmp, path);     // rename 原子,崩在中间也不留半截文件
}

// vault 里的一条记忆:YAML frontmatter + Markdown 正文
// ---\ntitle: ...\ntags: [...]\n---\n正文……

memory-wiki 把记忆当成文件而不是数据库行:每条记忆是一个 .md 文件,头部 YAML frontmatter 存元数据(标题、标签、时间),正文是 Markdown。写入走 content_store 的原子写入——先写 .tmp、fsync 落盘、再 rename 到位;rename 在同一文件系统上是原子操作,所以进程崩在写入中途也不会留下半截损坏文件。

检索是全文检索而非向量——这正是 lancedb 不可用时的天然降级目标。命中后,wiki 把相关条目组织成一个 prompt 段注入 system prompt,让 agent 直接读到结构化的持久记忆。

temp → fsync → rename

原子写入

崩溃安全的三步写盘,保证 vault 永远是一致状态,不会出现写了一半的 .md。

YAML + Markdown

人类可读

每条记忆是一个 .md 文件,可以直接用 Obsidian 打开、git 版本化、手工审计与删除。

全文检索

降级目标

不依赖嵌入/向量,所以是 lancedb 探针失败时的退路——向量挂了,wiki 全文检索照常工作。

本地优先不是口号:vault 是一堆你硬盘上的 Markdown 文件。可以 git 化、可以备份、可以审计、可以删——记忆从云服务的私有 schema 抢回到了用户自己的文件路径里。
05
active · session working memory

active-memory · 会话间活跃上下文

按会话滑动窗口维护的即时上下文。由 SessionMemoryState 跟踪当前会话的累计 token / tool 调用数 / turn 数,三个增量同时越线才触发后台抽取——避免每隔几句话就启一次抽取作业。

SessionMemoryState — 三增量门控
type SessionMemoryState = {
  tokensSinceExtract: number;   // 距上次抽取累计 token
  toolCallsSinceExtract: number;
  turnsSinceExtract: number;
};

// 只有三个增量【同时】跨过门槛才抽取
const shouldExtract =
  state.tokensSinceExtract >= 4000 &&     // token +4000
  state.toolCallsSinceExtract >= 8 &&   // tool  +8
  state.turnsSinceExtract >= 4;          // turn  +4

active-memory 关心的是「当前这轮对话刚刚发生了什么」——它维护一个按会话的滑动窗口,让 agent 在多轮之间保有即时工作记忆,而不必每次都回向量库/vault 捞。但是把会话压成结构化长期记忆是一笔有成本的后台作业(要跑 LLM 抽取),所以不能每句话都做。

SessionMemoryState 用三个增量门控解决这个节流问题:跟踪距上次抽取以来累计的 token、tool 调用数、turn 数,只有这三者同时跨过门槛(token +4000、tool +8、turn +4)才触发后台抽取,把会话压成结构化记忆条目(再交给 lancedb/wiki 沉淀)。三条件取「与」,意味着既要谈得够久、又要做了够多事、还要来回够多轮,才值得动用一次抽取——一次轻量寒暄绝不会触发。

表 02 · 抽取触发门槛(三者取 AND)
增量门槛含义
tokensSinceExtract+4000对话累计够长
toolCallsSinceExtract+8做了够多事(工具调用)
turnsSinceExtract+4来回够多轮
为什么取 AND 而不是 OR:三条件全满足才抽取,把"档案管理员"子作业的触发频率压到最低——避免每隔几句话就启一次昂贵的抽取,又保证真正信息密集的会话不会漏记。
06
recall · one memory, all platforms

召回与跨平台共享

记忆按 agentId + sessionKey 检索——同一用户在 Telegram、Discord、Slack… 任意平台发消息都路由回同一份记忆。这是 OpenClaw "一个助手、一套记忆、20+ 渠道" 真正成立的地方。

三个后端的检索都以 agentId + sessionKey 为键。sessionKey 是 OpenClaw 的跨平台身份锚点(如 telegram:123、discord:u_456),由 channel 层在入站时归一化产生。因为记忆挂在这把锚点上而不是某个平台账户上,所以"用户→记忆"的绑定可以跨平台共享:你在 Telegram 上告诉助手的偏好,切到 Discord 仍然记得。

图 01 · 召回 → 注入 → 沉淀闭环
用户消息
任意平台
recall
search()
agentId+sessionKey
inject
注入 prompt 段
相关记忆
agent_end
沉淀 store()
判重后入库

每次用户发消息:先以 agentId + sessionKey 调 search() 召回相关记忆,组织成 prompt 段注入 system prompt 让 agent 读到;对话结束时 agent_end 钩子触发沉淀——抽取、嵌入、0.95 判重、入库。召回与沉淀首尾相接,构成一个跨会话、跨平台的记忆闭环。

sessionKey

跨平台锚点

记忆绑在 agentId + sessionKey 上而非平台账户,所以同一用户跨 20+ 渠道共享同一份记忆。

recall → inject

召回即注入

每条用户消息先召回相关记忆并注入 system prompt,agent 带着上下文回答。

agent_end → store

对话即沉淀

对话结束自动沉淀,判重后入库——无需用户显式"保存记忆"。

三后端 + 跨平台 sessionKey:这是 OpenClaw 记忆设计相对 Kimi Code(以 compaction 为主)与 Odysseus(ChromaDB 混合检索)的差异点——记忆是可插拔后端,且天生跨平台。
记忆 · 三后端可插拔
extensions/memory-lancedb · memory-wiki · active-memory
packages/memory-host-sdk · MemorySearchManager 契约
← 返回 OpenClaw 总览 · github ↗