Files
ewoooc/docs/phase11_db_design_20260503.md
OoO 2f20d8d7ba
All checks were successful
CD Pipeline / deploy (push) Successful in 3m30s
db(p11): rag_query_log + learning_episodes — RAG 自主學習迴圈基礎
Operation Ollama-First v5.0 / Phase 11 RAG + 自主學習

migrations/027 — rag_query_log(每次 RAG 查詢的 audit log)
- query_text 4KB CHECK + 90 天保留
- VECTOR(1024) bge-m3 embedding (與 ai_insights 一致簽名)
- ivfflat lists=100 索引
- saved_call 欄位追蹤「成功攔截 LLM 呼叫」次數
- feedback_score 1-5(NULL=未反饋)
- 6 條 CHECK 含 chk_rag_saved_consistent

migrations/028 — learning_episodes(蒸餾池 → ai_insights 前哨)
- 8 狀態機:pending/approved/awaiting_review/rejected_*4/expired
- weight 0-1(>=0.8 觸發 PromotionGate Stage 4 人工驗收)
- 9 條 CHECK 含 chk_le_approved_consistent / chk_le_review_consistent
- partial index idx_le_status WHERE in (pending, awaiting_review)
- distilled_text 16KB 上限

docs/phase11_db_design — 設計文檔
- 6 大決策(兩表分離 / ivfflat / partial index / 軟連結 / 90天保留 / 應用層白名單)
- 6 大風險評估(R1 PII / R2 蒸餾失誤 / R3 ivfflat 退化 / R4 dangling FK / R5/R6 trade-off)
- Phase 11 上線後驗收 SQL(EXPLAIN ANALYZE)

PromotionGate 4 階段(v5.0 護欄 #1, ADR-033):
  Stage 1: quality_score >= 0.7
  Stage 2: 無幻覺檢測(規則引擎,零 LLM)
  Stage 3: 與既有 insight 相似度 < 0.95
  Stage 4: weight >= 0.8 必經 Telegram 👍/👎(24h 無回應 → expired)

A4 fullstack-engineer 同時在寫 services/rag_service.py + learning_pipeline.py,
service 完成後一起部署啟用。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:39:47 +08:00

9.6 KiB
Raw Permalink Blame History

Phase 11 DB 設計RAG + 自主學習迴圈

  • 戰役: Operation Ollama-First v5.0 — Phase 11
  • 作者: A3 db-expert
  • 日期: 2026-05-03 台北
  • migration: migrations/027_create_rag_query_log.sqlmigrations/028_create_learning_episodes.sql
  • 對應 ADR: ADR-029Hermes-First、ADR-002pgvector 唯一向量庫、ADR-007pgvector 啟用)
  • 前置 migration: 024ai_calls、025mcp_calls + ai_call_budgets、026embedding_signature

1. 為何分兩表rag_query_log vs learning_episodes

兩個表責任完全不同,混表會讓讀寫模式衝突保留週期混淆

維度 rag_query_log learning_episodes
角色 RAG 召回的 audit log 知識庫前哨(蒸餾池)
資料方向 從用戶/呼叫者「進來」 給 ai_insights「出去」
生命週期 90 天滾動刪除 長期approved/rejected 走冷儲檔)
寫入頻率 高(每次 RAG 召回都寫) 中(過 quality 才寫)
PII 風險 query_text = 用戶問題) distilled 已蒸餾)
典型查詢 「過去 24h 命中率」「caller 分布」 「待人工驗收清單」「Stage 3 dedup query」
是否進 RAG 召回語料 否(只是 log 否(只有晉升 ai_insights 後才進)

反證:若合表,會出現

  • query_text PII 與蒸餾文本同表→ 90 天保留無法分別套用
  • 高頻寫入 audit log 與低頻寫入蒸餾池共享 ivfflat 索引 → vacuum / REINDEX 衝突
  • promotion_status 對 audit log 無意義,但要忍受 NULL

故維持分表。


2. ivfflat lists=100 計算依據

pgvector 官方建議:

  • lists ≈ rows / 1000rows < 1M
  • lists ≈ sqrt(rows)rows ≥ 1M

rag_query_log 量推估

  • 假設 Phase 11 上線後每日 RAG 召回 5,000 次hermes_qa + openclaw_qa + 內部 caller
  • 90 天保留 → 穩態約 450k 行
  • lists ≈ 450 / 1,但太小(<10會退化成全掃取下限 100
  • 等流量上升到 1M 行時(約 200 天後若日 5k → 不會到 1MREINDEX ... WITH (lists=1000)

learning_episodes 量推估

  • 假設每日蒸餾 200 筆rejected ~70%、approved ~30%)→ 全保留
  • 一年約 73k 行2 年約 146k 行
  • lists=100 在 1M 以下都合理

重訓 SOP(寫入 ADR-029 後續維運章節):

-- 每月由 scheduler 檢查,若 EXPLAIN cost / actual_time 退化 5x重訓
REINDEX INDEX CONCURRENTLY idx_rag_query_log_embedding;
REINDEX INDEX CONCURRENTLY idx_le_embedding;

為何不用 HNSW009 ai_insights 用 HNSW

  • HNSW 寫入比 ivfflat 慢 5-10x高頻寫入的 rag_query_log 不適合)
  • HNSW 不需訓練,但索引大小約為 ivfflat 的 2-4×
  • ai_insights 是「讀多寫少」KM 沉澱)—— HNSW 合理
  • rag_query_log / learning_episodes 是「寫多讀中」—— ivfflat 合理

3. promotion_status 狀態機

                  ┌─────────────┐
                  │   pending   │ (初始)
                  └──────┬──────┘
                         │
            ┌────────────┴───────────────┐
            │                            │
   Stage 1: quality<0.7        Stage 2: 規則檢測幻覺
            │                            │
            ▼                            ▼
   ┌──────────────────┐      ┌───────────────────────┐
   │ rejected_quality │      │rejected_hallucination │
   └──────────────────┘      └───────────────────────┘

   Stage 1+2 通過:
            │
            ▼
   Stage 3: 與既有 insight cosine>0.95
            │
            ▼
   ┌────────────────────┐
   │ rejected_duplicate │ (若太相似)
   └────────────────────┘

   Stage 3 通過:
            │
   ┌────────┴────────────┐
   │                     │
 weight<0.8           weight>=0.8
   │                     │
   ▼                     ▼
┌──────────┐   ┌──────────────────┐
│ approved │   │ awaiting_review  │ ← Telegram 推播
└──────────┘   └────────┬─────────┘
   │                    │
   │         ┌──────────┼─────────────┐
   │         │          │             │
   │       人工 👍    人工 👎      24h 無反饋
   │         │          │             │
   │         ▼          ▼             ▼
   │    ┌──────────┐  ┌──────────────┐  ┌──────────┐
   │    │ approved │  │rejected_human│  │ expired  │── 降 weight=0.5 重走 Stage 4a
   │    └──────────┘  └──────────────┘  └──────────┘
   │         │
   ▼         ▼
   寫 ai_insights → insight_id 回填

關鍵 invariants已用 CHECK 強制)

  1. approved ⇔ insight_id IS NOT NULLchk_le_approved_consistent
  2. rejected_* ⇒ rejected_reason IS NOT NULLchk_le_rejected_reason
  3. human_approver IS NOT NULL ⇒ reviewed_at IS NOT NULLchk_le_review_consistent

4. 90 天保留策略

保留 工具 預計排程
rag_query_log 90 天 scheduler DELETE WHERE queried_at < NOW() - INTERVAL '90 days' 03:30 daily
learning_episodes (pending/awaiting_review) 永久(直到狀態變化)
learning_episodes (approved) 永久(蒸餾溯源)
learning_episodes (rejected_*/expired) 180 天後可冷儲檔 後續 ADR 定 monthly
ai_calls 90 天 (已存在 migration 024 註解) 03:00 daily
mcp_calls 90 天 同上 03:15 daily

為何 rag_query_log 與 ai_calls 同 90 天:兩者透過 request_id 串鏈若不同保留期會出現「ai_calls 已刪、rag_query_log 留著」的孤兒,反查 trace 會斷。

learning_episodes 不限期保留的依據:蒸餾池是「為什麼這條 insight 進了 KM」的證據鏈。rejected_* 也保留是為了防止同類錯誤被反覆生成PromotionGate Stage 3 dedup 可參考歷史 rejected


5. 風險評估

R1HIGH—— query_text PII 落地

  • 風險rag_query_log.query_text 是用戶原始輸入,可能含人名/手機/訂單號
  • 緩解
    1. CHECK octet_length <= 4096 限長度
    2. 90 天滾動刪除
    3. 應用層在寫入前對「明顯 PII pattern」做 redact\d{10} 手機)
    4. learning_episodes.distilled_text 必須是「蒸餾後」文本,禁止直接複製 query_text
  • 未解殘留風險90 天內 DBA query 仍可看到原始問題;建議搭配 PostgreSQL row-level audit log 追蹤誰查過

R2HIGH—— 蒸餾失誤污染 RAG

  • 風險:低品質 learning_episodes 過閘晉升 ai_insights → RAG 召回幻覺擴散
  • 緩解
    1. PromotionGate 4 階段quality / hallucination / duplicate / human
    2. weight>=0.8 強制人工驗收chk_le_review_consistent
    3. rejected_* 必填 rejected_reasonchk_le_rejected_reason事後可審計

R3MEDIUM—— ivfflat 索引膨脹 / 退化

  • 風險:高頻寫入 + 不重訓 → recall 退化
  • 緩解
    1. partial index WHERE query_embedding IS NOT NULL 縮體積
    2. monthly REINDEX CONCURRENTLY見上 §2 SOP
    3. EXPLAIN ANALYZE alarmcost > baseline 5x 時告警)

R4MEDIUM—— ai_insights 軟連結 dangling

  • 風險learning_episodes.insight_id 無 FK若 ai_insights archive蒸餾池會留 dangling pointer
  • 緩解
    1. archive 時保留 ai_insights 主鍵(採 status='archived' soft delete而非 DELETE
    2. 應用層 join 用 LEFT JOINdangling 顯示為 "已歸檔"

R5LOW—— used_results BIGINT[] 反正規化

  • 風險rag_query_log.used_results 用陣列存命中 ai_insights.id違反正規化
  • 緩解理由
    1. 召回每筆平均 3-5 個 id若拆 join table 會 5x 寫入放大
    2. 反向查詢「某 insight 被多少 RAG 命中」是低頻分析,可用 WHERE id = ANY(used_results) 或 GIN 索引補V2 再加)
  • 接受該風險

R6LOW—— caller 白名單未在 DB 強制

  • 風險:應用層可能寫入未知 caller污染統計
  • 緩解
    1. ai_calls 已有 caller 白名單註釋logger 統一強制
    2. 本表加 CHECK 會與 ai_calls 雙寫漂移;改由 application layer 單一真理源
  • 接受該風險

6. 驗收清單(給 critic

  • 027 / 028 連續編號,未跳號
  • BIGSERIAL 主鍵對齊 024/025
  • CHECK 風格對齊 critic-A11白名單 + size + range
  • partial index 對 sparse 欄位request_id / insight_id / status
  • ivfflat lists=100 + cosine + 1024 維對齊 bge-m3
  • GRANT 權限對齊momo + sequence
  • 不在 migration 內 CONCURRENTLY無既存大表
  • 回滾腳本附在 migration 頂部註解
  • 與 ai_calls/mcp_calls 透過 request_id 串鏈
  • PII 護欄query_text 4KB / distilled 16KB
  • 狀態機 invariant 用 CHECK 鎖死
  • 不自動 commit / 不自動 apply