feat(ai): Phase 24-A+B1 — AI Provider Registry + 絞殺者包裝 (ADR-052)
Some checks failed
E2E Health Check / e2e-health (push) Successful in 16s
CD Pipeline / build-and-deploy (push) Has been cancelled

Brain Layer 雙軌 Registry 架構:
- 新建 src/services/ai_providers/ 目錄 (interfaces + 4 providers)
  - OllamaProvider (local, rca/chat/code_review)
  - GeminiProvider (cloud, rca/chat)
  - ClaudeProvider (cloud, rca/chat/code_review)
  - OpenClawNemoProvider (cloud, rca — 委派 188→NIM)
- 擴展 ai_router.py 加入:
  - AIProviderRegistry (動態註冊/啟停)
  - AIRouterExecutor (Cache + 閘門 CB/RL/Sem + 執行)
- openclaw.py 絞殺者包裝: USE_AI_ROUTER=true 走新路徑
- config.py + ConfigMap 加入 USE_AI_ROUTER=false (安全預設)
- ADR-052 正式文件 (14 項決策 D1-D14)
- HARD_RULES v1.7 加入 AI Router 規範

安全: USE_AI_ROUTER=false 預設不啟用,需手動開啟觀察
回滾: kubectl set env deployment/awoooi-api USE_AI_ROUTER=false

2026-04-02 ogt: Phase 24 首批實作

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-02 13:16:09 +08:00
parent 1123eb4107
commit 73e8f8ab77
13 changed files with 1699 additions and 9 deletions

View File

@@ -8,11 +8,11 @@
| 欄位 | 值 |
|------|-----|
| **版本** | v1.6 |
| **版本** | v1.7 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
| **最後修改** | 2026-03-30 02:30 (台北) |
| **修改者** | Claude Code |
| **最後修改** | 2026-04-02 18:00 (台北) |
| **修改者** | Claude Code (Phase 24 AI Router 規範) |
### 變更紀錄
@@ -25,6 +25,7 @@
| v1.4 | 2026-03-25 | Claude Code | 新增 Change Annotation + 文件資訊區塊 |
| v1.5 | 2026-03-26 | Claude Code | 關聯紅區治理 (RED_ZONES.md) |
| v1.6 | 2026-03-30 | Claude Code | 🔴🔴🔴 前端內網 IP 禁令 (瀏覽器權限事故) |
| v1.7 | 2026-04-02 | Claude Code | Phase 24 AI Router 重構規範 (DI/隱私/絞殺者) |
---
@@ -48,6 +49,7 @@
| **時區** | **UTC/utcnow** | **台北時區 +8** | [→ Timezone Taipei](#timezone-taipei) |
| **變更追蹤** | **無註解** | **人事物+版本+台北時區** | [→ Change Annotation](#change-annotation) |
| **🔴🔴🔴 前端建置** | **內網 IP** | **公網域名** | [→ Frontend Internal IP](#frontend-internal-ip) |
| **AI Router** | **Router import 具體 Provider** | **只依賴 Protocol** | [→ OpenClaw](#openclaw) |
---
@@ -194,6 +196,24 @@ const { data } = useRealAPI()
**原因:** OpenClaw AI 是產品核心價值。
### Phase 24 AI Router 重構規範 (ADR-052, 2026-04-02)
```
❌ 禁止: 在 AIRouter (router.py) 中 import 具體 Provider 實作類別
✅ 正確: Router 只依賴 AIProvider ProtocolProvider 在 main.py DI 註冊
❌ 禁止: 將 ConsensusEngine (P0/P1) 納入 AIRouter
✅ 正確: ConsensusEngine 是獨立決策層,走 Claude Agent SDK
❌ 禁止: DIAGNOSE/CODE_REVIEW 意圖路由到 cloud Provider
✅ 正確: privacy_level="local" 強制本地 (零信任 ADR-023)
❌ 禁止: 遷移期間刪除 openclaw.py 舊 fallback chain
✅ 正確: USE_AI_ROUTER 絞殺者開關,新舊並存至 Phase B4
```
**Memory:** `project_phase24_ai_router.md`
---
## Git Safety

View File

@@ -0,0 +1,569 @@
# ADR-052: AI Provider Registry & Dual-Track Routing Architecture
> **狀態**: 📝 草案 (Pending Approval)
> **日期**: 2026-04-02
> **決策者**: 統帥 + 首席架構師
> **首席架構師評分**: 100% 技術背書
> **關聯 ADR**: ADR-006, ADR-015, ADR-017, ADR-023, ADR-024, ADR-036, ADR-044
> **Memory**: `project_phase24_ai_router.md`
---
## 背景
### 問題陳述
`openclaw.py` 已成長為 **1757 行的上帝類 (God Class)**,同時承擔 AI Provider 路由、快取管理、閘門控制、Langfuse 追蹤、SignOz 整合、結果解析、調優指令生成等 7+ 種職責。
AI Provider 的選擇與降級邏輯以 if/else hardcode 實作,無法即時切換:
```python
# 現狀 — openclaw.py:945-980
for provider in settings.AI_FALLBACK_ORDER:
if provider == "ollama":
response, success = await self._call_ollama(prompt)
elif provider == "gemini":
response, success, tokens, cost = await self._call_gemini(prompt)
elif provider == "nvidia":
...
elif provider == "claude":
response, success = await self._call_claude(prompt)
```
### 對比Action Layer 已有優雅解法
MCP Bridge (ADR-015) 已在 Action Layer 實作了 ProviderRegistry 模式:
```
Action Layer: MCP Bridge → ProviderRegistry → K8sProvider / SignOzProvider / DBProvider
Brain Layer: openclaw.py → if/else → _call_ollama / _call_gemini / _call_claude ← 問題所在
```
### 觸發原因
1. 統帥提問「OpenClaw / Ollama / Nemo 有沒有即時開關?」— 答案是**沒有**
2. 首席架構師評估Brain Layer 的 hardcoded dependencies 是系統成長期最大的技術債
3. 已驗證的 ProviderRegistry 模式可直接複製到 Brain Layer
---
## 決策
### 1. 雙軌 Registry 架構 (Dual-Registry Architecture)
將 MCP 已驗證的 ProviderRegistry 模式「向上」複製到 Brain Layer
```
[ Webhook / Telegram / Web ]
┌───────────────────────────┼───────────────────────────┐
│ BRAIN LAYER │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AIRouter (ai/router.py) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌───────┐ ┌─────┐ │ │
│ │ │OpenClaw│ │Gemini│ │Ollama│ │Nemotron│ │Claude│ │ │
│ │ │ Nemo │ │ │ │ │ │(Tool) │ │ │ │ │
│ │ └──────┘ └──────┘ └──────┘ └───────┘ └─────┘ │ │
│ │ ConfigMap ENABLE_* 開關 (熱切換) │ │
│ └─────────────────────────────────────────────────┘ │
├───────────────────────────────────────────────────────┤
│ ACTION LAYER │
│ ┌─────────────────────────────────────────────────┐ │
│ │ MCP Bridge (mcp/mcp_bridge.py) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │ │
│ │ │ K8s │ │SignOz│ │ DB │ │Filesystem│ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
```
### 2. OpenClawService 瘦身,不消滅
```
【遷移前】openclaw.py (1757 行上帝類)
┌─────────────────────────────────────────────────┐
│ SignOz 整合 → 保留 │
│ Prompt 組裝 → 保留 │
│ _call_ollama() → 抽出為 OllamaProvider │
│ _call_gemini() → 抽出為 GeminiProvider │
│ _call_claude() → 抽出為 ClaudeProvider │
│ _call_openclaw() → 抽出為 OpenClawNemoProvider │
│ _call_with_cache() → 移到 AIRouter │
│ _try_fallback_chain() → 移到 AIRouter │
│ Rate Limiter 整合 → 移到 AIRouter │
│ Circuit Breaker → 移到 AIRouter │
│ Langfuse Trace → 移到 AIRouter │
│ Mock Mode → 移到 AIRouter │
│ 結果解析 → 保留 │
│ Auto-tuning → 保留 │
│ ILLMProvider.call() → 保留 (內部改呼叫 Router) │
└─────────────────────────────────────────────────┘
【遷移後】
┌── openclaw.py (~800 行,業務編排) ──────────────┐
│ SignOz 整合 │
│ Prompt 組裝 │
│ 呼叫 AIRouter.route() ← 唯一改動點 │
│ 結果解析 + proposal_dict 組裝 │
│ Auto-tuning │
│ ILLMProvider.call() → 轉發給 AIRouter │
└──────────────────────────────────────────────────┘
┌── ai/router.py (~300 行) ────────────────────────┐
│ Cache 層 (Redis, 跨 Provider 共享) │
│ 閘門 (Circuit Breaker → Rate Limiter → Semaphore)│
│ Provider 選擇 + Fallback Chain │
│ Langfuse Trace (parent) │
│ Mock Mode 攔截 │
└──────────────────────────────────────────────────┘
┌── ai/providers/ (~60-100 行/each) ──────────────┐
│ ollama.py → _call_ollama 直接搬 │
│ gemini.py → _call_gemini 直接搬 │
│ claude.py → _call_claude 直接搬 │
│ openclaw_nemo.py → _call_openclaw_analyze 直接搬 │
│ nvidia_nemotron.py → 整合現有 nvidia_provider.py │
└──────────────────────────────────────────────────┘
```
### 3. 檔案結構
```
apps/api/src/services/ai/
├── __init__.py
├── interfaces.py # AIProvider Protocol + AIResult (類比 mcp/interfaces.py)
├── router.py # AIRouter + Cache + 閘門 + Fallback (類比 mcp/registry.py)
├── providers/
│ ├── __init__.py
│ ├── ollama.py # 從 openclaw.py _call_ollama 抽出
│ ├── gemini.py # 從 openclaw.py _call_gemini 抽出
│ ├── claude.py # 從 openclaw.py _call_claude 抽出
│ ├── openclaw_nemo.py # 從 openclaw.py _call_openclaw_analyze 抽出
│ └── nvidia_nemotron.py # 整合現有 nvidia_provider.py
└── health.py # AI Provider 健康檢查 (供 /health endpoint)
```
---
## 介面定義
### AIProvider Protocol
```python
from typing import Protocol, runtime_checkable, Any
from dataclasses import dataclass
@runtime_checkable
class AIProvider(Protocol):
"""AI Provider 標準介面 — 所有 LLM 引擎必須實作"""
@property
def name(self) -> str:
"""Provider 唯一名稱 (e.g., 'ollama', 'gemini', 'openclaw_nemo')"""
...
@property
def is_enabled(self) -> bool:
"""根據環境變數動態判斷是否啟用 (e.g., ENABLE_OLLAMA)"""
...
@property
def capabilities(self) -> set[str]:
"""支援的能力: {"rca", "tool_calling", "code_review", "chat"}"""
...
@property
def privacy_level(self) -> str:
"""'local' | 'cloud' — 決定是否可處理敏感資料 (ADR-023 零信任)"""
...
async def analyze(
self,
prompt: str,
context: dict[str, Any] | None = None,
trace_context: Any | None = None, # Langfuse Trace (ADR-017)
) -> "AIResult":
"""執行 AI 分析,回傳標準化結果"""
...
async def health_check(self) -> bool:
"""健康檢查 (供 /health endpoint)"""
...
@dataclass
class AIResult:
"""AI 分析標準化結果"""
raw_response: str
success: bool
provider: str
from_cache: bool = False
tokens: int = 0
cost_usd: float = 0.0
latency_ms: float = 0.0
```
### 與現有 ILLMProvider 的關係
```
ErrorAnalyzerService
→ ILLMProvider.call() ← 消費者介面 (不動)
→ OpenClawService.call() ← 業務編排 (保留)
→ AIRouter.route() ← 新增
→ AIProvider.analyze() ← 內部介面
```
兩個 Protocol 屬不同層次,共存不衝突:
- `ILLMProvider`: 消費者面向的簡化介面 (Service→Service)
- `AIProvider`: Router 內部的完整介面 (Router→Provider)
---
## 14 項架構決策
| # | 決策點 | 裁決 | 依據 |
|---|--------|------|------|
| **D1** | Intent/Complexity 整合 | Phase A 允許 None退化為靜態 Fallback | 首席架構師: 先接好神經突觸,再植入評分細胞 |
| **D2** | 雙 Fallback Chain (RCA vs Tool Calling) | 單 Router + `task_type` 參數 | 避免重複代碼 |
| **D3** | 閘門 (CB/RL/Sem) 歸屬 | **Router 統一管控** | Provider 保持純粹 (Stateless Compute Units) |
| **D4** | Cache 歸屬 | **Router 內部** | 跨 Provider 共享同一 Cache Key |
| **D5** | Langfuse Trace 歸屬 | **Router 建立 parent TraceProvider 寫 child Generation** | ADR-017 100% 追蹤全貌 |
| **D6** | 代碼位置 | `apps/api/src/services/ai/` | 務實優先8 個呼叫端都在 src/ 內 |
| **D7** | 隱私強制 | **NIM = cloudDIAGNOSE 強制 local** | 首席架構師: 零信任底線 |
| **D8** | 絞殺者開關 | `USE_AI_ROUTER` env var | 已驗證的 Strangler Fig 模式 (ADR-047) |
| **D9** | Phase C 動態控制 | Redis 優先env var fallback | 即時切換不重啟 Pod |
| **D10** | packages/ 遷移時機 | Phase C 之後 | 先穩定再跨 package |
| **D11** | OpenClawService 去留 | **瘦身為業務編排層,不消滅** | 8 個外部呼叫端 API 不變 |
| **D12** | ILLMProvider vs AIProvider | **兩個不同層次介面,共存** | 消費者介面 vs 內部介面 |
| **D13** | MOCK_MODE | **Router 層攔截** (不進閘門) | Mock 是跳過整個 AI 決策 |
| **D14** | ConsensusEngine (P0/P1) | **不納入 AIRouter** | 獨立的決策層邏輯 (Claude Agent SDK) |
---
## AIRouter 核心流程
```python
class AIRouter:
"""
Brain Layer 路由器 — 類比 MCP ProviderRegistry (ADR-015)
職責:
1. 動態啟用/停用 Provider (ConfigMap + Redis)
2. Cache 層保護算力
3. 閘門控制 (Circuit Breaker → Rate Limiter → Semaphore)
4. Primary → Fallback 降級鏈
5. Langfuse Trace 追蹤 (ADR-017)
"""
async def route(
self,
prompt: str,
task_type: str = "rca", # "rca" | "tool_calling" | "chat"
intent: str | None = None, # ADR-023 (Phase A: 可選)
complexity: int | None = None, # ADR-023 (Phase A: 可選)
context: dict | None = None,
require_local: bool = False, # 強制本地 Provider (隱私)
) -> AIResult:
# ① Mock Mode 攔截 (D13)
if settings.MOCK_MODE:
return AIResult(raw_response=self._mock(context), provider="mock", success=True)
# ② Cache 檢查 (D4)
cached = await self._cache.get(prompt, context)
if cached:
return cached
# ③ 建構 Provider 順序 (D1 + D2 + D7)
order = self._resolve_provider_order(task_type, intent, complexity, require_local)
# ④ Langfuse Trace (D5)
with langfuse_trace("ai_router_decision", metadata={...}) as trace:
# ⑤ 遍歷 Provider + 閘門 (D3)
for provider_name in order:
provider = self._providers.get(provider_name)
if not provider or not provider.is_enabled:
continue
# 閘門 1: Circuit Breaker
if self._guards[provider_name].is_circuit_open():
continue
# 閘門 2: Rate Limiter
allowed, reason = await self._rate_limiter.check_and_increment(provider_name)
if not allowed:
continue
# 閘門 3: Semaphore (並發控制)
async with self._semaphores[provider_name]:
try:
result = await provider.analyze(prompt, context, trace)
self._guards[provider_name].record_success()
await self._rate_limiter.record_cost(provider_name, result.cost_usd)
await self._cache.set(prompt, context, result)
return result
except Exception as e:
self._guards[provider_name].record_failure()
trace.event(f"{provider_name}_failed", metadata={"error": str(e)})
continue
raise AllProvidersFailedError(tried=order)
def _resolve_provider_order(self, task_type, intent, complexity, require_local):
"""
三層決策優先級:
1. 隱私強制 (ADR-023: DIAGNOSE/CODE_REVIEW → local only)
2. Complexity 路由 (ADR-023: 4→Gemini, 5→Claude)
3. Task-type Fallback Chain (ADR-006 + ADR-036)
"""
# 層 1: 隱私強制
if require_local or intent in ("DIAGNOSE", "CODE_REVIEW"):
return [n for n, p in self._providers.items() if p.privacy_level == "local"]
# 層 2: Complexity 路由 (Phase A: 可選)
if complexity is not None and complexity >= 4:
return self._complexity_routing.get(complexity, settings.AI_FALLBACK_ORDER)
# 層 3: Task-type Fallback
if task_type == "tool_calling":
return settings.TOOL_CALLING_FALLBACK_ORDER # ["nemotron","gemini","claude"]
return settings.AI_FALLBACK_ORDER # ["openclaw_nemo","gemini","ollama","claude"]
```
---
## ConfigMap 開關設計
```yaml
# k8s/awoooi-prod/04-configmap.yaml
# ============ Brain Layer: AI Provider 開關 ============
ENABLE_OPENCLAW_NEMO: "true" # OpenClaw 委派 (188→NIM)
ENABLE_OLLAMA: "true" # Ollama 本地 CPU
ENABLE_GEMINI: "true" # Gemini 雲端
ENABLE_CLAUDE: "false" # Claude (昂貴,僅緊急)
ENABLE_NEMOTRON: "true" # Nemotron Tool Calling
# 路由策略
AI_PRIMARY_PROVIDER: "openclaw_nemo"
AI_FALLBACK_ORDER: '["openclaw_nemo","gemini","ollama","claude"]'
TOOL_CALLING_FALLBACK_ORDER: '["nemotron","gemini","claude"]'
# 絞殺者開關 (Phase B1-B2)
USE_AI_ROUTER: "false" # false=舊路徑, true=新 AIRouter
# ============ Action Layer: MCP 開關 (不動) ============
# (保持現有 MCP 架構不動)
```
**即時切換指令:**
```bash
# 關閉 Gemini (省錢)
kubectl set env deployment/awoooi-api -n awoooi-prod ENABLE_GEMINI=false
# 切換主要 Provider
kubectl set env deployment/awoooi-api -n awoooi-prod AI_PRIMARY_PROVIDER=ollama
# 啟用新 AIRouter
kubectl set env deployment/awoooi-api -n awoooi-prod USE_AI_ROUTER=true
```
---
## 絞殺者遷移計畫
嚴格遵循 ADR-047 已驗證的四階段模式。
### Phase A: 基礎建設 (4h)
| 步驟 | 內容 | 時間 |
|------|------|------|
| A1 | `interfaces.py` — AIProvider Protocol + AIResult | 30m |
| A2 | `router.py` — AIRouter (Cache + 閘門 + 路由 + Trace) | 2h |
| A3 | `providers/openclaw_nemo.py` — 從 _call_openclaw_analyze 搬出 | 30m |
| A4 | `providers/gemini.py` — 從 _call_gemini 搬出 | 30m |
| A5 | `providers/ollama.py` — 從 _call_ollama 搬出 | 30m |
### Phase B1: 絞殺者包裝 (2h)
```python
# openclaw.py — 絞殺者分支
async def _call_with_fallback(self, prompt, ...):
if settings.USE_AI_ROUTER:
# 新路徑: AIRouter 統一管理
from src.services.ai.router import get_ai_router
router = get_ai_router()
result = await router.route(prompt, context=alert_context)
return result.raw_response, result.provider, result.success, result.tokens, result.cost_usd
else:
# 舊路徑: 原有 if/else chain (不動)
for provider in settings.AI_FALLBACK_ORDER:
...
```
### Phase B2: 觀察期 (48h)
| 檢查項 | 工具 | 通過條件 |
|--------|------|---------|
| Langfuse Trace 完整 | Langfuse UI | 新舊路徑 Trace 結構一致 |
| Telegram 通知正確 | Telegram | Provider 名稱 + 信心度正確 |
| 錯誤率無增加 | SignOz | Error rate ≤ 舊版 |
| 延遲無惡化 | SignOz | P99 latency ≤ 舊版 +10% |
**回滾條件:** 任一項不通過 → `kubectl set env ... USE_AI_ROUTER=false`
### Phase B3: 剩餘 Provider (3h)
| 步驟 | 內容 |
|------|------|
| B3.1 | `providers/claude.py` — 從 _call_claude 搬出 |
| B3.2 | `providers/nvidia_nemotron.py` — 整合 nvidia_provider.py |
| B3.3 | `health.py` — AI Provider 健康檢查 |
### Phase B4: 封存舊代碼 (1h)
```bash
# 移除 openclaw.py 中的 _call_ollama/_call_gemini/_call_claude
# 移除 _call_with_cache 中的舊 fallback chain
# 驗證瘦身至 ~800 行
```
### Phase C: 動態控制面板 (4h)
| 步驟 | 內容 |
|------|------|
| C1 | Redis-backed `_is_provider_enabled()` — Redis 優先env fallback |
| C2 | Telegram `/ai` command handler (status/enable/disable/primary/cost) |
| C3 | COMMANDER_CHAT_ID 白名單權限控制 |
```
/ai status → 顯示所有 Provider 狀態 + 健康 + 費用
/ai enable X → 啟用 Provider (寫 Redis)
/ai disable X → 停用 Provider (寫 Redis)
/ai primary X → 切換主要 Provider
/ai cost → 本月 Token/費用統計
```
**權限:**`COMMANDER_CHAT_ID` 白名單可操作 (Tier 3 操作)
---
## 閘門執行順序
```
請求進來
├─① MOCK_MODE? ──→ 直接回傳 (不進閘門)
├─② Cache Hit? ──→ 回傳快取 (記錄 provider="cache")
├─③ 建構 Provider 順序 (隱私 → Complexity → Fallback)
└─④ 遍歷 Provider:
├─ Circuit Breaker — is_open()? → skip (0ms)
├─ Rate Limiter — check_and_increment()? → skip (~1ms Redis)
├─ Semaphore — 並發上限 → 排隊等待
├─ provider.analyze() — 實際 AI 呼叫 (100ms-45s)
├─ 成功 → record_success + record_cost + write_cache → 回傳
└─ 失敗 → record_failure → 繼續下一個 Provider
```
---
## 隱私規則 (零信任)
| Provider | privacy_level | 可處理 DIAGNOSE? | 可處理 CODE_REVIEW? |
|----------|--------------|-----------------|-------------------|
| Ollama | `local` | ✅ | ✅ |
| OpenClaw Nemo (NIM) | `cloud` | ❌ | ❌ |
| Gemini | `cloud` | ❌ | ❌ |
| Claude | `cloud` | ❌ | ❌ |
| Nemotron | `cloud` | ❌ | ❌ |
> **首席架構師裁示 (Q2):** NVIDIA NIM 即使有企業協議,在架構定義上仍屬 cloud。
> 未來若在內網自建 NIM 伺服器,只需將其 privacy_level 改為 local。
---
## 與現有 ADR 的關係
| ADR | 關係 | 整合方式 |
|-----|------|---------|
| **ADR-006** | AI Fallback Strategy | AIRouter 取代 if/else chain保留 Fallback 順序概念 |
| **ADR-015** | MCP 模組化架構 | **直接複製** ProviderRegistry 模式到 Brain Layer |
| **ADR-017** | LLMOps Observability | Router 建立 Langfuse parent TraceProvider 寫 child |
| **ADR-023** | Smart Routing | Intent/Complexity 保留為可選參數Phase A 退化為靜態 |
| **ADR-024** | API Layer Architecture | AIRouter 屬 Service Layer符合四層架構 |
| **ADR-036** | Nemotron Tool Calling | 獨立 Fallback Chain via `task_type="tool_calling"` |
| **ADR-044** | OpenClaw+Nemotron 協作 | Phase 22 Telegram 雙軌不受影響 |
| **ADR-047** | Phase R 絞殺者審查 | 嚴格遵循已驗證的 Strangler Fig 四階段 |
---
## 不受影響的路徑
以下路徑**完全不納入 AIRouter**,保持獨立:
| 路徑 | 原因 |
|------|------|
| **ConsensusEngine** (P0/P1) | 多 Agent 共識 ≠ 單 LLM 路由 (Claude Agent SDK) |
| **OpenClaw 委派** (188→NIM) | 這是一個 Provider不是路由邏輯 |
| **MCP Bridge** (Action Layer) | 完全不同的層,已有自己的 Registry |
---
## 風險與緩解
| 風險 | 嚴重度 | 緩解措施 |
|------|--------|---------|
| 遷移期間 AI 仲裁中斷 | 高 | `USE_AI_ROUTER` 絞殺者開關1 秒回滾 |
| Cache Key 不相容 | 中 | 新舊路徑共用相同 `llm_cache:{hash}` 格式 |
| Provider 介面設計不完整 | 低 | 先只支援 `analyze()`Tool Calling 後續擴展 |
| 測試不足 | 低 | B2 觀察期 48h + Langfuse 100% Trace |
| openclaw.py 改壞 | 高 | B4 前完全不刪舊代碼,只新增 |
---
## 工時評估
| Phase | 內容 | 預估 |
|-------|------|------|
| A | Interface + AIRouter + 3 Provider | 4h |
| B1 | 絞殺者包裝 | 2h |
| B2 | 觀察期 | 48h |
| B3 | 剩餘 2 Provider + Health | 3h |
| B4 | 封存舊代碼 | 1h |
| C | Telegram/Redis 動態控制 | 4h |
| **總計** | | **~14h + 48h 觀察** |
---
## 驗收標準
| # | 項目 | 驗證方式 |
|---|------|---------|
| 1 | AIRouter 正確路由到 Primary Provider | Langfuse Trace 確認 |
| 2 | Fallback 降級正常 (Primary 失敗 → 次選) | 手動停 Ollama 驗證 |
| 3 | `ENABLE_*=false` 即時停用 Provider | kubectl set env 驗證 |
| 4 | DIAGNOSE Intent 強制走 local | Langfuse 不出現 cloud Provider |
| 5 | Cache 命中率不低於舊版 | Redis MONITOR 比對 |
| 6 | Telegram 顯示正確 Provider 名稱 | 實際告警驗證 |
| 7 | `USE_AI_ROUTER=false` 回滾正常 | 切換後服務不中斷 |
| 8 | Phase C: `/ai status` 回覆正確 | Telegram 驗證 |
---
## 參考
- ADR-015: MCP 模組化架構 (ProviderRegistry 模板)
- ADR-047: Phase R 絞殺者審查 (遷移驗證經驗)
- Memory: `project_phase24_ai_router.md`
- Memory: `feedback_strangler_fig_pattern.md`
- Memory: `feedback_lewooogo_modular_enforcement.md`