一句话总览:
OpenClaw 把"记忆"拆成两层:
- 短期记忆:当前会话里、这次请求要发给模型的那一小段上下文(会被裁剪/压缩)。
- 长期记忆:落在磁盘上的 Markdown 文件 + 向量/全文索引,用来跨会话、跨时间"回想"信息。
下面分块讲清楚这两层各自是什么、怎么实现、怎么协同。
一、短期记忆:会话上下文(Session Context)
1. 会话是怎么管理的?
文档:docs/concepts/session.md
- 每个 Agent 有一套会话系统:
- 主私聊:agent:<agentId>:<mainKey>(默认 main)。
- 不同频道/群聊有独立 session key。
- 状态保存在网关机器上:
- 元数据:~/.openclaw/agents/<agentId>/sessions/sessions.json
- 聊天记录(转录):~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl
JSONL 每一行是一条记录(用户/助手消息、工具调用等)。
2. "短期记忆"具体是什么?
一轮请求时,OpenClaw 会:
- 找到对应的 sessionId。
- 读取该 session 最近的一段聊天(从 JSONL 或内存缓存)。
- 加上:
- 系统提示(system prompt)
- 工具调用结果(如 memory_search、浏览器结果等)
- 运行时元信息 / 控制开关
- 拼成一个要发给模型的大 prompt。
这个 prompt + 最近几轮对话,就可以视为"短期记忆":
- 它是这次 LLM 调用真实看到的上下文。
- 大小受模型 contextWindow 限制(上下文窗口)。
- 当太长时,会触发:
- Session pruning:优先删掉老旧/低价值内容(如旧工具结果)。
- Compaction:把较早部分对话"压缩成摘要"。
这些裁剪/压缩只影响"送进模型的上下文",
不会立刻改写 JSONL 文件(JSONL 是原始历史)。
二、长期记忆:Markdown + 向量/全文索引
文档:docs/concepts/memory.md
核心代码:src/memory/*
1. 长期记忆的"源数据":两类 Markdown
默认工作区下有两类记忆文件(L18--L25):
- memory/YYYY-MM-DD.md
- 每日"流水账",追加型。
- Session 启动时会读取"今天 + 昨天"的内容。
- MEMORY.md(可选)
- 手工/半自动"长期记忆总结":
- 稳定偏好(比如"这个用户喜欢中文 + TypeScript")
- 长期存在的事实、约定。
- 只在 主私聊 session 中加载,避免在群里泄露。
写入建议:
- "决定、偏好、长期事实" → MEMORY.md
- "日常运行笔记、当天情况" → memory/当天日期.md
- 用户说"记住这个"时:应该显式写盘,而不是只让它停留在上下文里。
可以把这两类文件直接当作"人类可读的知识库笔记本"。
2. 向量/全文索引:怎么让机器"用起来"
Markdown 是本体,但要自动检索,需要一个索引引擎。
默认使用 SQLite 嵌入式数据库 构建索引,路径类似:
- ~/.openclaw/memory/<agentId>.sqlite
建表逻辑在 src/memory/memory-schema.ts,主要表:
- files
- 每行代表一个被索引的文件(比如 MEMORY.md、memory/2026-02-04.md)。
- 保存 path、source(memory / sessions)、hash、mtime、size。
- chunks
- 核心向量索引表:
- 一行 = 一个小片段(chunk):
- 来源路径 + 起止行号:path / source / start_line / end_line
- 文本内容:text
- 向量:embedding(JSON 字符串、内部是 [0.01, -0.03, ...])
- 使用的 embedding 模型名:model
- hash:chunk 内容的 hash
- embedding_cache
- 相同内容的 chunk 只算一次 embedding,用 hash 做 key 缓存。
- meta
- 保存当前索引用的模型、provider、chunk 大小等,便于判断是否要全量重建。
此外还有:
- chunks_vec:sqlite-vec 的虚拟向量表(FLOAT[N] 数组),用于在 SQLite 内部直接做向量距离搜索。
- chunks_fts:FTS5 全文检索表,用于关键词/BM25 搜索。
3. 向量是怎么算出来的?(Embedding Provider)
统一封装在 src/memory/embeddings.ts:
embeddings.tsLines 21-26
export type EmbeddingProvider = {
id: string; // "openai" | "gemini" | "local"
model: string; // 模型名或本地模型路径
embedQuery: (text: string) => Promise<number[]>;
embedBatch: (texts: string[]) => Promise<number[][]>;
};
支持三种后端:
- OpenAI 远程 embedding
- 默认模型:text-embedding-3-small。
- Gemini 远程 embedding
- 默认模型:gemini-embedding-001。
- 本地 embedding(node-llama-cpp)
- 默认模型:hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf。
- 会自动下载 GGUF 模型,使用 node-llama-cpp 在本机计算向量。
所有向量计算后会做标准化(单位向量):
embeddings.tsLines 9-15
// 非数值改成 0,然后进行 L2 归一化
4. 文本是如何切片(chunking 策略)的?
配置结构在 ResolvedMemorySearchConfig.chunking:
- 默认参数:
- tokens = 400:每个 chunk 目标 token 数。
- overlap = 80:相邻 chunk 重叠 token 数。
大致做法:
- 按顺序遍历 Markdown / 会话文本:
- 每 400 token 切成一段;
- 段与段之间重叠 80 token;
- 这样:
- 既不会把整篇长文塞进一个向量(太粗糙),
- 又能保证相邻语义关联内容有交叠,不容易被切断。
5. 检索策略:向量 + 全文混合
用户/Agent 通过工具 memory_search 调用索引时,核心流程(MemoryIndexManager.search):
- 对查询文本做一次 embedding(embedQuery),得到 query 向量。
- 向量检索(semantic):
- 在 chunks_vec / chunks 里找距离最近的一批 chunk(最多 N * candidateMultiplier)。
- 全文检索(keyword / BM25):
- 在 chunks_fts 里用 FTS5 做 MATCH,算 BM25 分数。
- 混合(hybrid):
- 参数:
- vectorWeight(默认 0.7)
- textWeight(默认 0.3)
- 将两个分数线性加权合并,得到最终得分。
- 按分数排序,取前 maxResults 个 snippet 返回(每段 ~700 字符)。
返回内容包括:
- snippet:一小段原文。
- path / startLine / endLine:可以精确回到原 Markdown 文件。
- source:"memory" 或 "sessions"(如果开启了 sessionMemory)。
Agent 会把这些 snippet 继续注入当次 LLM 调用的 prompt 中。
三、"短期 → 长期"的桥梁:自动 memory flush & sessionMemory
1. 自动 memory flush:上下文要满了,先"记个笔记"
当某个 session 的上下文 token 数接近模型窗口上限(考虑 reserveTokensFloor 和 softThreshold),
OpenClaw 会触发一次隐藏的提醒回合:
- system 提示类似:
- "Session nearing compaction. Store durable memories now."
- user 提示类似:
- "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store."
模型这时会:
- 把这次长对话中"值得长期记住的事实、偏好、结论"写入 Markdown(MEMORY.md / memory/当天.md)。
- 如果没有,就回复 NO_REPLY(这个回合对用户是不可见的)。
这相当于在"短期记忆即将被裁剪/摘要之前",
让模型主动把长远重要的信息搬运到长期记忆本子里。
2. sessionMemory:可选的"会话历史索引"
除了 Markdown,OpenClaw 还能选择性地把 会话转录 JSONL 自己也做成索引源(实验特性):
- 配置示例:
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"]
}
}
}
开启后:
- MemoryIndexManager 会监听 ~/.openclaw/agents/<agentId>/sessions/*.jsonl:
- 定期读取新追加的内容;
- 把 User/Assistant 的文本内容提取出来,做 chunk + embedding;
- 以 source="sessions" 的形式存入同一个 SQLite 索引。
- 搜索时,可以同时在:
- source="memory"(Markdown 记忆)
- source="sessions"(会话记录)
两者上进行混合查询。
这样,近期的对话记录本身也可以被语义检索,但和 Markdown 的长期记忆仍然有清晰区分。
四、索引何时更新?(用户手动改 Markdown 的情况)
当用户自己编辑 MEMORY.md 或 memory/*.md 时,更新流程大致是:
- 文件写入磁盘后,chokidar 监听到变更:
- 把 dirty = true,并启动一个防抖计时器。
- 防抖延迟结束后(默认 ~1.5 秒):
- 后台调用 MemoryIndexManager.sync({ reason: "watch" })。
- 对比每个文件的 hash:
- hash 没变 → 跳过。
- hash 变了 → 重切 chunk、重算 embeddings,更新 chunks / chunks_vec / chunks_fts。
- 完成后清理 stale 记录、更新 files 表,让索引与磁盘内容保持一致。
此外还有三种"顺带触发"的机会:
- 新会话开始时(onSessionStart)。
- 搜索前/后(onSearch)。
- 定时后台任务(intervalMinutes)。
你无需手动操作任何按钮:只要保存 Markdown,索引会在短时间内自动同步。
五、SQLite 与"嵌入式数据库"的理解
1. SQLite 文件是什么?
- 是一个普通的单文件,比如:~/.openclaw/memory/main.sqlite。
- 内部是 SQLite 规定的二进制数据库格式:
- 有表结构、索引、页、事务等。
- 通过 SQLite 库可以用 SQL 语句 SELECT/INSERT/UPDATE 操作。
2. "嵌入式数据库"是什么意思?
- 嵌入式数据库(如 SQLite)的特点:
- 没有单独的数据库服务进程:
- 没有 mysqld、postgres 那样的 daemon。
- 程序直接在进程里 import sqlite,然后 open("xxx.sqlite") 就能用。
- 执行 SQL 时,所有逻辑在当前进程内完成,直接读写这个文件。
- 与 MySQL / PostgreSQL 这种"服务器型数据库"相对:
- 那些需要先启动一个服务器进程:
- 程序再通过 TCP / Unix socket 连接。
- 数据文件的格式和管理主要由服务器自己负责,应用一般不直接操作文件。
在 OpenClaw 里:
选择 SQLite 做记忆索引,是因为它:
- 部署简单:不需要额外跑数据库服务。
- 跨平台:macOS / Linux 下都很好用。
- 单文件易管理:备份、移动、清理都方便。
六、整体串起来的一句话总结
- 短期记忆:
- 当前这次请求要发给模型的 prompt + 最近几轮对话。
- 存在内存 + JSONL 转录,受上下文窗口约束,会被裁剪/压缩。
- 长期记忆:
- MEMORY.md + memory/YYYY-MM-DD.md(以及可选的会话 JSONL)作为"记事本本体";
- SQLite / QMD 上的向量 + 全文索引,让这些记事本可以被语义+关键词混合检索;
- 通过 memory_search / memory_get 把相关片段再注入短期上下文,
- 再配合自动 memory flush 把快被遗忘的重要信息写回 Markdown。