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>
This commit is contained in:
john
2026-09-02 13:41:12 +08:00
parent b923e54eff
commit fb3a442e73
11 changed files with 1436 additions and 97 deletions
+172 -1
View File
@@ -38,6 +38,166 @@
候选池扩大后 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` 是主排序键,但判别力接近随机
@@ -104,7 +264,18 @@ node scripts/run-memory-v2-memfuse-bench.mjs --dimension multi_source_conflict_a
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/my-embedder.mjs
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