OpenClaw 记忆管理系统技术文档

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


一、系统概述

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

1.1 解决什么问题

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

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

1.2 核心设计思想

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

二、架构总览

2.1 三层架构

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        工具层                                │
│                  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 文件系统布局

yaml 复制代码
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 数据库表结构

sql 复制代码
-- 文件清单表
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 格式

生成的记忆文件示例:

markdown 复制代码
# 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 混合搜索原理

搜索分四个步骤完成:

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

5.2 向量搜索(语义理解)

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

核心 SQL(sqlite-vec):

sql 复制代码
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 ?

余弦相似度公式:

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

向量搜索的代码实现:

typescript 复制代码
// 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):

sql 复制代码
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 问题背景

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

比如搜索 "合同金额"

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

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

5.4.2 发展历史

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

5.4.3 完整公式

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

公式拆分两部分理解:

makefile 复制代码
BM25 = IDF × TF_score

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

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

IDF 公式:

ini 复制代码
              (N - n + 0.5)
IDF(qi) = log ───────────────
              (n + 0.5)

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

参数说明:

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

5.4.4 IDF 的作用

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

scss 复制代码
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 次好太多------这就是"饱和":

ini 复制代码
k1 = 1.2

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

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

5.4.6 TF_score 的作用:长度归一化

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

ini 复制代码
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 完整计算示例

场景:

arduino 复制代码
查询:["合同", "金额"]
总文档数 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

scss 复制代码
包含"合同"的文档 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)

ini 复制代码
文档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金额

ini 复制代码
文档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 中的实现

typescript 复制代码
// 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 直接调用:

sql 复制代码
-- 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 一句话总结

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

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

5.5 混合融合

typescript 复制代码
// 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年合同金额"
typescript 复制代码
// 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 问题背景

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

示例:

markdown 复制代码
不加 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 提出):

diff 复制代码
MMR = λ × relevance - (1 - λ) × max_similarity_to_selected

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

6.3 代码实现

typescript 复制代码
// 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 设计思想

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

公式:

scss 复制代码
衰减因子 = 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 代码实现

typescript 复制代码
// 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/ 目录下非日期命名的文件
typescript 复制代码
// temporal-decay.ts
function isEvergreenMemoryPath(filePath) {
  if (filePath === "MEMORY.md" || filePath === "memory.md") {
    return true;  // 常青,不衰减
  }
  // memory/ 下非日期命名也是常青
  return filePath.startsWith("memory/") && !isDatedMemoryPath(filePath);
}

八、完整数据流

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

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

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

yaml 复制代码
用户问:"昨天那个合同金额,加上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 文件监控

typescript 复制代码
// 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 分块策略

typescript 复制代码
// 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 向量生成与存储

typescript 复制代码
// 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 配置文件结构

json 复制代码
{
  "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"] 搜索哪些来源
vectorWeight 0.7 向量搜索权重
textWeight 0.3 关键词搜索权重
mmr.enabled false 是否开启 MMR 去重
mmr.lambda 0.7 MMR 参数,1=重相关,0=重多样
temporalDecay.enabled false 是否开启时间衰减
temporalDecay.halfLifeDays 30 半衰期(天)
minScore 0.3 最低分数阈值
maxResults 10 返回结果数量

十一、Agent 工具接口

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

typescript 复制代码
// 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

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

typescript 复制代码
// 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 记忆系统三问

markdown 复制代码
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
相关推荐
935962 小时前
练习题53-60
算法·深度优先
霖大侠2 小时前
Wavelet Meets Adam: Compressing Gradients forMemory-Efficient Training
人工智能·深度学习·算法·机器学习·transformer
AI成长日志3 小时前
【笔面试算法学习专栏】二分查找专题:力扣hot100经典题目深度解析
学习·算法·面试
lcreek3 小时前
流量优化之道:Ford-Fulkerson 最大流算法
算法·
垫脚摸太阳3 小时前
第 36 场 蓝桥·算法挑战赛·百校联赛---赛后复盘
数据结构·c++·算法
Aaswk3 小时前
刷题笔记(回溯算法)
数据结构·c++·笔记·算法·leetcode·深度优先·剪枝
NAGNIP4 小时前
一文搞懂CNN经典架构-ResNet!
算法·面试
计算机安禾4 小时前
【数据结构与算法】第14篇:队列(一):循环队列(顺序存储
c语言·开发语言·数据结构·c++·算法·visual studio
Frostnova丶4 小时前
(11)LeetCode 239. 滑动窗口最大值
数据结构·算法·leetcode