摘要 :Memory(记忆) 是 OpenClaw 的 状态持久化层 ,负责在会话之间保持上下文、存储长期知识、管理 Agent 的"记忆"。本文深入探讨 Memory 子系统的三大核心机制 :Memory I/O (记忆的读写/索引/Flush)、内置记忆后端 (Builtin/QMD/Honcho 等多种存储方案)、人格文件分界 (SOUL.md/AGENTS.md 等八类记忆文件)。基于 src/plugins/memory*.ts 和 src/plugin-sdk/memory*.ts 源码,揭示 OpenClaw 如何实现"有状态"的智能体。
关键词:OpenClaw;Memory;记忆子系统;持久化;Flush;Distill;SOUL.md;人格文件;内置记忆;QMD
系列文章:
- OpenClaw 深度解析与源代码导读 · 第1篇:系列导读------术语、版本与读源码方法
- OpenClaw 深度解析与源代码导读 · 第2篇:Skills------能力扩展平面与源码中的「目录即技能」
- OpenClaw 深度解析与源代码导读 · 第3篇:Gateway------常驻控制面、单端口多协议与进程骨架
- OpenClaw 深度解析与源代码导读 · 第4篇:Router------入站消息的分发中枢与决策逻辑
- OpenClaw 深度解析与源代码导读 · 第5篇:Brain------Prompt/Context/Harness Engineering 与执行框架
- OpenClaw 深度解析与源代码导读 · 第6篇:Hands------Shell、文件、浏览器与沙箱安全
源码版本说明 :本文引用路径基于 openclaw/openclaw 仓库;本地阅读使用的 commit 为 0dd4958bc8a78d26b3b526b1f2e63b15110c64a2(2026-04-11)。GitHub 上可按该 SHA 查看对应版本的源码。
1 Memory 在架构中的位置:Agent 的"长期记忆"
在OpenClaw 深度解析与源代码导读 · 第6篇:Hands------Shell、文件、浏览器与沙箱安全中,我们了解了 Hands 如何执行操作。但 Hands 执行后的结果需要被记住 ,否则每次对话都是"全新的开始"。这就是 Memory 的核心价值。
1.1 Memory 与周边组件的关系
Brain 层
Memory 层
会话层
加载到
写入
Flush
读取/检索
增强
当前对话上下文
历史消息记录
Builtin 记忆
QMD 后端
Honcho 服务
人格文件
SOUL.md/AGENTS.md
System Prompt 组装
上下文注入
| 组件 | 职责 | 与 Memory 的关系 | 数据流向 |
|---|---|---|---|
| Session | 当前对话的实时状态 | 写入 Memory | 会话结束 → Flush → 持久化 |
| Memory | 长期存储与检索 | 被 Brain 读取 | 存储 → 检索 → 注入上下文 |
| Brain | 推理与规划 | 使用 Memory 增强上下文 | 读取 Memory → 组装 Prompt |
| 人格文件 | Agent 的"性格"定义 | 加载到 System Prompt | SOUL.md → System Prompt |
💡 理解要点 :Memory 是 Agent 的"长期记忆"------它让 Agent 能够记住"你是谁"、"之前聊过什么"、"有什么偏好"。没有 Memory,每个会话都是"失忆"的状态。
2 Memory 子系统的核心架构
OpenClaw 的 Memory 子系统采用分层架构,支持多种后端存储方案。
2.1 三层架构模型
后端层
Builtin 后端
文件系统
QMD 后端
SQLite/向量搜索
Honcho 服务
外部 API
运行时层
Memory Plugin Runtime
Memory State Manager
Embedding Provider
应用层
Memory Flush
Memory Search
Prompt 注入
| 层级 | 模块 | 职责 | 关键文件 |
|---|---|---|---|
| 应用层 | Flush, Search, Prompt 注入 | 用户可见的 Memory 操作 | memory-flush.ts, memory-state.ts |
| 运行时层 | Plugin Runtime, State Manager | 后端抽象与管理 | memory-runtime.ts, memory-state.ts |
| 后端层 | Builtin/QMD/Honcho | 实际存储实现 | memory-core-host-*.ts |
2.2 Memory 核心类型定义
ts
// src/plugins/memory-state.ts(节选,概念结构)
export type MemoryCorpusSearchResult = {
corpus: string; // 语料库名称
path: string; // 文件路径
title?: string; // 标题
score: number; // 相似度分数
snippet: string; // 匹配片段
id?: string; // 唯一标识
citation?: string; // 引用标记
source?: string; // 来源
};
export type MemoryFlushPlan = {
softThresholdTokens: number; // 软阈值 Token 数
forceFlushTranscriptBytes: number; // 强制 Flush 字节数
reserveTokensFloor: number; // 预留 Token 下限
prompt: string; // Flush 时的 Prompt
systemPrompt: string; // Flush 时的 System Prompt
relativePath: string; // 存储路径
};
export type MemoryRuntimeBackendConfig =
| { backend: "builtin" } // 内置后端
| { backend: "qmd"; qmd?: MemoryRuntimeQmdConfig } // QMD 后端
| { backend: "honcho"; honcho?: { apiKey: string } }; // Honcho 后端
3 Memory I/O:读写、索引与 Flush
Memory 子系统的核心能力是持久化存储 和检索。这包括三个关键操作:写入(Flush)、读取(Search/Query)、索引(Indexing)。
3.1 Memory Flush:何时保存记忆?
Flush 是 Memory 的"保存"操作------将当前会话状态持久化到存储后端。
3.1.1 Flush 触发条件
ts
// src/auto-reply/reply/memory-flush.ts(节选)
export function shouldRunMemoryFlush(params: {
entry?: SessionEntry;
tokenCount?: number;
contextWindowTokens: number; // 上下文窗口大小
reserveTokensFloor: number; // 预留 Token 数
softThresholdTokens: number; // 软阈值
}): boolean {
// 计算阈值
const threshold = Math.max(0, contextWindow - reserveTokens - softThreshold);
// 触发条件:当前 Token >= 阈值
return totalTokens >= threshold;
}
触发条件:
| 场景 | 说明 | 配置项 |
|---|---|---|
| Token 阈值 | 当前 Token 数接近上下文上限 | reserveTokens, softThreshold |
| 会话结束 | 用户主动结束会话 | /new 或 `/reset |
| 定时 Flush | 定期自动保存 | memory.flushInterval |
3.1.2 Flush 阈值计算
阈值 = contextWindow - reserveTokens - softThreshold
示例(GPT-4 8K 窗口):
- contextWindow = 8192
- reserveTokens = 4000(预留)
- softThreshold = 1000(缓冲)
- 阈值 = 8192 - 4000 - 1000 = 3192
当 currentToken >= 3192 时触发 Flush
3.1.3 Flush 执行流程
存储后端 Memory Runtime Brain 存储后端 Memory Runtime Brain alt [Token >= 阈值] 检查 Token 计数 触发 Memory Flush 准备 Flush 数据 写入存储 确认保存 Flush 完成
3.2 Memory Search:如何检索记忆?
Search 是 Memory 的"读取"操作------从存储中检索相关记忆片段。
3.2.1 Search 接口定义
ts
// src/plugins/memory-state.ts(节选)
export type MemoryCorpusSupplement = {
search(params: {
query: string; // 搜索查询
maxResults?: number; // 最大结果数
agentSessionKey?: string; // Agent 会话标识
}): Promise<MemoryCorpusSearchResult[]>;
get(params: {
lookup: string; // 查找路径
fromLine?: number; // 起始行
lineCount?: number; // 行数
agentSessionKey?: string;
}): Promise<MemoryCorpusGetResult | null>;
};
3.2.2 Search 结果格式
ts
interface MemoryCorpusSearchResult {
corpus: "memory"; // 语料库
path: "/path/to/file.md"; // 文件路径
title: "用户偏好"; // 标题
score: 0.95; // 相似度(0-1)
snippet: "用户喜欢使用 TypeScript..."; // 片段
citation: "[^1^]"; // 引用标记
source: "user-preferences.md";
}
3.3 Memory Indexing:如何索引记忆?
Indexing 是 Memory 的"组织"操作------为记忆建立索引以支持快速检索。
| 后端类型 | 索引方式 | 特点 | 适用场景 |
|---|---|---|---|
| Builtin | 文件系统 + 可选本地索引(memory.indexing) |
落盘侧零外部存储依赖 ;启用 indexing 后由 Gateway memory-core 建 SQLite(FTS5 + 可选 sqlite-vec) 。若同时配置 agents.*.memorySearch 且 Embedding Provider 可用 ,则对分块做 向量化 + 语义召回 ,并常与 FTS 关键词 做 hybrid 合并;无 provider 时退化为 FTS-only |
单机使用、快速启动 |
| QMD | 独立 QMD 库内 SQLite + 向量(embedding 写进 QMD 配置) | 本地语义搜索、跨会话检索;embedding 作为该后端的一等配置 | 需要语义记忆的项目 |
| Honcho | 外部托管索引 | 云端、多 Agent 共享、高级推理 | 生产环境、团队协作 |
关于 Builtin 的索引:
- 默认状态:Builtin 仅使用文件系统存储,维护文件列表,搜索时基于文件名或简单遍历
- 启用 Indexing (
"indexing": {"enabled": true}):在本地索引库中建立 FTS5 ;若 Memory Search 已配置可用的 Embedding Provider ,同一管线还会写入 向量索引 ,检索可走 向量 + FTS(hybrid) ;若未配置 provider,则仅有 FTS-only
4 三种记忆后端详解
OpenClaw 的 Memory 子系统支持三种后端存储方案,从简单到复杂,适应不同需求。
4.1 后端对比总览
| 特性 | Builtin | QMD | Honcho |
|---|---|---|---|
| 部署方式 | 本地文件系统 | 本地 SQLite + 向量库 | 云端托管服务 |
| 依赖 | 落盘无强依赖;语义搜索 依赖 memorySearch + Embedding API/本地模型 |
SQLite、Embedding 模型 | API Key |
| 语义搜索 | ✅ 支持(需 memory.indexing + 已配置且可用的 memorySearch embedding provider;与 FTS hybrid) |
✅ 支持 | ✅ 支持 |
| 跨会话检索 | ❌ 有限 | ✅ 支持 | ✅ 支持 |
| 自动推理 | ❌ 不支持 | ❌ 不支持 | ✅ 自动洞察 |
| 多 Agent 共享 | ❌ 不支持 | ❌ 不支持 | ✅ 原生支持 |
| Token 节省 | 无 | 中等 | 60-90% |
| 适用场景 | 个人使用 | 本地项目 | 团队协作、生产环境 |
4.2 Builtin 后端(内置)
Builtin 是 OpenClaw 的默认记忆后端,基于文件系统实现:记忆 Markdown / JSONL 的落盘与目录结构本身可以不依赖任何外部服务。
本节叙述前提(重要)
下文在说明「启用
memory.indexing之后的索引与memory_search行为」时,默认已 为对应 Agent 配置好agents.defaults.memorySearch(或按 Agent 覆盖) ,且 Embedding Provider 可用 (例如 OpenAI、Gemini、或本地 embedding 模型),Gateway 侧的memory-core会对纳入索引的分块执行embedChunks→ 写入向量索引(sqlite-vec) ,并在默认 hybrid 策略下与 FTS5 关键词 结果合并排序。这与源码路径一致:
extensions/memory-core中 有provider时 走向量 +(可选)FTS;无provider时 退化为 FTS-only (writeChunks(..., [], false),仅关键词召回)。若你尚未配置 provider,请将下文「第 2 层」中的「向量 / hybrid」理解为不会出现,仅保留 FTS 路径。
4.2.1 存储结构
~/.openclaw/
├── memory/ # 记忆存储目录
│ ├── sessions/ # 会话记忆
│ │ ├── session-abc123/ # 具体会话
│ │ │ ├── messages.jsonl # 消息记录
│ │ │ ├── summary.md # 会话摘要
│ │ │ └── metadata.json # 元数据
│ │ └── session-def456/
│ ├── corpus/ # 语料库
│ │ ├── user-preferences.md # 用户偏好
│ │ ├── project-context.md # 项目上下文
│ │ └── agent-knowledge.md # Agent 知识
│ └── index/ # 索引文件
│ ├── embeddings.json
│ └── search-index.db
4.2.2 配置示例
(1)Builtin 落盘 + 本地索引开关 (memory)
json
{
"memory": {
"backend": "builtin",
"builtin": {
"storageDir": "~/.openclaw/memory",
"indexing": {
"enabled": true,
"chunkSize": 1000,
"overlap": 200
}
}
}
}
(2)Memory Search + Embedding(本节默认已配好) (agents)
语义召回由 memorySearch 与 Embedding Provider 决定,与 memory.backend 仍为 builtin 不矛盾 :builtin 负责「文件写哪里」;Gateway memory-core 在同一工作区上建立 FTS + 向量 索引(在 provider 可用时)。
json
{
"agents": {
"defaults": {
"memorySearch": {
"enabled": true,
"provider": "openai",
"model": "text-embedding-3-small"
}
}
}
}
实际键名与可选字段(fallback、hybrid 权重等)以你使用的 OpenClaw 版本配置 schema 为准;可用 openclaw doctor memory 检查 embeddings ready / FTS ready 等状态。
关键说明:
indexing.enabled = false(默认):仅文件系统存储,搜索基于文件名匹配或简单遍历indexing.enabled = true:启用 SQLite 侧索引管线 ------至少包含 FTS5 ;在 Memory Search + provider 可用 时,还会对分块做 Embedding 并写入向量索引 ,检索可走 hybrid(向量 + FTS)- 无 Embedding Provider (或未启用
memorySearch):索引侧仍可建 FTS ;语义向量召回不可用 ,检索为 FTS-only (源码中的fts-only模型标签与searchKeyword路径)
4.2.3 Builtin 索引与搜索机制
重要前提 :Builtin 的 本地 indexing 需 memory.builtin.indexing.enabled = true。在此前提下:FTS 始终(在 FTS 模块可用时)参与关键词索引;向量 与 hybrid 合并 仅在 Embedding Provider 就绪 时启用------本节默认后者成立。
4.2.3.1 记忆检索的三层架构总结(Builtin)
Builtin 记忆检索架构
| 维度 | 第 1 层:即时上下文 | 第 2 层:索引检索 | 第 3 层:档案加载 |
|---|---|---|---|
| 来源 | 当前会话消息(内存中) | corpus/*.md; sessions/*/summary |
sessions/*/messages.jsonl; 历史会话完整记录 |
| 方式 | 直接访问 | 向量相似度(sqlite-vec) + FTS5 关键词 (memory-core 默认 hybrid 时合并;仅向量或仅关键词取决于配置与健康状态) |
文件系统直接读取 |
| 范围 | 当前对话 | 所有提炼知识 | 指定会话完整内容 |
| 场景 | 当前对话引用 | 知识查找 | 完整复盘、审计 |
| 示例 | 「刚才说的......」 | 「我之前说过......」 | 「复述完整对话」 |
检索优先级(从上到下):
- 当前上下文(最近 N 条消息)
- 相关 Session Summary → 选择性加载 messages.jsonl
- 对已纳入索引的
corpus/*.md与sessions/*/summary.md:memory_search查询向量化 → 向量 Top-K ,并与 FTS 命中在 hybrid 流程中 merge / 重排(无 provider 时退化为仅 FTS) - 历史 messages.jsonl(按需完整加载)
4.2.3.2 对三层架构的解释:分别在解决什么问题?
第 1 层:即时上下文(Working Memory)
- 是什么:当前会话中最近的消息(还在内存里,尚未落盘也没必要索引)。
- 解决什么 :回答"刚才你说的是什么""你上一句提到的库是什么"这类当前对话引用。
第 2 层:索引检索(Index-based Retrieval,启用 indexing 后;本节默认已配 Memory Search + Embedding)
- 索引对象 :
corpus/*.md:提炼后的"高密度知识"(用户偏好、项目上下文、Agent 知识等)sessions/*/summary.md:会话摘要(用于快速定位"哪次会话谈过什么")
- 技术(写入路径) :对每个 Markdown 分块(
chunkMarkdown)后,写入memory-core的 SQLite :- FTS5 虚拟表:关键词/BM25 类召回;
- 向量表(sqlite-vec) :对同一分块调用配置的 Embedding Provider 写入向量(与源码
writeChunks+replaceMemoryVectorRow一致)。
- 技术(查询路径) :对用户查询做 embedQuery ,向量 Top-K 与 FTS 关键词(若启用 hybrid 且 FTS 可用)合并排序;无 provider 时仅有 FTS 分支。
- 解决什么 :
- "我之前说过喜欢什么语言?"→ 语义上接近「技术栈 / Python」的 chunk 可被向量命中;关键词强相关时 FTS 亦会抬高命中片段。
- "我上周讨论过数据库选型,当时选了什么?"→ 先由 summary 的向量或 FTS 定位会话,再按需加载
messages.jsonl。
第 3 层:档案加载(Archive Loading)
- 对象 :
sessions/*/messages.jsonl(原始对话记录)。 - 方式 :不走 FTS;当你已经知道要读哪次会话时,直接把对应
messages.jsonl读出来(全量或按范围读)。 - 解决什么 :回答"把那次对话完整复述一遍""给出精确引用/审计"的完整复盘需求。
4.2.3.3 Builtin indexing 的范围:哪些会被写进 SQLite?
下表在 「indexing 已启用 + Memory Search Embedding 可用」 的前提下描述(与 FTS-only 相比多了一列「向量」)。
| 存储类型 | 文件位置 | 是否进入 FTS5 | 是否写入向量索引 | 原因 |
|---|---|---|---|---|
| Corpus 文件 | corpus/*.md |
✅ 是 | ✅ 是 | 高密度知识:关键词 + 语义双通道召回 |
| Session 摘要 | sessions/*/summary.md |
✅ 是 | ✅ 是 | 用较短文本定位「哪次会话谈过什么」,宜建向量与 FTS |
| Session 消息 | sessions/*/messages.jsonl |
❌ 否(默认) | ❌ 否(默认) | 原始记录体量大、噪音多;更适合 summary 定位 → 按需读 jsonl |
为什么 messages.jsonl 默认不索引?
messages.jsonl(原始对话)
├── 数据量大:每轮对话都追加
├── 噪音较多:包含问候、过渡语句
├── 检索效率低:需要索引/搜索大量低密度文本
└── 更优路径:先用 summary.md 定位会话 → 再按需加载 messages.jsonl
corpus/*.md(提炼知识)
├── 结构化:按主题整理
├── 密度高:只保留关键信息
└── 适合索引:FTS 快速匹配 + **Embedding 向量** + 可引用 snippet(本节默认)
4.2.3.4 一个例子:从对话落盘 → 提炼 → 索引 → 检索
场景设定:用户正在开发一个 AI 项目,与 Agent 进行多轮技术讨论。
Step 1: 多轮对话与 Messages 写入(原始对话落盘)
对话开始(Session ID: session-2026-04-12-001)
[Round 1]
User: 我正在做一个 AI 项目,需要处理大量文本数据,用什么语言比较好?
Agent: 对于 AI 和文本处理,Python 是首选。它有丰富的库如 transformers、
numpy、pandas。您有特定的性能要求吗?
User: 性能要求不高,但我希望开发速度快,维护简单。
Agent: Python 非常适合快速原型开发。如果您后期需要性能优化,
可以考虑用 Rust 重写关键模块。
[Round 2]
User: 我之前用 Java 写过类似项目,感觉很繁琐。
Agent: Java 的 boilerplate 确实较多。Python 的语法更简洁。
您之前 Java 项目用了什么框架?
User: Spring Boot,配置太复杂了。
Agent: 了解。Python 的 FastAPI 或 Flask 会轻量很多。
[Round 3]
User: 对了,我不喜欢写文档,有没有能自动生成文档的方案?
Agent: 可以用 Sphinx 或 MkDocs,结合 docstring 自动生成。
或者让 Agent 帮您整理代码注释生成文档。
User: 好主意,这个任务可以交给你。
Agent: 好的,我会在处理代码时同步整理文档。
[对话结束,Flush 触发 - Token 阈值达到]
写入 messages.jsonl(完整对话记录):
jsonl
// ~/.openclaw/memory/sessions/session-2026-04-12-001/messages.jsonl
{"role": "user", "content": "我正在做一个 AI 项目,需要处理大量文本数据,用什么语言比较好?", "timestamp": "2026-04-12T09:00:00Z"}
{"role": "assistant", "content": "对于 AI 和文本处理,Python 是首选...", "timestamp": "2026-04-12T09:00:05Z"}
{"role": "user", "content": "性能要求不高,但我希望开发速度快,维护简单。", "timestamp": "2026-04-12T09:00:30Z"}
{"role": "assistant", "content": "Python 非常适合快速原型开发...", "timestamp": "2026-04-12T09:00:35Z"}
{"role": "user", "content": "我之前用 Java 写过类似项目,感觉很繁琐。", "timestamp": "2026-04-12T09:01:00Z"}
{"role": "assistant", "content": "Java 的 boilerplate 确实较多...", "timestamp": "2026-04-12T09:01:05Z"}
{"role": "user", "content": "Spring Boot,配置太复杂了。", "timestamp": "2026-04-12T09:01:30Z"}
{"role": "assistant", "content": "了解。Python 的 FastAPI 或 Flask 会轻量很多。", "timestamp": "2026-04-12T09:01:35Z"}
{"role": "user", "content": "对了,我不喜欢写文档,有没有能自动生成文档的方案?", "timestamp": "2026-04-12T09:02:00Z"}
{"role": "assistant", "content": "可以用 Sphinx 或 MkDocs...", "timestamp": "2026-04-12T09:02:05Z"}
{"role": "user", "content": "好主意,这个任务可以交给你。", "timestamp": "2026-04-12T09:02:30Z"}
{"role": "assistant", "content": "好的,我会在处理代码时同步整理文档。", "timestamp": "2026-04-12T09:02:35Z"}
注意 :messages.jsonl 是原始对话记录,用于:
- 直接加载到上下文(近期消息)
- 提取洞察生成 corpus 文件
- 默认不进入 FTS / 向量索引管线(因为量大且噪音多;与 §4.2.3.3 一致)
Step 2: 提取洞察并写入 Corpus 文件
系统分析对话,提取关键信息并更新语料库:
写入 user-preferences.md(用户偏好):
markdown
# 用户偏好档案
## 技术栈偏好
- **主要语言**: Python(2026-04-12)
- 原因: AI 项目、文本处理、开发速度快
- 使用场景: 快速原型开发、AI/ML 项目
- **不喜欢的语言**: Java(2026-04-12)
- 原因: 语法繁琐、boilerplate 代码多
- 负面体验: Spring Boot 配置复杂
## 工作习惯
- **文档态度**: 不喜欢手动写文档(2026-04-12)
- 期望: 自动生成文档
- 可行方案: Sphinx、MkDocs、Agent 辅助生成
## 项目背景
- **当前项目**: AI 文本处理项目(2026-04-12)
- 性能要求: 不高
- 优先级: 开发速度 > 性能
写入 project-context.md(项目上下文):
markdown
# 项目上下文
## AI 文本处理项目(2026-04-12 开启)
- **技术选型**: Python
- **候选框架**: FastAPI、Flask(轻量级替代 Spring Boot)
- **潜在优化**: 后期可用 Rust 重写关键模块
- **文档策略**: 自动生成(Sphinx/MkDocs)
写入 agent-knowledge.md(Agent 知识):
markdown
# Agent 知识
## 用户委托的任务
- [ ] 处理代码时同步整理文档(2026-04-12 委托)
- 来源: Session session-2026-04-12-001
- 期望输出: 自动生成项目文档
Step 3: 建立索引(启用 Indexing + Memory Search:分块写入 SQLite,FTS5 + 向量)
与源码对齐 :下段代码为教学示意 ;真实函数名、SQL、
chunks/chunks_fts/chunks_vec的写入顺序与分支条件 见文末 「§10 附录:OpenClawmemory-core索引写入源码(完整摘录与解释)」。
ts
// 1. 扫描 corpus/ 与 sessions/*/summary.md(示意;messages.jsonl 仍默认不入索引)
const indexedMarkdown = [
'corpus/user-preferences.md',
'corpus/project-context.md',
'corpus/agent-knowledge.md',
'sessions/session-2026-04-12-001/summary.md'
];
// 2. 分块 → 每块写入 chunks 表 + FTS 行 +(本节)向量表(对应源码 `writeChunks` 循环体)
for (const file of indexedMarkdown) {
const chunks = chunkMarkdown(readFile(file));
const embeddings = await embedChunks(chunks); // 对应 `embedChunksInBatches` / `embedChunksWithBatch`
for (let i = 0; i < chunks.length; i++) {
// 2a `INSERT INTO chunks ...` | 2b `INSERT INTO chunks_fts` | 2c `replaceMemoryVectorRow` → chunks_vec
// 单块 id 公式、SQL 文本与分支(vectorReady / fts.available)见 §10 附录全文。
}
}
// messages.jsonl:仍默认不进入上述索引管线(见 §4.2.3.3)
Step 4: 后续会话中的 Memory 搜索(典型场景;默认 hybrid)
以下 memory_search 路径在 Embedding Provider 可用 时,与 memory-core 的 searchVector + searchKeyword + mergeHybridResults 一致;示意中仍写出 SQL MATCH 便于对照 FTS 这一路 ,但最终排序 来自 hybrid 合并,而非纯 SQL 一路。
场景 A:用户询问「偏好 / 事实」(第 2 层:向量为主 + FTS 为辅)
User(3天后): 我之前说用什么语言做 AI 项目来着?
Agent: [memory_search]
1) q_vec = embedQuery("我之前说用什么语言做 AI 项目来着?")
2) vec_hits = topK_similarity(q_vec, corpus+summary chunks) // 语义:「语言选型 / Python」等
3) fts_hits = FTS MATCH 扩展关键词(如 "AI 项目 语言") // 词面命中补强
4) merged = mergeHybridResults(vec_hits, fts_hits, ...)
返回结果(示意,与合并分数一致):
1. path: user-preferences.md
snippet: "主要语言: Python(2026-04-12)原因: AI 项目、文本处理"
score: 0.95
citation: [^1^]
2. path: project-context.md
snippet: "技术选型: Python"
score: 0.88
citation: [^2^]
Agent 回复: 您之前提到要用 Python 做 AI 文本处理项目[^1^],
具体技术选型包括 FastAPI 或 Flask 作为 Web 框架[^2^]。
场景 B:Agent 主动提醒待办(第 2 层:向量 + FTS)
Agent(下次对话开始时): [内部 memory_search]
Query: "委托 Agent 任务 未完成"
→ embedQuery + vector topK +(可选)FTS「委托」「任务」
返回结果(示意):
path: agent-knowledge.md
snippet: "[ ] 处理代码时同步整理文档(2026-04-12 委托)"
Agent: 对了,之前您委托我在处理代码时同步整理文档,
我会继续留意这个功能。
场景 C:复杂查询(第 2 层:多子查询 + 各自 hybrid 再聚合)
User: 我之前不喜欢 Java 的什么特性?现在项目进展如何?
Agent: [并行 memory_search]
Query 1: "不喜欢 Java" → embed + FTS → 偏好类 chunk
Query 2: "项目进展" → embed + FTS → project-context chunk
结果 1(来自 user-preferences.md):
- 不喜欢的语言: Java
- 原因: 语法繁琐、boilerplate 代码多
- 负面体验: Spring Boot 配置复杂
结果 2(来自 project-context.md):
- AI 文本处理项目(2026-04-12 开启)
- 技术选型: Python(已确定)
- 候选框架: FastAPI、Flask
Agent 综合回复: 您之前提到不喜欢 Java 是因为语法繁琐、
boilerplate 代码多,特别是 Spring Boot 的配置很复杂。
目前您的 AI 文本处理项目已确定使用 Python,正在考虑
FastAPI 或 Flask 作为 Web 框架。
场景 D:搜索「我上周聊过什么」(第 2 层:Summary 向量 / FTS hybrid → 第 3 层:按需加载)
原始对话(messages.jsonl)仍默认不进入 FTS/向量索引;系统通过 summary.md 的索引分块 做 语义 + 关键词 召回以定位会话。
User: 我上周讨论过数据库选型,当时选了什么?
Agent: [检索流程]
Step 1: 在 sessions/*/summary.md 的索引分块上 memory_search
- 向量:embedQuery("数据库选型 上周") → topK 命中 summary 中「PostgreSQL vs MySQL」段落
- FTS:MATCH「数据库」「选型」等(hybrid 时与向量合并)
返回结果:
path: sessions/session-2026-04-10-003/summary.md
snippet: "会话主题:数据库选型...最终选择 PostgreSQL..."
session_id: session-2026-04-10-003
date: 2026-04-10
Step 2: 定位并加载原始对话
读取: sessions/session-2026-04-10-003/messages.jsonl
加载相关片段到上下文
Agent 回复: 上周(4月10日)您讨论了数据库选型,对比了 PostgreSQL 和 MySQL,
最终选择了 PostgreSQL,主要考虑它支持 JSON 和更好的扩展性。
工作机制说明:
┌─────────────────────────────────────────────────────────────┐
│ Session 记忆检索架构(本节默认) │
├─────────────────────────────────────────────────────────────┤
│ │
│ sessions/session-2026-04-10-003/ │
│ ├── messages.jsonl ← 原始对话(默认不入 FTS/向量,按需加载)│
│ ├── summary.md ← 会话摘要(✅ FTS5 + ✅ 向量 索引分块)│
│ └── metadata.json ← 会话元数据(主题、时间、标签) │
│ │
│ 检索流程: │
│ 1. memory_search(summary 索引):向量 Top-K +(可选)FTS │
│ 2. 读取 metadata.json → 确认会话信息 │
│ 3. 加载 messages.jsonl → 获取原始对话内容 │
│ │
└─────────────────────────────────────────────────────────────┘
补充:Session Summary 的生成机制
- 何时生成 Summary?
- 会话结束时(Flush 触发)
- 达到 Token 阈值触发 Compaction 时
- 手动执行
/summarize命令时
- 生成流程 :
messages.jsonl(完整对话)→ LLM 摘要 →summary.md(结构化摘要,随后进入 FTS + 向量 索引分块)
Summary 内容示例:
markdown
# Session Summary - session-2026-04-10-003
## 主题
数据库选型讨论:PostgreSQL vs MySQL
## 关键决策
- 选择 PostgreSQL
- 原因:JSON 支持、扩展性好
## 待办事项
- [ ] 安装 PostgreSQL
- [ ] 设计数据库 Schema
## 相关文件
- project-context.md(已更新数据库选型信息)
为什么这样设计?
| 对比 | messages.jsonl | summary.md |
|---|---|---|
| 体积 | 大(逐条记录) | 小(精炼摘要) |
| 检索效率 | 低(需遍历) | 高(FTS + 向量索引分块) |
| 用途 | 完整复盘 | 快速定位 |
| 更新频率 | 实时追加 | 阶段性生成 |
场景 E:完整复盘/审计(直接走第 3 层:加载 messages.jsonl)
User: 把我3天前关于 Python 的对话完整复述一遍
Agent: [注意:这不是 FTS 搜索,而是直接加载 messages.jsonl]
操作过程:
1. 确定目标会话: session-2026-04-12-001(通过时间或元数据)
2. 直接读取文件: ~/.openclaw/memory/sessions/session-2026-04-12-001/messages.jsonl
3. 加载完整对话内容到上下文
4. 整理后回复用户
Agent: 3天前(2026-04-12)您开启了 AI 项目的讨论,我们共进行了3轮对话...
[完整复述对话内容]
搜索方式对比(本节默认两者皆可用,并由 hybrid 合并):
| 方式 | 特点 | 查询侧示意 |
|---|---|---|
| FTS5 | 词面 / BM25 类相关性;对话式问句常配合 query-expansion 抽关键词 | chunks_fts MATCH '...' |
| 向量(sqlite-vec) | 语义相似度;查询先 embedQuery 再 Top-K | similarity(query_vec, chunk_vec) |
关键实现(与 extensions/memory-core 对齐的简化心智模型):
ts
// 分块(示意)
function chunkMarkdown(content: string): MemoryChunk[] { /* ... */ }
// 有 Embedding Provider:向量 +(默认)FTS hybrid
async function memorySearchHybrid(query: string) {
const keywordHits = await searchKeyword(query); // FTS5
const qVec = await embedQueryWithTimeout(query);
const vectorHits = await searchVector(qVec); // sqlite-vec
return mergeHybridResults({ vector: vectorHits, keyword: keywordHits, ... });
}
// 无 Provider:退化为 FTS-only(源码路径 !provider)
async function memorySearchFtsOnly(query: string) {
return searchKeyword(expandQueryForFts(query));
}
4.3 QMD 后端(Queryable Memory Database)
QMD (Queryable Memory Database)是 OpenClaw 的本地"可查询记忆数据库" :在本地落盘的同时,为记忆建立结构化索引(SQLite)+ 语义索引(Vector/Embedding),让你可以用自然语言在历史信息里做"像搜索一样"的召回。
4.3.1 QMD 的核心特点
| 特性 | 说明 |
|---|---|
| 本地存储 | 基于 SQLite,数据保存在本地文件 |
| 向量索引 | 使用 Embedding 模型生成向量索引 |
| 语义搜索 | 支持自然语言查询,找到语义相关的内容 |
| 跨会话检索 | 可以搜索历史会话中的相关内容 |
| 隐私保护 | 数据不出本地,适合敏感场景 |
4.3.2 QMD 的"记忆检索分层"是什么样的?
Builtin(本节前提)已是「工作区文件 + memory-core 本地 SQLite 」上的 FTS + 向量 hybrid ;QMD 则是另一套 以 QMD 专用库与配置为中心的本地记忆后端(embedding 常作为 QMD 配置的最高优先级)。为便于对照,仍可把 QMD 看成同样的三层,只是数据布局、跨会话范围与运维路径与 Builtin 不同:
| 层级 | 你在找什么 | QMD 怎么做 | 结果形态 |
|---|---|---|---|
| 第 1 层:即时上下文 | "刚才你说的那句" | 直接使用当前会话消息 | 直接引用原句 |
| 第 2 层:索引检索(增强版) | "我之前说过/做过/决定过什么" | FTS 关键词 + 向量相似度(可混合)检索历史内容 | 相关片段 + 打分 + 引用 |
| 第 3 层:档案加载 | "把那次对话完整复盘" | 定位会话/片段后,再按需加载原始记录(若保留) | 全量对话或大段原文 |
直觉上:在都启用语义索引时 ,Builtin 与 QMD 的「第 2 层」都可很强;差别更多在 后端是否是 Builtin 目录树 vs 独立 QMD DB 、以及 embedding / 跨会话策略 的配置入口与产品化路径。
4.3.3 QMD 工作原理(写入与查询)
查询(Query Path)
写入(Write Path)
检索
检索
消息/文档写入
分块 Chunking
FTS 全文索引
生成 Embedding
向量索引 Vector Index
SQLite
自然语言查询
查询扩展/关键词抽取
生成查询向量
向量相似度 Top-K
FTS MATCH(可选)
混合排序/去重 Merge
返回相关片段 + 分数 + 引用
4.3.4 QMD 配置示例
json
{
"memory": {
"backend": "qmd",
"qmd": {
"dbPath": "./.memory/qmd.db",
"embedding": {
"provider": "openai",
"model": "text-embedding-3-small"
},
"scope": {
"allowDirect": true,
"allowGroup": true,
"allowChannel": false
}
}
}
}
4.3.5 典型场景:什么时候你会"明显感觉到 QMD 比 Builtin 强"?
| 场景 | 说明 | 示例 |
|---|---|---|
| 概念同义检索 | 不记得关键词也能找到 | "上次聊的那个'入口层'是啥?"→ 召回 Gateway/Router 相关讨论 |
| 代码知识库 | 存储项目代码片段,支持语义搜索 | "找一下处理登录的代码"→ 召回鉴权中间件/handler 的片段 |
| 文档检索 | 长文档里定位"相关段落"而不是"关键词命中" | "查找关于 Docker 配置的部分"→ 召回网络/volume/权限相关段落 |
| 历史会话定位 | 用自然语言跨会话找"当时的结论" | "我之前问过怎么部署的问题"→ 召回结论与关键步骤 |
| 混合检索 | 既要"术语准确"又要"语义相关" | "找我们提到过的 'contextWindow' 计算规则"→ FTS 定位 + 向量补全 |
4.4 Honcho 服务(云端记忆)
Honcho (Honcho Memory Service)是一个第三方云端 AI 原生记忆服务 (honcho.dev)。和 QMD"把检索能力做在本地"不同,Honcho 走的是托管化(Managed)记忆:把会话、知识与画像(Representation)交给云端维护,并提供更高层的查询接口(例如问"这个用户有什么偏好?")。
4.4.1 Honcho 的核心能力
| 能力 | 说明 | 优势 |
|---|---|---|
| 自动推理 | 自动从对话中提取洞察和结论 | 无需手动整理,自动生成用户画像 |
| 完美回忆 | 支持自然语言查询历史对话 | "用户之前说过喜欢什么技术栈?" |
| Token 节省 | 相比传统方式节省 60-90% Token | 降低 API 成本 |
| 多 Agent 协作 | 多个 Agent 共享记忆 | 团队级知识沉淀 |
| Dialectic API | 支持问答式记忆查询 | 直接问 "这个用户有什么偏好?" |
4.4.2 Honcho 的数据模型(Data Model)
Workspace(工作空间)
├── Peer(实体:用户、Agent、群组)
│ ├── Session(会话线程)
│ │ ├── Message(消息)
│ │ └── Message(消息)
│ └── Representation(自动生成的画像/洞察)
└── Peer(另一个实体)
4.4.3 把 Honcho 和 Builtin/QMD 对齐理解:它"索引"的是什么?
在 Builtin/QMD 里,"索引"往往指 FTS/向量索引 ;在 Honcho 里,"索引"更多体现为云端的两类产物:
- History(历史):原始消息/会话(可被检索、可被按需取回)
- Representations(画像/洞察):从历史中自动提炼的高层记忆(偏好、决策、长期目标、常用工具等)
因此,Honcho 的检索通常是"先拿 Representations 快速回答",必要时再回查 History 取证------这与 Builtin "summary 定位 → messages 加载"在思路上是同构的,只是提炼与索引由云端托管完成。
4.4.4 Honcho 配置示例
json
{
"memory": {
"backend": "honcho",
"honcho": {
"apiKey": "${HONCHO_API_KEY}",
"baseUrl": "https://api.honcho.dev",
"workspace": "my-team-workspace",
"context": {
"maxTokens": 4000,
"includeHistory": true,
"includeRepresentations": true
}
}
}
}
4.4.5 Honcho 的典型使用方式:为什么它能省 Token?
核心原因 :你不需要把大量历史消息"塞回模型上下文"------而是让 Honcho 返回一小段可用的记忆摘要/画像,作为更短的上下文补充。
一个典型流程可以概括为:
用户问题
↓
Honcho Query(优先返回 Representations + 必要的引用)
↓
把"少量、高密度"的记忆片段拼到 Prompt 里
↓
LLM 生成回答(必要时再回查历史取证)
4.4.6 典型场景:哪些需求更适合 Honcho?
| 场景 | 说明 | 示例 |
|---|---|---|
| 用户画像 | 自动构建用户偏好模型 | "这个用户喜欢 Python 还是 JavaScript?" |
| 团队协作 | 多 Agent 共享项目知识 | "前端 Agent 知道后端 Agent 的 API 设计" |
| 长期关系 | 维护跨会话的用户关系 | "用户三个月前提过的需求" |
| 智能推荐 | 基于历史行为推荐 | "根据用户过去的代码风格生成建议" |
选择建议:如果你需要跨设备/跨成员共享 、需要长期沉淀用户画像 、或需要把"记忆工程"外包成托管能力,Honcho 往往比本地方案更省心;但它也引入了云端依赖与合规/隐私边界(这在生产环境需要单独评估)。
4.5 如何选择后端?
| 需求 | 推荐后端 | 理由 |
|---|---|---|
| 快速启动,简单使用 | Builtin | 零配置,开箱即用 |
| 本地项目,代码检索 | QMD | 本地语义搜索,隐私保护 |
| 团队协作,用户画像 | Honcho | 云端共享,自动推理 |
| 敏感数据,不出本地 | Builtin / QMD | 完全本地存储 |
| 生产环境,规模部署 | Honcho | 云端托管,可扩展 |
5 人格文件分界:八类记忆文件
OpenClaw 使用八类 Markdown 文件来定义 Agent 的"人格"和"记忆"。这些文件既是 Memory 的输入源,也是 Agent 行为的指导。
5.1 八类人格文件总览
| 文件 | 优先级 | 用途 | 示例内容 |
|---|---|---|---|
| AGENTS.md | 10(最高) | Agent 配置与行为规则 | 工具使用策略、响应风格 |
| SOUL.md | 20 | 人格与语气定义 | 性格描述、沟通方式 |
| IDENTITY.md | 30 | 身份标识 | Agent 名称、版本 |
| USER.md | 40 | 用户信息 | 用户偏好、背景 |
| TOOLS.md | 50 | 工具配置 | 工具使用指南 |
| BOOTSTRAP.md | 60 | 启动配置 | 初始化指令 |
| MEMORY.md | 70 | 记忆配置 | 记忆 Flush 策略 |
| HEARTBEAT.md | 动态 | 心跳检查配置 | 定期检查任务 |
5.1.1 八类文件的"内容清单"(可直接当模板用)
下面按每个文件给出一个推荐结构 (你可以按需删减)。这些文件的共同点是:越靠前的文件越"规范/规则",越靠后的文件越"运行时/策略"。
1) AGENTS.md(Agent Policy / Top-level Rules)
建议包含:
- 目标与边界:Agent 的职责范围、明确不做什么
- 交互风格:输出语言、格式偏好、是否先给结论、是否给步骤
- 工具与权限策略:默认允许/禁止、需要用户确认的操作类型
- 失败策略:信息不足时如何提问、遇到不确定时如何处理
- 工程约束:代码风格、提交规范、测试要求(如果是编码助手)
示例结构:
markdown
# AGENTS.md
## 目标
- 你是一个面向工程交付的助手:优先给可执行方案与可验证结果
## 禁止事项(Hard Rules)
- 不要泄露密钥/Token,不要输出隐私信息
- 不要执行破坏性命令(除非用户明确要求并二次确认)
## 输出规范(Response Style)
- 默认中文(第一次出现的术语补充英文)
- 重要结论加粗;代码/路径用反引号
- 长任务:先给短计划,再执行
## 工具使用策略(Tooling)
- 读文件前先说明目的;修改前先读取
- 写入前做最小变更;修改后检查 lints
## 不确定性处理(Uncertainty)
- 用"最可能的解释 + 可验证步骤"推进
2) SOUL.md(Persona / Tone / Communication)
建议包含:
- 身份设定:角色、语气(Tone)、交流态度(Attitude)
- 解释偏好:类比/图表/代码优先级
- 冲突处理:用户要求与安全/规范冲突时怎么说
- 个性化偏好:例如更偏"直给结论"或"循序渐进"
示例结构:
markdown
# SOUL.md
## 语气(Tone)
- 专业、耐心、克制,不夸张
## 解释风格(Explanation Style)
- 先给结论,再给原因与例子
- 复杂概念优先用流程图/表格
## 互动策略(Interaction)
- 对不清晰的需求:先复述理解,再提出 1-3 个关键澄清点
3) IDENTITY.md(Identity / Version / Capabilities Summary)
建议包含:
- 名称与版本:Agent 名称、版本号、变更摘要
- 能力范围:能做什么(高层),不能做什么
- 运行环境:OS/语言栈/默认工具链(可选)
- 联系信息/归属:团队/项目(可选)
示例结构:
markdown
# IDENTITY.md
## 名称与版本
- Name: OpenClaw-DocWriter
- Version: 0.3
## 能力概述
- 擅长:技术文档、源码解读、架构拆解、示例构造
- 不擅长:未经验证的具体实现细节(需要以仓库为准)
4) USER.md(User Profile / Preferences / Constraints)
建议包含:
- 用户偏好 :语言、格式、细节程度(与
user-preferences.md的关系见后文) - 背景信息:项目背景、技能水平、常用技术栈
- 约束条件:时间/资源/合规/部署环境
- 长期目标:例如"写系列博客""维护某项目"
示例结构:
markdown
# USER.md
## 偏好(Preferences)
- 输出:中文(术语首次出现补英文)
- 文档:强调"从源码到机制"的解释
## 背景(Background)
- 维护:OpenClaw 深度解析系列
- 环境:Windows + PowerShell
5) TOOLS.md(Tools Catalog / Usage / Safety)
建议包含:
- 可用工具清单:工具名、用途、输入输出要点
- 安全规则:哪些工具需要确认、哪些路径禁止写
- 调用风格:何时用搜索、何时读文件、何时跑命令
- 错误处理:工具失败如何降级/重试
示例结构:
markdown
# TOOLS.md
## 工具使用原则
- 修改前先读取;修改后检查 lints
## 危险操作
- 删除/覆盖/网络外发:必须明确确认
6) BOOTSTRAP.md(Bootstrap / Session Start Checklist)
建议包含:
- 启动时要做的事:读取哪些关键文件、确认哪些约束
- 工作区约定:目录结构、命名规范、输出位置
- 初始化自检:例如 git 状态、依赖是否齐全
示例结构:
markdown
# BOOTSTRAP.md
## 会话启动流程
- 读取计划文档与当前目标文件
- 明确"不引用/不链接"的写作约束(如适用)
- 若是代码任务:先检查 git status 与 lints
7) MEMORY.md(Memory Policy / Flush & Retrieval Strategy)
建议包含:
- 记忆写入策略:何时 flush,写入哪些 corpus 文件
- 记忆检索策略:优先检索什么(偏好/项目上下文/会话摘要)
- 隐私策略:哪些信息不进入长期记忆
- 后端选择:Builtin/QMD/Honcho 的使用建议(可选)
示例结构:
markdown
# MEMORY.md
## 写入(Flush Policy)
- 触发条件:会话结束 / Token 阈值 / 手动命令
- 写入目标:user-preferences.md、project-context.md、agent-knowledge.md
## 检索(Retrieval Policy)
- 优先级:即时上下文 → corpus/summary 索引 → 按需加载原始对话
## 隐私
- 不记录:密钥、个人敏感信息、未确认的推断
8) HEARTBEAT.md(Heartbeat / Cron / Periodic Tasks)
建议包含:
- 周期性任务:定期检查、提醒、清理、同步
- 触发频率:每小时/每天/每次会话开始等
- 输出格式:心跳报告模板(例如 checklist)
- 停止条件:何时不触发(例如用户显式关闭)
示例结构:
markdown
# HEARTBEAT.md
## 周期任务
- 每日:检查未完成事项(agent-knowledge.md)
- 每周:回顾项目上下文是否过期(project-context.md)
## 输出模板
- 今日待办:
- 风险提示:
- 下一步建议:
5.2 优先级与覆盖机制
ts
// src/agents/system-prompt.ts(节选)
const CONTEXT_FILE_ORDER = new Map<string, number>([
["agents.md", 10], // 最高优先级
["soul.md", 20],
["identity.md", 30],
["user.md", 40],
["tools.md", 50],
["bootstrap.md", 60],
["memory.md", 70], // 最低优先级
]);
优先级逻辑:数字越小优先级越高。高优先级文件可以覆盖低优先级文件的配置。
5.3 如何"写好这 8 类文件"?(落地建议)
上面 5.1.1 已经给了每个文件的推荐结构与模板。实践中建议按下面顺序落地,能最大化"规则不打架":
- 先写
AGENTS.md(规则)再写SOUL.md(语气):先把边界与工具策略定死,再定义沟通风格。 IDENTITY.md/USER.md只放稳定事实:身份、环境、长期偏好等;"临时偏好"不要写在这里,避免频繁变更。TOOLS.md/BOOTSTRAP.md保持可执行:写成 checklist 与明确操作规约,避免"空话原则"。MEMORY.md只写策略,不写具体偏好 :偏好与项目事实应进入corpus/(例如user-preferences.md、project-context.md)。HEARTBEAT.md只放周期性任务 :把"每次会话都要做"的内容放BOOTSTRAP.md,把"定期做"的放这里。
5.5 记忆文件的收集与更新机制
人格文件和记忆文件(如 user-preferences.md、project-context.md)不是静态的------它们可以通过多种方式被收集、创建和更新。
5.5.1 三种收集方式
| 方式 | 触发时机 | 执行者 | 说明 |
|---|---|---|---|
| 显式声明 | 初始化时 | 用户/开发者 | 手动创建和编辑文件 |
| 工具写入 | 对话中 | Agent | Agent 识别到偏好,使用 write 工具更新 |
| Learning 提取 | 定期/会话结束 | Learning 模块 | 自动从会话历史中提取洞察 |
5.5.2 显式声明(手动创建)
用户或开发者可以直接创建记忆文件:
bash
# 创建用户偏好文件
cat > ~/.openclaw/memory/corpus/user-preferences.md << 'EOF'
# 用户偏好
## 技术栈偏好
- 主要语言:TypeScript
- 前端框架:React
- 后端框架:Node.js/Express
## 代码风格
- 缩进:2 个空格
- 分号:使用
- 引号:单引号优先
## 沟通偏好
- 解释详细程度:详细
- 代码示例:需要完整示例
- 术语:技术术语保留英文
EOF
5.5.3 工具写入(Agent 自动更新)
Agent 可以在对话过程中使用 write 工具动态更新记忆文件:
User: 以后给我代码示例都用 Python 吧,我不太熟悉 JavaScript
Agent: 好的,我记下了。以后我会优先使用 Python 提供代码示例。
[Agent 内部执行:
write("memory/corpus/user-preferences.md",
"## 代码示例偏好\n- 优先语言:Python\n- 原因:用户不熟悉 JavaScript"
)
]
代码层面的实现:
ts
// Agent 识别到用户偏好后,调用 write 工具
const result = await writeTool.execute("update-preferences", {
path: "memory/corpus/user-preferences.md",
content: `# 用户偏好
## 技术栈偏好
- 主要语言:Python(用户明确偏好,2026-04-12)
`
});
5.5.4 Learning 自动提取
更高级的方式是通过 Learning 机制(第8篇内容)自动从会话中提取:
输出
更新 user-preferences.md
更新 project-context.md
更新 agent-knowledge.md
Learning 处理
分析对话内容
提取偏好/洞察
生成摘要
输入
会话历史
示例:用户偏好的演化
第一轮对话:
User: 你好,我是 Python 开发者
Agent: [写入 user-preferences.md] 主要语言:Python
第三轮对话:
User: 给我看看 Flask 的示例
Agent: [更新 user-preferences.md] Web 框架:Flask
第十轮对话:
User: 实际上我最近在用 FastAPI 了
Agent: [更新 user-preferences.md] Web 框架:FastAPI(从 Flask 迁移)
5.5.5 实际应用场景
| 场景 | 收集方式 | 说明 |
|---|---|---|
| 初始化配置 | 显式声明 | 项目开始时手动创建基础偏好 |
| 对话中调整 | 工具写入 | 用户说"我喜欢..."时即时更新 |
| 长期积累 | Learning 提取 | 从多轮对话中自动总结用户习惯 |
| 项目知识 | 混合 | 显式文档 + 对话中提取的项目细节 |
💡 最佳实践:
- 基础偏好(技术栈、沟通风格)→ 显式声明
- 动态偏好(临时需求、新习惯)→ 工具写入
- 长期洞察(用户画像、行为模式)→ Learning 提取
这种设计让记忆文件既可以是静态配置 (用户维护),也可以是动态记忆(Agent 自动更新),实现了"人机协作"的记忆管理。
6 Memory 与 Learning 的边界
根据计划文档,第7篇(Memory)与第8篇(Learning)有明确分工:
| 维度 | Memory(本篇) | Learning(第8篇,已发表) |
|---|---|---|
| 关注点 | 子系统实现 | 学习语义与自修改 |
| 核心问题 | "如何存储和检索?" | "如何学习和进化?" |
| 技术实现 | Flush/Index/Backend | Distill/自适应/文件重写 |
| 用户可见 | 记忆的读写操作 | 长期行为的改变 |
7 多 Agent 下的 Memory 隔离
在多 Agent 场景下,Memory 需要处理隔离与共享的平衡。
7.1 隔离模型
| 资源 | 隔离级别 | 说明 |
|---|---|---|
| 会话记忆 | 完全隔离 | 每个 Agent 有自己的会话历史 |
| 人格文件 | Agent 独立 | 每个 Agent 有自己的 SOUL.md |
| 共享知识 | 可选共享 | 项目级语料库可以共享 |
| 系统记忆 | 全局共享 | OpenClaw 级别的配置 |
7.2 多 Agent Memory 配置
json
{
"agents": {
"coding-agent": {
"workspace": "./workspaces/coding",
"memory": {
"backend": "builtin",
"storageDir": "./workspaces/coding/.memory"
}
},
"research-agent": {
"workspace": "./workspaces/research",
"memory": {
"backend": "qmd",
"qmd": {
"dbPath": "./workspaces/research/.memory/qmd.db"
}
}
}
}
}
8 本篇小结与下一篇预告
-
小结:
- Memory = Agent 的"长期记忆",让 Agent 能够记住"你是谁"、"之前聊过什么"
- 三层架构:应用层(Flush/Search)、运行时层(Runtime/State)、后端层(Builtin/QMD/Honcho)
- 三种记忆后端 :
- Builtin:本地文件系统,零依赖,适合个人使用
- QMD:本地向量数据库(SQLite),支持语义搜索,适合代码检索
- Honcho:云端记忆服务,自动推理,支持多 Agent 协作
- 核心操作:Flush(保存)、Search(检索)、Indexing(索引)
- 八类人格文件:AGENTS.md(最高优先级)→ SOUL.md → IDENTITY.md → ... → MEMORY.md
- 记忆文件的三种收集方式 :
- 显式声明:用户手动创建(适合基础偏好)
- 工具写入:Agent 动态更新(适合对话中识别的偏好)
- Learning 提取:自动从会话历史中提取(适合长期洞察)
- 多 Agent 隔离:每个 Agent 有独立的 Memory 存储,可选共享项目级知识
-
下一篇(第8篇)------Learning :与本篇(第7篇 Memory)侧重「如何存储与检索 」相对,第8篇关注「如何学习与进化」(语义与行为):从会话与运行痕迹中沉淀可复用的策略与偏好。
9 参考文献与链接
- OpenClaw 主仓库:https://github.com/openclaw/openclaw
- Memory System 文档(D1):http://clawdocs.org/architecture/memory-system
- 内置记忆文档(D2):docs/concepts/memory-builtin.md
- QMD 记忆文档(D2):docs/concepts/memory-qmd.md
- Honcho 记忆文档(D2):docs/concepts/memory-honcho.md
- 第8篇 Learning(已发表):12_learning_adaptation_openclaw.md
- D3 参考:
- 源码入口:
src/plugins/memory-state.ts------ Memory 状态管理src/plugins/memory-runtime.ts------ Memory 运行时src/auto-reply/reply/memory-flush.ts------ Memory Flushsrc/plugin-sdk/memory-*.ts------ Memory SDKsrc/memory-host-sdk/------ Memory Host SDK
- 索引写入(
chunks/chunks_fts/chunks_vec) :见本篇 §10 附录 (extensions/memory-core/src/memory/manager-embedding-ops.ts等)
10 附录:OpenClaw memory-core 索引写入源码(完整摘录与解释)
说明 :以下 TypeScript 代码摘录自 OpenClaw 主仓库中的
extensions/memory-core(与src/memory-host-sdk等共同构成 Gateway 侧记忆索引能力)。你本机若使用packages/memory-core的构建副本,请以 同一提交 下的对应文件为准。行号随版本可能变化,以文件内实际内容为准。
10.1 与 §4.2.3.4「Step 3」的对应关系
| 教学小节(Step 3) | 源码入口 | 行为摘要 |
|---|---|---|
| 扫描 Markdown → 分块 | indexFile 内 chunkMarkdown + filterNonEmptyMemoryChunks |
读文件、按配置分块;会话源可能 remapChunkLines |
embedChunks |
embedChunksInBatches / embedChunksWithBatch(未在下方全文展开) |
调用 Embedding Provider,得到 number[][] |
| 每块写入三张「逻辑表」 | 私有方法 writeChunks |
先 clearIndexedFileData 清旧索引,再循环:INSERT chunks →(条件)replaceMemoryVectorRow →(条件)INSERT chunks_fts |
| 向量表是否就绪 | ensureVectorReady + ensureVectorTable |
加载 sqlite-vec 扩展、用 vec0 建 chunks_vec |
writeChunks 单轮循环内的顺序(有向量时) :chunks 行(含 JSON 化 embedding)→ chunks_vec (BLOB)→ chunks_fts(全文索引行)。这与「先结构化落库、再加速检索」的分工一致。
10.2 manager-vector-write.ts(向量行:DELETE + INSERT)
chunks_vec 没有 使用 SQL 标准 UPSERT 名称的 API;封装为 replaceMemoryVectorRow :先按 id 删除旧行(忽略异常),再插入新 embedding(Float32Array → Buffer)。
typescript
import type { SQLInputValue } from "node:sqlite";
type VectorWriteDb = {
prepare: (sql: string) => {
run: (...params: SQLInputValue[]) => unknown;
};
};
const vectorToBlob = (embedding: number[]): Buffer =>
Buffer.from(new Float32Array(embedding).buffer);
export function replaceMemoryVectorRow(params: {
db: VectorWriteDb;
id: string;
embedding: number[];
tableName?: string;
}): void {
const tableName = params.tableName ?? "chunks_vec";
try {
params.db.prepare(`DELETE FROM ${tableName} WHERE id = ?`).run(params.id);
} catch {}
params.db
.prepare(`INSERT INTO ${tableName} (id, embedding) VALUES (?, ?)`)
.run(params.id, vectorToBlob(params.embedding));
}
10.3 manager-fts-state.ts(重索引前删除 FTS 行)
clearIndexedFileData 在删 chunks 行之前,会调用 deleteMemoryFtsRows :若存在当前 embedding model,则只删该 model 下该 path 的 FTS 行,避免多模型混写时的脏数据。
typescript
import type { DatabaseSync } from "node:sqlite";
import type { MemorySource } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
export function deleteMemoryFtsRows(params: {
db: DatabaseSync;
tableName?: string;
path: string;
source: MemorySource;
currentModel?: string;
}): void {
const tableName = params.tableName ?? "chunks_fts";
if (params.currentModel) {
params.db
.prepare(`DELETE FROM ${tableName} WHERE path = ? AND source = ? AND model = ?`)
.run(params.path, params.source, params.currentModel);
return;
}
params.db
.prepare(`DELETE FROM ${tableName} WHERE path = ? AND source = ?`)
.run(params.path, params.source);
}
10.4 manager-sync-ops.ts(常量、ensureVectorReady、ensureVectorTable)
向量表物理名为 chunks_vec ,与 manager-embedding-ops.ts 中 VECTOR_TABLE 一致;建表使用 sqlite-vec 的 vec0 模块,embedding 维度由首次成功 embedding 的向量长度决定。
typescript
const VECTOR_TABLE = "chunks_vec";
const FTS_TABLE = "chunks_fts";
const VECTOR_LOAD_TIMEOUT_MS = 30_000;
// ... class MemoryManagerSyncOps ...
protected async ensureVectorReady(dimensions?: number): Promise<boolean> {
if (!this.vector.enabled) {
return false;
}
if (!this.vectorReady) {
this.vectorReady = this.withTimeout(
this.loadVectorExtension(),
VECTOR_LOAD_TIMEOUT_MS,
`sqlite-vec load timed out after ${Math.round(VECTOR_LOAD_TIMEOUT_MS / 1000)}s`,
);
}
let ready = false;
try {
ready = (await this.vectorReady) || false;
} catch (err) {
const message = formatErrorMessage(err);
this.vector.available = false;
this.vector.loadError = message;
this.vectorReady = null;
log.warn(`sqlite-vec unavailable: ${message}`);
return false;
}
if (ready && typeof dimensions === "number" && dimensions > 0) {
this.ensureVectorTable(dimensions);
}
return ready;
}
private ensureVectorTable(dimensions: number): void {
if (this.vector.dims === dimensions) {
return;
}
if (this.vector.dims && this.vector.dims !== dimensions) {
this.dropVectorTable();
}
this.db.exec(
`CREATE VIRTUAL TABLE IF NOT EXISTS ${VECTOR_TABLE} USING vec0(\n` +
` id TEXT PRIMARY KEY,\n` +
` embedding FLOAT[${dimensions}]\n` +
`)`,
);
this.vector.dims = dimensions;
}
private dropVectorTable(): void {
try {
this.db.exec(`DROP TABLE IF EXISTS ${VECTOR_TABLE}`);
} catch (err) {
const message = formatErrorMessage(err);
log.debug(`Failed to drop ${VECTOR_TABLE}: ${message}`);
}
}
解释 :indexFile 在拿到非空 embeddings 后调用 ensureVectorReady(sample.length);若扩展加载失败则 vectorReady === false,writeChunks 仍会写 chunks 与 chunks_fts ,但跳过 replaceMemoryVectorRow,并在日志中提示向量召回降级(见 10.5 末尾 log.warn)。
10.5 memory-schema.ts(chunks 表与 chunks_fts 虚拟表 DDL 节选)
chunks.embedding 列为 TEXT ,存储 JSON.stringify(number[]) (与 chunks_vec 中的二进制向量互为备份/展示用途)。FTS5 表 chunks_fts 的列为可检索正文 text + 多个 UNINDEXED 元数据列,便于 MATCH 时带回 path / source / 行号等。
typescript
params.db.exec(`
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY,
path TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'memory',
start_line INTEGER NOT NULL,
end_line INTEGER NOT NULL,
hash TEXT NOT NULL,
model TEXT NOT NULL,
text TEXT NOT NULL,
embedding TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
// ... 若 params.ftsEnabled ...
const tokenizer = params.ftsTokenizer ?? "unicode61";
const tokenizeClause = tokenizer === "trigram" ? `, tokenize='trigram case_sensitive 0'` : "";
params.db.exec(
`CREATE VIRTUAL TABLE IF NOT EXISTS ${params.ftsTable} USING fts5(\n` +
` text,\n` +
` id UNINDEXED,\n` +
` path UNINDEXED,\n` +
` source UNINDEXED,\n` +
` model UNINDEXED,\n` +
` start_line UNINDEXED,\n` +
` end_line UNINDEXED\n` +
`${tokenizeClause});`,
);
(完整函数见仓库 src/memory-host-sdk/host/memory-schema.ts 或 extensions 构建链导出的等价模块。)
10.6 manager-embedding-ops.ts(clearIndexedFileData、writeChunks、indexFile)
以下为 索引写入的核心 :类 MemoryManagerEmbeddingOps 中的私有方法 clearIndexedFileData 、writeChunks ,以及入口 indexFile (含 FTS-only 与 带 provider 两分支)。
typescript
const VECTOR_TABLE = "chunks_vec";
const FTS_TABLE = "chunks_fts";
private clearIndexedFileData(pathname: string, source: MemorySource): void {
if (this.vector.enabled) {
try {
this.db
.prepare(
`DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`,
)
.run(pathname, source);
} catch {}
}
if (this.fts.enabled && this.fts.available) {
try {
deleteMemoryFtsRows({
db: this.db,
tableName: FTS_TABLE,
path: pathname,
source,
currentModel: this.provider?.model,
});
} catch {}
}
this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(pathname, source);
}
/**
* Write chunks (and optional embeddings) for a file into the index.
* Handles both the chunks table, the vector table, and the FTS table.
* Pass an empty embeddings array to skip vector writes (FTS-only mode).
*/
private writeChunks(
entry: MemoryFileEntry | SessionFileEntry,
source: MemorySource,
model: string,
chunks: MemoryChunk[],
embeddings: number[][],
vectorReady: boolean,
): void {
const now = Date.now();
this.clearIndexedFileData(entry.path, source);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const embedding = embeddings[i] ?? [];
const id = hashText(
`${source}:${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${model}`,
);
this.db
.prepare(
`INSERT INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
hash=excluded.hash,
model=excluded.model,
text=excluded.text,
embedding=excluded.embedding,
updated_at=excluded.updated_at`,
)
.run(
id,
entry.path,
source,
chunk.startLine,
chunk.endLine,
chunk.hash,
model,
chunk.text,
JSON.stringify(embedding),
now,
);
if (vectorReady && embedding.length > 0) {
replaceMemoryVectorRow({
db: this.db,
tableName: VECTOR_TABLE,
id,
embedding,
});
}
if (this.fts.enabled && this.fts.available) {
this.db
.prepare(
`INSERT INTO ${FTS_TABLE} (text, id, path, source, model, start_line, end_line)\n` +
` VALUES (?, ?, ?, ?, ?, ?, ?)`,
)
.run(chunk.text, id, entry.path, source, model, chunk.startLine, chunk.endLine);
}
}
if (this.vector.enabled && !vectorReady && chunks.length > 0) {
const errDetail = this.vector.loadError ? `: ${this.vector.loadError}` : "";
log.warn(
`chunks written for ${entry.path} without vector embeddings --- chunks_vec not updated (sqlite-vec unavailable${errDetail}). Vector recall degraded for this file.`,
);
}
this.upsertFileRecord(entry, source);
}
protected async indexFile(
entry: MemoryFileEntry | SessionFileEntry,
options: { source: MemorySource; content?: string },
) {
// FTS-only mode: no embedding provider, but we can still build a FTS index
if (!this.provider) {
if ("kind" in entry && entry.kind === "multimodal") {
return;
}
const content = options.content ?? (await fs.readFile(entry.absPath, "utf-8"));
const chunks = filterNonEmptyMemoryChunks(chunkMarkdown(content, this.settings.chunking));
if (options.source === "sessions" && "lineMap" in entry) {
remapChunkLines(chunks, entry.lineMap);
}
this.writeChunks(entry, options.source, "fts-only", chunks, [], false);
return;
}
let chunks: MemoryChunk[];
let structuredInputBytes: number | undefined;
if ("kind" in entry && entry.kind === "multimodal") {
if (!this.provider) {
log.debug("Skipping multimodal indexing in FTS-only mode", {
path: entry.path,
source: options.source,
});
this.clearIndexedFileData(entry.path, options.source);
this.upsertFileRecord(entry, options.source);
return;
}
const multimodalChunk = await buildMultimodalChunkForIndexing(entry);
if (!multimodalChunk) {
this.clearIndexedFileData(entry.path, options.source);
this.deleteFileRecord(entry.path, options.source);
return;
}
structuredInputBytes = multimodalChunk.structuredInputBytes;
chunks = [multimodalChunk.chunk];
} else {
const content = options.content ?? (await fs.readFile(entry.absPath, "utf-8"));
const baseChunks = filterNonEmptyMemoryChunks(chunkMarkdown(content, this.settings.chunking));
chunks = this.provider
? enforceEmbeddingMaxInputTokens(this.provider, baseChunks, EMBEDDING_BATCH_MAX_TOKENS)
: baseChunks;
if (options.source === "sessions" && "lineMap" in entry) {
remapChunkLines(chunks, entry.lineMap);
}
}
if (!this.provider) {
this.writeChunks(entry, options.source, "fts-only", chunks, [], false);
return;
}
let embeddings: number[][];
try {
embeddings = this.batch.enabled
? await this.embedChunksWithBatch(chunks, entry, options.source)
: await this.embedChunksInBatches(chunks);
} catch (err) {
const message = formatErrorMessage(err);
if (
"kind" in entry &&
entry.kind === "multimodal" &&
/(413|payload too large|request too large|input too large|too many tokens|input limit|request size)/i.test(
message,
)
) {
log.warn("memory embeddings: skipping multimodal file rejected as too large", {
path: entry.path,
bytes: structuredInputBytes,
provider: this.provider.id,
model: this.provider.model,
error: message,
});
this.clearIndexedFileData(entry.path, options.source);
this.upsertFileRecord(entry.path, options.source);
return;
}
throw err;
}
const sample = embeddings.find((embedding) => embedding.length > 0);
const vectorReady = sample ? await this.ensureVectorReady(sample.length) : false;
this.writeChunks(entry, options.source, this.provider.model, chunks, embeddings, vectorReady);
}
逐段解释(与 Step 3 对照)
clearIndexedFileData:对同一path+source先删chunks_vec中挂在旧chunks.id上的行,再删 FTS ,最后删chunks,保证重索引无孤儿向量/全文行。id计算 :hashText(\s o u r c e : {source}: source:{path}: s t a r t : {start}: start:{end}: c h u n k H a s h : {chunkHash}: chunkHash:{model}`),因此 **换模型重索引** 时 FTS 删除可带model` 条件(见 10.3)。INSERT INTO chunks:embedding列始终写入 JSON 字符串 ;FTS-only 时为"[]"。replaceMemoryVectorRow:仅当vectorReady && embedding.length > 0;否则仍可有 FTS 与chunks行。INSERT INTO chunks_fts:仅当this.fts.enabled && this.fts.available。indexFile:!this.provider时直接writeChunks(..., [], false),即 不向量化 ;有 provider 时先embedChunks*,再ensureVectorReady,最后writeChunks(..., this.provider.model, embeddings, vectorReady)。
10.7 小结表(写入条件)
| 步骤 | 条件 | SQL / API |
|---|---|---|
| 清旧数据 | 每次 writeChunks 开头 |
DELETE FROM chunks_vec ...、deleteMemoryFtsRows、DELETE FROM chunks |
写 chunks |
每个非空 chunk | INSERT INTO chunks ... ON CONFLICT DO UPDATE |
写 chunks_vec |
vectorReady && embedding.length > 0 |
replaceMemoryVectorRow → DELETE + INSERT |
写 chunks_fts |
fts.enabled && fts.available |
INSERT INTO chunks_fts ... |
至此,§4.2.3.4 Step 3 中的 2a / 2b / 2c 与源码 一一对应 ,可直接在本地 OpenClaw 仓库中 extensions/memory-core/src/memory/ 目录下跳转阅读 embedChunksInBatches 等上游调用链。