From 83737090bf1c76b9a7fa5371f8a5bc7a9b97fee4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Feb 2026 18:31:29 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20AI=20=E5=8E=82?= =?UTF-8?q?=E5=95=86=E5=8A=A8=E6=80=81=E9=85=8D=E7=BD=AE=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- AIProviderConfig.md | 912 ++++++++++++++++++++++++++++++++++++++++++++ DevelopmentPlan.md | 20 + FeatureSummary.md | 57 +++ PRD.md | 3 + RequirementsDoc.md | 2 + 5 files changed, 994 insertions(+) create mode 100644 AIProviderConfig.md diff --git a/AIProviderConfig.md b/AIProviderConfig.md new file mode 100644 index 0000000..5261370 --- /dev/null +++ b/AIProviderConfig.md @@ -0,0 +1,912 @@ +# 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 接口规范 | 待编写 | diff --git a/DevelopmentPlan.md b/DevelopmentPlan.md index 0782dc1..371b86a 100644 --- a/DevelopmentPlan.md +++ b/DevelopmentPlan.md @@ -27,6 +27,7 @@ | V1.2 | 2026-02-03 | Claude | Reviewer 修正:Logo检测改向量检索、Brief解析增VLM、弹性GPU、H5防锁屏、排期调整 | | V1.2.1 | 2026-02-03 | Claude | 补充多模态时间戳对齐流程图 (Gemini 建议) | | V1.3 | 2026-02-02 | Claude | **确立 TDD 为项目核心开发规范**,关联 tdd_plan.md | +| V1.4 | 2026-02-02 | Claude | **新增 AI 厂商动态配置架构**,支持数据库配置、运行时热更新、多租户隔离 | --- @@ -111,6 +112,24 @@ graph TD | **版面分析 (Layout)** | **PaddleOCR Layout / LayoutLMv3** | Brief PDF 版面分析,提取图文混排结构 | | **竞品 Logo 检测** | **Grounding DINO + Vector DB** | ⭐ V1.2 修正:改为向量检索方案,见下方说明 | +> ⭐ **V1.3 重要更新 - AI 厂商动态配置:** +> +> 本系统采用**商业 SaaS 级别的 AI 厂商动态配置架构**,详见 [AIProviderConfig.md](./AIProviderConfig.md)。 +> +> **核心特性:** +> - **数据库存储配置:** AI 厂商的 API Key、Base URL 等配置存储在数据库中,而非环境变量 +> - **运行时动态加载:** 管理员可在后台配置 AI 厂商,系统运行时动态读取配置初始化客户端 +> - **多租户隔离:** 不同品牌方可配置独立的 AI 厂商和配额 +> - **热更新:** 配置变更即时生效,无需重启服务 +> - **故障转移:** 主厂商不可用时自动切换到备用厂商 +> - **API Key 加密:** 使用 Fernet 对称加密存储敏感信息 +> +> **支持的厂商类型:** +> - 国内厂商:DeepSeek、通义千问、豆包、智谱、百川、Moonshot +> - 海外厂商:OpenAI、Anthropic(需注意合规) +> - 中转服务:OneAPI、OpenRouter +> - 本地部署:Ollama、vLLM + > ⚠️ **V1.2 重要修正 - Logo 检测架构变更:** > > **废弃方案:** ~~YOLOv8 Fine-tuning~~ @@ -494,5 +513,6 @@ sequenceDiagram | User_Role_Interfaces.md | 界面规范 | | tasks.md | 开发任务清单 | | **featuredoc/tdd_plan.md** | **TDD 实施计划(核心规范)** | +| **AIProviderConfig.md** | **AI 厂商动态配置架构设计(V1.3 新增)** | | 数据字典 | 待编写 | | API 接口规范 | 待编写 | diff --git a/FeatureSummary.md b/FeatureSummary.md index af86339..a1e946e 100644 --- a/FeatureSummary.md +++ b/FeatureSummary.md @@ -17,6 +17,7 @@ | V1.0 | 2026-02-02 | Claude | 基于 RD/PRD/UI 文档整合产出功能清单 | | V1.1 | 2026-02-02 | Claude | 根据 Gemini 修订意见调整:补充验收标准、Out of Scope、核心痛点细化 | | V1.2 | 2026-02-02 | Claude | 根据 Gemini 关键改进意见:优先级调整、功能拆分、新增功能、移动端适配 | +| V1.3 | 2026-02-02 | Claude | **新增 AI 厂商动态配置功能模块 (F-47~F-50)**,支持数据库配置、多租户隔离 | **Gemini 修订意见采纳情况:** @@ -686,6 +687,57 @@ V1 版本指出 3 个违规点:✅ 已修复 2 个 | ❌ 未修复 1 个 | --- | --- | --- | --- | --- | | F-46 | 负样本清洗与回流 | P2 | - | 系统 | +--- + +### 3.11 系统管理 - AI 厂商配置 (V1.4 新增) + +| 功能编号 | 功能名称 | 优先级 | 用户故事 | 使用角色 | +| --- | --- | --- | --- | --- | +| F-47 | AI 厂商动态配置 | P0 | - | 系统管理员 | +| F-48 | AI 厂商连通性测试 | P0 | - | 系统管理员 | +| F-49 | 多租户 AI 配置隔离 | P1 | - | 系统管理员/品牌方 | +| F-50 | API Key 轮换管理 | P1 | - | 系统管理员 | + +#### F-47 AI 厂商动态配置 ⭐ P0 + +**功能描述:** 系统管理员可在后台配置多个 AI 厂商(DeepSeek、OpenAI、通义千问、OneAPI 中转等),配置存储在数据库中,运行时动态加载,无需修改代码或重启服务。 + +**核心功能:** +- 支持添加、编辑、删除 AI 厂商配置 +- 配置 Base URL、API Key(加密存储)、默认模型 +- 为不同使用场景(Brief 解析、脚本预审、视频审核)指定不同厂商 +- 配置优先级和备用厂商(故障转移) + +**为什么是 P0:** 这是 AI 服务的基础设施,所有 AI 功能都依赖此配置。 + +**界面映射:** 系统管理后台 → AI 厂商管理 + +**技术文档:** 详见 [AIProviderConfig.md](./AIProviderConfig.md) + +--- + +#### F-48 AI 厂商连通性测试 + +**功能描述:** 配置 AI 厂商后,可测试连通性,验证 API Key 是否有效。 + +**界面映射:** 系统管理后台 → AI 厂商管理 → [测试连通性] + +--- + +#### F-49 多租户 AI 配置隔离 + +**功能描述:** 不同品牌方可配置独立的 AI 厂商,实现租户级别的配置隔离和配额管理。 + +**界面映射:** 品牌方后台 → 系统设置 → AI 配置 + +--- + +#### F-50 API Key 轮换管理 + +**功能描述:** 支持定期轮换 API Key,无需重启服务即可生效。 + +**界面映射:** 系统管理后台 → AI 厂商管理 → [轮换密钥] + #### F-46 负样本清洗与回流 (Feedback Loop) **功能描述:** 系统自动收集"人工驳回 AI 判定"的案例,清洗为微调数据集,用于后续模型优化。 @@ -730,6 +782,8 @@ V1 版本指出 3 个违规点:✅ 已修复 2 个 | ❌ 未修复 1 个 | F-19 | 风险列表展示 | 审核台 | | | F-20 | 确认/驳回操作 | 审核台 | | | F-33 | 核心指标卡片 | 数据看板 | | +| F-47 | AI 厂商动态配置 | 系统管理 | ⭐ V1.3 新增,AI 基础设施 | +| F-48 | AI 厂商连通性测试 | 系统管理 | ⭐ V1.3 新增 | ### 4.2 V1.1 (P1) - 首版后快速迭代 @@ -747,6 +801,8 @@ V1 版本指出 3 个违规点:✅ 已修复 2 个 | ❌ 未修复 1 个 | F-34~36 | 趋势图表与预警 | 数据看板 | | | F-38~40 | 审计日志与证据导出 | 审计 | | | F-43 | 舆情阈值设置 | 舆情 | | +| F-49 | 多租户 AI 配置隔离 | 系统管理 | ⭐ V1.3 新增 | +| F-50 | API Key 轮换管理 | 系统管理 | ⭐ V1.3 新增 | > ⚠️ **注意:** F-09 (语境理解) 和 F-17 (进度展示) 已提升至 P0 @@ -846,6 +902,7 @@ V1 版本指出 3 个违规点:✅ 已修复 2 个 | ❌ 未修复 1 个 | RequirementsDoc.md | 业务需求文档(用户故事、成功指标) | | PRD.md | 产品需求文档(功能需求、技术架构) | | User_Role_Interfaces.md | 用户角色与界面规范 | +| **AIProviderConfig.md** | **AI 厂商动态配置架构设计(V1.3 新增)** | | 技术设计文档 (TDD) | 待编写 | | API 接口规范 | 待编写 | | 数据字典 | 待编写 | diff --git a/PRD.md b/PRD.md index 8529402..ef43242 100644 --- a/PRD.md +++ b/PRD.md @@ -18,6 +18,7 @@ | V0.2 | 2026-01-30 | ClaudeCode | 根据 RD 审阅修订:补充技术架构、术语定义、用户故事引用、品牌方工作流 | | V0.3 | 2026-01-30 | Codex | 合规一致性修订:补充一致性定义、软性风控提示边界与特例记录规范 | | V0.4 | 2026-01-30 | Claude | 审阅调整:补充产品愿景与量化目标、假设与约束章节、细化背景数据 | +| V1.0 | 2026-02-02 | Claude | 新增 AI 厂商动态配置架构引用 | --- @@ -351,6 +352,7 @@ - **ASR/OCR**:支持普通话及主流方言的语音识别,支持复杂背景字幕识别 - **计算机视觉**:Logo 检测、物体识别、场景分类 - **消息队列**:异步处理视频审核任务,支持优先级调度 +- **AI 厂商动态配置**:支持在数据库中配置多个 AI 厂商(DeepSeek/OpenAI/OneAPI 等),运行时动态加载,支持多租户隔离和故障转移(详见 AIProviderConfig.md) --- @@ -378,6 +380,7 @@ ## 16. 相关文档 (References) - RequirementsDoc.md - 业务需求文档 +- **AIProviderConfig.md - AI 厂商动态配置架构设计** - 技术设计文档 (TDD) - 待编写 - API 接口规范 - 待编写 - 数据字典 - 待编写 diff --git a/RequirementsDoc.md b/RequirementsDoc.md index 5f776d7..4acde0f 100644 --- a/RequirementsDoc.md +++ b/RequirementsDoc.md @@ -187,6 +187,7 @@ * **ASR/OCR:** 支持普通话及主流方言的语音识别,支持复杂背景字幕识别 * **计算机视觉:** Logo 检测、物体识别、场景分类 * **消息队列:** 异步处理视频审核任务,支持优先级调度 +* **AI 厂商动态配置:** 支持在数据库中配置多个 AI 厂商(DeepSeek/OpenAI/OneAPI 等),运行时动态加载,支持多租户隔离和故障转移(详见 AIProviderConfig.md) --- @@ -241,6 +242,7 @@ ### 11.1 相关文档 * 技术设计文档 (TDD) - 待编写 +* **AIProviderConfig.md - AI 厂商动态配置架构设计** * API 接口规范 - 待编写 * 数据字典 - 待编写 * 测试计划 - 待编写