Files
awoooi/apps/api/src/services/ai_router.py
OG T 58b4004a18 feat(api): Phase 13.3 智能路由 (#85-87)
- IntentClassifier: 意圖分類 (告警/部署/查詢/維運/審查)
- ComplexityScorer: 複雜度評分 (1-5 分)
- AIRouter: 動態模型選擇 (整合 Intent + Complexity)
- 測試: 完整單元測試覆蓋

Phase 13.3 設計: project_phase13_3_smart_router.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-26 10:01:04 +08:00

186 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AI Router - Phase 13.3 #87
==========================
動態模型選擇器,整合意圖分類和複雜度評分
目標: 根據請求特性自動選擇最適模型
策略: Intent + Complexity → Model Selection
Phase 13.3 (2026-03-26): 初始實作
"""
from dataclasses import dataclass
import structlog
from src.services.complexity_scorer import (
ComplexityScore,
get_complexity_scorer,
)
from src.services.intent_classifier import (
IntentType,
get_intent_classifier,
)
logger = structlog.get_logger(__name__)
@dataclass
class RoutingDecision:
"""路由決策結果"""
model: str # 選擇的模型
intent: IntentType # 意圖分類
complexity: ComplexityScore # 複雜度評分
reason: str # 選擇原因
fallback_models: list[str] # 備援模型列表
class AIRouter:
"""
AI 路由器
整合 IntentClassifier 和 ComplexityScorer
動態選擇最適合的模型。
路由策略:
1. 意圖優先覆寫 (某些意圖強制使用特定模型)
2. 複雜度導向選擇
3. 成本/延遲平衡
"""
# 意圖強制覆寫
INTENT_OVERRIDES: dict[IntentType, str | None] = {
IntentType.CODE_REVIEW: "qwen2.5:7b-instruct", # 程式碼審查需要強模型
IntentType.DEPLOYMENT: None, # 不覆寫,依複雜度
IntentType.ALERT_TRIAGE: None,
IntentType.QUERY: "llama3.2:3b", # 查詢用快速模型
IntentType.MAINTENANCE: None,
IntentType.UNKNOWN: None,
}
# Fallback 順序
FALLBACK_ORDER = [
"qwen2.5:7b-instruct", # 本地主力
"llama3.2:3b", # 本地備援
"gemini", # 雲端備援
"claude", # 最終備援
]
def __init__(self):
self._intent_classifier = get_intent_classifier()
self._complexity_scorer = get_complexity_scorer()
async def route(
self,
text: str,
context: dict | None = None,
) -> RoutingDecision:
"""
路由請求到最適模型
Args:
text: 用戶輸入或告警內容
context: 額外上下文 (服務、指標等)
Returns:
RoutingDecision: 路由決策
"""
context = context or {}
# Step 1: 意圖分類
intent = await self._intent_classifier.classify(text)
# Step 2: 複雜度評分
complexity = self._complexity_scorer.score(context)
# Step 3: 模型選擇
model, reason = self._select_model(intent, complexity)
# Step 4: 建立 Fallback 列表
fallbacks = self._build_fallback_list(model)
decision = RoutingDecision(
model=model,
intent=intent,
complexity=complexity,
reason=reason,
fallback_models=fallbacks,
)
logger.info(
"ai_routing_decision",
model=model,
intent=intent.value,
complexity_score=complexity.score,
reason=reason,
)
return decision
def _select_model(
self,
intent: IntentType,
complexity: ComplexityScore,
) -> tuple[str, str]:
"""
選擇模型
Returns:
(model_name, reason)
"""
# 檢查意圖覆寫
override = self.INTENT_OVERRIDES.get(intent)
if override:
return override, f"意圖 {intent.value} 強制使用 {override}"
# 依複雜度選擇
model = complexity.recommended_model
reason = f"複雜度 {complexity.score}/5 → {model}"
# 特殊情況調整
if intent == IntentType.ALERT_TRIAGE and complexity.score >= 4:
# 高複雜度告警優先用雲端
model = "gemini"
reason = f"高複雜度告警 (score={complexity.score}) → 使用雲端模型"
return model, reason
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:
"""同步版本 (僅關鍵字匹配)"""
context = context or {}
intent = self._intent_classifier.classify_sync(text)
complexity = self._complexity_scorer.score(context)
model, reason = self._select_model(intent, complexity)
fallbacks = self._build_fallback_list(model)
return RoutingDecision(
model=model,
intent=intent,
complexity=complexity,
reason=reason,
fallback_models=fallbacks,
)
# 單例
_router: AIRouter | None = None
def get_ai_router() -> AIRouter:
"""取得 AIRouter 單例"""
global _router
if _router is None:
_router = AIRouter()
return _router