# ADR-028: LLM 路由統一準則 — Ollama-First 五大支柱

- **Status**: Accepted
- **Date**: 2026-05-03
- **Decision Maker**: 統帥
- **Author**: Operation Ollama-First v5.0（Codex / A12 planner）
- **Supersedes**: 無（補述 ADR-027，非取代）
- **Related**: ADR-002（pgvector 唯一向量庫）、ADR-003（Hermes embedding 本地化）、ADR-004（NemoTron 配額耗盡 fallback）、ADR-008（部署實機驗證）、ADR-013（AIOps AutoHeal）、ADR-018（四 Agent 控制面）、ADR-027（Primary Ollama on GCP）

---

## Context

ADR-027 在 2026-05-03 啟用 GCP 高效能主機作為 Primary Ollama，但戰役 v5.0 Phase 0 的 onboarder audit（`docs/phase0_audit_report_20260503.md`）揭露三項治理斷層：

1. **34 個 LLM 呼叫點分散治理失能**
   - audit 完整盤點 `services/` 與 `routes/` 共 34 處 LLM 呼叫，分散在 9 個 service / 4 個 route 檔。
   - `AIGenerationHistory` 結構化紀錄覆蓋率僅 4/34 = 11.8%，其餘 88% 完全沒結構化遙測。
   - 兩處「寫死 111」的呼叫點（`services/aider_heal_executor.py:48-49`、`services/code_review_pipeline_service.py:218-225`）違反 ADR-027 的 Primary/Fallback 原則。

2. **4 條獨立 fallback 鏈缺一致性**
   - 不存在「線性 LLM 鏈」假設：Ollama 主機級、Hermes 競價、NemoTron 派遣、OpenClaw Q&A、MCP 即時情報各自為政。
   - 各鏈 timeout / 探測 / 降級語意不一，故障時行為不可預測。

3. **provider/caller 命名漂移**
   - 戰役清單原列 26 個呼叫點，audit 補出 8 個遺漏；`openclaw_qa_nim` 此類 fallback 命名靠執行期動態組成（`services/openclaw_strategist_service.py:737`），缺集中字典。
   - DB 端在 Phase 1 已加 provider CHECK constraint（`migrations/024_create_ai_calls_table.sql:88-91`），但 caller 仍是自由字串（critic-A11 於 phase1_final_critic_signoff H5 點名）。

ADR-027 解決了「主機去哪裡」，但未統一「誰能呼叫」「呼叫去哪一台」「為何選這個」。本 ADR 是 ADR-027 的決策補述，把 Phase 0/1/2/3 戰役落地的成果固化為憲法級準則，供未來 Phase 4-12 與所有新 Agent 共同遵循。

---

## Decision — 五大支柱

### 支柱 1：三主機級聯（Triple-Host Cascade）

取代 ADR-027 原本「GCP / 111」二段架構。

| 角色 | 主機 | 儲存 | 用途 |
|---|---|---|---|
| Primary | `34.143.170.20` | SSD | 9× 加載 / 2× 推理（v5.0 戰役新主機）|
| Secondary | `34.21.145.224` | SSD | 同等效能備援，ADR-027 原 Primary 降為次主 |
| Fallback | `192.168.0.111` | HDD | 最後一道本地防線，統帥 Mac 上的 Ollama.app |

`services/ollama_service.resolve_ollama_host()`（Phase 2 A6 落地）按 Primary → Secondary → Fallback 順序探測：
- HTTP probe `GET /api/version`，2s timeout（Phase 2 B3 修補）
- 失敗主機標 `mark_unhealthy()` 30s（Phase 2 B4），TTL 內直接跳下一台
- 任何呼叫點都必須走 `resolve_ollama_host()`，禁止寫死任何 IP

### 支柱 2：Ollama 優先（Ollama-First）

預設所有 LLM 推理都走 Ollama（三主機級聯），只有支柱 4 列的 7 個鎖定場景才允許走 Gemini。

**判定原則**：
- 高頻、低延遲敏感、戰術層 → Ollama
- 低頻、戰略洞察、需 Grounding/長 context、HITL pre-fetch → Gemini

**成本影響**：Ollama 三主機都是 self-hosted，邊際 token 成本 = 0；Gemini 月支出受 7 個鎖定場景上限約束。

### 支柱 3：雙塔分工（Twin-Tower）

詳見 ADR-029。簡述：
- **Hermes 主入口（L1 戰術塔）**：高頻 Ollama-only，所有日常意圖分類、競價分析、KPI 計算、Q&A 第一響應走 Hermes。
- **OpenClaw 副引擎（L3 戰略塔）**：低頻鎖定 5 個 Gemini 場景，月/年報、Code Review、EA HITL、Meta 自審等戰略產出。

### 支柱 4：Gemini 鎖定 7 個場景

只有以下 7 個 caller 允許呼叫 `gemini` provider，其餘任何 caller 走 Gemini 必經 ADR：

| # | Caller | 檔案位置 | 用途 | 模型 |
|---|---|---|---|---|
| 1 | `mcp_l1_grounding` | `services/mcp_collector_service.py:32` (LOCKED-GEMINI 註解) | MCP L1 Grounding（即時情報）| `gemini-2.0-flash` |
| 2 | `mcp_l3_ollama` 兜底前的 L2 | `services/mcp_collector_service.py:33` MCP_FALLBACK_MODEL | MCP L2 Grounding | `gemini-1.5-flash` |
| 3 | `ppt_gemini` | `routes/openclaw_bot_routes.py:98` (LOCKED-GEMINI) + `_call_gemini` 約 line 2464 | PPT 簡報深度分析 | `gemini-2.0-flash` |
| 4 | `openclaw_weekly` / `openclaw_monthly` | `services/openclaw_strategist_service.py:1102` weekly + `:1628` monthly + `:40` STRATEGY_MODEL (LOCKED-GEMINI) | 週/月報敘事（**註**：annual 報告尚未實作，目前 OpenClaw 僅 weekly/monthly/daily/meta）| `gemini-2.5-flash` |
| 5 | `code_review_openclaw` | `services/code_review_pipeline_service.py:46` REVIEW_MODEL (LOCKED-GEMINI) | Code Review 高階評估 | `gemini-2.5-flash` |
| 6 | `ea_engine` (HITL) | `services/elephant_alpha_orchestrator.py:88` AgentCapability.openclaw model (LOCKED-GEMINI)；`hermes_ea_prefetch` 走 Hermes Ollama 不在此鎖定 | EA HITL escalation（ADR-021）| `gemini-2.0-flash` |
| 7 | `openclaw_qa` 升級分支 | `services/openclaw_strategist_service.py:56` `generate_strategy_response` (Phase 3 A7 已加 Ollama-first feature flag，flag=true 時 Ollama 失敗才升級 Gemini) | 複雜 SKU 推理（Hermes/qwen3 品質低時升級）| `gemini-2.5-flash` |

新增 Gemini caller 必須走 ADR review；DB CHECK constraint 將於 Phase 5 後補（critic-A11 H5）。

### 支柱 5：可觀測性先行（Observability-First）

所有 LLM 呼叫必須經 `services/ai_call_logger.log_ai_call()` 雙寫 `ai_calls` 表（Phase 1 A4 落地）。

**遙測契約**：
- caller / provider / model 必填（provider 由 DB CHECK 約束在白名單內）
- input_tokens / output_tokens / duration_ms / status / cost_usd 必填
- request_id 可選但 fallback 鏈必須串接（如 `code_review_hermes` → `code_review_openclaw` → `code_review_elephant` 三鏈共用）
- `meta JSONB` 不得超 8192 octet，`error TEXT` 不得超 4096 octet（migration 024 H2）
- chat_id 等 PII 必經 sha256 截 12 字後存入（Phase 2 後補丁，critic H6）

**自動化護欄**：
- `ai_call_budgets` 7 個 provider 月度預算（gemini/claude/nim/nim_via_elephant/openrouter/gcp_ollama/ollama_111）+ 3 條全供應商總額（daily/weekly/monthly）= 10 筆種子預算（migration 025）
- 每日 23:55 token report 推 Telegram（A5 落地）
- AIGenerationHistory 覆蓋率必須 ≥ 90%（v5.0 戰役 KPI），Phase 5 報表追蹤

---

## Provider 白名單（DB CHECK 約束）

`migrations/024_create_ai_calls_table.sql:51-58` `chk_ai_calls_provider` 鎖定以下 8 值：

| Provider | 主機 | 計費 | 用途 |
|---|---|---|---|
| `gcp_ollama` | Primary `34.143.170.20:11434` | 0（self-hosted）| 戰役 v5.0 主主機 |
| `ollama_secondary` | Secondary `34.21.145.224:11434` | 0 | 同等效能備援（架構保留，logger 端 url-based 推斷待 Phase 7+） |
| `ollama_111` | Fallback `192.168.0.111:11434` | 0 | 最後一道防線 |
| `gemini` | Google AI API | metered | 鎖定支柱 4 的 7 個場景 |
| `claude` | Anthropic API | metered | Phase 7 Frontier 升級保留：Code Review L0 (Opus 4.7) + EA HITL (Sonnet 4.6)；budgets 種子 $10/月已預設 |
| `nim` | NVIDIA NIM `https://integrate.api.nvidia.com/v1` | 80 calls/day 配額 | NemoTron 派遣（ADR-004）+ Code Review fallback |
| `nim_via_elephant` | NIM via `services/elephant_service.py` | 同 NIM 配額 | Code Review ElephantAlpha 49B 鏈 |
| `openrouter` | OpenRouter（保留） | metered | PPT deepseek-v3.2 鏈 + 預留 Phase 9 多供應商實驗 |

> **三層一致性備忘**（critic-A11 B4 修補）：DB CHECK = 8 個，ADR-028 = 8 個，`services/token_report_service.py` `_PROVIDER_DISPLAY` 後續需補 `ollama_secondary`（H5 待修，列為 Phase 7 整合任務）。

---

## Caller 白名單（程式碼集中字典）

戰役 v5.0 Phase 1 A4 logger 接入後，固定字串集中於 `services/ai_call_logger.py` 與各 caller。常見 13 + 5 NIM 變體：

```
hermes_intent / hermes_analyst / hermes_rule_engine
code_review_hermes / code_review_openclaw / code_review_elephant
openclaw_qa / openclaw_qa_nim
openclaw_weekly / openclaw_weekly_nim
openclaw_daily / openclaw_daily_nim
openclaw_monthly / openclaw_monthly_nim
openclaw_meta / openclaw_meta_nim
nemotron_dispatch
openclaw_bot_main / openclaw_bot_gemini / openclaw_bot_nim
embedding_worker / embedding_realtime
mcp_collector_l1 / mcp_collector_l2 / mcp_collector_l3
ppt_generator / ppt_generator_ollama / ppt_generator_nim
ea_hitl_prefetch / ea_autonomous_engine
aider_heal
sales_copy / trend_match / trend_search / product_insights / trend_keywords
telegram_copy / bot_api_copy / trend_crawler / ai_provider_generic
```

新增 caller 必須：
1. 加進 `services/ai_call_logger.py` `_KNOWN_CALLERS` 字典
2. 更新本 ADR 表格
3. 通過 critic 審查
4. Phase 5 後 DB CHECK constraint 將以格式約束（`^[a-z][a-z0-9_]{2,63}$`）NOT VALID 補上（critic-A11 H5 建議）

---

## 鎖定 Gemini 7 個場景（與支柱 4 對應，含理由）

| 場景 | 為何不能走 Ollama |
|---|---|
| MCP L1/L2 Grounding | Gemini Grounding 是即時 web search 唯一供應商，Ollama 無此能力（Tavily/Exa 走 Phase 10 不同路徑）|
| PPT 圖檢查 / 簡報分析 | Gemini 多模態 vision 能力遠超 Ollama 本地模型 |
| 週/月/年報敘事 | 商業敘事品質要求高，Gemini 2.5 Flash vs qwen3:14b 估差 10-20%（phase0_research_report Section 1）|
| Code Review 高階評估 | OpenClaw 級審查需 Gemini 2.5 Flash 的程式理解力，本地模型不足 |
| EA HITL pre-fetch | escalation 路徑 5s timeout 內必須拿到結構化 + 高品質回應，ADR-021 已定 |
| 複雜 SKU 推理 | Hermes/qwen3:14b 信心 < threshold 時才升級 Gemini，繁中商業情境短板（TMMLU+ 論文）|

---

## Alternatives Considered

| 方案 | 不採用原因 |
|---|---|
| **A. 單一供應商（全 Gemini 或全 NIM）** | 配額硬限（NIM 80 calls/day）+ 月成本不可控；雲端 API 出口流量風險；違反 FinOps 視角 |
| **B. 全本地（不留 Gemini）** | MCP Grounding / vision / 戰略敘事品質本地模型補不齊；繁中短板無解（phase0_research Section 1.4）|
| **C. 多供應商完全互通（任意 caller 任意 provider）** | 命名漂移無法治理；caller-provider 對應靠程式碼隱式約定，無 DB 護欄；token 報表 GROUP BY 失準 |
| **D. 線性 LLM 鏈（一條 fallback 鏈打天下）** | audit 已證明系統有 4 條獨立鏈（Ollama host / Hermes / NemoTron / OpenClaw Q&A / MCP），語意各異不可合併 |
| **E. 引入 LangChain / LiteLLM 統一抽象層** | 黑盒、難審計、增加依賴；既有 `ai_call_logger` + `resolve_ollama_host` 已具備統一語意，無需第三方 |

---

## Consequences

### 正面

1. **成本可控**：Gemini 鎖定 7 場景，月支出有上限；其餘 ~27 個 caller 全走 Ollama 邊際成本 = 0。
2. **遙測 100%**：A4 logger 接入後，`ai_calls` 覆蓋率從 11.8% 拉升至接近 100%，token / 成本 / 延遲 / 失敗率每日 23:55 自動入袋。
3. **fallback 行為可預測**：三主機級聯 + mark_unhealthy + HTTP probe 取代純 TCP 探測，process 卡死也能 5s 內切換。
4. **provider 命名治理**：DB CHECK constraint 鎖死 8 個 provider；caller 字典集中於程式碼，新增需 ADR。
5. **未來 Phase 4-12 有依據**：所有新 Agent / 新 caller 直接套用本 ADR 的五大支柱與白名單。

### 負面

1. **新 caller 引入有 ADR 摩擦**：每個新 LLM 呼叫點都要 update 本 ADR，但這是治理代價而非缺點。
2. **DB CHECK 變更需 migration**：provider 白名單擴增（如 Phase 9 加 `claude`）需新 migration + 滾動部署。
3. **Logger 額外延遲**：每個 LLM 呼叫多一層 fire-and-forget 寫入（測試顯示 < 5ms），但獨立 thread + dedicated session pool 可控。

### 風險與緩解

| 風險 | 機率 | 緩解 |
|---|---|---|
| Logger 失敗連鎖讓主流程崩 | 低 | `ai_call_logger` kill-switch 連續 10 次失敗自動關（Phase 1 已測試 52/52 pass）|
| caller typo 污染 token 報表 | 中 | Phase 5 後加格式 CHECK constraint；review 時 grep 比對白名單 |
| Gemini 配額耗盡（7 場景同時爆）| 低 | NIM `nvidia/llama-3.3-nemotron-super-49b-v1.5` 鏈 fallback；ADR-004 已定 |
| Primary 主機長期掛 | 低 | mark_unhealthy 30s + 三層級聯；最壞情況走 111 本地 |

---

## Verification（如何驗證已落地）

### V1：DB 層
```sql
-- provider CHECK 鎖定
SELECT pg_get_constraintdef(oid) FROM pg_constraint
WHERE conname = 'chk_ai_calls_provider';
-- 期望：CHECK (provider IN ('gcp_ollama','ollama_secondary','ollama_111','gemini','nim','nim_via_elephant','openrouter'))

-- 預算種子 10 筆
SELECT period, provider, budget_usd FROM ai_call_budgets ORDER BY period, provider NULLS FIRST;

-- 24h caller 分布（戰役 v5.0 上線後應 ≥ 13 個 distinct caller）
SELECT caller, COUNT(*) FROM ai_calls
WHERE called_at >= NOW() - INTERVAL '24h'
GROUP BY caller ORDER BY 2 DESC;
```

### V2：程式碼層
```bash
# 不應有寫死 IP
grep -rn "192.168.0.111" services/ routes/ | grep -v "OLLAMA_HOST_FALLBACK\|resolve_ollama_host\|test_"
grep -rn "34.143.170.20\|34.21.145.224" services/ routes/ | grep -v "OLLAMA_HOST_PRIMARY\|test_"

# 所有 LLM 呼叫應走 logger
grep -rn "ollama_service.generate\|google.generativeai\|openai.ChatCompletion" services/ routes/
# 每個 hit 上方應有 with log_ai_call(...) 或 @log_ai_call_decorator
```

### V3：每日 token report（每日 23:55 入 Telegram）
- Section 1 `ollama_pct` ≥ 80%（戰役 KPI）
- Section 5 「今日 Ollama Tokens vs 7 日均」應穩定，不應突然降為 0（M7 的 openclaw_bot_main 修復後）
- ai_insights `insight_type='daily_token_report'` 每日 1 筆

---

## Migration Plan

| Phase | 項目 | 狀態 | 文件 |
|---|---|---|---|
| 0 | LLM/MCP audit + 替代查證 | ✅ 完成 | `docs/phase0_audit_report_20260503.md` / `docs/phase0_research_report_20260503.md` |
| 1 | DB schema（024/025/026）+ ai_call_logger + token report | ✅ 完成（52/52 tests pass）| `docs/phase1_db_design_20260503.md` / `docs/phase1_critic_review_20260503.md` / `docs/phase1_final_critic_signoff_20260503.md` |
| 2 | resolve_ollama_host + HTTP probe + mark_unhealthy + AiderHeal/CodeReview lazy | ✅ 完成（13 + 43 tests pass）| `docs/phase2_deploy_verify_20260503.md` |
| 3 | OpenClaw Q&A 切 qwen3:14b（feature flag）| ✅ 完成（A7）| 待 Phase 4 黃金集 A/B |
| 4 | 黃金集 A/B 評測 + Hermes daily 摘要遷移（A8）| 規劃中 | — |
| 5 | Phase 5 報表上線 + caller CHECK NOT VALID | 規劃中 | — |
| 6 | 文件對齊（本 ADR + ADR-029 + ADR-027 附錄）| ✅ 完成 | 本檔 |
| 7-8 | OpenClaw 程式瘦身（A10）| 規劃中 | — |
| 9 | 預算守門 hard-stop + 多供應商實驗 | 規劃中 | — |
| 10 | MCP 5 顆 🟢 引入（postgres / omnisearch / filesystem / firecrawl / git）| 規劃中 | — |
| 11 | RAG 一致性護欄（embedding_signature 回填 + bge-m3 digest 鎖定）| 規劃中 | — |
| 12 | ai_usage_tracking deprecate + caller 集中 ADR refresh | 規劃中 | — |

---

## References

- ADR-027 Primary Ollama on GCP（戰役起點）
- ADR-029 Hermes-First 雙塔分工（本 ADR 支柱 3 展開）
- ADR-004 NemoTron 配額耗盡 fallback（白名單 NIM provider 來源）
- ADR-013 AIOps AutoHeal（aider_heal_executor 修補上下文）
- ADR-018 四 Agent 控制面（Hermes/NemoTron/OpenClaw/ElephantAlpha 角色定義）
- ADR-021 EA HITL pre-fetch（Gemini 場景 6 來源）
- `docs/phase0_audit_report_20260503.md`（34 呼叫點完整盤點）
- `docs/phase0_research_report_20260503.md`（Qwen / DeepSeek-R1 / Search API 三項紅綠燈）
- `docs/phase1_db_design_20260503.md`（DB schema 設計理由）
- `docs/phase1_final_critic_signoff_20260503.md`（H5/H6 caller / chat_id 護欄缺口）
- `docs/phase2_deploy_verify_20260503.md`（resolve_ollama_host / mark_unhealthy 落地驗證）
- 相關 memory：`reference_111_mac_ollama.md`、`reference_env_map.md`、`feedback_docs_vs_reality.md`
