fix(phase24): 首席架構師 Review C1/C2/C3/I4 修復
C1 (P0): AIRouterExecutor.execute() 補 Langfuse Trace (D5)
- 建立 langfuse_trace("ai_router_execute") 包住整個執行鏈
- 成功時記錄 generation (model/input/output/tokens/cost)
- prod 所有 AI 呼叫現在有 LLMOps 追蹤
C2 (P0): 絞殺者改為呼叫 AIRouter.route() 智慧路由
- 先取得 RoutingDecision (意圖分類 + 複雜度評分)
- provider_order 從 selected_provider + fallback_chain 動態生成
- D1 意圖路由矩陣、D7 隱私保護 (DIAGNOSE 強制 local) 生效
C3 (P1): 型別標注 typo 修復
- AIProviderEnumEnum → AIProviderEnum
- AIProviderEnumProtocol → AIProviderProtocol
I4 (P1): interfaces.py AIProvider Protocol 補 close() 定義
S1: ai_router.py 模組版本標頭更新至 v4.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,10 @@ class AIProvider(Protocol):
|
||||
"""健康檢查 (供 /health endpoint 和 AIRouter 動態路由)"""
|
||||
...
|
||||
|
||||
async def close(self) -> None:
|
||||
"""關閉 HTTP 連線 / 釋放資源 (shutdown hook, ADR-052 I5)"""
|
||||
...
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 工具函數
|
||||
|
||||
@@ -17,11 +17,11 @@ AI Router - Phase 13.3 #87
|
||||
│ DELETE/CRITICAL │ Claude │ 強制使用最強模型 │
|
||||
└─────────────────┴───────────────┴──────────────────────────────┘
|
||||
|
||||
版本: v3.0
|
||||
版本: v4.0
|
||||
建立: 2026-03-26 (台北時區)
|
||||
建立者: Claude Code
|
||||
最後修改: 2026-03-26 (台北時區)
|
||||
修改者: Claude Code
|
||||
最後修改: 2026-04-02 (台北時區)
|
||||
修改者: ogt (首席架構師 Review C1/C2/C3 修復)
|
||||
|
||||
變更紀錄:
|
||||
| 版本 | 日期 | 執行者 | 變更內容 |
|
||||
@@ -29,6 +29,7 @@ AI Router - Phase 13.3 #87
|
||||
| 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 完整路由決策矩陣 |
|
||||
| v4.0 | 2026-04-02 | ogt (首席架構師) | Phase 24 AIProvider Registry + Executor; C1 Langfuse Trace; C2 AIRouter.route(); C3 型別 typo; I4 Protocol close |
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -75,7 +76,7 @@ class AIProviderEnum(Enum):
|
||||
|
||||
|
||||
# Provider 對應延遲預算 (ms)
|
||||
PROVIDER_LATENCY_BUDGET: dict[AIProviderEnumEnum, int] = {
|
||||
PROVIDER_LATENCY_BUDGET: dict[AIProviderEnum, int] = {
|
||||
AIProviderEnum.OLLAMA: 60000, # 本地,允許較長處理時間
|
||||
AIProviderEnum.GEMINI: 30000, # 雲端,較低延遲
|
||||
AIProviderEnum.CLAUDE: 30000, # 雲端,較低延遲
|
||||
@@ -676,7 +677,7 @@ class AIProviderRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._providers: dict[str, AIProviderProtocol] = {}
|
||||
|
||||
def register(self, provider: AIProviderEnumProtocol) -> None:
|
||||
def register(self, provider: AIProviderProtocol) -> None:
|
||||
"""註冊 Provider (啟動時呼叫)"""
|
||||
self._providers[provider.name] = provider
|
||||
status = "enabled" if provider.is_enabled else "disabled"
|
||||
@@ -816,6 +817,23 @@ class AIRouterExecutor:
|
||||
logger.debug("ai_router_cache_read_failed", error=str(e))
|
||||
|
||||
# ③ 遍歷 Provider + 閘門 (D3)
|
||||
# 2026-04-02 ogt: C1 修復 — 建立 Langfuse Trace (D5)
|
||||
# 包住整個執行鏈,記錄每個 Provider 的 generation
|
||||
try:
|
||||
from src.services.langfuse_client import langfuse_trace
|
||||
_lf_trace_ctx = langfuse_trace(
|
||||
"ai_router_execute",
|
||||
metadata={
|
||||
"provider_order": provider_order,
|
||||
"prompt_length": len(prompt),
|
||||
"require_local": require_local,
|
||||
"alert_type": (context or {}).get("alert_type", ""),
|
||||
},
|
||||
)
|
||||
_lf_trace_ctx.__enter__()
|
||||
except Exception:
|
||||
_lf_trace_ctx = None
|
||||
|
||||
errors: list[str] = []
|
||||
|
||||
for provider_name in provider_order:
|
||||
@@ -834,7 +852,8 @@ class AIRouterExecutor:
|
||||
continue
|
||||
|
||||
# 閘門 2: Rate Limiter
|
||||
if provider_name in ("nvidia", "gemini", "claude"):
|
||||
# 2026-04-02 Claude Code: Phase 24 B3 — 加入 nemotron Rate Limiter
|
||||
if provider_name in ("nvidia", "gemini", "claude", "nemotron"):
|
||||
try:
|
||||
from src.services.ai_rate_limiter import get_ai_rate_limiter
|
||||
rate_limiter = get_ai_rate_limiter()
|
||||
@@ -882,6 +901,20 @@ class AIRouterExecutor:
|
||||
tokens=result.tokens,
|
||||
from_cache=False,
|
||||
)
|
||||
# D5: 記錄 Langfuse generation
|
||||
if _lf_trace_ctx:
|
||||
try:
|
||||
_lf_trace_ctx.generation(
|
||||
name=f"{provider_name}_call",
|
||||
model=provider_name,
|
||||
input=prompt[:500],
|
||||
output=result.raw_response[:500],
|
||||
usage={"total": result.tokens} if result.tokens else None,
|
||||
metadata={"cost_usd": result.cost_usd, "latency_ms": round(result.latency_ms, 1)},
|
||||
)
|
||||
_lf_trace_ctx.__exit__(None, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
# Provider 回傳 success=False
|
||||
@@ -895,6 +928,11 @@ class AIRouterExecutor:
|
||||
|
||||
# 全部失敗
|
||||
logger.error("ai_router_all_providers_failed", tried=provider_order, errors=errors)
|
||||
if _lf_trace_ctx:
|
||||
try:
|
||||
_lf_trace_ctx.__exit__(None, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
return AIResult(
|
||||
raw_response="",
|
||||
success=False,
|
||||
@@ -925,8 +963,9 @@ def _init_registry() -> AIProviderRegistry:
|
||||
registry.register(ClaudeProvider())
|
||||
registry.register(OpenClawNemoProvider())
|
||||
|
||||
# NvidiaProvider 整合現有的 nvidia_provider.py (Phase 24-B3)
|
||||
# 暫時不註冊,Tool Calling 仍走現有路徑
|
||||
# 2026-04-02 Claude Code: Phase 24 B3 — 加入 NemotronProvider (tool_calling 優先)
|
||||
from src.services.ai_providers.nemotron import NemotronProvider
|
||||
registry.register(NemotronProvider())
|
||||
|
||||
return registry
|
||||
|
||||
|
||||
@@ -917,19 +917,38 @@ class OpenClawService:
|
||||
# =================================================================
|
||||
if settings.USE_AI_ROUTER:
|
||||
try:
|
||||
from src.services.ai_router import get_ai_executor
|
||||
# 2026-04-02 ogt: C2 修復 — 呼叫 AIRouter.route() 智慧路由 (非靜態 order)
|
||||
# D1 意圖分類路由、D7 隱私保護 (DIAGNOSE/CODE_REVIEW 強制 local) 生效
|
||||
from src.services.ai_router import get_ai_router, get_ai_executor, IntentType
|
||||
router = get_ai_router()
|
||||
executor = get_ai_executor()
|
||||
|
||||
# Step 1: 取得路由決策 (含意圖分類 + 複雜度評分)
|
||||
decision = await router.route(prompt, alert_context)
|
||||
|
||||
# Step 2: 從 RoutingDecision 建立 provider_order (主 + fallback)
|
||||
provider_order = [decision.selected_provider.value] + [
|
||||
p.value for p, _ in decision.fallback_chain
|
||||
if p.value != decision.selected_provider.value
|
||||
]
|
||||
|
||||
# Step 3: D7 隱私 — DIAGNOSE/CODE_REVIEW 強制 local
|
||||
require_local = decision.intent in (IntentType.DIAGNOSE, IntentType.CODE_REVIEW)
|
||||
|
||||
result = await executor.execute(
|
||||
prompt=prompt,
|
||||
provider_order=settings.AI_FALLBACK_ORDER,
|
||||
provider_order=provider_order,
|
||||
context=alert_context,
|
||||
cache_ttl=3600,
|
||||
require_local=require_local,
|
||||
)
|
||||
logger.info(
|
||||
"phase24_ai_router_used",
|
||||
provider=result.provider,
|
||||
success=result.success,
|
||||
latency_ms=round(result.latency_ms, 1),
|
||||
intent=decision.intent.value,
|
||||
routing_reason=decision.routing_reason,
|
||||
)
|
||||
return result.raw_response, result.provider, result.success, result.tokens, result.cost_usd
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user