# 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=20,commit 待填) `rankHybridCandidates` 改为 lexical / vector 双路 Reciprocal Rank Fusion(RRF 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 + RRF(k=20,commit 待填) 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=20,commit 待填) Latin 查询下检测 vector 分 spread(max − 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=20,commit 待填) `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=20,commit 待填) `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.8pp(DashScope),recall@k 再抬 0.2pp;rankingLoss 仍 ~53%,下一刀继续压排序。 ### lexical 支持门控(k=20,commit 待填) 诊断 109 个 rank-only 失败:median gold rank 62,仅 22 题 gold 在 rank 21–30(near-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 rank(51+)占 rank-only 的 ~58%,需候选生成或嵌入质量才能再抬。 ### 分层 keyword 候选生成(k=20,commit 待填) 根因: 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=20,commit 待填) 诊断:1147 个两路均未进的 gold 事件里,118 个 vector rank 101–200、179 个在 topScore−0.12 margin 内。 改动: - SQL / corpus pool:top-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**。 ### cross_device 弱维优化(k=20,commit 待填) `cross_device_*` 题全带 `question_device`(如 `phone_sarah`),证据跨 watch/phone/car 等设备,且 fusion 维平均 **15.2** 条 gold/题(recall@k 天然偏低,hitAny 更关键)。 改动: - 含 `cross_device_*` 题的 scenario **自动启用 `[device · location]` source-tags**(写入语料前缀,供嵌入/lexical 共用) - `resolve({ recallContext: { device, user } })`:device 拆词优先 keyword(`phone`/`sarah`),嵌入 query 追加 `device context:` 行 - `latinWordQueryCoverage` 对 source-tag 前缀 token(含 `watch_ethan` → `ethan`)额外 +35% 权重上限 | 维度 | 指标 | 优化前 | 优化后 | Δ | |---|---|---|---|---| | information_fusion | candidateRecall | 70.1% | **78.6%** | +8.5pp | | information_fusion | hitAny@k | 62.0% | **69.0%** | +7.0pp | | information_fusion | recall@k | 11.0% | 11.8% | +0.8pp | | causal_reasoning | candidateRecall | 66.0% | **82.3%** | +16.3pp | | causal_reasoning | hitAny@k | 81.0% | 79.4% | −1.6pp | | causal_reasoning | recall@k | 22.8% | 21.7% | −1.1pp | | **全量 357** | recall@k | 28.8% | **28.6%** | ≈0 | | **全量 357** | hitAny@k | 74.8% | **75.6%** | +0.8pp | | **全量 357** | MRR | 0.392 | **0.397** | +0.005 | fusion 仍是最弱维,但候选/ hitAny 明显提升;全量指标基本持平略升。生产侧需在写入记忆时 materialize 与 bench 一致的 device 前缀(或等价 metadata)。 ### 语义嵌入(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 # 指定某个 key(memind_adm Providers 页对应 id) MEMIND_EMBEDDING_LLM_KEY_ID= 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 | MIT(Darwin Agent Team, Xiaomi) | 可自由使用数据集与论文思想 | | JKRiver / Riverse | **AGPL-3.0 或商业双授权** | **只做架构级参考,禁止移植代码** | JKRiver 的 `LICENSE` 明确把"集成进专有产品"和"作为商业 SaaS 提供而不开源修改"列为需要单独商业授权的场景。Memind 是带计费的商业 SaaS,AGPL 的网络 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 映射