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

208 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-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 / 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 後續維運章節):
```sql
-- 每月由 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 NULL`chk_le_approved_consistent
2. `rejected_* ⇒ rejected_reason IS NOT NULL`chk_le_rejected_reason
3. `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. 風險評估
### 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
- [x] 027 / 028 連續編號,未跳號
- [x] BIGSERIAL 主鍵對齊 024/025
- [x] CHECK 風格對齊 critic-A11白名單 + size + range
- [x] partial index 對 sparse 欄位request_id / insight_id / status
- [x] ivfflat lists=100 + cosine + 1024 維對齊 bge-m3
- [x] GRANT 權限對齊momo + sequence
- [x] 不在 migration 內 CONCURRENTLY無既存大表
- [x] 回滾腳本附在 migration 頂部註解
- [x] 與 ai_calls/mcp_calls 透過 request_id 串鏈
- [x] PII 護欄query_text 4KB / distilled 16KB
- [x] 狀態機 invariant 用 CHECK 鎖死
- [x] 不自動 commit / 不自動 apply