# ADR-033: RAG 治理三護欄 — Promotion Gate / Firecrawl 資源 / BGE-M3 一致性 - **Status**: Accepted - **Date**: 2026-05-03 - **Decision Maker**: 統帥 - **Author**: Operation Ollama-First v5.0(Owen 三點專業洞察 → v5.0 強化) - **Related**: ADR-032(RAG 自主學習迴圈)、ADR-031(MCP 自建)、ADR-002(pgvector)、ADR-027(Primary Ollama on GCP) --- ## Context 戰役 v4.0 階段 Owen 提出三點專業洞察,被升級為 v5.0 護欄級鐵律: 1. **學習污染風險**:LLM 幻覺自動進 RAG → 正反饋錯誤循環 2. **Firecrawl 資源消耗**:自建 Playwright 池吃 188 主機記憶體 3. **BGE-M3 Embedding 一致性**:floating tag → RAG 召回率悄悄退化 這三點**不是普通建議,而是 RAG 系統能否安全長期運轉的命脈**。本 ADR 鎖定三護欄的設計決策與驗收條件。 --- ## Decision — 三護欄架構 ### 護欄 #1:Promotion Gate(學習污染防護) **核心原則**:反饋按鈕從「選配」升級為「強制晉升門檻」。learning_episodes → ai_insights 必經 4 階段嚴格門檻。 #### 4 階段晉升閘 ``` learning_episodes (pending) ↓ Stage 1: quality_score >= 0.7(蒸餾器自動評分) ↓ Stage 2: 無幻覺檢測(規則引擎,零 LLM) ↓ Stage 3: 與既有 insight 相似度 < 0.95(去重) ↓ Stage 4: weight >= 0.8 必經 Telegram 👍/👎 人工驗收 ai_insights (approved) ``` #### Stage 2 幻覺檢測規則 ```python HALLUCINATION_PATTERNS = [ # 規則 1:含「可能 / 也許 / 我猜測」+ 缺具體數字 lambda txt: any(p in txt for p in ['可能', '也許', '我猜', '推測']) and not any(c.isdigit() for c in txt), # 規則 2:自相矛盾(同段含 'A=X' 又含 'A=Y') detect_contradiction, # 規則 3:引用不存在 SKU/品牌(查 DB) lambda txt: not _verify_skus_exist(extract_skus(txt)), ] ``` #### Stage 4 強制門檻(Owen 鐵律) - weight >= 0.8 → 推 Telegram + 等 24h 👍/👎 - 24h 無回應 → `expired`(weight 降 0.5,不晉升) - 用戶 👎 → `rejected_human`(永不晉升) - 用戶 👍 → `approved` 寫 ai_insights **無條件規則**:高權重 episode 不能跳 Stage 4,即使 Stage 1-3 都過。 ### 護欄 #2:Firecrawl 資源護欄(188 主機保護) #### Docker 限制 ```yaml # docker-compose.mcp.yml(Phase 10 將部署) services: firecrawl-self: image: firecrawl/firecrawl:latest deploy: resources: limits: memory: 2g # ⭐ Owen 要求硬上限 cpus: '1.5' environment: - PLAYWRIGHT_BROWSER_POOL_MAX=3 # 瀏覽器池上限 - SCRAPE_TIMEOUT_MS=30000 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3002/health"] interval: 30s ``` #### Chrome 殘留清理 sidecar ```yaml chrome-reaper: image: alpine:3 command: | sh -c "while true; do docker exec firecrawl-self pkill -f 'chrome.*--type=zygote' 2>/dev/null; docker exec firecrawl-self pkill -f 'chrome.*--type=renderer' 2>/dev/null; sleep 3600; done" ``` #### Telegram 告警 - 每小時檢查 firecrawl 容器 RSS - > 1.8GB → 🟠 P2 告警(記憶體即將達上限) ### 護欄 #3:BGE-M3 Embedding 一致性(RAG 命脈) #### 風險來源 - `bge-m3:latest` floating tag → Ollama upgrade 跳版本 - normalize / pooling 參數未顯式傳遞 → server-side 預設改變無感知 - 跨主機(GCP / Secondary / 111)模型版本可能不一致 #### 簽名鎖定機制 ```python # services/rag_service.py def get_embedding_signature( model: str = 'bge-m3:latest', dim: int = 1024, normalize: bool = True, ) -> str: """SHA1({model}|{normalize}|{dim})[:12]""" raw = f"{model}|{str(normalize).lower()}|{dim}" return hashlib.sha1(raw.encode()).hexdigest()[:12] ``` #### Schema 強制(migration 026) ```sql ALTER TABLE ai_insights ADD COLUMN IF NOT EXISTS embedding_signature VARCHAR(64); CREATE INDEX CONCURRENTLY idx_ai_insights_embedding_signature ON ai_insights (embedding_signature) WHERE embedding IS NOT NULL; ``` #### 啟動時驗證(Phase 11.0 護欄) ```python def verify_embedding_consistency(): """RAG service 啟動時跑: 用同一段測試文字呼叫 GCP / Secondary / 111 三主機, 驗證 cosine 距離 < 1e-4(浮點誤差),否則拒絕啟動。 """ test_text = "momo電商競品分析測試向量一致性檢查" embeddings = { host: call_ollama(host, 'bge-m3:latest', test_text) for host in [GCP_PRIMARY, GCP_SECONDARY, OLLAMA_111] } diffs = [cosine_distance(embeddings[a], embeddings[b]) for a, b in itertools.combinations(embeddings, 2)] if max(diffs) > 1e-4: raise EmbeddingInconsistencyError(...) ``` #### RAG 查詢時保護 ```python # rag_service.py:_select_hits for hit in candidates: if hit.embedding_signature != current_signature: logger.warning(f"Signature mismatch: hit={hit.id}, " f"expected={current_signature}, got={hit.embedding_signature}") continue # 不採用該筆 ``` --- ## Alternatives Considered | 方案 | 否決理由 | |---|---| | **A. RAG 不要反饋按鈕(純自動晉升)** | LLM 幻覺進 RAG 後正反饋錯誤循環,是 RAG 系統最危險的失敗模式 | | **B. Firecrawl 不限資源(讓它跑)** | 188 主機跑 5+ project(reference_188_multi_project),OOM 會拖垮其他容器 | | **C. BGE-M3 用 :latest 接受漂移** | 模型升級時無告警,RAG 召回率悄悄退化,問題暴露時難回溯 | | **D. 三護欄都用 LLM 做(如 LLM 蒸餾、LLM 幻覺檢測)** | 循環燒錢 + 引入新幻覺風險(LLM 檢測 LLM 幻覺)| | **E. Stage 4 改為非強制(高 weight 直接 approved)** | 違反 Owen 鐵律 — 統帥反饋是 RAG 系統不被污染的最後一道防線 | --- ## Consequences ### 正面(5) 1. **學習污染防火牆**:4 階段閘 + 強制人工驗收,幻覺進 RAG 機率 < 5% 2. **資源預測性**:Firecrawl mem_limit 2g + chrome-reaper,188 主機絕對安全 3. **模型升級可控**:embedding_signature 不變才 RAG 採用,模型漂移立即可見 4. **PII 安全**:human_approver SHA1[:8],反饋紀錄不暴露 Telegram username 5. **成本可控**:純規則引擎(Stage 1-3)+ 24h auto-expire(Stage 4),零 LLM 成本 ### 負面(3) 1. **Stage 4 統帥疲勞**:高權重 episode 都要看 Telegram → mitigate by `expired` 自動降級 2. **Firecrawl mem 2g 上限可能太小**:複雜 SPA 爬蟲可能超 → 監控告警 + 可調 env 3. **Embedding signature 變更需全表回填**:PG14 ADD COLUMN metadata-only 不鎖表,但回填 14k+ 筆需 worker 跑數小時 ### 風險(4) 1. **Stage 2 規則漏判**:規則引擎可能誤放幻覺進 → mitigate by Stage 4 人工最後關 2. **Firecrawl OOM 連鎖**:mem_limit 觸發 OOM kill → mitigate by healthcheck + 重啟策略 3. **Embedding 模型升級時 RAG 完全失效**:所有 hit signature 不符 → 安全降級為「LLM-only」直到回填完成 4. **24h expired 太久**:用戶可能來不及反饋 → 可調 `HUMAN_REVIEW_TIMEOUT_HOURS` --- ## Verification ### V1:Promotion Gate 阻擋率(部署 1 週後) ```sql SELECT promotion_status, COUNT(*) FROM learning_episodes WHERE created_at > NOW() - INTERVAL '7 days' GROUP BY promotion_status; -- 期望: rejected_hallucination >= 1(證明 Stage 2 真的擋下幻覺) -- 期望: approved + awaiting_review > 50% ``` ### V2:Stage 4 反饋率 ```sql SELECT COUNT(*) FILTER (WHERE promotion_status = 'awaiting_review') AS pending, COUNT(*) FILTER (WHERE promotion_status = 'approved' AND human_approver IS NOT NULL) AS human_approved, COUNT(*) FILTER (WHERE promotion_status = 'rejected_human') AS human_rejected, COUNT(*) FILTER (WHERE promotion_status = 'expired') AS expired FROM learning_episodes; -- 期望: human_approved + human_rejected > expired(統帥真的有看 Telegram) ``` ### V3:Firecrawl 資源(部署後) ```bash ssh ollama@192.168.0.188 'docker stats firecrawl-self --no-stream --format "{{.MemUsage}}"' # 期望 < 1.8GB(mem_limit 2GB 的 90%) ``` ### V4:Embedding 一致性 ```sql SELECT embedding_signature, COUNT(*), MIN(created_at), MAX(created_at) FROM ai_insights WHERE embedding IS NOT NULL GROUP BY embedding_signature ORDER BY MAX(created_at) DESC; -- 期望: 單一簽名(多個 = 模型漂移) ``` --- ## Migration Plan | 護欄 | 部分 | 狀態 | |---|---|---| | #1 PromotionGate Schema | learning_episodes 8 狀態機 | ✅ migration 028 commit 2f20d8d | | #1 PromotionGate Service | 4 階段邏輯 + reject/promote | ✅ services/learning_pipeline.py commit c7d6db3 | | #1 反饋按鈕 | rag_feedback + promotion_review | ✅ telegram_templates + bot routes commit c7d6db3 | | #1 awaiting_review 推播 | Telegram 推 episode 給統帥看 | ⏳ Phase 12+ | | #2 Firecrawl mem_limit | docker-compose.mcp.yml | ⏳ Phase 10 部署 | | #2 chrome-reaper sidecar | 同上 | ⏳ Phase 10 | | #2 RSS 監控告警 | scheduler 加每小時 task | ⏳ Phase 10 | | #3 embedding_signature 欄位 | ai_insights 加欄位 | ✅ migration 026 commit 4648673 | | #3 簽名計算 | rag_service.get_embedding_signature() | ✅ commit c7d6db3 | | #3 啟動驗證 verify_consistency | 跨主機 cosine 比對 | ⏳ Phase 11+ 補(Phase 11.0 規格) | | #3 既有 14k 筆回填 | UPDATE ai_insights SET embedding_signature = ... | ⏳ Phase 11+ 補 | --- ## References - `migrations/026_add_embedding_signature.sql`(含 pgcrypto extension) - `migrations/028_create_learning_episodes.sql`(8 狀態機 CHECK) - `services/rag_service.py:get_embedding_signature()` - `services/learning_pipeline.py`(PromotionGate 4 階段) - `tests/test_promotion_gate.py`(23 unit tests) - ADR-002(pgvector 唯一) - ADR-027(三主機架構) - ADR-032(RAG 自主學習迴圈) - ADR-031(MCP 自建 — Phase 10 將補)