Skip to content

[BUG] memory-upgrader 和 memory-compactor 破壞 text/l0_abstract 結構,導致 BM25/FTS 召回率下降 #786

@jlin53882

Description

@jlin53882

Bug Report: memory-upgrader 和 memory-compactor 破壞 text/l0_abstract 結構,導致 BM25/FTS 召回率下降

Repository: CortexReach/memory-lancedb-pro
Severity: Major
Type: Bug / Architecture


摘要

memory-upgradermemory-compactortext 欄位和 metadata 中的 l0_abstract/l1_overview/l2_content 三層 enrichment 結構處理不一致,導致:

  1. 升級後 entry 的 text 欄位被替換成 l0_abstract,原始豐富內容丢失
  2. 合併後 entry 的 metadata 完全不包含 L0/L1/L2 enrichment,導致 scoreLexicalHit 行為異常
  3. 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");

問題

  1. 合併後的 text 是多行字串用 \n join,沒有 L0/L1/L2 三層結構
  2. 合併後 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_abstractundefinednormalizeSearchText 會產生 "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(無結構) 完全不存在

後果

  1. 合併後的 entry 回頭污染 BM25/FTS 精確度text 變成粗糙的 newlines join
  2. 多次合併後 L0/L1/L2 階層完全消失 — 只能靠 legacy upgrader 補救,但升級又要把 text 換成 l0_abstract,再次破壞 FTS
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions