fix(phase24): 首席架構師 Review C1/C2/C3/I4 修復
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m12s
E2E Health Check / e2e-health (push) Successful in 18s

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:
OG T
2026-04-02 21:47:06 +08:00
parent 9d00b0389e
commit 5a8aae89c4
3 changed files with 72 additions and 10 deletions

View File

@@ -129,6 +129,10 @@ class AIProvider(Protocol):
"""健康檢查 (供 /health endpoint 和 AIRouter 動態路由)"""
...
async def close(self) -> None:
"""關閉 HTTP 連線 / 釋放資源 (shutdown hook, ADR-052 I5)"""
...
# =============================================================================
# 工具函數

View File

@@ -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

View File

@@ -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: