Bug Report: memory-upgrader 和 memory-compactor 破壞 text/l0_abstract 結構,導致 BM25/FTS 召回率下降
Repository: CortexReach/memory-lancedb-pro
Severity: Major
Type: Bug / Architecture
摘要
memory-upgrader 和 memory-compactor 對 text 欄位和 metadata 中的 l0_abstract/l1_overview/l2_content 三層 enrichment 結構處理不一致,導致:
- 升級後 entry 的
text 欄位被替換成 l0_abstract,原始豐富內容丢失
- 合併後 entry 的
metadata 完全不包含 L0/L1/L2 enrichment,導致 scoreLexicalHit 行為異常
scoreLexicalHit 的權重設計與實際資料形態脫節
🔴 RED #1 — memory-upgrader:text = l0_abstract 破壞 BM25/FTS 召回率
檔案: src/memory-upgrader.ts 第 370-372 行
await this.store.update(entry.id, {
// Update text to L0 abstract for better search indexing
text: enriched.l0_abstract,
metadata: stringifySmartMetadata(newMetadata),
});
問題:升級後,entry.text 被置換成 l0_abstract(一句話摘要)。原始文字只存在於 metadata.l2_content。
驗證:store.ts 第 1114-1119 行,scoreLexicalHit 對以下來源評分:
const score = scoreLexicalHit(trimmedQuery, [
{ text: entry.text, weight: 1 }, // ← text 現在只有 L0 abstract
{ text: metadata.l0_abstract, weight: 0.98 }, // ← 與 entry.text 重複
{ text: metadata.l1_overview, weight: 0.92 },
{ text: metadata.l2_content, weight: 0.96 }, // ← 完整內容,但 weight 只有 0.96
]);
後果:
| 情境 |
後果 |
| 使用者搜尋完整句子中的某個片語 |
若片語不在 L0 abstract 內,查無結果 |
| 純 BM25/FTS 查詢 |
只對 L0 句子精確匹配,召回率大幅下降 |
| 合併升級前的 legacy 記憶 |
原本內容豐富的文字被截斷成一句話 |
🔴 RED #2 — memory-compactor:合併後 text 直接用 newlines join,L0/L1/L2 三層結構全部消失
檔案: src/memory-compactor.ts 第 188-200 行
// --- text: deduplicate lines ---
const seen = new Set<string>();
const lines: string[] = [];
for (const m of members) {
for (const line of m.text.split("\n")) {
const trimmed = line.trim();
if (trimmed && !seen.has(trimmed.toLowerCase())) {
seen.add(trimmed.toLowerCase());
lines.push(trimmed);
}
}
}
const text = lines.join("\n");
問題:
- 合併後的
text 是多行字串用 \n join,沒有 L0/L1/L2 三層結構
- 合併後
metadata 只存 { compacted: true, sourceCount: N, compactedAt: timestamp },完全沒有任何 l0_abstract / l1_overview / l2_content
驗證:store.ts 第 1115-1116 行:
{ text: metadata.l0_abstract, weight: 0.98 }, // undefined → normalizeSearchText(undefined) = "undefined"
若 metadata.l0_abstract 為 undefined,normalizeSearchText 會產生 "undefined" 字串,導致 BM25 評分極低或錯誤。
後果:
| 情境 |
後果 |
合併後的 entry 被 scoreLexicalHit 查詢 |
l0_abstract = undefined → BM25 評分失準 |
| 新 entry 需要再次升級或合併 |
缺少 memory_category,視為 legacy,觸發錯誤的 category reverse-map |
使用者查詢原本在 l2_content 的片語 |
只剩 text(newlines join)可用,召回率下降 |
🔴 RED #3 — 架構不一致:upgrader 和 compactor 的 text 策略衝突
根本問題:整個系統存在兩套互相衝突的 text 策略:
| 元件 |
text 策略 |
metadata.l0_abstract |
| SmartExtractor(新建) |
存完整內容或 L1 overview |
✅ 有 |
| memory-upgrader(升級) |
置換成 l0_abstract |
✅ 有(但與 text 重複) |
| memory-compactor(合併) |
newlines.join(無結構) |
❌ 完全不存在 |
後果:
- 合併後的 entry 回頭污染 BM25/FTS 精確度 —
text 變成粗糙的 newlines join
- 多次合併後 L0/L1/L2 階層完全消失 — 只能靠 legacy upgrader 補救,但升級又要把
text 換成 l0_abstract,再次破壞 FTS
confidence 在合併後沒有合理計算 — sourceCount: N 不會影響任何 search ranking 行為
🔴 RED #4 — scoreLexicalHit:權重設計與實際使用脫節
檔案: src/store.ts 第 1114-1119 行
const score = scoreLexicalHit(trimmedQuery, [
{ text: entry.text, weight: 1 },
{ text: metadata.l0_abstract, weight: 0.98 },
{ text: metadata.l1_overview, weight: 0.92 },
{ text: metadata.l2_content, weight: 0.96 },
]);
問題:
entry.text weight 最高(1.0),但 upgrader 把 text 換成 l0_abstract,導致 l0_abstract 被查了兩次
l2_content(完整內容)weight 只有 0.96,沒有被充分使用
🟡 YELLOW — rerankResults 的 topN 無上限(與 reranker 排隊問題關聯)
檔案: src/retriever.ts 第 1246-1249 行
const { headers, body } = buildRerankRequest(
provider,
this.config.rerankApiKey || "",
model,
query,
documents, // = results.map(r => r.entry.text)
results.length, // ← topN = 全部 candidates,無上限
);
問題:當候選者數量多時,rerank API 收到大量文件,增加延遲和成本。在高並發場景下,會對 rerank API 造成瞬間高流量,可能觸發 rate limit。
建議修復方向
修復順序
| 優先順序 |
問題 |
修復方向 |
| 1 |
RED #2 — compactor 不產生 L0/L1/L2 |
合併後主動呼叫 buildSmartMetadata 產生 enrichment |
| 2 |
RED #1 — upgrader text 被覆寫 |
保留原始 text 或改用 l2_content 作為 text |
| 3 |
RED #3 — 架構不一致 |
統一 upgrader/compactor/scoreLexicalHit 三者對 text 欄位的使用假設 |
| 4 |
RED #4 — 權重與實際資料脫節 |
調整 scoreLexicalHit 權重 |
| 5 |
YELLOW — rerank topN 無上限 |
topN = Math.min(results.length, this.config.candidatePoolSize) |
具體程式碼修改建議
針對 RED #1(upgrader)
// memory-upgrader.ts:370-372 — 建議修改為:
await this.store.update(entry.id, {
// 保留完整內容用於 FTS,l0_abstract 存在 metadata
text: enriched.l2_content ?? enriched.l1_overview ?? enriched.l0_abstract,
metadata: stringifySmartMetadata(newMetadata),
});
針對 RED #2(compactor)
// memory-compactor.ts — 建議修改為:
const mergedMetadata = buildSmartMetadata(
{ ...members[0], text, metadata: "{}" },
{
l0_abstract: lines[0]?.slice(0, 100) ?? "",
l1_overview: lines.slice(0, 3).map(l => `- ${l}`).join("\n"),
l2_content: text,
memory_category: detectedCategory,
tier: "working",
access_count: 0,
confidence: 0.6, // 合併後 confidence 降低
}
);
針對 YELLOW(rerank topN)
// retriever.ts:1246-1249 — 建議修改為:
const topN = Math.min(results.length, this.config.candidatePoolSize);
const { headers, body } = buildRerankRequest(
provider,
this.config.rerankApiKey || "",
model,
query,
documents,
topN, // ← 有上限
);
嚴重程度評估
| 問題 |
等級 |
阻擋 |
RED #1 — upgrader text = l0_abstract |
Major |
❌ |
| RED #2 — compactor 無 L0/L1/L2 |
Critical |
✅ |
| RED #3 — 架構不一致 |
Major |
❌ |
| RED #4 — scoreLexicalHit 權重脫節 |
Minor |
❌ |
| YELLOW — rerank topN 無上限 |
Medium |
❌ |
是否為架構問題:主要是 架構問題(upgrader/compactor/scoreLexicalHit 三者對 text 欄位的使用假設不一致),實作問題(compactor 未產生 enrichment metadata)為副。
Report generated by: Review Claw(稽核爪)
Analysis source: hermes-knowledge/Code_Analysis/memory-lancedb-pro
Date: 2026-05-08
Bug Report: memory-upgrader 和 memory-compactor 破壞 text/l0_abstract 結構,導致 BM25/FTS 召回率下降
Repository: CortexReach/memory-lancedb-pro
Severity: Major
Type: Bug / Architecture
摘要
memory-upgrader和memory-compactor對text欄位和metadata中的l0_abstract/l1_overview/l2_content三層 enrichment 結構處理不一致,導致:text欄位被替換成l0_abstract,原始豐富內容丢失metadata完全不包含 L0/L1/L2 enrichment,導致scoreLexicalHit行為異常scoreLexicalHit的權重設計與實際資料形態脫節🔴 RED #1 — memory-upgrader:text = l0_abstract 破壞 BM25/FTS 召回率
檔案:
src/memory-upgrader.ts第 370-372 行問題:升級後,
entry.text被置換成l0_abstract(一句話摘要)。原始文字只存在於metadata.l2_content。驗證:
store.ts第 1114-1119 行,scoreLexicalHit對以下來源評分:後果:
🔴 RED #2 — memory-compactor:合併後 text 直接用 newlines join,L0/L1/L2 三層結構全部消失
檔案:
src/memory-compactor.ts第 188-200 行問題:
text是多行字串用\njoin,沒有 L0/L1/L2 三層結構metadata只存{ compacted: true, sourceCount: N, compactedAt: timestamp },完全沒有任何l0_abstract/l1_overview/l2_content驗證:
store.ts第 1115-1116 行:若
metadata.l0_abstract為undefined,normalizeSearchText會產生"undefined"字串,導致 BM25 評分極低或錯誤。後果:
scoreLexicalHit查詢l0_abstract = undefined→ BM25 評分失準memory_category,視為 legacy,觸發錯誤的 category reverse-mapl2_content的片語text(newlines join)可用,召回率下降🔴 RED #3 — 架構不一致:upgrader 和 compactor 的 text 策略衝突
根本問題:整個系統存在兩套互相衝突的
text策略:text策略metadata.l0_abstractl0_abstracttext重複)newlines.join(無結構)後果:
text變成粗糙的 newlines jointext換成l0_abstract,再次破壞 FTSconfidence在合併後沒有合理計算 —sourceCount: N不會影響任何 search ranking 行為🔴 RED #4 — scoreLexicalHit:權重設計與實際使用脫節
檔案:
src/store.ts第 1114-1119 行問題:
entry.textweight 最高(1.0),但 upgrader 把text換成l0_abstract,導致l0_abstract被查了兩次l2_content(完整內容)weight 只有 0.96,沒有被充分使用🟡 YELLOW — rerankResults 的 topN 無上限(與 reranker 排隊問題關聯)
檔案:
src/retriever.ts第 1246-1249 行問題:當候選者數量多時,rerank API 收到大量文件,增加延遲和成本。在高並發場景下,會對 rerank API 造成瞬間高流量,可能觸發 rate limit。
建議修復方向
修復順序
buildSmartMetadata產生 enrichmenttext被覆寫text或改用l2_content作為texttext欄位的使用假設scoreLexicalHit權重topN = Math.min(results.length, this.config.candidatePoolSize)具體程式碼修改建議
針對 RED #1(upgrader)
針對 RED #2(compactor)
針對 YELLOW(rerank topN)
嚴重程度評估
text = l0_abstract是否為架構問題:主要是 架構問題(upgrader/compactor/scoreLexicalHit 三者對
text欄位的使用假設不一致),實作問題(compactor 未產生 enrichment metadata)為副。Report generated by: Review Claw(稽核爪)
Analysis source: hermes-knowledge/Code_Analysis/memory-lancedb-pro
Date: 2026-05-08