feat(ai): Phase 24-A+B1 — AI Provider Registry + 絞殺者包裝 (ADR-052)
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:
@@ -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 Protocol,Provider 在 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
|
||||
|
||||
569
docs/adr/ADR-052-ai-provider-registry-dual-track-routing.md
Normal file
569
docs/adr/ADR-052-ai-provider-registry-dual-track-routing.md
Normal 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 Trace,Provider 寫 child Generation** | ADR-017 100% 追蹤全貌 |
|
||||
| **D6** | 代碼位置 | `apps/api/src/services/ai/` | 務實優先,8 個呼叫端都在 src/ 內 |
|
||||
| **D7** | 隱私強制 | **NIM = cloud,DIAGNOSE 強制 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 Trace,Provider 寫 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`
|
||||
Reference in New Issue
Block a user