OpenClaw 记忆管理系统技术文档

36 阅读16分钟

来源:OpenClaw 源码分析 版本:2026.3.26 日期:2026-03-30


一、系统概述

OpenClaw 是一个多渠道 AI 网关,支持 Telegram、Discord、WhatsApp、飞书等平台的消息统一接入。其记忆管理系统为 AI 提供跨对话、跨时间的持久记忆能力。

1.1 解决什么问题

AI 对话天然是无状态的——每次对话都是全新的开始。记忆系统让 AI 能够:

  • 记住用户的偏好和习惯
  • 记住之前的讨论结论和决策
  • 跨越多天甚至数周持续对话

1.2 核心设计思想

Markdown 文件 ──→ 唯一真相源(可读、可编辑、可版本控制)
SQLite 索引  ──→ 加速结构(丢了可重建)
嵌入模型    ──→ 语义理解(不只是关键词匹配)

二、架构总览

2.1 三层架构

┌─────────────────────────────────────────────────────────────┐
│                        工具层                                │
│                  memory_search / memory_get                 │
│              (Agent 自动调用的记忆检索工具)                 │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                       Hook 层                               │
│              session-memory hook                            │
│         (对话结束时自动提取并保存会话摘要)                  │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                       引擎层                                │
│  ┌─────────────────────┐    ┌─────────────────────────┐    │
│  │    Builtin 引擎      │    │       QMD 引擎          │    │
│  │  (SQLite + sqlite-vec)│   │     (外部 CLI)          │    │
│  └─────────────────────┘    └─────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                       存储层                                │
│         memory/*.md + SQLite (chunks / chunks_vec / chunks_fts)│
└─────────────────────────────────────────────────────────────┘

2.2 模块职责

模块文件路径职责
记忆管理器extensions/memory-core/src/memory/manager.ts协调搜索、同步、配置
混合搜索extensions/memory-core/src/memory/hybrid.ts向量分 + 关键词分融合
MMR 去重extensions/memory-core/src/memory/mmr.ts去除搜索结果中的重复内容
时间衰减extensions/memory-core/src/memory/temporal-decay.ts让旧记忆的分数自然下降
向量嵌入extensions/memory-core/src/memory/embeddings.ts调用嵌入模型生成向量
文件同步extensions/memory-core/src/memory/manager-sync-ops.ts监控文件变化,执行索引
搜索执行extensions/memory-core/src/memory/manager-search.ts分别执行向量搜索和关键词搜索
工具注册extensions/memory-core/src/tools.ts给 Agent 暴露 memory_search / memory_get 工具
会话钩子src/hooks/bundled/session-memory/handler.ts对话结束时自动提取摘要
存储基础设施packages/memory-host-sdk/src/host/memory-schema.ts定义 SQLite 表结构

三、存储结构

3.1 文件系统布局

workspace/
├── MEMORY.md                           用户直接编辑的长期记忆
├── memory/                             按日期和话题整理的记忆文件
   ├── 2026-03-27-api-design.md
   ├── 2026-03-28-python-tips.md
   └── MEMORY.md
└── .openclaw/
    └── memory.db                       SQLite 索引数据库(自动生成)

3.2 SQLite 数据库表结构

-- 文件清单表
CREATE TABLE files (
  path TEXT PRIMARY KEY,
  source TEXT NOT NULL DEFAULT 'memory',   -- memory 或 sessions
  hash TEXT NOT NULL,                      -- 内容哈希,检测变化
  mtime INTEGER NOT NULL,                  -- 修改时间
  size INTEGER NOT NULL
);

-- 文本分块表(核心)
CREATE TABLE 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,                  -- 向量(JSON 字符串存储)
  updated_at INTEGER NOT NULL
);

-- 向量索引表(sqlite-vec 扩展)
CREATE VIRTUAL TABLE chunks_vec USING vec0(
  id TEXT PRIMARY KEY,
  embedding FLOAT[1536]                    -- 维度由嵌入模型决定
);

-- 全文搜索索引表(FTS5)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
  text,                                    -- 要搜索的内容
  id UNINDEXED,
  path UNINDEXED,
  source UNINDEXED,
  model UNINDEXED,
  start_line UNINDEXED,
  end_line UNINDEXED
);

-- 嵌入缓存表(避免重复嵌入,节省 API 调用)
CREATE TABLE embedding_cache (
  provider TEXT NOT NULL,                  -- openai / gemini / ollama
  model TEXT NOT NULL,                    -- text-embedding-3-small
  provider_key TEXT NOT NULL,
  hash TEXT NOT NULL,                      -- 文本哈希
  embedding TEXT NOT NULL,
  dims INTEGER,
  updated_at INTEGER NOT NULL,
  PRIMARY KEY (provider, model, provider_key, hash)
);

四、记忆来源

4.1 手动写入(常青记忆)

用户可直接编辑两个位置:

文件用途是否衰减
MEMORY.md(根目录)长期记忆:偏好、习惯、重要事实
memory/*.md主题记忆:按项目/话题分类

4.2 自动提取(会话记忆)

当用户执行 /new/reset(开启新对话)时,session-memory hook 自动触发:

  1. 读取最近 15 条消息(可配置)
  2. 调用 LLM 生成描述性标题
  3. 保存为 memory/YYYY-MM-DD-slug.md 格式

生成的记忆文件示例:

# Session: 2026-03-30 09:50:00 UTC

- **Session Key**: agent:main
- **Session ID**: abc123
- **Source**: webchat

## Conversation Summary

用户查询了历年合同金额数据。
最终查询范围:2023年、2024年、2025年(不含2026年)。

结果:
- 2023年:12,345 万元
- 2024年:15,678 万元
- 2025年:18,234 万元

五、搜索算法详解

5.1 混合搜索原理

搜索分四个步骤完成:

Step 1:用户查询 "昨天那个合同金额"Step 2:并行执行两种搜索
          ├─ 向量搜索(语义)
          │   embed() → [0.031, -0.008, ...] → 余弦相似度
          │   → score: 0.82
          │
          └─ 关键词搜索(精确)
              extractKeywords() → ["昨天", "合同", "金额"]
              → BM25 匹配
              → textScore: 0.65Step 3:加权融合
          score = 0.7 × 0.82 + 0.3 × 0.65 = 0.769Step 4:按分数降序 + 可选的时间衰减/MMR
          ↓
          返回 Top 10 结果

5.2 向量搜索(语义理解)

原理: 将文本和查询都转成高维向量,比较方向是否一致。

核心 SQL(sqlite-vec):

SELECT c.id, c.path, c.text,
       vec_distance_cosine(v.embedding, ?) AS dist   -- 查询向量
FROM chunks_vec v
JOIN chunks c ON c.id = v.id
WHERE c.model = ?
ORDER BY dist ASC        -- dist 越小 = 越相似
LIMIT ?

余弦相似度公式:

余弦相似度 = (A · B) / (|A| × |B|)
余弦距离   = 1 - 余弦相似度
最终分数   = 1 - dist

向量搜索的代码实现:

// manager-search.ts
async function searchVector(params) {
  // ① 把用户查询转成向量
  const queryVec = await this.provider.embed("昨天那个合同金额");
  // → [0.031, -0.008, 0.112, ...]  1536维

  // ② 余弦距离搜索
  const rows = this.db.prepare(`
    SELECT c.id, c.path, c.text,
           vec_distance_cosine(v.embedding, ?) AS dist
      FROM chunks_vec v
      JOIN chunks c ON c.id = v.id
     WHERE c.model = ?
     ORDER BY dist ASC
     LIMIT ?
  `).all(
    Buffer.from(new Float32Array(queryVec).buffer),
    this.providerModel,
    this.limit
  );

  // ③ 距离转分数:dist 越小越相似
  return rows.map(row => ({
    score: 1 - row.dist,    // 0 = 完全相同, 1 = 完全相反
    snippet: row.text.slice(0, 700),
  }));
}

向量搜索的适用场景:

  • 同义词理解:"合同" ≈ "协议"
  • 语义相近:"缓存穿透" ≈ "缓存击穿" ≈ "布隆过滤器解决的数据问题"
  • 拼写容错:有一定容错能力

5.3 关键词搜索(BM25 精确匹配)

原理: 分词 → 倒排索引 → BM25 评分。

核心 SQL(FTS5):

SELECT id, path, text,
       bm25(chunks_fts) AS rank    -- BM25 评分
FROM chunks_fts
WHERE chunks_fts MATCH '"昨天" AND "合同" AND "金额"'
ORDER BY rank ASC                  -- rank 越小越相关
LIMIT ?

关键词搜索的适用场景:

  • 精确术语:搜索 "HTTP 状态码 500" 必须精确匹配
  • 数值/人名:搜索 "2023年"、"John"、"13888888888"
  • 品牌/型号:搜索 "iPhone 16 Pro Max"

5.4 BM25 算法详解

5.4.1 问题背景

给定一个查询词和一堆文档,如何判断哪个文档更相关?

比如搜索 "合同金额"

文档A:合同金额是12,345万元         ✓ 包含"合同""金额"
文档B:合同条款中的金额规定...       ✓ 包含"合同""金额"
文档C:甲乙双方签订了一份合同...     ✗ 只有"合同",没有"金额"

A 和 B 都提到了,但哪个更相关?BM25 就是用来量化这个相关程度的。

5.4.2 发展历史

算法思想问题
TF(词频)词出现越多越相关文章越长词越多,长文档永远排前面
TF-IDF常见词(如"的")降权但仍未解决长度不公平的问题
BM25TF × IDF + 长度归一化 + 词频饱和目前最广泛使用的检索算法之一

5.4.3 完整公式

BM25(D, Q) = Σ IDF(qi) × ──────────────────────────────
                      tf + k1 × (1 - b + b × |D|/avgdl)

公式拆分两部分理解:

BM25 = IDF × TF_score

第一部分:IDF(逆文档频率)
  → 词越常见,权重越低
  → "的"出现在几乎所有文档里,IDF → 0
  → "合同"只出现在少数文档,IDF → 高

第二部分:TF_score(词频得分)
  → 词在文档中出现越多越相关
  → 但有上限(k1 控制饱和)
  → 短文档比长文档更有优势(b 控制归一化)

IDF 公式:

              (N - n + 0.5)
IDF(qi) = log ───────────────
              (n + 0.5)

N = 总文档数
n = 包含词 qi 的文档数

参数说明:

参数典型值含义
k11.2词频饱和参数,词出现次数再多也不会无限增加权重
b0.75文档长度归一化参数
N-总文档数
n-包含目标词的文档数
`D`-当前文档的词数
avgdl-所有文档的平均词数
tf-词在文档中出现的次数

5.4.4 IDF 的作用

IDF 过滤掉无意义的常见词:

IDF("合同")   高  → 包含"合同"的文档很少(10篇/1000篇)
               → log(1000/10) = 2.19 → 词很关键

IDF("的")    低  → 几乎所有文档都有"的"(990篇/1000篇)
               → log(1000/990) = 0.01 → 词基本没用

IDF("金额")   高  → 只有少数文档有"金额"(80篇/1000篇)
               → log(1000/80) = 2.51 → 词很关键

5.4.5 TF_score 的作用:词频饱和

词出现 100 次不会比出现 10 次好太多——这就是"饱和":

k1 = 1.2

tf=1:    (1.2+11  / (1+1.2)    = 2.2/2.2   = 1.00
tf=5:    (1.2+15  / (5+1.2)    = 11/6.2    = 1.77
tf=10:   (1.2+110 / (10+1.2)   = 22/11.2   = 1.96
tf=100:  (1.2+1100/ (100+1.2)   = 220/101.2 = 2.17
tf=1000: (1.2+11000/(1000+1.2) = 2200/1001 = 2.20

曲线:快速上升到 1.5~2.0 左右,然后趋于平坦
     这就是"饱和"——再多的重复出现也没用了

5.4.6 TF_score 的作用:长度归一化

短文档不应因为篇幅短就输给长文档:

b = 0.75,k1 = 1.2,avgdl = 200

文档A:|D|=7词(短),tf=2 时:
  TF = 2.2×2 / (2 + 1.2×(1-0.75+0.75×7/200))
     = 4.4 / 2.75 = 1.60

文档B:|D|=500词(长),tf=2 时:
  TF = 2.2×2 / (2 + 1.2×(1-0.75+0.75×500/200))
     = 4.4 / 4.30 = 1.02

同样出现2次,短文档得分更高(1.60 vs 1.02)

5.4.7 完整计算示例

场景:

查询:["合同", "金额"]
总文档数 N = 1000,平均长度 avgdl = 200

文档A:"合同金额是12,345万元"          (短,tf合同=1, tf金额=1, |D|=7)
文档B:"关于合同、合同条款及金额规定..."  (中,tf合同=2, tf金额=1, |D|=20)
文档C:"合同金额计算公式如下..."         (短,tf合同=1, tf金额=1, |D|=6)
文档D:"甲乙双方签订了一份合同..."        (长,tf合同=1, tf金额=0, |D|=100

Step 1:计算 IDF

包含"合同"的文档 n = 100
包含"金额"的文档 n = 80

IDF("合同") = log((1000-100+0.5)/(100+0.5)) = log(900.5/100.5) = 2.19
IDF("金额") = log((1000-80+0.5)/(80+0.5))   = log(920.5/80.5)  = 2.44

Step 2:计算 TF_score(k1=1.2, b=0.75, avgdl=200)

文档A:|D|=7
  TF合同 = 2.2×1 / (1 + 1.2×(1-0.75+0.75×7/200))   = 2.2/1.69 = 1.30
  TF金额 = 2.2×1 / (1 + 1.2×(1-0.75+0.75×7/200))   = 2.2/1.69 = 1.30

文档B:|D|=20
  TF合同 = 2.2×2 / (2 + 1.2×(1-0.75+0.75×20/200))  = 4.4/2.87 = 1.53
  TF金额 = 2.2×1 / (1 + 1.2×(1-0.75+0.75×20/200))  = 2.2/1.87 = 1.18

文档D:|D|=100
  TF合同 = 2.2×1 / (1 + 1.2×(1-0.75+0.75×100/200)) = 2.2/2.35 = 0.94
  TF金额 = 0(不包含"金额")

Step 3:计算 BM25 = IDF合同 × TF合同 + IDF金额 × TF金额

文档A:2.19×1.30 + 2.44×1.30 = 2.85 + 3.17 = 6.02
文档B:2.19×1.53 + 2.44×1.18 = 3.35 + 2.88 = 6.23  ← 最高
文档C:2.19×1.30 + 2.44×1.30 = 6.02(结构同A)
文档D:2.19×0.94 + 2.44×0   = 2.06                ← 只命中一个词

排序:B(6.23)> A/C(6.02)> D(2.06)

5.4.8 OpenClaw 中的实现

// hybrid.ts — BM25 分数转标准 0-1 分数
// FTS5 已内置 BM25 算法,这里只做分数转换

export function bm25RankToScore(rank: number): number {
  if (!Number.isFinite(rank)) {
    return 1 / (1 + 999);    // 异常值 → 最低分
  }
  if (rank < 0) {
    // rank 负数 = FTS5 布尔精确匹配(布尔查询命中)
    const relevance = -rank;  // rank=-1 → relevance=1
    return relevance / (1 + relevance);  // → 0.5
  }
  // rank=0 → score=1.0(最相关)
  // rank=1 → score=0.5
  // rank=9 → score=0.1
  return 1 / (1 + rank);
}

FTS5 内置了 BM25 评分,OpenClaw 直接调用:

-- FTS5 自动计算 BM25 值
SELECT id, path, text,
       bm25(chunks_fts) AS rank    -- rank 是原始 BM25 值
FROM chunks_fts
WHERE chunks_fts MATCH '"合同" AND "金额"'
ORDER BY rank ASC                  -- rank 越小越相关
LIMIT 10

5.4.9 一句话总结

BM25 = 词的重要程度(IDF)× 词在文档中的出现情况(TF_score)

IDF      → 过滤掉"的""是"这类无意义的词
TF_score → 词出现多更相关,但有上限,不被长文档刷分

5.5 混合融合

// hybrid.ts
async function mergeHybridResults({ vector, keyword, vectorWeight, textWeight }) {
  // ① 按 id 合并(同一条记忆可能被两种方式找到)
  // chunk_2 同时命中:vectorScore=0.82, textScore=0.65

  // ② 加权融合(默认 70% 向量 + 30% 关键词)
  const score = 0.7 * 0.82 + 0.3 * 0.65;
  // score = 0.574 + 0.195 = 0.769

  // ③ 时间衰减(可选,默认关闭)
  // 旧记忆分数自然下降

  // ④ MMR 去重(可选,默认关闭)
  // 删除与已选结果重复的内容

  return decayed.sort((a, b) => b.score - a.score);
}

5.6 两种搜索的对比总结

维度向量搜索关键词搜索
引擎sqlite-vec 扩展SQLite 内置 FTS5
核心函数vec_distance_cosine()MATCH + bm25()
擅长语义理解、同义词、近义词精确术语、数值、人名
短板精确匹配不如 FTS不理解语义
典型应用"上次讨论的缓存方案""2023年合同金额"
// hybrid.ts
async function mergeHybridResults({ vector, keyword, vectorWeight, textWeight }) {
  // ① 按 id 合并(同一条记忆可能被两种方式找到)
  // chunk_2 同时命中:vectorScore=0.82, textScore=0.65

  // ② 加权融合(默认 70% 向量 + 30% 关键词)
  const score = 0.7 * 0.82 + 0.3 * 0.65;
  // score = 0.574 + 0.195 = 0.769

  // ③ 时间衰减(可选,默认关闭)
  // 旧记忆分数自然下降

  // ④ MMR 去重(可选,默认关闭)
  // 删除与已选结果重复的内容

  return decayed.sort((a, b) => b.score - a.score);
}

5.5 两种搜索的对比总结

维度向量搜索关键词搜索
引擎sqlite-vec 扩展SQLite 内置 FTS5
核心函数vec_distance_cosine()MATCH + bm25()
擅长语义理解、同义词、近义词精确术语、数值、人名
短板精确匹配不如 FTS不理解语义
典型应用"上次讨论的缓存方案""2023年合同金额"

六、MMR 去重算法

6.1 问题背景

向量搜索可能返回多条内容高度相似的记忆,浪费结果配额,降低答案多样性。

示例:

不加 MMR:
1. Python 性能优化技巧 (0.90)
2. Python 代码优化实践 (0.88) ← 和第 1 条高度重复
3. Redis 缓存穿透 (0.85)
4. Python 内存管理 (0.78)

加 MMR:
1. Python 性能优化技巧 (0.90)
2. Redis 缓存穿透 (0.85)     ← 被第 3 条替换,更有多样性
3. GIL 对性能的影响 (0.80)   ← 新角度
4. Python 内存管理 (0.78)

6.2 算法原理

MMR(Maximal Marginal Relevance,1998 年 Carbonell & Goldstein 提出):

MMR = λ × relevance - (1 - λ) × max_similarity_to_selected

参数含义:
- λ = 0.7(默认)
- λ = 1.0:只看相关性,完全忽略多样性
- λ = 0.0:只看多样性,完全忽略相关性

6.3 代码实现

// hybrid.ts / mmr.ts
export function mmrRerank<T extends MMRItem>(items, config) {
  const { lambda = 0.7 } = config;
  const selected = [];
  const remaining = new Set(items);

  while (remaining.size > 0) {
    let bestItem = null;
    let bestMMRScore = -Infinity;

    for (const candidate of remaining) {
      // 计算与已选结果的最大 Jaccard 相似度
      const maxSim = maxSimilarityToSelected(candidate, selected);

      // MMR 公式
      const mmrScore = lambda * candidate.score - (1 - lambda) * maxSim;

      if (mmrScore > bestMMRScore) {
        bestMMRScore = mmrScore;
        bestItem = candidate;
      }
    }

    selected.push(bestItem);
    remaining.delete(bestItem);
  }

  return selected;
}

// Jaccard 相似度:集合交集/并集
function jaccardSimilarity(setA: Set<string>, setB: Set<string>): number {
  const intersectionSize = [...setA].filter(x => setB.has(x)).length;
  const unionSize = setA.size + setB.size - intersectionSize;
  return unionSize === 0 ? 0 : intersectionSize / unionSize;
}

七、时间衰减机制

7.1 设计思想

记忆会随时间"褪色",越旧的记忆权重越低。

公式:

衰减因子 = e^(-λ × age_in_days)
其中 λ = ln(2) / halfLifeDays

7.2 半衰期效果

记忆年龄半衰期 30 天半衰期 60 天
0 天100%100%
7 天85%92%
30 天50%71%
90 天13%35%
180 天2%12%

7.3 代码实现

// temporal-decay.ts
export function calculateTemporalDecayMultiplier({ ageInDays, halfLifeDays }) {
  // λ = ln(2) / halfLifeDays
  // 半衰期 30 天 → λ ≈ 0.023
  const lambda = Math.LN2 / halfLifeDays;

  // 衰减 = e^(-λ × age)
  return Math.exp(-lambda * Math.max(0, ageInDays));
}

// 应用衰减
function applyTemporalDecay(score, ageInDays, halfLifeDays) {
  return score * calculateTemporalDecayMultiplier({ ageInDays, halfLifeDays });
}

7.4 常青记忆

以下内容不受时间衰减影响:

  • MEMORY.md(根目录)
  • memory.md(根目录)
  • memory/ 目录下非日期命名的文件
// temporal-decay.ts
function isEvergreenMemoryPath(filePath) {
  if (filePath === "MEMORY.md" || filePath === "memory.md") {
    return true;  // 常青,不衰减
  }
  // memory/ 下非日期命名也是常青
  return filePath.startsWith("memory/") && !isDatedMemoryPath(filePath);
}

八、完整数据流

8.1 写入流(记忆如何被保存)

用户输入 /new(开启新对话)
        ↓
session-memory hook 触发
        ↓
读取最近 15 条消息
        ↓
LLM 生成摘要 + 描述性标题
        ↓
写入 memory/2026-03-30-contract-history-query.md
        ↓
chokidar 监控检测到新文件
        ↓
标记 dirty = true
        ↓
下次 sync() 时执行:
  读取文件 → 分块(按段落)→ 嵌入(调用 AI)→ 存入 SQLite

8.2 读取流(记忆如何被检索)

用户问:"昨天那个合同金额,加上2022年的"
        
Agent 自动调用 memory_search 工具
        
MemoryIndexManager.search()
        
┌─────────────────┬─────────────────┐
    向量搜索           关键词搜索     
 chunks_vec       chunks_fts       
 余弦相似度匹配     BM25 精确匹配     
 score: 0.82      textScore: 0.65 
└────────┬────────┴────────┬────────┘
                         
    混合融合(70% + 30%)
         
    综合分: 0.769
         
    时间衰减(1天几乎无影响)
         
    MMR 去重
         
    返回 Top 10 结果
        
Agent 看到记忆片段  补充查询 2022   完整回答

九、索引流程源码解析

9.1 文件监控

// manager-sync-ops.ts
protected ensureWatcher() {
  this.watcher = chokidar.watch([
    path.join(workspaceDir, "MEMORY.md"),
    path.join(workspaceDir, "memory", "**", "*.md"),
  ], {
    ignoreInitial: true,
    awaitWriteFinish: {
      stabilityThreshold: 500,  // 等 500ms 等写入完成
      pollInterval: 100,
    },
  });

  this.watcher.on("add",    () => this.dirty = true);
  this.watcher.on("change", () => this.dirty = true);
  this.watcher.on("unlink", () => this.dirty = true);
}

9.2 分块策略

// manager-sync-ops.ts
protected splitIntoChunks(content, maxChars = 500) {
  // 按段落拆分,每块不超过 500 字符
  // 保留行号信息用于引用
  const paragraphs = content.split(/\n\n+/);
  const chunks = [];
  let current = "";
  let startLine = 1;

  for (const para of paragraphs) {
    if (current.length + para.length > maxChars && current.length > 0) {
      chunks.push({ text: current, startLine, endLine });
      current = "";
    }
    current += para + "\n\n";
  }
  return chunks;
}

9.3 向量生成与存储

// manager-sync-ops.ts
async indexFile(entry, options) {
  const chunks = this.splitIntoChunks(content);

  for (const chunk of chunks) {
    // ① 生成向量
    const embedding = await this.provider.embed(chunk.text);
    // → [0.023, -0.015, 0.087, ...]  1536维

    // ② 存入 chunks 表(原文)
    this.db.prepare(`
      INSERT INTO chunks (id, path, text, embedding, ...)
      VALUES (?, ?, ?, ?, ...)
    `).run(chunk.id, entry.path, chunk.text, JSON.stringify(embedding));

    // ③ 存入向量表(sqlite-vec 格式,必须是 Float32Array buffer)
    const vecBlob = Buffer.from(new Float32Array(embedding).buffer);
    this.db.prepare(`
      INSERT INTO chunks_vec (id, embedding) VALUES (?, ?)
    `).run(chunk.id, vecBlob);

    // ④ 存入全文索引(FTS5)
    this.db.prepare(`
      INSERT INTO chunks_fts (id, text, path, ...) VALUES (?, ?, ?, ...)
    `).run(chunk.id, chunk.text, entry.path, ...);
  }
}

十、配置参数

10.1 配置文件结构

{
  "memory": {
    "provider": "openai",
    "model": "text-embedding-3-small",
    "sources": ["memory", "sessions"],
    "query": {
      "hybrid": {
        "enabled": true,
        "vectorWeight": 0.7,
        "textWeight": 0.3,
        "mmr": {
          "enabled": false,
          "lambda": 0.7
        },
        "temporalDecay": {
          "enabled": false,
          "halfLifeDays": 30
        }
      },
      "minScore": 0.3,
      "maxResults": 10
    }
  }
}

10.2 参数说明

参数默认值说明
provider"auto"嵌入模型:openai / gemini / ollama / local
model-具体模型名称
sources["memory"]搜索哪些来源
vectorWeight0.7向量搜索权重
textWeight0.3关键词搜索权重
mmr.enabledfalse是否开启 MMR 去重
mmr.lambda0.7MMR 参数,1=重相关,0=重多样
temporalDecay.enabledfalse是否开启时间衰减
temporalDecay.halfLifeDays30半衰期(天)
minScore0.3最低分数阈值
maxResults10返回结果数量

十一、Agent 工具接口

11.1 memory_search

Agent 每次收到消息时自动调用此工具。

// tools.ts
export function createMemorySearchTool() {
  return {
    name: "memory_search",
    description: "Mandatory recall step: semantically search MEMORY.md + memory/*.md...",

    async execute({ cfg, agentId }, params) {
      const { query, maxResults, minScore } = params;

      const memory = await getMemoryManagerContext({ cfg, agentId });
      const results = await memory.manager.search(query, {
        maxResults,
        minScore,
        sessionKey: sessionKey,
      });

      return jsonResult({ results });
    }
  };
}

11.2 memory_get

根据路径精确读取记忆文件片段。

// tools.ts
export function createMemoryGetTool() {
  return {
    name: "memory_get",
    description: "Safe snippet read from MEMORY.md or memory/*.md...",

    async execute({ cfg, agentId }, params) {
      const { path, from, lines } = params;

      const result = await memory.manager.readFile({
        relPath: path,
        from,
        lines,
      });

      return jsonResult(result);
    }
  };
}

十二、总结

12.1 核心设计原则

原则实现
Markdown 是唯一真相源文件可读可编辑,索引可重建
向量 + 关键词互补hybrid.ts 混合融合
按需启用高级功能MMR 和时间衰减默认关闭
节省 API 调用embedding_cache 缓存已嵌入的内容

12.2 记忆系统三问

1. 记什么?   → memory/*.md 文件(手动写入 + 自动提取)
2. 怎么记?   → LLM 摘要 + SQLite 向量索引
3. 怎么找?   → 混合搜索(向量+关键词)+ MMR + 时间衰减

附录:源码文件索引

功能源码文件
表结构定义packages/memory-host-sdk/src/host/memory-schema.ts
核心管理器extensions/memory-core/src/memory/manager.ts
向量搜索extensions/memory-core/src/memory/manager-search.ts
混合融合extensions/memory-core/src/memory/hybrid.ts
MMR 去重extensions/memory-core/src/memory/mmr.ts
时间衰减extensions/memory-core/src/memory/temporal-decay.ts
嵌入生成extensions/memory-core/src/memory/embeddings.ts
文件同步extensions/memory-core/src/memory/manager-sync-ops.ts
Agent 工具extensions/memory-core/src/tools.ts
会话钩子src/hooks/bundled/session-memory/handler.ts
工具注册入口extensions/memory-core/index.ts