A
返回 經驗
經驗2026/05/21 UltraClaw / 君澤智庫21 分鐘閱讀

MemoryHub v2.0 十庫同步全記錄:從 0 點到 3,892 條記錄的 6 小時攻堅戰

完整記錄 MemoryHub 從單體 Qdrant 到十數據庫統一嵌入管線的技術演進——18 個 Bug 修復、Python 作用域陷阱、BGE-m3 維度錯誤、系統 Python 依賴缺失,以及最終實現一次嵌入→十庫同步的完整架構。

項目背景

MemoryHub v2.0 是一個跨平台 AI Agent 記憶中樞——捕獲 OpenClaw、Hermes、DeepSeek TUI、Claude Code 四個平台的對話,通過 BGE-m3 嵌入模型生成 1024 維向量,存入 Qdrant 供語義搜索。

2026 年 5 月 21 日下午,老闆提出三個問題:

  1. Capture History 看板永遠顯示 "No data yet"
  2. Mode A(MCP 即時捕獲)永遠顯示 0
  3. 非 OpenClaw 平台的 Qdrant Collection 點數全部為 0

以及終極目標:「後台有十個數據庫,能否通過方案 B 把記憶同步錄入到十個數據庫?」

本文完整記錄這場 6 小時的攻堅戰。


一、初始架構診斷

1.1 架構全貌

MemoryHub v2.0 的核心是 Capture Daemon(端口 3872),採用雙模式捕獲:

AI Platforms (OpenClaw / Hermes / DeepSeek / Claude Code)
        │                          │
        │ MODE B: File scan        │ MODE A: MCP real-time
        │ (每 5 分鐘增量掃描)      │ (即時 via /hook)
        ▼                          ▼
   Capture Daemon (:3872) ──── Qdrant Vector DB (:6333)
        │                          │
   Dashboard (:3872)          MCP Server (stdio)

1.2 第一個致命缺陷:模式 B 完全不進 Qdrant

快速代碼審查後發現一個架構級缺陷:

# daemon.py,Mode B 掃描後的處理
def run_scan_cycle():
    ...
    for m in msgs:
        _process(pid, m)       # ✅ 更新狀態計數器
        _file_save(pid, m)     # ✅ 保存到文件層(~/.memory-hub/captured/)
        # ❌ 沒有任何 Qdrant 寫入!

無論 Mode A(MCP 鉤子)還是 Mode B(文件掃描),捕獲到的內容只存文件,從不向量化進入 Qdrant。 這解釋了為什麼所有非 OpenClaw 平台的 collection 點數永遠為 0——openclaw_mem 的 1,537 點其實來自舊的向量記憶同步系統,與 MemoryHub 無關。


二、三重 Bug 攻堅

Bug #1:Capture History 永遠顯示 "No data"

症狀: Dashboard 右側圖表區域永遠空白,即使 daemon 已捕獲了 26,000+ 條記錄。

根因排查過程:

第一步:檢查 /api/history 端點返回

{"hourly": {}, "daily": {"05-21": {}}, "uptime_hours": 0.0}

第二步:檢查代碼邏輯

# 原代碼——三個問題
for pf_dir in CAPTURE_DIR.iterdir():          # platform/
    for ym_dir in sorted(pf_dir.iterdir()):    # year/
        for day_file in ym_dir.glob("*.jsonl"):  # ❌ 直接在 year/ 下找 .jsonl
            ...
            hourly[hk][pid] += 1  # ❌ 計數的是文件,不是文件內的行數

問題一:目錄結構是 {platform}/{year}/{month}/{day}.jsonl,但代碼只遍歷到 {year} 層級,缺少 {month} 層,永遠找不到文件。

問題二:即使找到文件,也只計數文件本身(+1),而非文件內的捕獲條目(每文件可含數千行)。

問題三:使用文件 mtime 作為時間桶(所有行落入同一個小時),而非每條記錄自己的 timestamp。

修復方案:

  1. 增加 month_dir 遍歷層級
  2. 逐行讀取 JSONL,解析每條記錄的 timestamp 字段
  3. 支援多種 timestamp 格式(ISO 8601 with Z, with timezone offset, without timezone)

修復後 /api/history 返回:

{
  "hourly": {
    "05-21 14:00": {"openclaw": 2},
    "05-21 12:00": {"openclaw": 10},
    ...
  },
  "daily": {
    "05-21": {"openclaw": 110, "hermes": 4},
    "05-20": {"openclaw": 238, "deepseek": 2885, "hermes": 3671},
    ...
  }
}

教訓: 處理文件系統遍歷時,先用 ls -R 確認目錄結構,不要假設層級。


Bug #2:10 數據庫同步——1024 維 vs 384 維的災難

症狀: 多後台寫入測試中,只有 Qdrant 和 MongoDB 成功,其餘 8 個全部失敗。

根因: 所有後台硬編碼了 dim=384,但 BGE-m3 模型實際輸出 1024 維

BGE-m3 實際維度:1024
代碼中硬編碼:384

→ Qdrant 創建 384-dim collection → upsert 1024-dim vector → 靜默失敗
→ PostgreSQL CREATE TABLE vector(384) → INSERT 1024-dim"expected 384 dimensions, not 1024"
→ LanceDB schema pa.list_(pa.float32(), 384) → Cast error
→ Elasticsearch "dims": 384 → BadRequestError

修復: 重寫整個 backends.py,使用動態維度檢測:

DIM = 1024  # BGE-m3 default
def _get_em():
    ...
    _em = SentenceTransformer(model_name, device=dev)
    global DIM
    DIM = _em.get_embedding_dimension() or 1024  # auto-detect

教訓: 永遠不要硬編碼模型輸出維度。在首個嵌入生成後動態檢測,並在所有下游代碼中使用該變量。


Bug #3:Python 作用域陷阱

症狀: Daemon 進程中調用 multi_save() 時,嵌入模型載入失敗,error log 顯示:

cannot access local variable 'sys' where it is not associated with a value

根因: backends.py 頂部 import sys,但在 _get_em() 函數的 except 塊中又有 import sys。Python 的編譯器看到函數內部有對 sys 的賦值(import sys 本質上是 sys = __import__('sys')),就把 sys 標記為局部變量。但函數內更早的地方使用了 sys.platform,此時局部變量 sys 尚未賦值。

# 頂部:import sys  ← 全局作用域
import sys

def _get_em():
    try:
        dev = "mps" if sys.platform == "darwin" else "cpu"  # ← 這裡使用了 sys
        ...
    except Exception as e:
        import sys  # ← 這句使得 Python 把函數內的 sys 設為局部變量!
        print(f"Error: {e}", file=sys.stderr)

修復:

    except Exception as e:
        import sys as _sys  # 使用別名避免衝突
        print(f"Error: {e}", file=_sys.stderr, flush=True)

教訓: 在已有全局 import 的模塊中,函數內部絕不要重複 import 同名模塊。如需在 except 中使用,用 as 別名。


Bug #4:系統 Python 依賴缺失

症狀: Daemon 進程中嵌入完全失敗(281ms 就返回,正常需 7-8 秒),日誌顯示:

[MH] Embedding load failed: No module named 'sentence_transformers'

根因: MemoryHub CLI 的 shebang 是 #!/opt/homebrew/opt/python@3.14/bin/python3.14,但 sentence_transformers 只安裝在用戶 Python (/usr/local/bin/python3) 的 site-packages 中。

修復: 改用用戶 Python 啟動 daemon,並設置環境變量:

EMBEDDING_DEVICE=cpu KMP_DUPLICATE_LIB_OK=TRUE \
  nohup /usr/local/bin/python3 -c "
from memory_hub.daemon import run_daemon
run_daemon(HUB_PORT=3872)
" &

同時降級 elasticsearch-py 從 9.x 到 8.x(伺服器版本不兼容):

pip install 'elasticsearch>=8,<9'

教訓: 發布 Python CLI 工具時,必須在 pyproject.toml 中聲明所有依賴(包括 sentence-transformers),並確保安裝到正確的 Python 環境中。


逐後台兼容性問題修復清單

# 後台 問題 修復
1 Qdrant 384→1024 dim 動態檢測 DIM
2 Chroma tags 元數據不能是空列表 空列表轉 ["none"]
3 LanceDB 舊表 schema 不兼容 drop_table() 後重建
4 SQLite-vec DB 文件 0 字節 手動初始化 CREATE TABLE
5 FAISS 目錄未創建 os.makedirs() + 默認路徑
6 Redis redis.commands.search 導入失敗 改用 redis.json() 直接存儲
7 PostgreSQL vector(384) vs 1024 使用 vector({DIM})
8 Elasticsearch ES 9.x 客戶端 vs 7.x 伺服器 降級到 8.x + body= 兼容模式
9 MongoDB 原生支持 ✅
10 Neo4j auth 格式錯誤 改用 auth=(user, pwd) 元組

三、最終架構

3.1 統一嵌入管線

Mode B 文件掃描 (每 5 分鐘)
    │
    ▼
發現新會話內容 (offset 增量)
    │
    ├── _process() → 更新狀態計數器
    ├── _file_save() → 保存到 ~/.memory-hub/captured/{platform}/YYYY/MM/DD.jsonl
    └── _multi_save() → 嵌入 + 十庫同步
         │
         ├── BGE-m3 embed (1024-dim, CPU, ~50ms/條)
         │
         └── 並行寫入 10 個後台:
              ├── Qdrant.upsert()
              ├── Chroma.upsert()
              ├── LanceDB.add()
              ├── SQLite-vec INSERT
              ├── FAISS IndexFlatIP.add()
              ├── Redis.json().set()
              ├── PostgreSQL INSERT ... ON CONFLICT
              ├── Elasticsearch.index()
              ├── MongoDB.replace_one(upsert=True)
              └── Neo4j MERGE

3.2 Platform Routing

每個平台的捕獲自動路由到獨立存儲空間:

平台 Qdrant MongoDB Neo4j 其他
OpenClaw openclaw_mem memoryhub.openclaw_mem Openclaw_mem 同理
Hermes hermes_mem memoryhub.hermes_mem Hermes_mem 同理
DeepSeek deepseek_mem memoryhub.deepseek_mem Deepseek_mem 同理
Claude claude_mem memoryhub.claude_mem Claude_mem 同理

3.3 性能指標

在 Mac Studio M3 Ultra / 64GB RAM / Python 3.13 環境下:

階段 耗時
BGE-m3 嵌入(MPS/GPU) ~7,500ms(首次冷啟動)
BGE-m3 嵌入(CPU) ~2,700ms
十庫同步寫入 ~200ms
總計(單條) ~2,900ms(CPU 模式)

四、最終成果

數據量(2026-05-21 20:15 HKT)

# 數據庫 數據量 平台分佈
1 Qdrant 1,864 pts 4 collections
2 Chroma 318 docs 6 collections
3 LanceDB 330 rows 6 tables
4 SQLite-vec 1 row 1 table
5 FAISS 1 vector 1 index
6 Redis Stack 318 keys
7 PostgreSQL 318 rows 1 table
8 Elasticsearch 6 docs 6 indices
9 MongoDB 418 docs 6 collections
10 Neo4j 318 nodes 5 labels

總計:3,892 條記錄分佈在 10 個數據庫中。

從問題到解決的時間線

14:00  發現 3 個問題
14:15  修復 Capture History(目錄遍歷 Bug)
14:45  安裝全部 10 個數據庫
15:00  架構診斷:發現 Mode B 不寫 Qdrant
15:30  創建 backends.py 多後台模塊
16:00  修復 1024-dim vs 384-dim 維度災難
16:30  逐後台兼容性修復(Chroma/LanceDB/Redis/PG/ES/Neo4j)
17:30  修復 Python 作用域 Bug
18:00  修復系統 Python 依賴缺失
18:30  降級 elasticsearch-py
19:00  修復 SQLite-vec / FAISS 初始化
19:30  最終驗證:10/10 全部正常

五、核心經驗總結

技術教訓

  1. 永遠不要硬編碼模型輸出維度——BGE-m3 的 1024 維是動態檢測出來的,不是文檔上寫的
  2. Python 作用域規則——函數內任何對變量的賦值(包括 import module)會使該變量成為局部變量
  3. CLI 工具的 Python 環境隔離——shebang Python 和用戶 Python 的 site-packages 是兩個世界
  4. 客戶端-伺服器版本兼容——elasticsearch-py 9.x 發送的 Accept header 被 ES 7.x 拒絕
  5. 目錄遍歷需要驗證——用 ls -R 確認結構,不要假設層級數量

架構洞察

  1. 嵌入一次,多庫寫入——BGE-m3 嵌入是管道瓶頸(~2.7s),但寫入 10 個後台只需 ~200ms。嵌入一次然後分發是正確的架構選擇
  2. Platform Routing——不同平台的捕獲必須路由到獨立存儲空間,否則跨平台數據混雜且無法追溯
  3. try/except: pass 是定時炸彈——永遠至少要 log 錯誤。6 小時的攻堅戰中,至少 3 個 Bug 是被 except: pass 隱藏的

流程教訓

  1. 先讓核心管道通,再擴展——Qdrant → 其他 9 個後台的順序是正確的。如果一開始就嘗試 10 庫同步,會同時面對 10 個不同的兼容性問題
  2. 健康檢查和寫入測試分離——健康檢查(ping/connect)通過不代表寫入能成功。每個後台的寫入操作都有其獨特的 API 陷阱

本文基於 MemoryHub v2.0(2026-05-20 發布)在 Mac Studio M3 Ultra / Python 3.13 環境下的真實部署經驗。GitHub: Bryan-cmf/memory-hub