OpenClaw 深度解析与源代码导读 · 第7篇:Memory 子系统——持久化、内置记忆与「人格文件」分界

摘要Memory(记忆) 是 OpenClaw 的 状态持久化层 ,负责在会话之间保持上下文、存储长期知识、管理 Agent 的"记忆"。本文深入探讨 Memory 子系统的三大核心机制Memory I/O (记忆的读写/索引/Flush)、内置记忆后端 (Builtin/QMD/Honcho 等多种存储方案)、人格文件分界SOUL.md/AGENTS.md 等八类记忆文件)。基于 src/plugins/memory*.tssrc/plugin-sdk/memory*.ts 源码,揭示 OpenClaw 如何实现"有状态"的智能体。

关键词:OpenClaw;Memory;记忆子系统;持久化;Flush;Distill;SOUL.md;人格文件;内置记忆;QMD

系列文章

源码版本说明 :本文引用路径基于 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 的"读取"操作------从存储中检索相关记忆片段。

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>;
};
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-coreSQLite(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-coreprovider 走向量 +(可选)FTS;provider 退化为 FTS-onlywriteChunks(..., [], 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

语义召回由 memorySearchEmbedding Provider 决定,与 memory.backend 仍为 builtin 不矛盾builtin 负责「文件写哪里」;Gateway memory-core 在同一工作区上建立 FTS + 向量 索引(在 provider 可用时)。

json 复制代码
{
  "agents": {
    "defaults": {
      "memorySearch": {
        "enabled": true,
        "provider": "openai",
        "model": "text-embedding-3-small"
      }
    }
  }
}

实际键名与可选字段(fallbackhybrid 权重等)以你使用的 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 的 本地 indexingmemory.builtin.indexing.enabled = true。在此前提下:FTS 始终(在 FTS 模块可用时)参与关键词索引;向量hybrid 合并 仅在 Embedding Provider 就绪 时启用------本节默认后者成立


4.2.3.1 记忆检索的三层架构总结(Builtin)

Builtin 记忆检索架构

维度 第 1 层:即时上下文 第 2 层:索引检索 第 3 层:档案加载
来源 当前会话消息(内存中) corpus/*.mdsessions/*/summary sessions/*/messages.jsonl; 历史会话完整记录
方式 直接访问 向量相似度(sqlite-vec) + FTS5 关键词memory-core 默认 hybrid 时合并;仅向量或仅关键词取决于配置与健康状态) 文件系统直接读取
范围 当前对话 所有提炼知识 指定会话完整内容
场景 当前对话引用 知识查找 完整复盘、审计
示例 「刚才说的......」 「我之前说过......」 「复述完整对话」

检索优先级(从上到下):

  1. 当前上下文(最近 N 条消息)
  2. 相关 Session Summary → 选择性加载 messages.jsonl
  3. 对已纳入索引的 corpus/*.mdsessions/*/summary.mdmemory_search 查询向量化 → 向量 Top-K ,并与 FTS 命中在 hybrid 流程中 merge / 重排(无 provider 时退化为仅 FTS)
  4. 历史 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-KFTS 关键词(若启用 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 附录:OpenClaw memory-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-coresearchVector + 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 + 向量 hybridQMD 则是另一套 以 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 已经给了每个文件的推荐结构与模板。实践中建议按下面顺序落地,能最大化"规则不打架":

  1. 先写 AGENTS.md(规则)再写 SOUL.md(语气):先把边界与工具策略定死,再定义沟通风格。
  2. IDENTITY.md / USER.md 只放稳定事实:身份、环境、长期偏好等;"临时偏好"不要写在这里,避免频繁变更。
  3. TOOLS.md / BOOTSTRAP.md 保持可执行:写成 checklist 与明确操作规约,避免"空话原则"。
  4. MEMORY.md 只写策略,不写具体偏好 :偏好与项目事实应进入 corpus/(例如 user-preferences.mdproject-context.md)。
  5. HEARTBEAT.md 只放周期性任务 :把"每次会话都要做"的内容放 BOOTSTRAP.md,把"定期做"的放这里。

5.5 记忆文件的收集与更新机制

人格文件和记忆文件(如 user-preferences.mdproject-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 提取 从多轮对话中自动总结用户习惯
项目知识 混合 显式文档 + 对话中提取的项目细节

💡 最佳实践

  1. 基础偏好(技术栈、沟通风格)→ 显式声明
  2. 动态偏好(临时需求、新习惯)→ 工具写入
  3. 长期洞察(用户画像、行为模式)→ 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.mdIDENTITY.md → ... → MEMORY.md
    • 记忆文件的三种收集方式
      • 显式声明:用户手动创建(适合基础偏好)
      • 工具写入:Agent 动态更新(适合对话中识别的偏好)
      • Learning 提取:自动从会话历史中提取(适合长期洞察)
    • 多 Agent 隔离:每个 Agent 有独立的 Memory 存储,可选共享项目级知识
  • 下一篇(第8篇)------Learning :与本篇(第7篇 Memory)侧重「如何存储与检索 」相对,第8篇关注「如何学习与进化」(语义与行为):从会话与运行痕迹中沉淀可复用的策略与偏好。


9 参考文献与链接

  1. OpenClaw 主仓库:https://github.com/openclaw/openclaw
  2. Memory System 文档(D1):http://clawdocs.org/architecture/memory-system
  3. 内置记忆文档(D2):docs/concepts/memory-builtin.md
  4. QMD 记忆文档(D2):docs/concepts/memory-qmd.md
  5. Honcho 记忆文档(D2):docs/concepts/memory-honcho.md
  6. 第8篇 Learning(已发表):12_learning_adaptation_openclaw.md
  7. D3 参考:
  8. 源码入口:
    • src/plugins/memory-state.ts ------ Memory 状态管理
    • src/plugins/memory-runtime.ts ------ Memory 运行时
    • src/auto-reply/reply/memory-flush.ts ------ Memory Flush
    • src/plugin-sdk/memory-*.ts ------ Memory SDK
    • src/memory-host-sdk/ ------ Memory Host SDK
  9. 索引写入(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 → 分块 indexFilechunkMarkdown + filterNonEmptyMemoryChunks 读文件、按配置分块;会话源可能 remapChunkLines
embedChunks embedChunksInBatches / embedChunksWithBatch(未在下方全文展开) 调用 Embedding Provider,得到 number[][]
每块写入三张「逻辑表」 私有方法 writeChunks clearIndexedFileData 清旧索引,再循环:INSERT chunks →(条件)replaceMemoryVectorRow →(条件)INSERT chunks_fts
向量表是否就绪 ensureVectorReady + ensureVectorTable 加载 sqlite-vec 扩展、用 vec0chunks_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 删除旧行(忽略异常),再插入新 embeddingFloat32ArrayBuffer)。

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(常量、ensureVectorReadyensureVectorTable

向量表物理名为 chunks_vec ,与 manager-embedding-ops.tsVECTOR_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 === falsewriteChunks 仍会写 chunkschunks_fts ,但跳过 replaceMemoryVectorRow,并在日志中提示向量召回降级(见 10.5 末尾 log.warn)。


10.5 memory-schema.tschunks 表与 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.tsextensions 构建链导出的等价模块。)


10.6 manager-embedding-ops.tsclearIndexedFileDatawriteChunksindexFile

以下为 索引写入的核心 :类 MemoryManagerEmbeddingOps 中的私有方法 clearIndexedFileDatawriteChunks ,以及入口 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 对照)

  1. clearIndexedFileData :对同一 path + source 先删 chunks_vec 中挂在旧 chunks.id 上的行,再删 FTS ,最后删 chunks,保证重索引无孤儿向量/全文行。
  2. 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)。
  3. INSERT INTO chunksembedding 列始终写入 JSON 字符串 ;FTS-only 时为 "[]"
  4. replaceMemoryVectorRow :仅当 vectorReady && embedding.length > 0 ;否则仍可有 FTS 与 chunks 行。
  5. INSERT INTO chunks_fts :仅当 this.fts.enabled && this.fts.available
  6. indexFile!this.provider 时直接 writeChunks(..., [], false) ,即 不向量化 ;有 provider 时先 embedChunks* ,再 ensureVectorReady ,最后 writeChunks(..., this.provider.model, embeddings, vectorReady)

10.7 小结表(写入条件)

步骤 条件 SQL / API
清旧数据 每次 writeChunks 开头 DELETE FROM chunks_vec ...deleteMemoryFtsRowsDELETE FROM chunks
chunks 每个非空 chunk INSERT INTO chunks ... ON CONFLICT DO UPDATE
chunks_vec vectorReady && embedding.length > 0 replaceMemoryVectorRowDELETE + 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 等上游调用链。

相关推荐
2301_775148152 小时前
如何管理RAC归档日志_共享存储中的FRA配置与双节点访问
jvm·数据库·python
RoboWizard2 小时前
移动固态硬盘的耐用性如何,怎么判断使用寿命?
服务器·数据库·负载均衡
qq_330037992 小时前
php怎么实现接口请求日志记录_php如何自动记录入参出参与耗时
jvm·数据库·python
2401_865382502 小时前
各省政务信息化项目验收材料清单汇总及差异分析
java·开发语言·数据库
pele2 小时前
如何用 contextmenu 事件自定义鼠标右键菜单的显示逻辑
jvm·数据库·python
2301_773553622 小时前
怎样禁用phpMyAdmin的控制台历史记录_防凭证与查询留存
jvm·数据库·python
m0_743623922 小时前
Go语言怎么实现生产者消费者_Go语言生产者消费者模式教程【精通】
jvm·数据库·python
baidu_340998822 小时前
CSS Grid布局如何为特定项目指定位置_使用grid-row和grid-column
jvm·数据库·python
weixin_458580122 小时前
如何用 blur 与 focusout 区分不冒泡与冒泡的失焦事件
jvm·数据库·python