All checks were successful
CD Pipeline / deploy (push) Successful in 3m30s
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>
9.6 KiB
9.6 KiB
Phase 11 DB 設計:RAG + 自主學習迴圈
- 戰役: Operation Ollama-First v5.0 — Phase 11
- 作者: A3 db-expert
- 日期: 2026-05-03 台北
- migration:
migrations/027_create_rag_query_log.sql、migrations/028_create_learning_episodes.sql - 對應 ADR: ADR-029(Hermes-First)、ADR-002(pgvector 唯一向量庫)、ADR-007(pgvector 啟用)
- 前置 migration: 024(ai_calls)、025(mcp_calls + ai_call_budgets)、026(embedding_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 / 1000(rows < 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 → 不會到 1M),再
REINDEX ... 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;
為何不用 HNSW(009 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 強制):
approved ⇔ insight_id IS NOT NULL(chk_le_approved_consistent)rejected_* ⇒ rejected_reason IS NOT NULL(chk_le_rejected_reason)human_approver IS NOT NULL ⇒ reviewed_at IS NOT NULL(chk_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. 風險評估
R1(HIGH)—— query_text PII 落地
- 風險:
rag_query_log.query_text是用戶原始輸入,可能含人名/手機/訂單號 - 緩解:
- CHECK
octet_length <= 4096限長度 - 90 天滾動刪除
- 應用層在寫入前對「明顯 PII pattern」做 redact(如
\d{10}手機) learning_episodes.distilled_text必須是「蒸餾後」文本,禁止直接複製 query_text
- CHECK
- 未解殘留風險:90 天內 DBA query 仍可看到原始問題;建議搭配 PostgreSQL row-level audit log 追蹤誰查過
R2(HIGH)—— 蒸餾失誤污染 RAG
- 風險:低品質
learning_episodes過閘晉升ai_insights→ RAG 召回幻覺擴散 - 緩解:
- PromotionGate 4 階段(quality / hallucination / duplicate / human)
weight>=0.8強制人工驗收(chk_le_review_consistent)rejected_*必填 rejected_reason(chk_le_rejected_reason),事後可審計
R3(MEDIUM)—— ivfflat 索引膨脹 / 退化
- 風險:高頻寫入 + 不重訓 → recall 退化
- 緩解:
- partial index
WHERE query_embedding IS NOT NULL縮體積 - monthly REINDEX CONCURRENTLY(見上 §2 SOP)
- EXPLAIN ANALYZE alarm(cost > baseline 5x 時告警)
- partial index
R4(MEDIUM)—— ai_insights 軟連結 dangling
- 風險:
learning_episodes.insight_id無 FK,若 ai_insights archive,蒸餾池會留 dangling pointer - 緩解:
- archive 時保留 ai_insights 主鍵(採 status='archived' soft delete,而非 DELETE)
- 應用層 join 用 LEFT JOIN,dangling 顯示為 "已歸檔"
R5(LOW)—— used_results BIGINT[] 反正規化
- 風險:
rag_query_log.used_results用陣列存命中 ai_insights.id,違反正規化 - 緩解理由:
- 召回每筆平均 3-5 個 id,若拆 join table 會 5x 寫入放大
- 反向查詢「某 insight 被多少 RAG 命中」是低頻分析,可用
WHERE id = ANY(used_results)或 GIN 索引補(V2 再加)
- 接受該風險
R6(LOW)—— caller 白名單未在 DB 強制
- 風險:應用層可能寫入未知 caller,污染統計
- 緩解:
- ai_calls 已有 caller 白名單註釋,logger 統一強制
- 本表加 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