feat(api): Phase 13.2 AI Rate Limiter + RAG 基礎設施 (#84)

Rate Limiter (防止 Gemini 用量暴衝):
- ai_rate_limiter.py: RPM/Daily/Token 三層閥值
- openclaw.py: 整合 rate limit 檢查,超限自動降級
- health.py: /health/ai-usage 監控端點

RAG Tool 基礎 (#84 進行中):
- embedding_service.py: Ollama embedding 封裝
- rag_service.py: Redis vector search 服務

閥值設定:
- Gemini: 10 RPM, 500/day, 100K tokens/day
- Claude: 5 RPM, 200/day, 50K tokens/day

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 15:52:57 +08:00
parent 30145c7d7e
commit bf32c4b1f2
5 changed files with 1048 additions and 13 deletions

View File

@@ -35,6 +35,7 @@ from src.models.ai import (
OpenClawDecision,
)
from src.services.langfuse_client import langfuse_trace
from src.services.model_registry import get_model_registry
from src.services.signoz_client import GoldMetrics, get_signoz_client
from src.utils.k8s_naming import normalize_resource_name
from src.utils.timezone import now_taipei_iso
@@ -270,17 +271,22 @@ class OpenClawService:
prompt_length=len(prompt),
)
# 從 ModelRegistry 取得模型配置
registry = get_model_registry()
model_name = registry.get_model("ollama", "rca")
options = registry.get_provider_options("ollama")
response = await client.post(
f"{settings.OLLAMA_URL}/api/generate",
json={
"model": "qwen2.5:7b-instruct", # 使用更大的模型提高品質
"model": model_name,
"prompt": prompt,
"stream": False,
"format": "json", # 強制 JSON 輸出
"options": {
"num_predict": 1024, # 增加輸出長度
"temperature": 0.1, # 低溫度確保穩定輸出
"top_p": 0.9,
"num_predict": options.get("num_predict", 1024),
"temperature": options.get("temperature", 0.1),
"top_p": options.get("top_p", 0.9),
},
},
timeout=httpx.Timeout(float(settings.OPENCLAW_TIMEOUT), connect=10.0),
@@ -324,9 +330,12 @@ class OpenClawService:
try:
client = await self._get_client()
# Gemini 1.5 Flash 支援 JSON Mode
# 從 ModelRegistry 取得模型配置
registry = get_model_registry()
model_name = registry.get_model("gemini", "rca")
response = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={settings.GEMINI_API_KEY}",
f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={settings.GEMINI_API_KEY}",
json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
@@ -737,6 +746,24 @@ class OpenClawService:
return response, provider, success, False # from_cache=False
# =========================================================================
# Public LLM Interface (ILLMProvider Protocol)
# =========================================================================
async def call(self, prompt: str) -> tuple[str, str, bool]:
"""
呼叫 LLM (ILLMProvider Protocol 實作)
#39 Error Analyzer Agent 使用此方法
Args:
prompt: 完整的 prompt
Returns:
(response, provider, success)
"""
return await self._call_with_fallback(prompt)
# =========================================================================
# Fallback Chain
# =========================================================================
@@ -784,7 +811,23 @@ class OpenClawService:
DeepLinking.langfuse_trace_url(trace.langfuse_trace_id),
)
# Phase 13.2: Rate Limiter 整合 (2026-03-26)
# 防止雲端 API 用量暴衝,超限自動降級
from src.services.ai_rate_limiter import get_ai_rate_limiter
rate_limiter = get_ai_rate_limiter()
for provider in settings.AI_FALLBACK_ORDER:
# Rate Limit 檢查 (gemini/claude 需檢查ollama 不限)
if provider in ("gemini", "claude"):
allowed, reason = await rate_limiter.check_and_increment(provider)
if not allowed:
logger.warning(
"ai_rate_limit_skip",
provider=provider,
reason=reason,
)
continue # 跳過此 provider嘗試下一個
logger.info("ai_provider_attempt", provider=provider)
start_time = time.time()
@@ -829,13 +872,9 @@ class OpenClawService:
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)
"""取得 provider 對應的模型名稱 (從 ModelRegistry)"""
registry = get_model_registry()
return registry.get_model(provider, "rca")
# =========================================================================
# Response Parsing (防禦性解析)