From 5a8aae89c476fc3176b2b3ef70267adb18b2f9e0 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 2 Apr 2026 21:47:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(phase24):=20=E9=A6=96=E5=B8=AD=E6=9E=B6?= =?UTF-8?q?=E6=A7=8B=E5=B8=AB=20Review=20C1/C2/C3/I4=20=E4=BF=AE=E5=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/services/ai_providers/interfaces.py | 4 ++ apps/api/src/services/ai_router.py | 55 ++++++++++++++++--- apps/api/src/services/openclaw.py | 23 +++++++- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/ai_providers/interfaces.py b/apps/api/src/services/ai_providers/interfaces.py index d5ee5e46..ddd0acb0 100644 --- a/apps/api/src/services/ai_providers/interfaces.py +++ b/apps/api/src/services/ai_providers/interfaces.py @@ -129,6 +129,10 @@ class AIProvider(Protocol): """健康檢查 (供 /health endpoint 和 AIRouter 動態路由)""" ... + async def close(self) -> None: + """關閉 HTTP 連線 / 釋放資源 (shutdown hook, ADR-052 I5)""" + ... + # ============================================================================= # 工具函數 diff --git a/apps/api/src/services/ai_router.py b/apps/api/src/services/ai_router.py index faef105f..2035006e 100644 --- a/apps/api/src/services/ai_router.py +++ b/apps/api/src/services/ai_router.py @@ -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 diff --git a/apps/api/src/services/openclaw.py b/apps/api/src/services/openclaw.py index 8b036559..8348b5d3 100644 --- a/apps/api/src/services/openclaw.py +++ b/apps/api/src/services/openclaw.py @@ -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: