Architecture & Scaling Deep Dive · arXiv:2610.30755

Raw-RAG 如何管理日漸膨脹的對話與向量索引?

基於 Litmeb/RAW-RAG (rag_adapter.py / rag_pgvector_store.py) 原始碼與本機實測數據:寫入端採用 Append-Only 增量尾部切塊(零表徵損失),讀取端採用 常數預算兩階段漏斗 + 時序重排展開(隔離 LLM Context 膨脹)。

寫入端單步計算複雜度
O(ΔL) · 0 LLM Calls
僅重切與嵌入最後一個未滿 256 tokens 的尾部區塊,歷史區塊完全鎖定不重算。
寫入期資訊流失 (Representation Loss)
0.0% (零有損壓縮)
對比複雜記憶系統在寫入期因 LLM 摘要/萃取直接損失 21.9% 狀態演進證據。
讀取端 Reader LLM 上下文上限
常數 O(k) ≈ 3.8K tokens
無論歷史膨脹至 32K、197K 或百萬 tokens,永遠只取 Top-128 → Rerank Top-15。
百萬級長對話 RAM 保護
pgvector + 暫態釋放
支援外部 PostgreSQL 向量庫與單次檢索後立即釋放 RAM 快取 (gc.collect())。

1 架構對比:為什麼不在寫入端用 LLM 壓縮對話?

主流複雜記憶系統試圖在寫入時用 LLM 萃取「原子事實」或「摘要」來控制膨脹,實測卻造成永久表徵損失與碎片數量反向暴增 5.38 倍;Raw-RAG 則將「儲存增長」與「讀取視窗」徹底解耦。

傳統複雜記憶系統 (Mem0 / MemOS / Atomic Facts) 寫入端有損壓縮
1. 持續膨脹的對話流 (N tokens) 每輪對話進入系統,觸發寫入期記憶管理管線。
↓
2. 寫入期呼叫 LLM 萃取 / 合併 / 覆寫 將原始對話拆解為原子事實或滾動摘要(建庫耗時暴增 655×,約 89.2 秒)。
↓
副作用 A:永久表徵損失 (R − C = −21.9%) 跨回合的狀態演進因果鏈在寫入時被截斷,即使全庫塞進 LLM 也無法救回。
副作用 B:細碎物件反向膨脹 (5.38×) 原本 24 個 256-token 區塊被拆成 129 條細碎事實,檢索噪音反而更大。
Raw-RAG 架構 (Litmeb/RAW-RAG) 讀取端固定預算漏斗
1. 持續膨脹的對話流 (N tokens) 每輪對話進入 RAGAdapter.add_incremental_message。
↓
2. 寫入期:零 LLM 呼叫、增量尾部切塊 O(ΔL) 原文 100% 保留於 256-token 區塊(overlap 32),建庫僅需 0.14 秒。
↓
優勢 A:零表徵損失 + 完整演進路徑保留 所有歷史修改紀錄、時間戳記與雙向對白原封不動保留在索引中。
優勢 B:讀取端常數上限 (Top-128 → Top-15) 透過兩階段檢索漏斗,送入 Reader LLM 的 Context 永遠鎖定在 ~3.8K tokens。

2 寫入端機制:增量尾部切塊與嵌入 (Incremental Tail-Only Update)

當對話逐句增加時,RAGAdapter.add_incremental_message 不會重切或重算整段歷史。前 N-1 個已填滿 256 tokens 的區塊被標記為 stable_chunks(不可變),每次只重算最後一個未滿區塊與新延伸出的 tail_chunks。

不可變歷史區 (Immutable Stable Chunks: old_chunks[:first_changed]) — 零重算 可變尾部區 (Mutable Tail Chunks) — 僅此區呼叫 Embedding Chunk #0 (256 tok) Tokens [0 : 256] ✓ 向量已快取 (鎖定) Chunk #1 (256 tok) Tokens [224 : 480] (ovlp 32) ✓ 向量已快取 (鎖定) ... Chunk #(k-1) 已滿 256 tokens ✓ 向量已快取 (鎖定) Chunk #k (原未滿尾部) 補入新訊息 tokens 填滿 ↻ 重新解碼 + 重新 Embed Chunk #(k+1) (新溢出) 含前塊 32-token overlap ★ 新區塊首次 Embed 直接沿用記憶體向量:stable_embeddings = old_embeddings[:first_changed] 時間複雜度 O(1),完全不觸發 GPU 模型推論 呼叫 _embed_texts(tail_texts, stage="add_incremental") 僅計算 1~2 個尾部區塊 → Append 寫入 rag_incremental_updates.jsonl
對應實作:rag_adapter.py :: RAGAdapter.add_incremental_message (L335-L500) 增量日誌:rag_incremental_updates.jsonl

3 讀取與狀態演進管理:兩階段常數漏斗 + 時序重排 + 動態回合展開

既然資料庫不刪除舊狀態,當使用者反覆變更狀態(例如:台北 → 台南 → 取消改台中)時,Raw-RAG 如何避免舊資料誤導 Reader?關鍵在於「檢索時用 256-token 緊湊區塊,送給 LLM 前依原始時間排序並動態還原為完整對話回合」。

1. 膨脹的底層索引 (N 區塊) 例如 LongMemEval 5,898 個區塊 Chunk #12 (1月 · Turn 4) 「我目前住在台北...」 Chunk #13 ~ #2409 (數千閒聊雜訊) Chunk #2410 (8月 · Turn 612) 「下個月準備搬去台南...」 Chunk #4900 (11月 · Turn 1280) 「台南租屋取消,改搬去台中」 2. 常數預算檢索漏斗 將 O(N) 歷史壓縮為固定 O(k) Step A: 時序範圍過濾 _filter_chunks_for_question_date 先依問題時間戳剔除未來或無效區間 Step B: Dense 粗排 Top-128 Qwen3-Embedding-4B 餘弦相似度 從數千區塊快速召回 128 個候選 Step C: Reranker 精排 Top-15 Qwen3-Reranker-4B Cross-Encoder 支援 dynamic_top_k (門檻 > 0.5) 3. 時序重排 + 動態回合展開 不改資料庫,在呈現期還原因果鏈 Step D: 依原始對話時序重排 _selected_chunk_time_sort_key 按 (start_turn, start_token) 升冪排列: #12 (1月) → #2410 (8月) → #4900 (11月) Step E: 動態展開為完整對話回合 _expand_chunk_to_turns (Display-Time) 透過 turn_spans 將 256-tok 切片 展開為完整 User/Assistant 對白並去重 消除切片截斷造成的語意破碎 4. Reader LLM 推論 固定 ~3.8K–5K Context Reader 看到的時序證據: 1. [1月] 住台北 (舊) 2. [8月] 預計搬台南 (過渡) 3. [11月] 取消台南改台中 (最新) 順序閱讀即可消歧最新狀態 MEME 32K 實測準確率: • Tracking (覆寫): 100% • Deletion (刪除): 100%
時序過濾:_filter_chunks_for_question_date (L1021-L1065) 時序重排:_selected_chunk_time_sort_key (L750-L766) 動態展開:_expand_chunk_to_turns (L1612-L1650)

4 儲存與記憶體防爆:本機暫態釋放 vs. PostgreSQL pgvector

當單一評估集或對話膨脹至數百萬 tokens 時,若將所有浮點向量常駐於 RAM 或寫入單一 JSON 檔會導致記憶體與磁碟爆滿。Raw-RAG 提供兩條分流路徑:

儲存後端路由 (rag.yaml) storage.backend 設定 路徑 A:本機超長對話模式 (local + persist_embeddings=False) • rag_index.json 僅存文字與時序 Metadata(防止 JSON 膨脹至數 GB) • 檢索時呼叫 _hydrate_conversation_embeddings 按需批次生成向量 檢索完畢立即釋放 RAM • 呼叫 release_conversation_search_cache() • 移除 conv_index["embeddings"] 並執行 gc.collect() 路徑 B:外部資料庫模式 (storage.backend: "pgvector") • 透過 RAGPgVectorStore.upsert_chunks 寫入 PostgreSQL • 向量與文字持久化於外部資料表,本機程序零向量常駐負擔 資料庫端 Cosine Similarity 粗排 • _get_pgvector_candidates 於 DB 端直接篩出 Top-128 • 本機僅對這 128 個區塊執行 Cross-Encoder Rerank

5 實測邊界:Raw-RAG 在哪些膨脹場景勝出?何處觸及極限?

根據我們在 MEME (32K-Filler) 六大任務的實測分解,固定 Top-15 視窗能完美解決「局部狀態演進」,但在「跨超過 15 個區塊的全局統計聚合 (Aggregation)」會觸及視窗與計數瓶頸:

MEME (32K-Filler) 各任務準確率實測對比 (Raw-RAG vs. 全庫 Oracle C) Tracking (狀態覆寫) 100.0% (Oracle C: 80.0%) Deletion (狀態刪除/取消) 100.0% (Oracle C: 100.0%) Threshold (累積門檻判斷) 80.0% (Oracle C: 80.0%) Cascade (跨實體連鎖更新) 80.0% (Oracle C: 80.0%) Implicit (隱式約束推理) 50.0% (Oracle C: 50.0%) Aggregation (全局統計計數) 20.0% ← 固定 Top-15 視窗與新舊狀態並存干擾計數 為什麼 Aggregation 是唯一短板? 1. 當問題要求「統計全歷史所有符合條件 的事件總數」,且碎片散落 >15 個區塊 時,固定 Top-15 無法一次撈齊。 2. 新舊狀態同時進入 Top-15 時,小型 Reader 容易將「已取消的舊紀錄」重複 計入總數(論文建議此處才需結構化狀態表)。