智展AI智展AI
首页市场Agent广场LLM排行榜AI芯片榜

AI 动态

AI 工具

ChatGPT iconChatGPTOpenAI iconOpenAIClaude iconClaudeGemini iconGeminiDeepSeek iconDeepSeekKimi iconKimi豆包 icon豆包通义 icon通义智谱GLM icon智谱GLMOpenClaw iconOpenClawOpenRouter iconOpenRouter

行情与资讯

Twitter/X iconTwitter/XBinance iconBinanceOKX iconOKX

开发者生态

GitHub iconGitHubHuggingFace iconHuggingFace魔塔社区 icon魔塔社区PyTorch iconPyTorchNVIDIA Dev iconNVIDIA DevAMD ROCm iconAMD ROCmLinux Kernel 开发者 iconLinux Kernel 开发者Android 开发者 iconAndroid 开发者地平线开发者论坛 icon地平线开发者论坛知乎 icon知乎

© 2025 智展AI

返回首页
技术AI

OpenClaw记忆检索

OpenClaw记忆检索

2026年02月27日
56 分钟阅读
13 次阅读

目录

  1. 为什么需要记忆检索
  2. FTS5 全文搜索
  3. sqlite-vec 向量搜索
  4. 完整搜索流程
  5. MMR 多样性重排
  6. Jaccard 相似度
  7. 整体技术对比

一、为什么需要记忆检索

用户问 Agent 一句话:“上周我们讨论的 API 鉴权方案是什么?”

AI 要去自己的”记忆库”(本地 Markdown 文件 + 历史对话)里找相关片段来回答。这个”找”的过程就是 记忆检索(Memory Search)。

问题:怎么衡量”相关”?

有两种思路:

思路技术特点
字面匹配:看词有没有出现FTS5(Full Text Search)快速精确,但换个说法就找不到
语义匹配:看意思像不像sqlite-vec理解同义词,但需要 Embedding 模型

OpenClaw 同时使用两者,融合后得到最优结果。


二、FTS5 全文搜索

2.1 是什么

FTS = Full-Text Search(全文搜索),FTS5 是 SQLite 内置的全文搜索引擎(第5版)。

它的核心是倒排索引(Inverted Index):

普通查询(慢,逐行扫描):
SELECT * FROM chunks WHERE text LIKE '%鉴权%';

FTS5 查询(快,走索引,毫秒级):
SELECT * FROM chunks_fts WHERE chunks_fts MATCH '"鉴权" AND "API"';

2.2 OpenClaw 建表方式

// src/memory/memory-schema.ts
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
  text,           -- 📌 只有这一列被全文索引
  id UNINDEXED,   -- 这些列不索引,只是附带存储
  path UNINDEXED,
  source UNINDEXED,
  model UNINDEXED,
  start_line UNINDEXED,
  end_line UNINDEXED
);

2.3 查询语句构建

// src/memory/hybrid.ts
// 用户输入:"API 鉴权方案"
// 经过这个函数变成 FTS 查询语句
export function buildFtsQuery(raw: string): string | null {
  const tokens = raw.match(/[\p{L}\p{N}_]+/gu)
    ?.map((t) => t.trim())
    .filter(Boolean) ?? [];
  // 结果:["API", "鉴权", "方案"]

  const quoted = tokens.map((t) => `"${t}"`);
  return quoted.join(" AND ");
  // 最终查询:`"API" AND "鉴权" AND "方案"`
  // 含义:三个词必须同时出现
}

2.4 BM25 评分转换

FTS5 使用 BM25 算法评分(Google 早期也用的标准算法,词频 × 稀有度)。返回值是负数,越小越相关,需要转换为[0,1]区间:

// src/memory/hybrid.ts
export function bm25RankToScore(rank: number): number {
  const normalized = Math.max(0, rank);   // 负数变0
  return 1 / (1 + normalized);           // 归一化到 (0,1]
}
// rank=0  → score=1.0(完美匹配)
// rank=1  → score=0.5
// rank=9  → score=0.1

2.5 FTS5 的局限

只能找”有这个词”的文档,找不到”语义相似”的内容。

例如:用户说”认证机制”,FTS5 找不到只写了 auth的片段。


三、sqlite-vec 向量搜索

3.1 sqlite-vec是什么

sqlite-vec 是一个 SQLite 扩展(.so / .dylib/ .dll动态库),给 SQLite 增加向量计算能力。

类比:SQLite 默认只会算加减乘除,sqlite-vec 给它装了一个”几何计算器”,让它能算向量距离。

3.2 向量(Embedding)是什么

Embedding 模型(如 OpenAI text-embedding-3-small)把一段文字变成一串数字:

"API 鉴权方案"  → [0.12, -0.34, 0.89, 0.01, ...]  // 1536 个数字
"OAuth2 认证"  → [0.11, -0.31, 0.91, 0.02, ...]  // ✅ 很相近!
"今天天气不错"  → [0.88,  0.22, -0.54, 0.77, ...]  // ❌ 差很远

语义越相近,向量越相近(在高维空间中的夹角越小)。

3.3 余弦相似度

衡量两个向量”方向”有多接近:

相似度 = cos(θ) = A·B / (|A| × |B|)

结果范围:[-1, 1]
  1.0 = 完全相同方向(语义完全一致)
  0.0 = 垂直(语义无关)
 -1.0 = 相反方向(语义相反)

实际用余弦距离(= 1 - 相似度):距离越小,越相关。

3.4 OpenClaw 的 SQL 查询

-- 查找与用户问题最相近的前 N 个文本块
SELECT
  c.id, c.path, c.start_line, c.end_line, c.text, c.source,
  vec_distance_cosine(v.embedding, ?) AS dist  -- ← sqlite-vec 提供的函数
FROM chunks_vec v          -- ← sqlite-vec 虚拟表(加速 ANN 搜索)
JOIN chunks c ON c.id = v.id
WHERE c.model = ?          -- 只找同一个 embedding 模型的结果
ORDER BY dist ASC          -- 距离最小的排最前
LIMIT ?

vec_distance_cosine由 sqlite-vec 扩展提供。若无此扩展,则需把所有向量读出来在 JS 里逐个计算,性能极差。

3.5 降级策略

// src/memory/manager-search.ts
try {
  db.loadExtension(extensionPath);  // 加载 sqlite-vec.so(SIMD 加速)
  vectorReady = true;
} catch {
  // 降级:用 JS 实现的 cosineSimilarity 手动遍历(慢但可用)
}

3.6 支持的 Embedding Provider

Provider模型示例
OpenAItext-embedding-3-small / text-embedding-3-large
Geminitext-embedding-004
Voyagevoyage-3 / voyage-3-lite
Mistralmistral-embed
Localnode-llama-cpp(本地模型)

四、完整搜索流程

用户输入:"上周讨论的 API 鉴权方案"
                │
                ▼
┌────────────────────────────────────────┐
│           并行执行两路搜索               │
│                                        │
│  路① FTS5 关键词搜索                   │
│  buildFtsQuery(...)                    │
│  → "\"API\" AND \"鉴权\" AND \"方案\""  │
│  SQL: SELECT ... FROM chunks_fts       │
│       WHERE chunks_fts MATCH ?         │
│       ORDER BY bm25(chunks_fts)        │
│                                        │
│  得到:[chunk_A(0.8), chunk_C(0.5)]    │
│                                        │
│  路② 向量语义搜索                      │
│  embedding("上周讨论的 API 鉴权方案")   │
│  → [0.12, -0.34, ...] (1536维)        │
│  SQL: SELECT ...,                      │
│       vec_distance_cosine(v.embedding,?)│
│       FROM chunks_vec ORDER BY dist    │
│                                        │
│  得到:[chunk_B(0.92), chunk_A(0.85)]  │
└──────────────────┬─────────────────────┘
                   │
                   ▼
┌────────────────────────────────────────┐
│         合并(Hybrid Merge)            │
│                                        │
│  按 id 合并两路结果:                   │
│  chunk_A: vectorScore=0.85, textScore=0.8  │
│  chunk_B: vectorScore=0.92, textScore=0    │
│  chunk_C: vectorScore=0,    textScore=0.5  │
│                                        │
│  加权合分:                             │
│  score = vectorWeight × v + textWeight × t  │
│  chunk_A: 0.7×0.85 + 0.3×0.8 = 0.835  │
│  chunk_B: 0.7×0.92 + 0.3×0   = 0.644  │
│  chunk_C: 0.7×0    + 0.3×0.5 = 0.150  │
└──────────────────┬─────────────────────┘
                   │
                   ▼  (可选)
┌────────────────────────────────────────┐
│      时间衰减(Temporal Decay)          │
│  最近的文件加分,很久之前的减分           │
│  用文件修改时间计算半衰期(halfLifeDays) │
└──────────────────┬─────────────────────┘
                   │
                   ▼  (可选,默认关闭)
┌────────────────────────────────────────┐
│         MMR 重排(见下节)              │
└──────────────────┬─────────────────────┘
                   │
                   ▼
          最终结果:[chunk_A, chunk_B, chunk_C]
          注入到 AI 上下文中

加权合并的源码

// src/memory/hybrid.ts
const merged = Array.from(byId.values()).map((entry) => {
  const score =
    params.vectorWeight * entry.vectorScore +
    params.textWeight  * entry.textScore;
  return { path, startLine, endLine, score, snippet, source };
});

五、MMR 多样性重排

5.1 问题:纯相关性排序的缺陷

假设记忆库里有 10 个文档都在讲”OAuth2 认证”,纯相关性排序会返回:

第1名:oauth2_guide.md(第 1-20 行)
第2名:oauth2_guide.md(第21-40 行)  ← ⚠️ 和第1名几乎一样!
第3名:oauth2_guide.md(第41-60 行)  ← ⚠️ 还是重复!
...

这就出现了冗余:10个结果说的是同一件事,浪费 Token,AI 也看不到其他相关内容。

5.2 MMR 的核心思想

MMR = Maximal Marginal Relevance(最大边际相关性),每次选下一个结果时,同时考虑:

  • ✅ 相关性(跟查询有多像)
  • ✅ 多样性(跟已选结果有多不同)

5.3 公式

MMR(d) = λ × Relevance(d, query) - (1 - λ) × max Similarity(d, selected)

λ = 0.7(默认):偏向相关性,同时引入30%多样性惩罚
// src/memory/mmr.ts
export function computeMMRScore(
  relevance: number,      // 跟查询的相似度(归一化到 [0,1])
  maxSimilarity: number,  // 跟已选结果中最相似的那个的相似度
  lambda: number,         // 默认 0.7
): number {
  return lambda * relevance - (1 - lambda) * maxSimilarity;
}

5.4 迭代选择算法

// src/memory/mmr.ts
// 步骤1:预先对所有 snippet 分词,建 token 缓存
for (const item of items) {
  tokenCache.set(item.id, tokenize(item.content));
  // "API OAuth2 token" → Set{"api", "oauth2", "token"}
}

// 步骤2:分数归一化到 [0,1](与相似度量纲统一)
const normalizeScore = (score) => (score - minScore) / scoreRange;

// 步骤3:迭代贪心选择
while (remaining.size > 0) {
  let bestItem = null, bestMMRScore = -Infinity;

  for (const candidate of remaining) {
    const relevance = normalizeScore(candidate.score);
    const maxSim   = maxSimilarityToSelected(candidate, selected, tokenCache);
    const mmrScore = computeMMRScore(relevance, maxSim, lambda);

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

  selected.push(bestItem);   // 选中!
  remaining.delete(bestItem);
}

5.5 具体示例

假设搜索”API 鉴权”,得到3个候选:

候选score内容
A0.9“OAuth2 bearer token 鉴权方式”
B0.8“OAuth2 access token 刷新机制”(与A很像)
C0.7“API Key 静态鉴权配置”(与A不同)

无 MMR(纯相关性):

结果:A → B → C
问题:A 和 B 几乎说的同一件事,C 被压到末位

有 MMR(λ=0.7):

第1轮:selected=[], 直接选最高分 → 选 A

第2轮:selected=[A]
  B 的 MMR = 0.7×0.8 - 0.3×jaccardSim(B,A)
           = 0.56   - 0.3×0.6 = 0.38  ← A和B共同词多,被惩罚
  C 的 MMR = 0.7×0.7 - 0.3×jaccardSim(C,A)
           = 0.49   - 0.3×0.1 = 0.46  ← A和C差异大,惩罚小
  → 选 C(而非 B!)

第3轮:只剩 B → 选 B

最终结果:A → C → B
✅ A(OAuth2)和 C(API Key)提供互补视角,信息量更大

六、Jaccard 相似度

MMR 中用于衡量两段文本”内容重叠度”的算法:

// src/memory/mmr.ts

// 分词:只保留字母和数字
export function tokenize(text: string): Set<string> {
  return new Set(text.toLowerCase().match(/[a-z0-9_]+/g) ?? []);
}
// "OAuth2 bearer token" → Set{"oauth2", "bearer", "token"}

// Jaccard = |交集| / |并集|
export function jaccardSimilarity(setA, setB): number {
  let intersectionSize = 0;
  for (const token of smaller) {
    if (larger.has(token)) intersectionSize++;
  }
  const unionSize = setA.size + setB.size - intersectionSize;
  return intersectionSize / unionSize;
}

举例:

A = {"oauth2", "bearer", "token"}
B = {"oauth2", "access", "token"}

交集 = {"oauth2", "token"}  → size = 2
并集 = {"oauth2", "bearer", "token", "access"}  → size = 4

Jaccard(A, B) = 2 / 4 = 0.5

为什么 MMR 用 Jaccard 而不是余弦相似度?

因为此时已经把所有 snippet 文本(最长 700 字符)都读到记忆中了,算 Jaccard 词袋相似度比重新调用 Embedding API 便宜得多,也足够准确。


七、整体技术对比

维度FTS5(关键词)sqlite-vec(向量)MMR(重排)
本质倒排索引,找词近邻搜索,找意思贪心选择,找多样性
擅长精确词匹配、代码、专有名词同义词、换说法、语义理解去冗余、保多样
弱点换个词就找不到精确词匹配不如 FTS计算量 O(n²)
打分算法BM25(词频 × 稀有度)余弦距离Jaccard 相似度
时间复杂度O(log n),极快O(n)(ANN 近似后更快)O(n²)
依赖SQLite 内置,无需额外安装sqlite-vec 扩展 + Embedding API已有结果列表即可
OpenClaw 默认开启✅ hybrid.enabled=true 时✅ 有 Provider 时❌ 需 opt-in

八、三种搜索模式总结

OpenClaw 根据运行环境自动降级到最优模式:

有 Embedding Provider?
        │
    ┌───┴───┐
   Yes      No
    │        │
    ▼        ▼
hybrid.enabled?   FTS-only 模式
   │    │         (仅关键词搜索)
  Yes   No
   │    │
   ▼    ▼
Hybrid  纯向量
模式    模式
模式触发条件能力
Hybrid(混合)有 Provider + hybrid=true向量 + BM25 双路搜索,效果最好
向量 only有 Provider + hybrid=false纯语义,适合语义强的查询
FTS only无 Provider纯关键词,无需 API,离线可用
返回首页

评论

加载评论中...