594 lines
22 KiB
Python
594 lines
22 KiB
Python
"""
|
||
AI Router - Phase 13.3 #87
|
||
==========================
|
||
智能 AI 路由器,根據意圖和複雜度動態選擇 AI Provider
|
||
|
||
目標: 根據請求特性自動選擇最適模型
|
||
策略: Intent Classifier + Complexity Scorer → Routing Decision
|
||
延遲目標: < 50ms (規則引擎優先)
|
||
|
||
路由決策矩陣 (ADR-023):
|
||
┌─────────────────┬───────────────┬──────────────────────────────┐
|
||
│ 複雜度 + 風險 │ Provider │ 備註 │
|
||
├─────────────────┼───────────────┼──────────────────────────────┤
|
||
│ 1-2 + LOW │ Ollama │ 快速本地處理 │
|
||
│ 3 + MEDIUM │ Ollama │ fallback → Gemini │
|
||
│ 4-5 + HIGH │ Gemini │ fallback → Claude │
|
||
│ DELETE/CRITICAL │ Claude │ 強制使用最強模型 │
|
||
└─────────────────┴───────────────┴──────────────────────────────┘
|
||
|
||
版本: v3.0
|
||
建立: 2026-03-26 (台北時區)
|
||
建立者: Claude Code
|
||
最後修改: 2026-03-26 (台北時區)
|
||
修改者: Claude Code
|
||
|
||
變更紀錄:
|
||
| 版本 | 日期 | 執行者 | 變更內容 |
|
||
|------|------|--------|----------|
|
||
| v1.0 | 2026-03-26 | Claude Code | 初始實作 |
|
||
| v2.0 | 2026-03-26 | Claude Code | 支援 IntentResult + 新意圖類型 |
|
||
| v3.0 | 2026-03-26 | Claude Code | Phase 13.3 #87 完整路由決策矩陣 |
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from dataclasses import dataclass, field
|
||
from enum import Enum
|
||
|
||
import structlog
|
||
|
||
from src.services.complexity_scorer import (
|
||
ComplexityScore,
|
||
get_complexity_scorer,
|
||
)
|
||
from src.services.intent_classifier import (
|
||
IntentResult,
|
||
IntentType,
|
||
RiskLevel,
|
||
get_intent_classifier,
|
||
normalize_intent,
|
||
)
|
||
from src.services.model_registry import get_model_registry
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
|
||
# =============================================================================
|
||
# Provider 定義
|
||
# =============================================================================
|
||
|
||
|
||
class AIProvider(Enum):
|
||
"""AI 提供者"""
|
||
|
||
OLLAMA = "ollama"
|
||
GEMINI = "gemini"
|
||
CLAUDE = "claude"
|
||
# 2026-03-29 ogt: ADR-036 Nemotron Tool Calling (83.3% 精準度)
|
||
NVIDIA = "nvidia"
|
||
|
||
|
||
# Provider 對應延遲預算 (ms)
|
||
PROVIDER_LATENCY_BUDGET: dict[AIProvider, int] = {
|
||
AIProvider.OLLAMA: 60000, # 本地,允許較長處理時間
|
||
AIProvider.GEMINI: 30000, # 雲端,較低延遲
|
||
AIProvider.CLAUDE: 30000, # 雲端,較低延遲
|
||
# 2026-03-29 ogt: ADR-036 Nemotron Tool Calling (延遲 11-45s)
|
||
AIProvider.NVIDIA: 60000, # Tool Calling 專用,允許較長時間
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class RoutingDecision:
|
||
"""
|
||
路由決策結果 (Phase 13.3 #87)
|
||
|
||
包含完整的路由資訊,供 OpenClaw 主流程使用
|
||
"""
|
||
|
||
# 核心決策
|
||
selected_provider: AIProvider # 選擇的 AI Provider
|
||
selected_model: str # 選擇的模型名稱
|
||
fallback_chain: list[tuple[AIProvider, str]] # 備援鏈 [(provider, model), ...]
|
||
routing_reason: str # 路由決策原因
|
||
latency_budget_ms: int # 延遲預算 (毫秒)
|
||
|
||
# 分類結果
|
||
intent: IntentType # 意圖分類 (正規化後)
|
||
intent_result: IntentResult # 完整 Intent 分類結果
|
||
complexity: ComplexityScore # 複雜度評分
|
||
risk_level: RiskLevel = field(default=RiskLevel.MEDIUM) # 風險等級
|
||
|
||
# 路由 metadata
|
||
routing_latency_ms: float = 0.0 # 路由決策耗時 (ms)
|
||
|
||
# 向後相容 (deprecated)
|
||
model: str = "" # -> selected_model
|
||
reason: str = "" # -> routing_reason
|
||
fallback_models: list[str] = field(default_factory=list) # -> fallback_chain
|
||
|
||
def __post_init__(self):
|
||
"""初始化後設定衍生欄位"""
|
||
self.risk_level = self.intent_result.risk_level
|
||
# 向後相容
|
||
self.model = self.selected_model
|
||
self.reason = self.routing_reason
|
||
self.fallback_models = [model for _, model in self.fallback_chain]
|
||
|
||
def to_dict(self) -> dict:
|
||
"""轉換為字典 (API 回應用)"""
|
||
return {
|
||
"selected_provider": self.selected_provider.value,
|
||
"selected_model": self.selected_model,
|
||
"fallback_chain": [
|
||
{"provider": p.value, "model": m} for p, m in self.fallback_chain
|
||
],
|
||
"routing_reason": self.routing_reason,
|
||
"latency_budget_ms": self.latency_budget_ms,
|
||
"intent": self.intent.value,
|
||
"risk_level": self.risk_level.value,
|
||
"complexity_score": self.complexity.score,
|
||
"routing_latency_ms": round(self.routing_latency_ms, 2),
|
||
}
|
||
|
||
|
||
class AIRouter:
|
||
"""
|
||
AI 路由器 (Phase 13.3 #87)
|
||
|
||
整合 IntentClassifier 和 ComplexityScorer,
|
||
動態選擇最適合的 AI Provider 和模型。
|
||
|
||
路由決策矩陣:
|
||
┌─────────────────┬───────────────┬──────────────────────────────┐
|
||
│ 複雜度 + 風險 │ Provider │ 備註 │
|
||
├─────────────────┼───────────────┼──────────────────────────────┤
|
||
│ 1-2 + LOW │ Ollama │ 快速本地處理 │
|
||
│ 3 + MEDIUM │ Ollama │ fallback → Gemini │
|
||
│ 4-5 + HIGH │ Gemini │ fallback → Claude │
|
||
│ DELETE/CRITICAL │ Claude │ 強制使用最強模型 │
|
||
└─────────────────┴───────────────┴──────────────────────────────┘
|
||
|
||
路由策略 (按優先級):
|
||
1. CRITICAL 風險強制使用 Claude
|
||
2. DELETE 意圖強制使用 Claude
|
||
3. HIGH 風險或複雜度 4-5 → Gemini
|
||
4. 其他情況 → Ollama (成本優先)
|
||
"""
|
||
|
||
def __init__(self):
|
||
self._intent_classifier = get_intent_classifier()
|
||
self._complexity_scorer = get_complexity_scorer()
|
||
self._model_registry = get_model_registry()
|
||
|
||
# 從 ModelRegistry 取得模型配置
|
||
self._ollama_default = self._model_registry.get_model("ollama", "default")
|
||
self._ollama_summary = self._model_registry.get_model("ollama", "summary")
|
||
self._gemini_default = self._model_registry.get_model("gemini", "default")
|
||
self._claude_default = self._model_registry.get_model("claude", "default")
|
||
# 2026-03-29 ogt: ADR-036 Nemotron Tool Calling
|
||
self._nvidia_default = self._model_registry.get_model("nvidia", "default")
|
||
|
||
# Provider 對應模型映射
|
||
self._provider_models: dict[AIProvider, str] = {
|
||
AIProvider.OLLAMA: self._ollama_default,
|
||
AIProvider.GEMINI: self._gemini_default,
|
||
AIProvider.CLAUDE: self._claude_default,
|
||
AIProvider.NVIDIA: self._nvidia_default, # ADR-036
|
||
}
|
||
|
||
# 完整 Fallback 鏈 (Provider, Model)
|
||
# 2026-03-30 ogt: NVIDIA 成為首選仲裁,加入 Fallback 鏈首位
|
||
self._full_fallback_chain: list[tuple[AIProvider, str]] = [
|
||
(AIProvider.NVIDIA, self._nvidia_default),
|
||
(AIProvider.GEMINI, self._gemini_default),
|
||
(AIProvider.CLAUDE, self._claude_default),
|
||
(AIProvider.OLLAMA, self._ollama_default),
|
||
]
|
||
|
||
# Tool Calling 專用 Fallback 鏈 (ADR-036)
|
||
self._tool_calling_fallback_chain: list[tuple[AIProvider, str]] = [
|
||
(AIProvider.NVIDIA, self._nvidia_default),
|
||
(AIProvider.GEMINI, self._gemini_default),
|
||
(AIProvider.CLAUDE, self._claude_default),
|
||
]
|
||
|
||
# 意圖對應 Provider 強制覆寫 (None = 依複雜度決定)
|
||
self._intent_provider_overrides: dict[IntentType, AIProvider | None] = {
|
||
# 四大核心意圖
|
||
IntentType.RESTART: None, # 依複雜度
|
||
IntentType.SCALE: None, # 依複雜度
|
||
IntentType.CONFIG: None, # 依複雜度 (但 HIGH 會升級)
|
||
IntentType.DIAGNOSE: AIProvider.OLLAMA, # 診斷優先本地 (隱私)
|
||
# 輔助意圖
|
||
IntentType.DELETE: AIProvider.CLAUDE, # CRITICAL → 強制 Claude
|
||
IntentType.ROLLBACK: None, # 依複雜度
|
||
IntentType.UNKNOWN: None,
|
||
# 舊版兼容
|
||
IntentType.CODE_REVIEW: None,
|
||
IntentType.DEPLOYMENT: None,
|
||
IntentType.ALERT_TRIAGE: AIProvider.OLLAMA,
|
||
IntentType.QUERY: AIProvider.OLLAMA,
|
||
IntentType.MAINTENANCE: None,
|
||
}
|
||
|
||
# 向後相容
|
||
self._default_model = self._ollama_default
|
||
self._summary_model = self._ollama_summary
|
||
self._fallback_order = [
|
||
self._ollama_default,
|
||
self._ollama_summary,
|
||
"gemini",
|
||
"claude",
|
||
]
|
||
|
||
async def route(
|
||
self,
|
||
text: str,
|
||
context: dict | None = None,
|
||
) -> RoutingDecision:
|
||
"""
|
||
路由請求到最適 AI Provider 和模型
|
||
|
||
延遲目標: < 50ms (規則引擎優先,LLM 分類時可能稍長)
|
||
|
||
Args:
|
||
text: 用戶輸入或告警內容
|
||
context: 額外上下文 (服務、指標等)
|
||
|
||
Returns:
|
||
RoutingDecision: 完整路由決策
|
||
"""
|
||
start_time = time.perf_counter()
|
||
context = context or {}
|
||
|
||
# Step 1: 意圖分類 (返回 IntentResult, 規則引擎 < 10ms)
|
||
intent_result = await self._intent_classifier.classify(text)
|
||
intent = normalize_intent(intent_result.intent)
|
||
|
||
# Step 2: 複雜度評分 (< 10ms)
|
||
complexity = self._complexity_scorer.score(context)
|
||
|
||
# Step 3: Provider + Model 選擇 (< 1ms)
|
||
provider, model, reason = self._select_provider_and_model(
|
||
intent, intent_result, complexity
|
||
)
|
||
|
||
# Step 4: 建立 Fallback 鏈
|
||
fallback_chain = self._build_fallback_chain(provider)
|
||
|
||
# Step 5: 計算延遲預算
|
||
latency_budget = PROVIDER_LATENCY_BUDGET.get(provider, 30000)
|
||
|
||
# 計算路由決策耗時
|
||
routing_latency = (time.perf_counter() - start_time) * 1000
|
||
|
||
decision = RoutingDecision(
|
||
selected_provider=provider,
|
||
selected_model=model,
|
||
fallback_chain=fallback_chain,
|
||
routing_reason=reason,
|
||
latency_budget_ms=latency_budget,
|
||
intent=intent,
|
||
intent_result=intent_result,
|
||
complexity=complexity,
|
||
routing_latency_ms=routing_latency,
|
||
)
|
||
|
||
logger.info(
|
||
"ai_routing_decision",
|
||
provider=provider.value,
|
||
model=model,
|
||
intent=intent.value,
|
||
intent_confidence=intent_result.confidence,
|
||
risk_level=intent_result.risk_level.value,
|
||
complexity_score=complexity.score,
|
||
reason=reason,
|
||
latency_budget_ms=latency_budget,
|
||
routing_latency_ms=round(routing_latency, 2),
|
||
fallback_count=len(fallback_chain),
|
||
)
|
||
|
||
return decision
|
||
|
||
def _select_provider_and_model(
|
||
self,
|
||
intent: IntentType,
|
||
intent_result: IntentResult,
|
||
complexity: ComplexityScore,
|
||
) -> tuple[AIProvider, str, str]:
|
||
"""
|
||
選擇 Provider 和模型 (Phase 13.3 #87 核心邏輯)
|
||
|
||
路由決策矩陣:
|
||
┌─────────────────┬───────────────┬──────────────────────────────┐
|
||
│ 複雜度 + 風險 │ Provider │ 備註 │
|
||
├─────────────────┼───────────────┼──────────────────────────────┤
|
||
│ 1-2 + LOW │ Ollama │ 快速本地處理 │
|
||
│ 3 + MEDIUM │ Ollama │ fallback → Gemini │
|
||
│ 4-5 + HIGH │ Gemini │ fallback → Claude │
|
||
│ DELETE/CRITICAL │ Claude │ 強制使用最強模型 │
|
||
└─────────────────┴───────────────┴──────────────────────────────┘
|
||
|
||
Args:
|
||
intent: 正規化後的意圖
|
||
intent_result: 完整分類結果
|
||
complexity: 複雜度評分
|
||
|
||
Returns:
|
||
(provider, model, reason)
|
||
"""
|
||
risk = intent_result.risk_level
|
||
score = complexity.score
|
||
|
||
# =======================================================================
|
||
# 規則 1: CRITICAL 風險強制 Claude (最高優先級)
|
||
# =======================================================================
|
||
if risk == RiskLevel.CRITICAL:
|
||
provider = AIProvider.CLAUDE
|
||
model = self._claude_default
|
||
reason = f"CRITICAL 風險 ({intent.value}) 強制使用 Claude"
|
||
return provider, model, reason
|
||
|
||
# =======================================================================
|
||
# 規則 2: DELETE 意圖強制 Claude (不可逆操作)
|
||
# =======================================================================
|
||
if intent == IntentType.DELETE:
|
||
provider = AIProvider.CLAUDE
|
||
model = self._claude_default
|
||
reason = "DELETE 意圖 (不可逆) 強制使用 Claude"
|
||
return provider, model, reason
|
||
|
||
# =======================================================================
|
||
# 規則 3: 檢查意圖強制覆寫
|
||
# =======================================================================
|
||
provider_override = self._intent_provider_overrides.get(intent)
|
||
if provider_override is not None:
|
||
provider = provider_override
|
||
model = self._provider_models[provider]
|
||
reason = f"意圖 {intent.value} 指定使用 {provider.value}"
|
||
return provider, model, reason
|
||
|
||
# =======================================================================
|
||
# 規則 4: 複雜度 4-5 或 HIGH 風險 → Nvidia Nemotron
|
||
# =======================================================================
|
||
if score >= 4 or risk == RiskLevel.HIGH:
|
||
provider = AIProvider.NVIDIA
|
||
model = self._nvidia_default
|
||
reason = f"複雜度={score}/5, 風險={risk.value} → Nvidia (fallback Gemini)"
|
||
return provider, model, reason
|
||
|
||
# =======================================================================
|
||
# 規則 5: 複雜度 3 + MEDIUM → Ollama (fallback Gemini)
|
||
# =======================================================================
|
||
if score == 3:
|
||
provider = AIProvider.OLLAMA
|
||
model = self._ollama_default
|
||
reason = f"複雜度={score}/5, 風險={risk.value} → Ollama (fallback Gemini)"
|
||
return provider, model, reason
|
||
|
||
# =======================================================================
|
||
# 規則 6: 複雜度 1-2 + LOW/MEDIUM → Ollama (快速本地處理)
|
||
# =======================================================================
|
||
provider = AIProvider.OLLAMA
|
||
# 低複雜度使用輕量模型 (更快回應)
|
||
model = self._ollama_summary if score <= 1 else self._ollama_default
|
||
reason = f"複雜度={score}/5, 風險={risk.value} → Ollama (成本優先)"
|
||
return provider, model, reason
|
||
|
||
def _select_model(
|
||
self,
|
||
intent: IntentType,
|
||
intent_result: IntentResult,
|
||
complexity: ComplexityScore,
|
||
) -> tuple[str, str]:
|
||
"""
|
||
選擇模型 (向後相容方法)
|
||
|
||
Deprecated: 請使用 _select_provider_and_model
|
||
|
||
Args:
|
||
intent: 正規化後的意圖
|
||
intent_result: 完整分類結果
|
||
complexity: 複雜度評分
|
||
|
||
Returns:
|
||
(model_name, reason)
|
||
"""
|
||
_, model, reason = self._select_provider_and_model(
|
||
intent, intent_result, complexity
|
||
)
|
||
return model, reason
|
||
|
||
def _build_fallback_chain(
|
||
self, selected_provider: AIProvider
|
||
) -> list[tuple[AIProvider, str]]:
|
||
"""
|
||
建立 Fallback 鏈 (排除已選 Provider)
|
||
|
||
Fallback 順序: Ollama → Gemini → Claude
|
||
|
||
Args:
|
||
selected_provider: 已選擇的 Provider
|
||
|
||
Returns:
|
||
Fallback 鏈 [(provider, model), ...]
|
||
"""
|
||
fallback_chain: list[tuple[AIProvider, str]] = []
|
||
|
||
for provider, model in self._full_fallback_chain:
|
||
if provider != selected_provider:
|
||
fallback_chain.append((provider, model))
|
||
|
||
return fallback_chain
|
||
|
||
def _build_fallback_list(self, selected_model: str) -> list[str]:
|
||
"""建立 Fallback 列表 (向後相容)"""
|
||
fallbacks = [m for m in self._fallback_order if m != selected_model]
|
||
return fallbacks
|
||
|
||
def route_sync(
|
||
self,
|
||
text: str,
|
||
context: dict | None = None,
|
||
) -> RoutingDecision:
|
||
"""
|
||
同步版本路由 (僅關鍵字匹配,保證 < 50ms)
|
||
|
||
適用場景: 需要快速決策,不需要 LLM 分類的情況
|
||
|
||
Args:
|
||
text: 用戶輸入或告警內容
|
||
context: 額外上下文
|
||
|
||
Returns:
|
||
RoutingDecision: 路由決策
|
||
"""
|
||
start_time = time.perf_counter()
|
||
context = context or {}
|
||
|
||
# 同步分類 (僅規則引擎, < 10ms)
|
||
intent_result = self._intent_classifier.classify_sync(text)
|
||
intent = normalize_intent(intent_result.intent)
|
||
|
||
# 複雜度評分 (< 10ms)
|
||
complexity = self._complexity_scorer.score(context)
|
||
|
||
# Provider + Model 選擇
|
||
provider, model, reason = self._select_provider_and_model(
|
||
intent, intent_result, complexity
|
||
)
|
||
|
||
# 建立 Fallback 鏈
|
||
fallback_chain = self._build_fallback_chain(provider)
|
||
|
||
# 延遲預算
|
||
latency_budget = PROVIDER_LATENCY_BUDGET.get(provider, 30000)
|
||
|
||
# 計算路由決策耗時
|
||
routing_latency = (time.perf_counter() - start_time) * 1000
|
||
|
||
return RoutingDecision(
|
||
selected_provider=provider,
|
||
selected_model=model,
|
||
fallback_chain=fallback_chain,
|
||
routing_reason=reason,
|
||
latency_budget_ms=latency_budget,
|
||
intent=intent,
|
||
intent_result=intent_result,
|
||
complexity=complexity,
|
||
routing_latency_ms=routing_latency,
|
||
)
|
||
|
||
# =========================================================================
|
||
# Tool Calling 路由 (ADR-036)
|
||
# =========================================================================
|
||
|
||
def route_tool_calling(self) -> tuple[AIProvider, str, list[tuple[AIProvider, str]]]:
|
||
"""
|
||
Tool Calling 專用路由 (ADR-036)
|
||
|
||
Tool Calling 任務優先使用 Nemotron (83.3% 精準度),
|
||
Fallback 到 Gemini/Claude。
|
||
|
||
Returns:
|
||
(provider, model, fallback_chain)
|
||
"""
|
||
provider = AIProvider.NVIDIA
|
||
model = self._nvidia_default
|
||
fallback_chain = [
|
||
(p, m) for p, m in self._tool_calling_fallback_chain if p != provider
|
||
]
|
||
|
||
logger.info(
|
||
"tool_calling_routing",
|
||
provider=provider.value,
|
||
model=model,
|
||
fallback_count=len(fallback_chain),
|
||
)
|
||
|
||
return provider, model, fallback_chain
|
||
|
||
def get_tool_calling_fallback_chain(self) -> list[tuple[AIProvider, str]]:
|
||
"""取得 Tool Calling Fallback 鏈"""
|
||
return self._tool_calling_fallback_chain.copy()
|
||
|
||
# =========================================================================
|
||
# 便捷方法
|
||
# =========================================================================
|
||
|
||
def get_provider_for_intent(self, intent: IntentType) -> AIProvider:
|
||
"""取得意圖對應的 Provider (不考慮複雜度)"""
|
||
override = self._intent_provider_overrides.get(intent)
|
||
return override if override else AIProvider.OLLAMA
|
||
|
||
def get_model_for_provider(self, provider: AIProvider) -> str:
|
||
"""取得 Provider 對應的模型"""
|
||
return self._provider_models.get(provider, self._ollama_default)
|
||
|
||
def get_routing_matrix(self) -> list[dict]:
|
||
"""
|
||
取得路由決策矩陣 (用於 API 文檔或除錯)
|
||
|
||
Returns:
|
||
路由規則清單
|
||
"""
|
||
return [
|
||
{
|
||
"rule": 1,
|
||
"condition": "CRITICAL risk",
|
||
"provider": "claude",
|
||
"reason": "不可逆/高風險操作強制最強模型",
|
||
},
|
||
{
|
||
"rule": 2,
|
||
"condition": "DELETE intent",
|
||
"provider": "claude",
|
||
"reason": "刪除操作強制最強模型",
|
||
},
|
||
{
|
||
"rule": 3,
|
||
"condition": "Intent override",
|
||
"provider": "depends",
|
||
"reason": "特定意圖有預設 Provider",
|
||
},
|
||
{
|
||
"rule": 4,
|
||
"condition": "complexity >= 4 OR HIGH risk",
|
||
"provider": "nvidia",
|
||
"reason": "高複雜度需要 Nvidia Nemotron 強大推理能力",
|
||
},
|
||
{
|
||
"rule": 5,
|
||
"condition": "complexity == 3",
|
||
"provider": "ollama",
|
||
"reason": "中等複雜度本地處理",
|
||
},
|
||
{
|
||
"rule": 6,
|
||
"condition": "complexity 1-2",
|
||
"provider": "ollama",
|
||
"reason": "低複雜度快速處理",
|
||
},
|
||
]
|
||
|
||
|
||
# 單例
|
||
_router: AIRouter | None = None
|
||
|
||
|
||
def get_ai_router() -> AIRouter:
|
||
"""取得 AIRouter 單例"""
|
||
global _router
|
||
if _router is None:
|
||
_router = AIRouter()
|
||
return _router
|
||
|
||
|
||
def reset_ai_router() -> None:
|
||
"""重置單例 (用於測試)"""
|
||
global _router
|
||
_router = None
|