# ADR-032: RAG 自主學習迴圈 — Distiller + PromotionGate + 反饋環

- **Status**: Accepted
- **Date**: 2026-05-03
- **Decision Maker**: 統帥
- **Author**: Operation Ollama-First v5.0（Phase 11 落地後追認）
- **Supersedes**: 無
- **Related**: ADR-002（pgvector 唯一向量庫）、ADR-007（持久化雙寫）、ADR-028（LLM 路由統一準則）、ADR-029（Hermes-First 雙塔分工）、ADR-033（RAG 三護欄）

---

## Context

戰役 v5.0 Wave 1 完成後，momo-pro 已具備 ai_calls / mcp_calls / ai_call_budgets 觀測層，但仍是「無狀態 LLM 用戶」 — 每次 Hermes/OpenClaw 提問都重新燒 token，重複問題沒被攔截。

Phase 0 audit 發現：
- 統帥 Telegram 答題 30%+ 是同一類問題（「PChome 補貼」「家電促銷檔期」「SKU 競爭力分析」）
- ai_insights 已累積 14k+ 筆（pgvector + bge-m3）但**沒有 RAG 攔截層**，全部走 LLM
- 預估：30% 流量可被 RAG cache 攔截 = 月省 ~9M Hermes/OpenClaw tokens

**Owen 提出三大風險**（v5.0 強化護欄）：
1. **學習污染**：LLM 幻覺自動進 RAG → 正反饋錯誤循環（ADR-033 護欄 #1）
2. **資源消耗**：自建 Firecrawl Playwright 池吃 188 主機記憶體（ADR-033 護欄 #2）
3. **Embedding 一致性**：bge-m3 floating tag → RAG 召回率悄悄退化（ADR-033 護欄 #3）

本 ADR 鎖定 **「LLM 結果 → 蒸餾 → Promotion Gate → ai_insights → RAG」自主學習迴圈** 的設計與護欄。

---

## Decision

### 1. 雙表分離設計

| 表 | 用途 | 保留期 | PII 等級 |
|---|---|---|---|
| `rag_query_log` (migration 027) | 每次 RAG 查詢的 audit log | 90 天 | 中（query_text 可能含用戶問題）|
| `learning_episodes` (migration 028) | LLM/MCP 結果蒸餾池 | 永久（蒸餾溯源）| 低（distilled_text 已過 PII redact）|
| `ai_insights` (既有) | 已驗收的知識庫 | 永久 | 經 PromotionGate 過濾 |

### 2. 自主學習迴圈

```
┌─────────────────────────────────────────────────────────────┐
│  LLM 呼叫（Hermes/OpenClaw）                                 │
│           ↓                                                  │
│  RAG-first 攔截（cosine >= 0.85 命中）                      │
│      ↓ 命中                ↓ miss                            │
│  return synthesize     LLM 跑                                │
│  (rag_hit=true)             ↓                                │
│                       Distiller 蒸餾                         │
│                             ↓                                │
│                       learning_episodes (pending)            │
│                             ↓                                │
│                       PromotionGate 4 階段                   │
│                             ↓                                │
│                       ai_insights (approved)                 │
│                             ↓                                │
│                       下次 RAG 查得到                        │
└─────────────────────────────────────────────────────────────┘
```

### 3. Distiller 規則引擎（純 Hermes，零 LLM 成本）

```python
QUALITY_RULES = {
    'mcp_result':  lambda c: 0.8 if len(c) > 200 and len(keywords(c)) >= 2 else 0.4,
    'llm_json_ok': lambda c: 0.9,                       # 結構化 JSON + status='ok'
    'llm_text':    lambda c: 0.6 if has_zh_numbers(c) else 0.3,
    'thumb_up':    lambda _: 1.0,                        # 用戶 👍 反饋
    'thumb_down':  lambda _: 0.0,                        # 負樣本不晉升
}
```

**為何不用 LLM 蒸餾？** 避免循環燒錢（Distiller 跑頻率高，用 LLM 等於每次 RAG miss 都燒兩次）。

### 4. PromotionGate 4 階段晉升閘（v5.0 護欄 #1）

```python
class PromotionGate:
    STAGE_1_AUTO_QUALITY = 0.7        # 蒸餾品質分
    STAGE_2_HALLUCINATION_RULES = [
        '可能/也許/我猜測 + 缺具體數字',
        '自相矛盾（同段含 A=X 又 A=Y）',
        '引用不存在 SKU/品牌（查 DB）',
    ]
    STAGE_3_DEDUP_THRESHOLD = 0.95    # cosine 相似度
    STAGE_4_HUMAN_REVIEW_WEIGHT = 0.8 # 高權重必經 👍/👎
```

**鐵律**：weight ≥ 0.8 的 episode **不能跳 Stage 4**，必須推 Telegram 等 24h 反饋。

| 結果 | promotion_status |
|---|---|
| 4 階段全過 | `approved` → 寫入 ai_insights |
| Stage 1 失敗 | `rejected_quality` |
| Stage 2 失敗 | `rejected_hallucination` |
| Stage 3 失敗 | `rejected_duplicate` |
| Stage 4 推送等待 | `awaiting_review` |
| 24h 無反饋 | `expired`（weight 降為 0.5，不晉升但保留）|
| 用戶 👎 | `rejected_human` |

### 5. Telegram 反饋環（強制晉升門檻）

```python
# services/telegram_templates.py
def rag_feedback_keyboard(rag_query_log_id: int) -> dict:
    return {'inline_keyboard': [[
        {'text': '👍 有用', 'callback_data': f'rag_fb:{id}:5'},
        {'text': '👎 沒用', 'callback_data': f'rag_fb:{id}:1'},
    ]]}

def promotion_review_keyboard(episode_id: int) -> dict:
    return {'inline_keyboard': [[
        {'text': '✅ 通過晉升', 'callback_data': f'pg_ok:{id}'},
        {'text': '❌ 拒絕', 'callback_data': f'pg_no:{id}'},
    ]]}
```

`routes/openclaw_bot_routes.py` 三組 callback handler 已就位（Phase 11 落地）。

### 6. Feature Flag 灰度

```python
RAG_ENABLED = os.getenv('RAG_ENABLED', 'false').lower() == 'true'
RAG_DEFAULT_THRESHOLD = float(os.getenv('RAG_DEFAULT_THRESHOLD', '0.85'))
RAG_DEFAULT_TOP_K = int(os.getenv('RAG_DEFAULT_TOP_K', '5'))
```

**預設 OFF**，戰前部署後行為與 v4.x 完全相同。灰度開啟條件：
1. ANTHROPIC_API_KEY 已設（Phase 7 已備）
2. learning_episodes 累積 100+ 筆
3. RAG_ENABLED=true + threshold=0.90（保守起步）
4. 1 週後 feedback_score ≥ 4 比率 > 70% → threshold 降至 0.85

### 7. 失敗安全（fire-and-forget 哲學）

| 失敗模式 | 行為 |
|---|---|
| DB 寫入 rag_query_log 失敗 | 主流程不爆，logger.warning |
| embedding 失敗 | 不查 DB 直接 fallback LLM |
| signature 不一致 | log warning + 不採該筆 hit |
| Distiller 寫 learning_episodes 失敗 | LLM 結果照樣回 caller |
| PromotionGate Stage 1-3 失敗 | episode 留 learning_episodes（不晉升即可，無 DB 副作用）|

---

## Alternatives Considered

| 方案 | 否決理由 |
|---|---|
| **A. 直接 ai_insights 寫入（無蒸餾池）** | LLM 幻覺直接污染知識庫，無 PromotionGate 阻擋（核心風險）|
| **B. LLM 蒸餾（用 Gemini 寫 distill prompt）** | 循環燒錢：每次 RAG miss → LLM call → 又燒 LLM 蒸餾 = 2× 成本 |
| **C. 純 push 不 pull（無反饋按鈕）** | 統帥無法糾正幻覺，正反饋錯誤循環（Owen 強調的痛點）|
| **D. 跳過 dedup（Stage 3）** | ai_insights 將累積大量重複，RAG 查詢無謂耗時 |
| **E. 用 ChromaDB / Qdrant 替代 pgvector** | 違反 ADR-002（pgvector 唯一）+ 增加運維面 |

---

## Consequences

### 正面（5）
1. **重複問題攔截**：預估月省 ~9M Hermes/OpenClaw tokens（Hermes 流量 -30%）
2. **自主學習**：每次 LLM 結果都進蒸餾池，知識庫持續成長
3. **錯誤可糾正**：👎 反饋直接降權，避免幻覺循環污染
4. **零 LLM 蒸餾成本**：Distiller 純規則引擎
5. **PII 安全**：query_text 截 4KB + human_approver SHA1[:8]

### 負面（3）
1. **複雜度↑**：兩表 + 4 階段閘 + 反饋環，新人理解曲線陡
2. **Stage 4 人工驗收延遲**：高權重 episode 必須等 24h 才能晉升
3. **embedding 寫入路徑暫缺**（已知 limitation）：Stage 3 dedup 待 Phase 12+ 補

### 風險（4）
1. **Distiller 規則漂移**：規則引擎可能漏判幻覺 → mitigate by Stage 4 人工驗收
2. **Stage 4 人工疲勞**：統帥不可能 24h 看 Telegram → mitigate by `expired` 自動降級
3. **ai_insights 膨脹**：學習迴圈累積快 → mitigate by Stage 3 dedup（Phase 12+ 啟用）
4. **PromotionGate worker cron 未掛**（已知）：需 Phase 12+ 排程任務

---

## Verification

### V1：RAG 攔截率（部署 1 週後）
```sql
SELECT
  COUNT(*) FILTER (WHERE saved_call) AS hit_count,
  COUNT(*) AS total,
  ROUND(100.0 * COUNT(*) FILTER (WHERE saved_call) / COUNT(*), 1) AS hit_pct
FROM rag_query_log
WHERE queried_at > NOW() - INTERVAL '7 days';
-- 期望 hit_pct >= 25%
```

### V2：晉升通過率
```sql
SELECT promotion_status, COUNT(*)
FROM learning_episodes
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY promotion_status;
-- 期望 approved + awaiting_review 占 >50%；rejected_hallucination < 10%
```

### V3：反饋分布
```sql
SELECT feedback_score, COUNT(*)
FROM rag_query_log
WHERE feedback_score IS NOT NULL
GROUP BY feedback_score;
-- 期望 score=5 比率 > 60%
```

### V4：Embedding 一致性（v5.0 護欄 #3）
```sql
SELECT embedding_signature, COUNT(*)
FROM ai_insights
WHERE embedding IS NOT NULL
GROUP BY embedding_signature;
-- 期望單一簽名（多個 = 模型版本漂移，需處理）
```

---

## Migration Plan

| Phase | 工作 | 狀態 |
|---|---|---|
| 11.1 | rag_query_log + learning_episodes schema | ✅ migration 027/028 commit 2f20d8d |
| 11.2 | rag_service.py + learning_pipeline.py | ✅ commit c7d6db3 |
| 11.3 | Hermes/OpenClaw RAG-first 整合 | ✅ commit c7d6db3 |
| 11.4 | Telegram 反饋按鈕 + callback | ✅ commit c7d6db3 |
| 11.5 | learning_episodes.embedding 寫入 | ⏳ Phase 12+ |
| 11.6 | PromotionGate worker cron 掛排程 | ⏳ Phase 12+ |
| 11.7 | awaiting_review Telegram 推播 | ⏳ Phase 12+（callback 已就位）|
| 11.8 | RAG_ENABLED=true 灰度啟用 | ⏳ 1 週觀察期後 |

---

## References

- `migrations/027_create_rag_query_log.sql`
- `migrations/028_create_learning_episodes.sql`
- `services/rag_service.py`（532 行）
- `services/learning_pipeline.py`（750 行）
- `tests/test_rag_service.py` + `test_learning_pipeline.py` + `test_promotion_gate.py`（70 unit tests）
- `docs/phase11_db_design_20260503.md`
- ADR-002（pgvector 唯一向量庫）
- ADR-007（雙寫保證）
- ADR-029（Hermes-First 雙塔分工）
- ADR-033（RAG 三護欄）— 即將補
