- 新增 AIProviderConfig.md:详细设计 AI 厂商动态配置系统 - 数据库存储配置(而非环境变量) - 运行时动态加载,支持热更新 - 多租户隔离,支持品牌方独立配置 - API Key 加密存储 - 故障转移机制 - 更新 DevelopmentPlan.md (V1.4): - 在 AI 模型选型章节添加动态配置说明 - 添加 AIProviderConfig.md 到相关文档 - 更新 FeatureSummary.md (V1.3): - 新增系统管理模块 (F-47~F-50) - F-47: AI 厂商动态配置 (P0) - F-48: AI 厂商连通性测试 (P0) - F-49: 多租户 AI 配置隔离 (P1) - F-50: API Key 轮换管理 (P1) - 更新 RequirementsDoc.md 和 PRD.md: - 在技术架构概述中添加 AI 配置管理说明 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
913 lines
30 KiB
Markdown
913 lines
30 KiB
Markdown
# AIProviderConfig.md - AI 厂商动态配置架构设计
|
||
|
||
| 文档类型 | **Technical Design (技术设计文档)** |
|
||
| --- | --- |
|
||
| **项目名称** | SmartAudit (AI 营销内容合规审核平台) |
|
||
| **版本号** | V1.0 |
|
||
| **日期** | 2026-02-02 |
|
||
| **侧重** | AI 厂商动态配置、多租户隔离、运行时热更新 |
|
||
|
||
---
|
||
|
||
## 版本历史 (Version History)
|
||
|
||
| 版本 | 日期 | 作者 | 变更说明 |
|
||
| --- | --- | --- | --- |
|
||
| V1.0 | 2026-02-02 | Claude | 初稿:AI 厂商动态配置架构设计 |
|
||
|
||
---
|
||
|
||
## 1. 设计背景与目标
|
||
|
||
### 1.1 问题陈述
|
||
|
||
传统方案将 AI 模型的 API Key 和 Base URL 写死在环境变量中,存在以下问题:
|
||
|
||
1. **灵活性差:** 切换 AI 厂商需要修改环境变量并重启服务
|
||
2. **多租户困难:** 无法支持不同品牌方使用不同的 AI 厂商
|
||
3. **安全隐患:** 环境变量容易泄露,难以细粒度管理
|
||
4. **运维成本高:** 密钥轮换需要重新部署
|
||
|
||
### 1.2 设计目标
|
||
|
||
实现**商业 SaaS 级别的 AI 厂商动态配置系统**:
|
||
|
||
| 目标 | 描述 |
|
||
| --- | --- |
|
||
| **动态配置** | 管理员在后台配置 AI 厂商,无需修改代码或重启服务 |
|
||
| **多厂商支持** | 支持 DeepSeek、OpenAI、阿里云、OneAPI 中转等多种厂商 |
|
||
| **多租户隔离** | 不同品牌方可配置独立的 AI 厂商和配额 |
|
||
| **热更新** | 配置变更即时生效,无需重启服务 |
|
||
| **安全存储** | API Key 加密存储,支持密钥轮换 |
|
||
| **故障转移** | 主厂商不可用时自动切换到备用厂商 |
|
||
|
||
---
|
||
|
||
## 2. 系统架构
|
||
|
||
### 2.1 架构概览
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ 管理后台 (Admin Portal) │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ AI 厂商配置页面:添加/编辑/删除/测试连通性 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ API 层 (FastAPI) │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ POST /admin/ai-providers - 创建 AI 厂商配置 │ │
|
||
│ │ GET /admin/ai-providers - 获取厂商列表 │ │
|
||
│ │ PUT /admin/ai-providers/{id} - 更新配置 │ │
|
||
│ │ POST /admin/ai-providers/{id}/test - 测试连通性 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ AI 客户端工厂 (AIClientFactory) │
|
||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||
│ │ • 根据配置动态创建 AI 客户端实例 │ │
|
||
│ │ • 支持连接池和客户端复用 │ │
|
||
│ │ • 配置变更时自动刷新客户端 │ │
|
||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────┼───────────────┐
|
||
▼ ▼ ▼
|
||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||
│ DeepSeek │ │ OpenAI │ │ OneAPI │
|
||
│ Client │ │ Client │ │ (中转) │
|
||
└─────────────┘ └─────────────┘ └─────────────┘
|
||
```
|
||
|
||
### 2.2 核心组件
|
||
|
||
| 组件 | 职责 |
|
||
| --- | --- |
|
||
| **AIProviderConfig** | 数据模型,存储厂商配置 |
|
||
| **AIClientFactory** | 工厂类,根据配置创建客户端 |
|
||
| **AIClientRegistry** | 注册表,缓存和管理客户端实例 |
|
||
| **ConfigWatcher** | 监听配置变更,触发客户端刷新 |
|
||
| **SecretsManager** | 加密存储和解密 API Key |
|
||
|
||
---
|
||
|
||
## 3. 数据模型设计
|
||
|
||
### 3.1 AI 厂商配置表 (ai_provider_configs)
|
||
|
||
```sql
|
||
CREATE TABLE ai_provider_configs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- 基础信息
|
||
name VARCHAR(100) NOT NULL, -- 配置名称,如 "生产环境 DeepSeek"
|
||
provider_type VARCHAR(50) NOT NULL, -- 厂商类型:deepseek/openai/oneapi/aliyun/...
|
||
description TEXT, -- 配置说明
|
||
|
||
-- 连接配置
|
||
base_url VARCHAR(500) NOT NULL, -- API Base URL
|
||
api_key_encrypted BYTEA NOT NULL, -- 加密后的 API Key
|
||
|
||
-- 模型配置
|
||
default_model VARCHAR(100), -- 默认模型,如 "deepseek-chat"
|
||
available_models JSONB DEFAULT '[]', -- 可用模型列表
|
||
|
||
-- 能力标签
|
||
capabilities JSONB DEFAULT '[]', -- 支持的能力:["chat", "vision", "embedding"]
|
||
|
||
-- 使用场景
|
||
use_cases JSONB DEFAULT '[]', -- 适用场景:["brief_parsing", "script_review", "video_audit"]
|
||
|
||
-- 租户隔离
|
||
tenant_id UUID, -- 所属租户(品牌方),NULL 表示全局配置
|
||
|
||
-- 优先级与状态
|
||
priority INT DEFAULT 100, -- 优先级,数字越小优先级越高
|
||
is_enabled BOOLEAN DEFAULT true, -- 是否启用
|
||
is_default BOOLEAN DEFAULT false, -- 是否为默认配置
|
||
|
||
-- 限流配置
|
||
rate_limit_rpm INT DEFAULT 60, -- 每分钟请求限制
|
||
rate_limit_tpm INT DEFAULT 100000, -- 每分钟 Token 限制
|
||
|
||
-- 故障转移
|
||
fallback_provider_id UUID, -- 备用厂商配置 ID
|
||
|
||
-- 扩展配置
|
||
extra_config JSONB DEFAULT '{}', -- 厂商特定配置
|
||
|
||
-- 元数据
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
created_by UUID,
|
||
|
||
-- 约束
|
||
CONSTRAINT uk_tenant_default UNIQUE (tenant_id, is_default)
|
||
WHERE is_default = true
|
||
);
|
||
|
||
-- 索引
|
||
CREATE INDEX idx_provider_tenant ON ai_provider_configs(tenant_id);
|
||
CREATE INDEX idx_provider_type ON ai_provider_configs(provider_type);
|
||
CREATE INDEX idx_provider_enabled ON ai_provider_configs(is_enabled);
|
||
CREATE INDEX idx_provider_use_cases ON ai_provider_configs USING GIN(use_cases);
|
||
```
|
||
|
||
### 3.2 厂商类型枚举
|
||
|
||
```python
|
||
from enum import Enum
|
||
|
||
class AIProviderType(str, Enum):
|
||
"""支持的 AI 厂商类型"""
|
||
|
||
# 国内厂商
|
||
DEEPSEEK = "deepseek" # DeepSeek
|
||
QWEN = "qwen" # 阿里云通义千问
|
||
DOUBAO = "doubao" # 字节豆包
|
||
ZHIPU = "zhipu" # 智谱 GLM
|
||
BAICHUAN = "baichuan" # 百川
|
||
MOONSHOT = "moonshot" # Moonshot (Kimi)
|
||
|
||
# 海外厂商(需注意合规)
|
||
OPENAI = "openai" # OpenAI
|
||
ANTHROPIC = "anthropic" # Anthropic Claude
|
||
|
||
# 中转服务
|
||
ONEAPI = "oneapi" # OneAPI 中转
|
||
OPENROUTER = "openrouter" # OpenRouter
|
||
|
||
# 本地部署
|
||
OLLAMA = "ollama" # Ollama 本地
|
||
VLLM = "vllm" # vLLM 部署
|
||
|
||
# ASR/OCR 专用
|
||
ALIYUN_ASR = "aliyun_asr" # 阿里云 ASR
|
||
ALIYUN_OCR = "aliyun_ocr" # 阿里云 OCR
|
||
PADDLEOCR = "paddleocr" # PaddleOCR 本地
|
||
WHISPER = "whisper" # OpenAI Whisper
|
||
|
||
|
||
class AICapability(str, Enum):
|
||
"""AI 能力标签"""
|
||
CHAT = "chat" # 对话/文本生成
|
||
VISION = "vision" # 图像理解
|
||
EMBEDDING = "embedding" # 向量嵌入
|
||
ASR = "asr" # 语音识别
|
||
OCR = "ocr" # 文字识别
|
||
TTS = "tts" # 语音合成
|
||
|
||
|
||
class AIUseCase(str, Enum):
|
||
"""AI 使用场景"""
|
||
BRIEF_PARSING = "brief_parsing" # Brief 解析
|
||
SCRIPT_REVIEW = "script_review" # 脚本预审
|
||
VIDEO_AUDIT = "video_audit" # 视频审核
|
||
CONTEXT_CLASSIFICATION = "context_classification" # 语境分类
|
||
SENTIMENT_ANALYSIS = "sentiment_analysis" # 情感分析
|
||
LOGO_DETECTION = "logo_detection" # Logo 检测
|
||
ASR_TRANSCRIPTION = "asr_transcription" # 语音转写
|
||
OCR_EXTRACTION = "ocr_extraction" # 文字提取
|
||
```
|
||
|
||
### 3.3 使用日志表 (ai_usage_logs)
|
||
|
||
```sql
|
||
CREATE TABLE ai_usage_logs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
provider_id UUID NOT NULL REFERENCES ai_provider_configs(id),
|
||
tenant_id UUID,
|
||
|
||
-- 请求信息
|
||
use_case VARCHAR(50) NOT NULL,
|
||
model VARCHAR(100),
|
||
|
||
-- 用量统计
|
||
prompt_tokens INT DEFAULT 0,
|
||
completion_tokens INT DEFAULT 0,
|
||
total_tokens INT DEFAULT 0,
|
||
|
||
-- 性能指标
|
||
latency_ms INT,
|
||
status VARCHAR(20), -- success/error/timeout
|
||
error_message TEXT,
|
||
|
||
-- 时间
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
|
||
-- 分区键
|
||
created_date DATE DEFAULT CURRENT_DATE
|
||
) PARTITION BY RANGE (created_date);
|
||
|
||
-- 按月分区
|
||
CREATE TABLE ai_usage_logs_2026_02 PARTITION OF ai_usage_logs
|
||
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 核心代码设计
|
||
|
||
### 4.1 配置模型 (Pydantic)
|
||
|
||
```python
|
||
# app/models/ai_provider.py
|
||
|
||
from pydantic import BaseModel, Field, SecretStr
|
||
from typing import Optional, List
|
||
from uuid import UUID
|
||
from datetime import datetime
|
||
from enum import Enum
|
||
|
||
|
||
class AIProviderCreate(BaseModel):
|
||
"""创建 AI 厂商配置请求"""
|
||
name: str = Field(..., max_length=100)
|
||
provider_type: AIProviderType
|
||
description: Optional[str] = None
|
||
base_url: str
|
||
api_key: SecretStr # 接收时为明文,存储时加密
|
||
default_model: Optional[str] = None
|
||
available_models: List[str] = []
|
||
capabilities: List[AICapability] = []
|
||
use_cases: List[AIUseCase] = []
|
||
tenant_id: Optional[UUID] = None
|
||
priority: int = 100
|
||
is_enabled: bool = True
|
||
is_default: bool = False
|
||
rate_limit_rpm: int = 60
|
||
rate_limit_tpm: int = 100000
|
||
fallback_provider_id: Optional[UUID] = None
|
||
extra_config: dict = {}
|
||
|
||
|
||
class AIProviderResponse(BaseModel):
|
||
"""AI 厂商配置响应"""
|
||
id: UUID
|
||
name: str
|
||
provider_type: AIProviderType
|
||
description: Optional[str]
|
||
base_url: str
|
||
# 注意:不返回 api_key
|
||
default_model: Optional[str]
|
||
available_models: List[str]
|
||
capabilities: List[AICapability]
|
||
use_cases: List[AIUseCase]
|
||
tenant_id: Optional[UUID]
|
||
priority: int
|
||
is_enabled: bool
|
||
is_default: bool
|
||
rate_limit_rpm: int
|
||
rate_limit_tpm: int
|
||
fallback_provider_id: Optional[UUID]
|
||
extra_config: dict
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
```
|
||
|
||
### 4.2 AI 客户端工厂
|
||
|
||
```python
|
||
# app/services/ai/client_factory.py
|
||
|
||
from abc import ABC, abstractmethod
|
||
from typing import Dict, Optional, Type
|
||
from functools import lru_cache
|
||
import asyncio
|
||
from openai import AsyncOpenAI
|
||
|
||
from app.models.ai_provider import AIProviderType
|
||
from app.services.secrets_manager import SecretsManager
|
||
|
||
|
||
class BaseAIClient(ABC):
|
||
"""AI 客户端基类"""
|
||
|
||
def __init__(self, config: dict):
|
||
self.config = config
|
||
self.base_url = config["base_url"]
|
||
self.api_key = config["api_key"]
|
||
self.default_model = config.get("default_model")
|
||
|
||
@abstractmethod
|
||
async def chat(self, messages: list, model: str = None, **kwargs) -> dict:
|
||
"""对话接口"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
async def health_check(self) -> bool:
|
||
"""健康检查"""
|
||
pass
|
||
|
||
|
||
class OpenAICompatibleClient(BaseAIClient):
|
||
"""OpenAI 兼容客户端 (适用于 DeepSeek, OneAPI, Moonshot 等)"""
|
||
|
||
def __init__(self, config: dict):
|
||
super().__init__(config)
|
||
self.client = AsyncOpenAI(
|
||
api_key=self.api_key,
|
||
base_url=self.base_url,
|
||
)
|
||
|
||
async def chat(self, messages: list, model: str = None, **kwargs) -> dict:
|
||
model = model or self.default_model
|
||
response = await self.client.chat.completions.create(
|
||
model=model,
|
||
messages=messages,
|
||
**kwargs
|
||
)
|
||
return {
|
||
"content": response.choices[0].message.content,
|
||
"usage": {
|
||
"prompt_tokens": response.usage.prompt_tokens,
|
||
"completion_tokens": response.usage.completion_tokens,
|
||
"total_tokens": response.usage.total_tokens,
|
||
},
|
||
"model": response.model,
|
||
}
|
||
|
||
async def health_check(self) -> bool:
|
||
try:
|
||
await self.client.models.list()
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
class AIClientFactory:
|
||
"""AI 客户端工厂"""
|
||
|
||
# 厂商类型到客户端类的映射
|
||
_client_classes: Dict[AIProviderType, Type[BaseAIClient]] = {
|
||
AIProviderType.DEEPSEEK: OpenAICompatibleClient,
|
||
AIProviderType.OPENAI: OpenAICompatibleClient,
|
||
AIProviderType.ONEAPI: OpenAICompatibleClient,
|
||
AIProviderType.QWEN: OpenAICompatibleClient,
|
||
AIProviderType.MOONSHOT: OpenAICompatibleClient,
|
||
AIProviderType.ZHIPU: OpenAICompatibleClient,
|
||
# 可扩展更多厂商...
|
||
}
|
||
|
||
def __init__(self, secrets_manager: SecretsManager):
|
||
self.secrets_manager = secrets_manager
|
||
self._client_cache: Dict[str, BaseAIClient] = {}
|
||
self._cache_lock = asyncio.Lock()
|
||
|
||
async def get_client(self, provider_config: dict) -> BaseAIClient:
|
||
"""获取或创建 AI 客户端"""
|
||
cache_key = f"{provider_config['id']}:{provider_config['updated_at']}"
|
||
|
||
if cache_key in self._client_cache:
|
||
return self._client_cache[cache_key]
|
||
|
||
async with self._cache_lock:
|
||
# 双重检查
|
||
if cache_key in self._client_cache:
|
||
return self._client_cache[cache_key]
|
||
|
||
# 解密 API Key
|
||
api_key = await self.secrets_manager.decrypt(
|
||
provider_config["api_key_encrypted"]
|
||
)
|
||
|
||
config = {
|
||
**provider_config,
|
||
"api_key": api_key,
|
||
}
|
||
|
||
# 创建客户端
|
||
provider_type = AIProviderType(provider_config["provider_type"])
|
||
client_class = self._client_classes.get(provider_type)
|
||
|
||
if not client_class:
|
||
raise ValueError(f"Unsupported provider type: {provider_type}")
|
||
|
||
client = client_class(config)
|
||
|
||
# 缓存客户端
|
||
self._client_cache[cache_key] = client
|
||
|
||
# 清理旧缓存
|
||
self._cleanup_old_cache(provider_config['id'])
|
||
|
||
return client
|
||
|
||
def _cleanup_old_cache(self, provider_id: str):
|
||
"""清理同一 provider 的旧缓存"""
|
||
keys_to_remove = [
|
||
k for k in self._client_cache.keys()
|
||
if k.startswith(f"{provider_id}:")
|
||
]
|
||
# 保留最新的一个
|
||
for key in keys_to_remove[:-1]:
|
||
del self._client_cache[key]
|
||
|
||
def invalidate_cache(self, provider_id: str = None):
|
||
"""使缓存失效"""
|
||
if provider_id:
|
||
keys_to_remove = [
|
||
k for k in self._client_cache.keys()
|
||
if k.startswith(f"{provider_id}:")
|
||
]
|
||
for key in keys_to_remove:
|
||
del self._client_cache[key]
|
||
else:
|
||
self._client_cache.clear()
|
||
```
|
||
|
||
### 4.3 AI 服务路由器
|
||
|
||
```python
|
||
# app/services/ai/router.py
|
||
|
||
from typing import Optional, List
|
||
from uuid import UUID
|
||
|
||
from app.models.ai_provider import AIUseCase, AICapability
|
||
from app.repositories.ai_provider_repo import AIProviderRepository
|
||
from app.services.ai.client_factory import AIClientFactory, BaseAIClient
|
||
|
||
|
||
class AIServiceRouter:
|
||
"""AI 服务路由器 - 根据场景选择合适的 AI 厂商"""
|
||
|
||
def __init__(
|
||
self,
|
||
provider_repo: AIProviderRepository,
|
||
client_factory: AIClientFactory,
|
||
):
|
||
self.provider_repo = provider_repo
|
||
self.client_factory = client_factory
|
||
|
||
async def get_client_for_use_case(
|
||
self,
|
||
use_case: AIUseCase,
|
||
tenant_id: Optional[UUID] = None,
|
||
required_capabilities: List[AICapability] = None,
|
||
) -> BaseAIClient:
|
||
"""
|
||
根据使用场景获取合适的 AI 客户端
|
||
|
||
优先级:
|
||
1. 租户专属配置 (tenant_id 匹配)
|
||
2. 全局默认配置 (tenant_id = NULL)
|
||
3. 按 priority 排序
|
||
"""
|
||
# 查询符合条件的配置
|
||
configs = await self.provider_repo.find_by_use_case(
|
||
use_case=use_case,
|
||
tenant_id=tenant_id,
|
||
capabilities=required_capabilities,
|
||
enabled_only=True,
|
||
)
|
||
|
||
if not configs:
|
||
raise ValueError(
|
||
f"No AI provider configured for use case: {use_case}"
|
||
)
|
||
|
||
# 选择优先级最高的配置
|
||
selected_config = configs[0]
|
||
|
||
# 创建并返回客户端
|
||
client = await self.client_factory.get_client(selected_config)
|
||
|
||
# 健康检查,失败则尝试备用
|
||
if not await client.health_check():
|
||
if selected_config.get("fallback_provider_id"):
|
||
fallback_config = await self.provider_repo.get_by_id(
|
||
selected_config["fallback_provider_id"]
|
||
)
|
||
if fallback_config:
|
||
client = await self.client_factory.get_client(fallback_config)
|
||
|
||
return client
|
||
|
||
async def chat(
|
||
self,
|
||
messages: list,
|
||
use_case: AIUseCase,
|
||
tenant_id: Optional[UUID] = None,
|
||
model: str = None,
|
||
**kwargs
|
||
) -> dict:
|
||
"""统一的对话接口"""
|
||
client = await self.get_client_for_use_case(
|
||
use_case=use_case,
|
||
tenant_id=tenant_id,
|
||
required_capabilities=[AICapability.CHAT],
|
||
)
|
||
|
||
return await client.chat(messages, model=model, **kwargs)
|
||
```
|
||
|
||
### 4.4 管理后台 API
|
||
|
||
```python
|
||
# app/api/v1/endpoints/admin/ai_providers.py
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from typing import List, Optional
|
||
from uuid import UUID
|
||
|
||
from app.models.ai_provider import (
|
||
AIProviderCreate,
|
||
AIProviderUpdate,
|
||
AIProviderResponse,
|
||
)
|
||
from app.services.ai_provider_service import AIProviderService
|
||
from app.api.deps import get_current_admin_user
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.post("", response_model=AIProviderResponse, status_code=status.HTTP_201_CREATED)
|
||
async def create_ai_provider(
|
||
request: AIProviderCreate,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""创建 AI 厂商配置(仅管理员)"""
|
||
return await service.create(request, created_by=current_user.id)
|
||
|
||
|
||
@router.get("", response_model=List[AIProviderResponse])
|
||
async def list_ai_providers(
|
||
tenant_id: Optional[UUID] = None,
|
||
provider_type: Optional[str] = None,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""获取 AI 厂商配置列表"""
|
||
return await service.list(tenant_id=tenant_id, provider_type=provider_type)
|
||
|
||
|
||
@router.get("/{provider_id}", response_model=AIProviderResponse)
|
||
async def get_ai_provider(
|
||
provider_id: UUID,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""获取单个 AI 厂商配置"""
|
||
provider = await service.get_by_id(provider_id)
|
||
if not provider:
|
||
raise HTTPException(status_code=404, detail="Provider not found")
|
||
return provider
|
||
|
||
|
||
@router.put("/{provider_id}", response_model=AIProviderResponse)
|
||
async def update_ai_provider(
|
||
provider_id: UUID,
|
||
request: AIProviderUpdate,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""更新 AI 厂商配置"""
|
||
provider = await service.update(provider_id, request)
|
||
if not provider:
|
||
raise HTTPException(status_code=404, detail="Provider not found")
|
||
return provider
|
||
|
||
|
||
@router.delete("/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||
async def delete_ai_provider(
|
||
provider_id: UUID,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""删除 AI 厂商配置"""
|
||
success = await service.delete(provider_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="Provider not found")
|
||
|
||
|
||
@router.post("/{provider_id}/test")
|
||
async def test_ai_provider(
|
||
provider_id: UUID,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""测试 AI 厂商连通性"""
|
||
result = await service.test_connection(provider_id)
|
||
return {
|
||
"success": result.success,
|
||
"latency_ms": result.latency_ms,
|
||
"error": result.error,
|
||
}
|
||
|
||
|
||
@router.post("/{provider_id}/rotate-key", response_model=AIProviderResponse)
|
||
async def rotate_api_key(
|
||
provider_id: UUID,
|
||
new_api_key: str,
|
||
service: AIProviderService = Depends(),
|
||
current_user = Depends(get_current_admin_user),
|
||
):
|
||
"""轮换 API Key"""
|
||
return await service.rotate_api_key(provider_id, new_api_key)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 安全设计
|
||
|
||
### 5.1 API Key 加密存储
|
||
|
||
```python
|
||
# app/services/secrets_manager.py
|
||
|
||
from cryptography.fernet import Fernet
|
||
from cryptography.hazmat.primitives import hashes
|
||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||
import base64
|
||
import os
|
||
|
||
|
||
class SecretsManager:
|
||
"""密钥管理器 - 负责加密/解密敏感信息"""
|
||
|
||
def __init__(self, master_key: str):
|
||
"""
|
||
初始化密钥管理器
|
||
|
||
Args:
|
||
master_key: 主密钥,从安全存储(如 Vault、KMS)获取
|
||
"""
|
||
# 从主密钥派生加密密钥
|
||
salt = os.environ.get("ENCRYPTION_SALT", "smartaudit").encode()
|
||
kdf = PBKDF2HMAC(
|
||
algorithm=hashes.SHA256(),
|
||
length=32,
|
||
salt=salt,
|
||
iterations=100000,
|
||
)
|
||
key = base64.urlsafe_b64encode(kdf.derive(master_key.encode()))
|
||
self.fernet = Fernet(key)
|
||
|
||
async def encrypt(self, plaintext: str) -> bytes:
|
||
"""加密明文"""
|
||
return self.fernet.encrypt(plaintext.encode())
|
||
|
||
async def decrypt(self, ciphertext: bytes) -> str:
|
||
"""解密密文"""
|
||
return self.fernet.decrypt(ciphertext).decode()
|
||
```
|
||
|
||
### 5.2 权限控制
|
||
|
||
| 操作 | 系统管理员 | 品牌方管理员 | 代理商 | 达人 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 创建全局配置 | ✅ | ❌ | ❌ | ❌ |
|
||
| 创建租户配置 | ✅ | ✅ (仅自己租户) | ❌ | ❌ |
|
||
| 查看配置列表 | ✅ (全部) | ✅ (仅自己租户) | ❌ | ❌ |
|
||
| 修改配置 | ✅ | ✅ (仅自己租户) | ❌ | ❌ |
|
||
| 删除配置 | ✅ | ✅ (仅自己租户) | ❌ | ❌ |
|
||
| 查看 API Key | ❌ | ❌ | ❌ | ❌ |
|
||
| 轮换 API Key | ✅ | ✅ (仅自己租户) | ❌ | ❌ |
|
||
|
||
---
|
||
|
||
## 6. 配置热更新
|
||
|
||
### 6.1 更新机制
|
||
|
||
```python
|
||
# app/services/ai/config_watcher.py
|
||
|
||
import asyncio
|
||
from datetime import datetime
|
||
from typing import Callable, List
|
||
|
||
from app.repositories.ai_provider_repo import AIProviderRepository
|
||
from app.services.ai.client_factory import AIClientFactory
|
||
|
||
|
||
class ConfigWatcher:
|
||
"""配置变更监听器"""
|
||
|
||
def __init__(
|
||
self,
|
||
provider_repo: AIProviderRepository,
|
||
client_factory: AIClientFactory,
|
||
poll_interval: int = 30, # 秒
|
||
):
|
||
self.provider_repo = provider_repo
|
||
self.client_factory = client_factory
|
||
self.poll_interval = poll_interval
|
||
self._last_check = datetime.min
|
||
self._running = False
|
||
self._callbacks: List[Callable] = []
|
||
|
||
def on_config_change(self, callback: Callable):
|
||
"""注册配置变更回调"""
|
||
self._callbacks.append(callback)
|
||
|
||
async def start(self):
|
||
"""启动监听"""
|
||
self._running = True
|
||
while self._running:
|
||
await self._check_for_changes()
|
||
await asyncio.sleep(self.poll_interval)
|
||
|
||
async def stop(self):
|
||
"""停止监听"""
|
||
self._running = False
|
||
|
||
async def _check_for_changes(self):
|
||
"""检查配置变更"""
|
||
changed_configs = await self.provider_repo.find_updated_since(
|
||
self._last_check
|
||
)
|
||
|
||
if changed_configs:
|
||
self._last_check = datetime.utcnow()
|
||
|
||
# 使相关缓存失效
|
||
for config in changed_configs:
|
||
self.client_factory.invalidate_cache(config["id"])
|
||
|
||
# 触发回调
|
||
for callback in self._callbacks:
|
||
await callback(changed_configs)
|
||
```
|
||
|
||
### 6.2 应用启动集成
|
||
|
||
```python
|
||
# app/main.py
|
||
|
||
from contextlib import asynccontextmanager
|
||
from fastapi import FastAPI
|
||
|
||
from app.services.ai.config_watcher import ConfigWatcher
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# 启动时
|
||
config_watcher = ConfigWatcher(
|
||
provider_repo=app.state.provider_repo,
|
||
client_factory=app.state.client_factory,
|
||
)
|
||
asyncio.create_task(config_watcher.start())
|
||
|
||
yield
|
||
|
||
# 关闭时
|
||
await config_watcher.stop()
|
||
|
||
|
||
app = FastAPI(lifespan=lifespan)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 使用示例
|
||
|
||
### 7.1 在业务代码中使用
|
||
|
||
```python
|
||
# app/services/brief_parser.py
|
||
|
||
from app.services.ai.router import AIServiceRouter
|
||
from app.models.ai_provider import AIUseCase
|
||
|
||
|
||
class BriefParserService:
|
||
"""Brief 解析服务"""
|
||
|
||
def __init__(self, ai_router: AIServiceRouter):
|
||
self.ai_router = ai_router
|
||
|
||
async def parse_brief(self, content: str, tenant_id: UUID = None) -> dict:
|
||
"""解析 Brief 文档"""
|
||
|
||
messages = [
|
||
{"role": "system", "content": "你是一个专业的 Brief 解析助手..."},
|
||
{"role": "user", "content": f"请解析以下 Brief 内容:\n{content}"},
|
||
]
|
||
|
||
# 自动选择合适的 AI 厂商
|
||
result = await self.ai_router.chat(
|
||
messages=messages,
|
||
use_case=AIUseCase.BRIEF_PARSING,
|
||
tenant_id=tenant_id,
|
||
)
|
||
|
||
return self._parse_response(result["content"])
|
||
```
|
||
|
||
### 7.2 管理员配置流程
|
||
|
||
```
|
||
1. 管理员登录后台
|
||
2. 进入「系统设置 → AI 厂商管理」
|
||
3. 点击「添加厂商」
|
||
4. 填写配置:
|
||
- 名称:生产环境 DeepSeek
|
||
- 厂商类型:DeepSeek
|
||
- Base URL:https://api.deepseek.com/v1
|
||
- API Key:sk-xxx
|
||
- 默认模型:deepseek-chat
|
||
- 适用场景:Brief 解析、脚本预审
|
||
- 优先级:10
|
||
5. 点击「测试连通性」
|
||
6. 保存配置
|
||
7. 配置立即生效,无需重启服务
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 监控与告警
|
||
|
||
### 8.1 监控指标
|
||
|
||
| 指标 | 说明 | 告警阈值 |
|
||
| --- | --- | --- |
|
||
| `ai_request_total` | AI 请求总数 | - |
|
||
| `ai_request_latency_p99` | P99 延迟 | > 10s |
|
||
| `ai_request_error_rate` | 错误率 | > 5% |
|
||
| `ai_token_usage_total` | Token 使用量 | 接近配额 80% |
|
||
| `ai_provider_health` | 厂商健康状态 | 连续失败 > 3 次 |
|
||
|
||
### 8.2 告警规则
|
||
|
||
```yaml
|
||
# prometheus/alerts/ai_provider.yml
|
||
groups:
|
||
- name: ai_provider
|
||
rules:
|
||
- alert: AIProviderHighErrorRate
|
||
expr: rate(ai_request_errors_total[5m]) / rate(ai_request_total[5m]) > 0.05
|
||
for: 2m
|
||
labels:
|
||
severity: warning
|
||
annotations:
|
||
summary: "AI 厂商 {{ $labels.provider }} 错误率过高"
|
||
|
||
- alert: AIProviderDown
|
||
expr: ai_provider_health == 0
|
||
for: 1m
|
||
labels:
|
||
severity: critical
|
||
annotations:
|
||
summary: "AI 厂商 {{ $labels.provider }} 不可用"
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 相关文档
|
||
|
||
| 文档 | 说明 |
|
||
| --- | --- |
|
||
| DevelopmentPlan.md | 开发计划(已更新 AI 配置章节) |
|
||
| RequirementsDoc.md | 需求文档 |
|
||
| FeatureSummary.md | 功能清单 |
|
||
| API 接口规范 | 待编写 |
|