"""大模型回退层:Mock 与 OpenAI 兼容 API 两种后端。""" from __future__ import annotations import asyncio from typing import Dict, Optional from .models import ExpertResponse class FallbackProvider: name: str = "fallback" async def generate(self, query: str) -> ExpertResponse: raise NotImplementedError class MockFallback(FallbackProvider): """确定性 mock 大模型:标识为 fallback,便于测试升级路径。""" def __init__(self, model: str = "mock-large"): self.model = model self.name = f"fallback-{model}" async def generate(self, query: str) -> ExpertResponse: await asyncio.sleep(0.002) body = ( f"(大模型回退)「{query}」\n\n" "这是一条来自大模型回退路径的完整回答。\n" "要点:\n" "1. 对复杂/跨域任务给出综合推理\n" "2. 补充领域专家未覆盖的上下文\n" "3. 给出可执行的后续建议\n" ) return ExpertResponse( text=body, model_used=self.model, latency_ms=2.0, tokens=120, cost_est=2.0 * 120 / 1_000_000, ) class APIFallback(FallbackProvider): """OpenAI 兼容大模型 API(如 DeepSeek / OpenAI / 本地 vLLM)。""" def __init__(self, model: str, base_url: str, api_key: str): self.model = model self.base_url = base_url.rstrip("/") self.api_key = api_key self.name = f"fallback-{model}" self._client = None def _get_client(self): if self._client is None: import httpx self._client = httpx.AsyncClient(timeout=90.0) return self._client async def generate(self, query: str) -> ExpertResponse: client = self._get_client() resp = await client.post( f"{self.base_url}/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, json={ "model": self.model, "messages": [{"role": "user", "content": query}], "temperature": 0.3, "max_tokens": 2048, }, ) resp.raise_for_status() data = resp.json() body = data["choices"][0]["message"]["content"] usage = data.get("usage", {}) tokens = usage.get("completion_tokens", int(len(body) / 2.2)) return ExpertResponse( text=body, model_used=self.model, latency_ms=0.0, tokens=tokens, cost_est=2.0 * tokens / 1_000_000, ) def build_fallback(cfg: Dict) -> FallbackProvider: """cfg 为 fallback 段配置。""" ftype = cfg.get("type", "mock") model = cfg.get("model", "deepseek-chat") if ftype == "mock": return MockFallback(model=model) if ftype == "api": import os api_key = cfg.get("api_key") or os.environ.get(cfg.get("api_key_env", "API_KEY")) if not api_key: raise RuntimeError( f"APIFallback 缺少 API Key:请设置环境变量 {cfg.get('api_key_env')} 或配置 api_key" ) return APIFallback(model, cfg.get("base_url", "https://api.deepseek.com/v1"), api_key) raise ValueError(f"未知 fallback 类型: {ftype}(支持 mock | api)")