Initial commit: InternalAuditInterprise
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
"""LLM Provider 抽象层。
|
||||
|
||||
通过统一接口隔离 LLM 实现,使开发期可用公网千问、生产期无缝切换本地 vLLM。
|
||||
强约束:"数据零出域"红线由 provider 工厂在 prod 环境拦截公网 Provider。
|
||||
"""
|
||||
|
||||
from app.llm.base import ChatMessage, LLMProvider, LLMResponse
|
||||
from app.llm.factory import get_llm_provider
|
||||
|
||||
__all__ = ["ChatMessage", "LLMProvider", "LLMResponse", "get_llm_provider"]
|
||||
@@ -0,0 +1,44 @@
|
||||
"""LLM Provider 抽象接口与数据模型。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatMessage:
|
||||
role: str # "system" | "user" | "assistant"
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
content: str
|
||||
model: str
|
||||
provider: str
|
||||
# 是否经过出域(公网)通道,便于审计轨迹记录
|
||||
egress: bool = False
|
||||
raw: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class LLMProvider(abc.ABC):
|
||||
"""所有 LLM 实现的统一接口。
|
||||
|
||||
业务代码只依赖本接口;切换公网/本地仅改配置,不改调用方。
|
||||
"""
|
||||
|
||||
#: provider 名称
|
||||
name: str = "base"
|
||||
#: 是否走公网(出域)。prod 环境禁止 egress=True 的 provider。
|
||||
egress: bool = False
|
||||
|
||||
@abc.abstractmethod
|
||||
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||
"""同步对话补全。"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def health(self) -> bool:
|
||||
"""探活:provider 是否可用。"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,31 @@
|
||||
"""LLM Provider 工厂:按配置创建 provider,并执行数据零出域红线校验。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.config import EGRESS_PROVIDERS, LLMProviderName, Settings, get_settings
|
||||
from app.llm.base import LLMProvider
|
||||
from app.llm.providers import DashScopeProvider, VllmProvider
|
||||
|
||||
|
||||
class EgressPolicyError(RuntimeError):
|
||||
"""数据零出域红线违规。"""
|
||||
|
||||
|
||||
def get_llm_provider(settings: Settings | None = None) -> LLMProvider:
|
||||
settings = settings or get_settings()
|
||||
|
||||
# 红线:prod 环境禁止公网 provider
|
||||
if settings.is_prod and settings.llm_provider in EGRESS_PROVIDERS:
|
||||
raise EgressPolicyError(
|
||||
f"数据零出域红线违规:prod 环境禁止使用公网 LLM Provider "
|
||||
f"'{settings.llm_provider.value}'。"
|
||||
)
|
||||
|
||||
if settings.llm_provider == LLMProviderName.dashscope:
|
||||
return DashScopeProvider(
|
||||
api_key=settings.dashscope_api_key, model=settings.dashscope_model
|
||||
)
|
||||
if settings.llm_provider == LLMProviderName.vllm:
|
||||
return VllmProvider(base_url=settings.vllm_base_url, model=settings.vllm_model)
|
||||
|
||||
raise ValueError(f"未知的 LLM Provider: {settings.llm_provider}")
|
||||
@@ -0,0 +1,80 @@
|
||||
"""具体 LLM Provider 实现:DashScope(公网千问,仅 dev)、vLLM(本地,prod)。
|
||||
|
||||
两者均走 OpenAI 兼容的 /chat/completions 协议。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from app.llm.base import ChatMessage, LLMProvider, LLMResponse
|
||||
|
||||
|
||||
class DashScopeProvider(LLMProvider):
|
||||
"""公网千问(DashScope,OpenAI 兼容模式)。仅限开发测试,且只允许脱敏/样例假数据。"""
|
||||
|
||||
name = "dashscope"
|
||||
egress = True # 走公网,出域
|
||||
|
||||
_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
|
||||
def __init__(self, api_key: str, model: str, timeout: float = 30.0) -> None:
|
||||
self._api_key = api_key
|
||||
self._model = model
|
||||
self._timeout = timeout
|
||||
|
||||
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||
payload = {
|
||||
"model": self._model,
|
||||
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||
**kwargs,
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||
with httpx.Client(timeout=self._timeout) as client:
|
||||
resp = client.post(
|
||||
f"{self._BASE_URL}/chat/completions", json=payload, headers=headers
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
return LLMResponse(
|
||||
content=content, model=self._model, provider=self.name, egress=True, raw=data
|
||||
)
|
||||
|
||||
def health(self) -> bool:
|
||||
return bool(self._api_key)
|
||||
|
||||
|
||||
class VllmProvider(LLMProvider):
|
||||
"""本地 vLLM(OpenAI 兼容)。生产使用,数据不出域。"""
|
||||
|
||||
name = "vllm"
|
||||
egress = False
|
||||
|
||||
def __init__(self, base_url: str, model: str, timeout: float = 60.0) -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._model = model
|
||||
self._timeout = timeout
|
||||
|
||||
def chat(self, messages: list[ChatMessage], **kwargs) -> LLMResponse:
|
||||
payload = {
|
||||
"model": self._model,
|
||||
"messages": [{"role": m.role, "content": m.content} for m in messages],
|
||||
**kwargs,
|
||||
}
|
||||
with httpx.Client(timeout=self._timeout) as client:
|
||||
resp = client.post(f"{self._base_url}/chat/completions", json=payload)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
return LLMResponse(
|
||||
content=content, model=self._model, provider=self.name, egress=False, raw=data
|
||||
)
|
||||
|
||||
def health(self) -> bool:
|
||||
try:
|
||||
with httpx.Client(timeout=5.0) as client:
|
||||
resp = client.get(f"{self._base_url}/models")
|
||||
return resp.status_code == 200
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
Reference in New Issue
Block a user