項目背景
MemoryHub v2.0 是一個跨平台 AI Agent 記憶中樞——捕獲 OpenClaw、Hermes、DeepSeek TUI、Claude Code 四個平台的對話,通過 BGE-m3 嵌入模型生成 1024 維向量,存入 Qdrant 供語義搜索。
2026 年 5 月 21 日下午,老闆提出三個問題:
- Capture History 看板永遠顯示 "No data yet"
- Mode A(MCP 即時捕獲)永遠顯示 0
- 非 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。
修復方案:
- 增加
month_dir遍歷層級 - 逐行讀取 JSONL,解析每條記錄的 timestamp 字段
- 支援多種 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 全部正常
五、核心經驗總結
技術教訓
- 永遠不要硬編碼模型輸出維度——BGE-m3 的 1024 維是動態檢測出來的,不是文檔上寫的
- Python 作用域規則——函數內任何對變量的賦值(包括
import module)會使該變量成為局部變量 - CLI 工具的 Python 環境隔離——shebang Python 和用戶 Python 的 site-packages 是兩個世界
- 客戶端-伺服器版本兼容——elasticsearch-py 9.x 發送的 Accept header 被 ES 7.x 拒絕
- 目錄遍歷需要驗證——用
ls -R確認結構,不要假設層級數量
架構洞察
- 嵌入一次,多庫寫入——BGE-m3 嵌入是管道瓶頸(~2.7s),但寫入 10 個後台只需 ~200ms。嵌入一次然後分發是正確的架構選擇
- Platform Routing——不同平台的捕獲必須路由到獨立存儲空間,否則跨平台數據混雜且無法追溯
- try/except: pass 是定時炸彈——永遠至少要 log 錯誤。6 小時的攻堅戰中,至少 3 個 Bug 是被
except: pass隱藏的
流程教訓
- 先讓核心管道通,再擴展——Qdrant → 其他 9 個後台的順序是正確的。如果一開始就嘗試 10 庫同步,會同時面對 10 個不同的兼容性問題
- 健康檢查和寫入測試分離——健康檢查(ping/connect)通過不代表寫入能成功。每個後台的寫入操作都有其獨特的 API 陷阱
本文基於 MemoryHub v2.0(2026-05-20 發布)在 Mac Studio M3 Ultra / Python 3.13 環境下的真實部署經驗。GitHub: Bryan-cmf/memory-hub