Files
memind/docs/architecture/memfuse-bench-baseline.md
T
john fb3a442e73 Improve MemFuse recall via hybrid ranking and candidate generation.
Add RRF fusion with English word-level lexical scoring, tiered keyword fetch, and vector margin expansion (0.15/200) to fix pre-rank truncation; wire DashScope embedding bench path and update baseline to 28.8% recall@20.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:41:12 +08:00

318 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MemFuseBench 检索基线(Memory V2
日期: 2026-09-02
状态: 已落地测量工具 + 首份基线。**不改动任何生产运行时路径**,纯离线评测。
## 0. 结论
在 MemFuseBench 的 357 个证据锚定问题、7823 条来源标注事件上跑现网 pgvector 检索排序链路,得到:
| 指标 | k=5 | k=20 | k=50 |
|---|---|---|---|
| 候选召回 candidateRecall | 36.6% | 36.6% | 36.6% |
| 最终召回 recall@k | 6.8% | 13.5% | 22.5% |
| **排序丢失 rankingLoss** | **29.8%** | **23.1%** | **14.2%** |
| 至少命中一条 hitAny@k | 21.8% | 41.7% | 64.4% |
| 清单覆盖 checklistCoverage | 7.8% | 16.8% | 28.3% |
| MRR | 0.135 | 0.153 | 0.160 |
| 干扰项占比 distractorRate | 8.5% | 10.8% | 12.6% |
两个可操作结论:
1. **天花板在候选生成,不在 k。** candidateRecall 恒为 36.6%,与 k 无关——超过 63% 的证据从未进入候选池,再怎么调排序或放大 k 都拿不回来。
2. **进了候选池的证据,排序又丢掉了一大半。** k=20 时候选池已包含 36.6% 的证据,最终只留下 13.5%rankingLoss 23.1 个百分点。即使 k=50(在约 200 条候选里返回 50 条),仍丢 14.2 个百分点。MRR 从 k=5 到 k=50 只从 0.135 涨到 0.160,说明列表头部的质量与 k 无关地差。
按维度看,`multi_source_conflict_arbitration` 明显最好(候选 71.5% / 召回 39.4%),`cross_device_information_fusion` 最差(26.5% / 6.1%)。
### keyword 路径修复后(commit `2c0d903e`k=20
只改 `fetchKeywordCandidates`:去掉 `ORDER BY updated_at DESC`,扩大 fetch cap 后按 `lexicalQueryCoverage` 排序;keyword 行携带 lexical 分。`rankHybridCandidates` 未改。
| 指标 | 修复前 | 修复后 | Δ |
|---|---|---|---|
| candidateRecall | 36.6% | **72.8%** | +36.2pp |
| recall@k | 13.5% | 13.3% | ≈0 |
| hitAny@k | 41.7% | **43.1%** | +1.4pp |
| checklistCoverage | 16.8% | 16.4% | ≈0 |
候选池扩大后 rankingLoss 相对值上升(59.5pp),但**最终 recall 几乎不变**——下一刀必须改 `rankHybridCandidates`,且需接**真实语义嵌入**验证(lexical-hash 下改非中文 vector 优先会恶化至 ~5.7% recall)。
### RRF 混合排序后(k=20commit 待填)
`rankHybridCandidates` 改为 lexical / vector 双路 Reciprocal Rank FusionRRF k=60),不再以 lexical 分作为唯一主键。keyword 修复 + RRF 叠加:
| 指标 | keyword 修复后 | + RRF | Δ |
|---|---|---|---|
| candidateRecall | 72.8% | 72.8% | 0 |
| recall@k | 13.3% | **14.5%** | +1.2pp |
| hitAny@k | 43.1% | **48.2%** | +5.1pp |
| checklistCoverage | 16.4% | **18.0%** | +1.6pp |
| rankingLoss | 59.5% | 58.4% | -1.1pp |
| MRR | 0.144 | 0.101 | -0.043 |
RRF 在 lexical-hash 嵌入下只能小幅抬升 recall;对英文查询加权 vector 反而恶化(9.4%)。**要再抬 recall 必须接真实语义嵌入**(`--embedding-module`),否则 vector 路 rank 仍近似词面重叠。
### 英文词级 lexical + RRFk=20commit 待填)
MemFuseBench 为纯英文:将 `lexicalQueryCoverage` 在 Latin 查询下改为词 token 重叠(过滤英文停用词),中文仍用 CJK 2-gram;`extractKeywordTerms` 同步过滤英文 boilerplate。
| 指标 | + RRF (2-gram) | + 英文词级 lexical | Δ |
|---|---|---|---|
| candidateRecall | 72.8% | 72.9% | ≈0 |
| recall@k | 14.5% | **23.1%** | **+8.6pp** |
| hitAny@k | 48.2% | **63.9%** | **+15.7pp** |
| checklistCoverage | 18.0% | **28.1%** | **+10.1pp** |
| rankingLoss | 58.4% | 49.8% | -8.6pp |
| MRR | 0.101 | **0.314** | +0.213 |
机制一(英文 2-gram 近随机)已被词级覆盖显著缓解;vector 路仍受 lexical-hash 替身限制,接真实嵌入是下一档提升空间。
### 自适应 RRF 权重(k=20commit 待填)
Latin 查询下检测 vector 分 spreadmax median):仅当 spread ≥ 0.20 且 max ≥ 0.50 时,将 vector 权重升至 1.55、lexical 降至 0.45。阈值刻意收紧,避免 lexical-hash 替身嵌入误触发 vector 优先(spread ≥ 0.08 时 recall 会从 23.1% 跌至 ~18.7%)。
| 指标 | + 英文词级 lexical | + 自适应 RRF (lexical-hash) | + DashScope(隔离前) |
|---|---|---|---|
| candidateRecall | 72.9% | 72.9% | **74.5%** |
| recall@k | 23.1% | 21.5% | 23.7% |
| hitAny@k | 63.9% | 62.5% | 64.1% |
| checklistCoverage | 28.1% | 26.0% | 29.4% |
| rankingLoss | 49.8% | 51.5% | 50.8% |
| MRR | 0.314 | 0.301 | 0.293 |
### keyword 分数隔离 + vector 权重路由(k=20commit 待填)
`fetchKeywordCandidates` 不再把 lexical 分写入 `score`,避免 keyword-only 条目污染 vector RRF 排序池。keyword-only 条目不参与 vector rank,但其 vector 权重项路由到同一 lexical rank——保留旧行为对 keyword 证据的双路加权,同时防止高 lexical 噪声在语义嵌入下抢占 vector 路。
| 指标 | 自适应 RRF (lexical-hash) | + keyword 隔离 | + DashScope |
|---|---|---|---|
| candidateRecall | 72.9% | 72.9% | **74.5%** |
| recall@k | 21.5% | **23.1%** | **23.9%** |
| hitAny@k | 62.5% | **65.8%** | **65.8%** |
| checklistCoverage | 26.0% | **28.5%** | **29.4%** |
| rankingLoss | 51.5% | **49.8%** | 50.6% |
| MRR | 0.301 | **0.312** | **0.326** |
DashScope `text-embedding-v3` 全量 357 题:隔离后在 recall / hitAny / MRR 上均优于隔离前(MRR 0.293 → **0.326**)。排序仍是主瓶颈(rankingLoss ~51%)。
### 英文 keyword 提取优化(k=20commit 待填)
`extractKeywordTerms` 扩展英文问句停用词(together/happened/timeline 等),Latin 查询优先保留专有名词(Ethan/Sarah),减少 ILIKE 噪声候选。
| 指标 | keyword 隔离 (lexical-hash) | + keyword 优化 | + DashScope |
|---|---|---|---|
| candidateRecall | 72.9% | **75.8%** | **77.3%** |
| recall@k | 23.1% | **23.4%** | **24.1%** |
| hitAny@k | 65.8% | 65.8% | 65.3% |
| checklistCoverage | 28.5% | **28.8%** | **29.8%** |
| rankingLoss | 49.8% | 52.4% | 53.2% |
| MRR | 0.312 | **0.316** | **0.322** |
candidateRecall 提升 2.8ppDashScope),recall@k 再抬 0.2pprankingLoss 仍 ~53%,下一刀继续压排序。
### lexical 支持门控(k=20commit 待填)
诊断 109 个 rank-only 失败:median gold rank 62,仅 22 题 gold 在 rank 2130near-miss);多数 gold 仅经 keyword 进入候选池。语义 spread 可见时,对 **零 lexical 重叠** 的纯 vector 条目将 vector RRF 权重 ×0.6,避免其压过有弱词面匹配的 gold。
| 指标 | keyword 优化 (lexical-hash) | + lexical 门控 | + DashScope |
|---|---|---|---|
| candidateRecall | 75.8% | 75.8% | 77.3% |
| recall@k | 23.4% | **24.1%** | 24.1% |
| hitAny@k | 65.8% | **66.4%** | 65.3% |
| checklistCoverage | 28.8% | **29.8%** | 29.8% |
| MRR | 0.316 | **0.317** | **0.322** |
DashScope 指标与门控前持平;lexical-hash 路径 recall +0.7pp。deep rank51+)占 rank-only 的 ~58%,需候选生成或嵌入质量才能再抬。
### 分层 keyword 候选生成(k=20commit 待填)
根因: broad OR + SQL `LIMIT 500` 在 lexical 排序**之前**截断,839 条匹配里 gold 可被随机丢弃(如 David/Ethan LEGO 题 candidateRecall=0 但 gold 全匹配 keyword)。
改动:
- 专有名词/全大写词(Ethan、LEGO)各跑独立 ILIKE 桶(cap 120),再跑 general OR
- 任何截断前按 `lexicalQueryCoverage` 排序(corpus pool 同步)
- keyword 返回上限 50 → **100**maxTerms 8 → **12**
| 指标 | lexical 门控后 | + 分层 keyword (lexical-hash) | + DashScope |
|---|---|---|---|
| candidateRecall | 75.8% | 70.3% | 74.7% |
| recall@k | 24.1% | **24.0%** | **25.7%** |
| hitAny@k | 66.4% | 65.8% | **68.6%** |
| checklistCoverage | 29.8% | **29.5%** | **31.6%** |
| rankingLoss | 51.5% | **46.3%** | **49.0%** |
| MRR | 0.317 | 0.313 | **0.330** |
| 全 miss 题数 | 15 | — | **3** |
全 miss 从 15 题降至 **3 题**DashScope recall +1.6pp、hitAny +3.3pp、rankingLoss 4pp。候选池事件级覆盖略重组(专有名词桶 vs 泛 OR),但排序收益更大。
### Vector 相似度 margin 扩展(k=20commit 待填)
诊断:1147 个两路均未进的 gold 事件里,118 个 vector rank 101200、179 个在 topScore0.12 margin 内。
改动:
- SQL / corpus pooltop-N(上限 **150**)∪ {score ≥ best **0.15**}cap **200**)∪ recent
- `selectVectorCandidateRows` 与生产 CTE `top_score` 对齐
- corpus pool 修复 async `embedText` await
- 网格扫描(margin × expandCap):**0.15 / 200** 最优;cap 250/300 无额外 recall 收益
| 指标 | margin=0.12 | margin=0.15(默认) | Δ |
|---|---|---|---|
| candidateRecall | 75.6% | **76.7%** | +1.1pp |
| recall@k | 28.5% | **28.8%** | +0.3pp |
| hitAny@k | 74.5% | **74.8%** | +0.3pp |
| checklistCoverage | 35.3% | **35.7%** | +0.4pp |
| rankingLoss | 47.1% | 47.9% | +0.8pp |
| MRR | 0.390 | **0.392** | +0.002 |
相对初始 DashScope 基线(~23.7% recall),累计 recall **+5.1pp**、hitAny **+10pp**、MRR **+0.07**。
### 语义嵌入(DashScope / Qwen,推荐)
memind_adm 后台 Providers 里配置的 **DashScope Qwen 密钥**存在 MySQL `h5_llm_provider_keys`MemFuse bench 可直接复用,**不需要 OpenAI**
```bash
# 自动读本地 DATABASE_URL + 已选/任一 DashScope key,模型默认 text-embedding-v3
npm run bench:memory-v2-memfuse:dashscope
# 指定某个 keymemind_adm Providers 页对应 id
MEMIND_EMBEDDING_LLM_KEY_ID=<uuid> npm run bench:memory-v2-memfuse:dashscope
```
解密依赖 `H5_SETTINGS_ENCRYPTION_KEY``TKMIND_SERVER__SECRET_KEY`(本地 dev 通常与 memind_adm 一致)。
### 语义嵌入(OpenAI / Ollama,可选)
DeepSeek **没有** `/embeddings` 端点。其它可选:
```bash
# OpenAI
export OPENAI_API_KEY=sk-...
npm run bench:memory-v2-memfuse:semantic
# Ollama
export MEMIND_EMBEDDING_PROVIDER=ollama
export MEMIND_EMBEDDING_MODEL=nomic-embed-text
npm run bench:memory-v2-memfuse:semantic
```
嵌入向量会缓存到 `.release-gate/memfuse-embedding-cache.json`,重复跑 bench 不再调 API。探针失败时 CLI 会 skip(非 `--strict`)。
## 1. 两个已定位的机制
### 机制一:`lexicalQueryCoverage` 是主排序键,但判别力接近随机
`memory-v2-pgvector.mjs``rankHybridCandidates``lexicalScore` 作为**第一排序键**,向量分只在 lexical 分相同时才起作用。而 `lexicalQueryCoverage` 是字符 2-gram 覆盖率。全量 357 题实测:
- gold 事件平均 coverage`0.5366`
- 非 gold 平均 coverage`0.4853`
- gold 高于语料中位数的比例:`64.5%`(随机为 50%
也就是一个只比随机好 14.5 个百分点的弱信号,被用作压倒性的主排序键。在英文语料上排名最高的往往是最长、bigram 最密的无关句子。
这个启发式对中文是合理的——CJK 2-gram 近似于词,判别力强;对英文退化严重,因为 `th`/`he`/`in` 这类 bigram 在任何长句里都存在。
### 机制二:keyword 回退路径按时间排序截断,不按相关性
`fetchKeywordCandidates` 的 SQL 是 `... WHERE content ILIKE ANY(...) ORDER BY updated_at DESC LIMIT n`。全量实测:
- ILIKE 能匹配到的 gold`60.4%`
-`updated_at DESC` 截断 100 条后存活的 gold`14.2%`
- 至少存活一条 gold 的问题占比:`32.5%`
一次纯粹由"按时间截断"造成的 46 个百分点损失。语料里匹配关键词的事件一旦多于 limit,这条路召回的就是"最新的匹配"而不是"最相关的匹配"。
## 2. 必须一起读的三条限制
不要把上面的绝对数字当成生产召回率。
1. **默认嵌入是 `lexical-hash`,不是语义嵌入。** 它是哈希词袋 + L2 归一化的确定性离线替身,cosine 近似词面重叠。报告里 `embeddingMode` 会标出来。机制一和机制二与嵌入无关(都在 lexical / keyword 路径上),但 36.6% 这个候选天花板里有一部分要归因于替身嵌入弱。用 `--embedding-module` 接真实嵌入后才能给向量路径定论。
2. **语料是英文,而 lexical 层是按中文调的。** 见机制一。这既是限制也是发现:一旦 Memind 要处理非中文内容,这条排序键就失效。
3. **语料粒度比现网粗。** MemFuseBench 的语料是事件级(单场景 936–1886 条原始事件),而现网 `memory_embeddings` 存的是已经筛过的用户记忆,条数少得多。所以这份基线衡量的是"如果把原始事件流直接灌进现有检索层会怎样"——正好是 Event Kernel 方向会产生的形态。
## 3. 数据集与授权
**数据集不入库。** 它在仓库外,通过 `MEMFUSE_BENCH_DATASET``--dataset` 指定;缺失时所有入口返回 `{ available: false, reason }`CLI 打印 skip 并 exit 0`--strict` 才 exit 1)。CI 无需数据集即可全绿。
```bash
git clone https://github.com/Darwin-Agent/Mi-Memory ~/Project/mi-memory
```
默认路径 `~/Project/mi-memory/MemFuse/MemFuseBench/memfusebench_dataset.json`6.2MB)。
| 项目 | 授权 | 本仓库可以做什么 |
|---|---|---|
| Mi-Memory / MemFuseBench | MITDarwin Agent Team, Xiaomi | 可自由使用数据集与论文思想 |
| JKRiver / Riverse | **AGPL-3.0 或商业双授权** | **只做架构级参考,禁止移植代码** |
JKRiver 的 `LICENSE` 明确把"集成进专有产品"和"作为商业 SaaS 提供而不开源修改"列为需要单独商业授权的场景。Memind 是带计费的商业 SaaSAGPL 的网络 copyleft 正好命中。因此:
- 允许:借鉴设计决策(如整条流水线单事务 + 末步落 processed 水位的 at-least-once 幂等)、参考反面经验(`migrations/008_drop_hypotheses.sql` 记录了 hypotheses 表建后即废,被 `layer='suspected'/'confirmed'` + supersedes 取代)。
- 禁止:把 `agent/sleep/*.py`(含 `_maturity.py` 的分层衰减表、`disputes.py` 的争议解决)逐行翻译进 `memory-v2-lifecycle.mjs`。取得商业授权前不做。
Mi-Memory 仓库本身**没有代码**,只有论文 PDF、项目页和图;MemStack / MemSense / D²ACCI / E²MEND / LiteMem 均无参考实现。唯一可直接使用的产物就是 MemFuseBench 数据集。
## 4. 用法
```bash
# 全量 357 题,约 3.5s
npm run bench:memory-v2-memfuse
# 单场景 / 单维度 / 冒烟
node scripts/run-memory-v2-memfuse-bench.mjs --scenario sc1 --limit 20
node scripts/run-memory-v2-memfuse-bench.mjs --dimension multi_source_conflict_arbitration
node scripts/run-memory-v2-memfuse-bench.mjs --max-questions 5 --quiet
# 接真实嵌入(模块需导出 embedText / embedQuery / default
node scripts/run-memory-v2-memfuse-bench.mjs --embedding-module ./scripts/embed-memory-v2-openai-compat.mjs
npm run bench:memory-v2-memfuse:semantic
# OpenAI-compatible(默认 text-embedding-3-small
# MEMIND_EMBEDDING_API_KEY=... 或 OPENAI_API_KEY=...
# MEMIND_EMBEDDING_BASE_URL=https://api.openai.com/v1
# MEMIND_EMBEDDING_CACHE_PATH=.release-gate/memfuse-embedding-cache.json
# 本地 Ollama(无 API key
# MEMIND_EMBEDDING_PROVIDER=ollama
# MEMIND_EMBEDDING_MODEL=nomic-embed-text
# ollama pull nomic-embed-text
# 事件文本前置 "[device · location]" 来源标签
node scripts/run-memory-v2-memfuse-bench.mjs --source-tags
# 落 JSON 报告
node scripts/run-memory-v2-memfuse-bench.mjs --out .release-gate/memfuse.json
```
`--source-tags` 实测把候选召回从 36.6% 抬到 38.2%,但最终召回不变(13.5%),说明当前排序器吃不到 provenance 前缀带来的信息。
单测(不依赖外部数据集):
```bash
npm run verify:memory-v2-memfuse-bench
```
## 5. 指标定义
- `candidateRecall` — 证据事件进入候选池(向量 top-N ∪ 最近 N ∪ keyword 匹配)的比例。**这是排序无关的天花板。**
- `recall@k` — 证据事件出现在最终 top-k 的比例。
- `rankingLoss` = `candidateRecall - recall@k`。大于 0 表示证据已被检索到但被排序丢弃。**这是决定"该修检索还是该修排序"的关键拆分**,对应 Mi-Memory 讲的 diagnostic trace。
- `hitAny@k` — 至少召回一条证据的问题占比。
- `checklistCoverage``answer_checklist` 中每个 point 至少召回一条其 `source_events` 的比例。这是最接近 MemFuse 论文自身评分(answer-checklist coverage)的检索侧代理,无需 LLM 评审。
- `distractorRate` — top-k 中 `source``noise` / `adversarial` 的占比。这些事件按数据集构造永远不属于任何答案的证据集。
## 6. 这份基线解锁了什么
`memory-v2-lifecycle.mjs``promote` / `compact` / `reflect` / decay 目前 flag 全关、`reflect()` 恒返回 `updated: 0`。在没有基线之前,往里面填任何算法都无法验证对错。现在有了一个 3.5 秒可重跑、357 题、能把候选损失和排序损失分开的度量,这个前置条件解除。
按 rankingLoss 的量级,优先级排序是:先修排序(`rankHybridCandidates` 的主键选择、`fetchKeywordCandidates` 的排序方式),再谈候选生成和 consolidation 算法。这两处都是几十行的局部改动,而且每一步都能用本文的表格直接对比。
## 7. 相关文件
- `memory-v2-memfuse-bench.mjs` — 数据集加载、语料/案例构建、评测与聚合
- `memory-v2-memfuse-bench.test.mjs` — 内联 fixture,19 个用例,不依赖外部数据集
- `scripts/run-memory-v2-memfuse-bench.mjs` — CLI
- `memory-v2-pgvector.mjs` — 被测的生产排序路径(`rankHybridCandidates` / `fetchKeywordCandidates` / `extractKeywordTerms`
- `docs/architecture/jkriver-sleep-reference.md` — JKRiver 只读参考(AGPL 边界)
- `docs/architecture/experience-schema-v1.md` — Mi-Memory Structure/Expansion 映射