feat(api): Phase 15.1 Langfuse LLMOps 整合 + 模型升級
## 新功能 - Langfuse 自建部署 (192.168.0.110:3100) - langfuse_client.py - LLM 呼叫追蹤包裝 - OpenClaw 整合 Langfuse trace ## 模型升級 (統帥批准) - 生產預設: llama3.2:3b → qwen2.5:7b-instruct - 摘要任務: llama3.2:3b (速度優先) ## 配置更新 - requirements.txt: +langfuse>=2.0.0 - config.py: +LANGFUSE_* 設定 - models.json: 更新 Ollama 模型配置 - K8s: Secret + ConfigMap 更新 ## 審查通過 - 模組化檢查 ✅ - 核心測試 31/31 ✅ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ from src.core.redis_client import get_redis
|
||||
from src.models.ai import (
|
||||
OpenClawDecision,
|
||||
)
|
||||
from src.services.langfuse_client import langfuse_trace
|
||||
from src.services.signoz_client import GoldMetrics, get_signoz_client
|
||||
from src.utils.timezone import now_taipei_iso
|
||||
|
||||
@@ -360,7 +361,7 @@ class OpenClawService:
|
||||
response = await client.post(
|
||||
f"{settings.OLLAMA_URL}/api/generate",
|
||||
json={
|
||||
"model": "llama3.2:3b", # 使用更大的模型提高品質
|
||||
"model": "qwen2.5:7b-instruct", # 使用更大的模型提高品質
|
||||
"prompt": prompt,
|
||||
"stream": False,
|
||||
"format": "json", # 強制 JSON 輸出
|
||||
@@ -823,34 +824,75 @@ class OpenClawService:
|
||||
|
||||
若 MOCK_MODE=True,直接回傳模擬結果。
|
||||
若所有 Provider 失敗,fallback 到 Mock。
|
||||
|
||||
Phase 15.1: 整合 Langfuse LLMOps 追蹤
|
||||
"""
|
||||
# Mock Mode: 開發測試用
|
||||
if settings.MOCK_MODE:
|
||||
logger.info("mock_mode_enabled", using="mock_llm")
|
||||
return self._generate_mock_response(alert_context or {}, signoz_metrics), "mock", True
|
||||
|
||||
for provider in settings.AI_FALLBACK_ORDER:
|
||||
logger.info("ai_provider_attempt", provider=provider)
|
||||
# Phase 15.1: Langfuse 追蹤整合
|
||||
with langfuse_trace(
|
||||
"openclaw_fallback_chain",
|
||||
metadata={
|
||||
"prompt_length": len(prompt),
|
||||
"fallback_order": settings.AI_FALLBACK_ORDER,
|
||||
"alert_fingerprint": (alert_context or {}).get("fingerprint", "unknown"),
|
||||
},
|
||||
) as trace:
|
||||
for provider in settings.AI_FALLBACK_ORDER:
|
||||
logger.info("ai_provider_attempt", provider=provider)
|
||||
|
||||
if provider == "ollama":
|
||||
response, success = await self._call_ollama(prompt)
|
||||
elif provider == "gemini":
|
||||
response, success = await self._call_gemini(prompt)
|
||||
elif provider == "claude":
|
||||
response, success = await self._call_claude(prompt)
|
||||
else:
|
||||
logger.warning("unknown_ai_provider", provider=provider)
|
||||
continue
|
||||
start_time = time.time()
|
||||
model_name = self._get_model_name(provider)
|
||||
|
||||
if success:
|
||||
logger.info("ai_provider_success", provider=provider)
|
||||
return response, provider, True
|
||||
if provider == "ollama":
|
||||
response, success = await self._call_ollama(prompt)
|
||||
elif provider == "gemini":
|
||||
response, success = await self._call_gemini(prompt)
|
||||
elif provider == "claude":
|
||||
response, success = await self._call_claude(prompt)
|
||||
else:
|
||||
logger.warning("unknown_ai_provider", provider=provider)
|
||||
continue
|
||||
|
||||
logger.warning("ai_provider_failed_fallback", provider=provider)
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
|
||||
# 所有 Provider 失敗時,fallback 到 Mock (優雅降級)
|
||||
logger.warning("all_providers_failed_using_mock", fallback="mock_llm")
|
||||
return self._generate_mock_response(alert_context or {}, signoz_metrics), "mock_fallback", True
|
||||
# Langfuse: 記錄每次 LLM 呼叫
|
||||
trace.generation(
|
||||
name=f"{provider}_call",
|
||||
model=model_name,
|
||||
input=prompt[:500], # 截斷避免過長
|
||||
output=response[:500] if success else f"ERROR: {response[:200]}",
|
||||
metadata={
|
||||
"success": success,
|
||||
"latency_ms": round(latency_ms, 2),
|
||||
"provider": provider,
|
||||
},
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("ai_provider_success", provider=provider, latency_ms=latency_ms)
|
||||
# Langfuse: 記錄成功評分
|
||||
trace.score(name="provider_success", value=1.0, comment=f"Success via {provider}")
|
||||
return response, provider, True
|
||||
|
||||
logger.warning("ai_provider_failed_fallback", provider=provider, latency_ms=latency_ms)
|
||||
|
||||
# 所有 Provider 失敗時,fallback 到 Mock (優雅降級)
|
||||
logger.warning("all_providers_failed_using_mock", fallback="mock_llm")
|
||||
trace.score(name="provider_success", value=0.0, comment="All providers failed, using mock")
|
||||
return self._generate_mock_response(alert_context or {}, signoz_metrics), "mock_fallback", True
|
||||
|
||||
def _get_model_name(self, provider: str) -> str:
|
||||
"""取得 provider 對應的模型名稱"""
|
||||
model_map = {
|
||||
"ollama": "qwen2.5:7b-instruct",
|
||||
"gemini": "gemini-1.5-flash",
|
||||
"claude": "claude-3-haiku-20240307",
|
||||
}
|
||||
return model_map.get(provider, provider)
|
||||
|
||||
# =========================================================================
|
||||
# Response Parsing (防禦性解析)
|
||||
|
||||
Reference in New Issue
Block a user