多 Provider 图片生成服务 — 架构设计文档
脱敏声明: 文档中所有具体域名、API Key、SDK 包名、模型名称均已替换为占位符描述,正式部署时请替换为实际值。 适用场景: 文档生成、图片创作、AI 图像生成等需要调用外部图像 API 的服务。
1. 背景与目标
1.1 为什么需要多 Provider?
在 AI 图像生成应用中,依赖单一模型供应商存在以下风险:
| 风险类型 | 描述 |
|---|---|
| 可用性风险 | 单点故障导致服务不可用,影响用户体验 |
| 成本风险 | 高峰时段 API 定价波动影响成本可控性 |
| 能力覆盖风险 | 单一模型在特定类型图片(如文字排版、风格一致性)上表现不稳定 |
| 网络依赖风险 | 某些 Provider 在特定地区无法访问或响应缓慢 |
1.2 本服务的设计目标
| 目标 | 说明 |
|---|---|
| 高可用 | 主备 Provider 自动切换,单一节点故障不影响整体服务 |
| 成本可控 | 优先使用低单价 Provider,必要时切换高单价兜底 |
| 能力互补 | 不同模型各有优势,按场景类型选择最适合的生成器 |
| 灵活扩展 | 新增 Provider 只需实现统一接口,无需修改核心调度逻辑 |
| 网络兼容 | 自动适配不同 Provider 的网络访问要求(如代理/直连) |
2. 整体架构
2.1 架构总览
┌─────────────────────────────────────────────────────────┐
│ 调用入口层 │
│ 文档生成服务 / 任务池 / SSE 流式响应 │
└──────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ ImageGenerationService │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 调度策略:Primary / Fallback │ │
│ │ · 有本地参考图 → 优先 Primary(支持图片理解) │ │
│ │ · Primary 重试耗尽 → 自动切换 Fallback │ │
│ │ · 无本地参考图 + 有远端资产ID → 直接用 Fallback │ │
│ └─────────────────────────────────────────────────┘ │
└──────────┬──────────────────────────┬──────────────────┘
│ primary_provider │ fallback_provider
▼ ▼
┌──────────────────────┐ ┌────────────────────────────────┐
│ PrimaryGenerator │ │ FallbackGenerator │
│ ┌────────────────┐ │ │ ┌────────────────────────┐ │
│ │ Provider A │ │ │ │ Provider B(资产引用) │ │
│ │ · 本地参考图 │ │ │ │ · 资产 ID 列表 │ │
│ │ · base64 上传 │ │ │ │ · 无需本地文件 │ │
│ │ · 流式 SSE │ │ │ · 异步轮询结果 │ │
│ └────────────────┘ │ │ └────────────────────────┘ │
└──────────────────────┘ └────────────────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────────┐
│ 供应商 A │ │ 供应商 B │
│ (海外 API) │ │ (国内直连 API) │
│ VPN 路由 │ │ 直连 │
└─────────────┘ └─────────────────┘
2.2 核心设计思想
- 注册表模式:所有 Provider 在模块加载时自动注册,新增 Provider 零改动
- 主备降级:Primary 失败自动切换 Fallback,保证服务可用性
- 滑动窗口并发池:控制并发数、失败重试、自动入队调度
- 网络兼容:不同 Provider 的网络要求(代理/直连)在 Provider 层各自处理
3. 核心组件详解
3.1 Provider 注册中心
文件:src/core/providers/provider_registry.py
采用注册表模式(Registry Pattern),所有 Provider 在模块加载时自动注册到全局字典:
from typing import Dict, Type
PROVIDER_REGISTRY: Dict[str, Type["BaseImageGenerator"]] = {}
def register_provider(name: str, generator_class: Type["BaseImageGenerator"]) -> None:
"""注册 Provider,自动校验接口兼容性"""
if not issubclass(generator_class, BaseImageGenerator):
raise TypeError(f"{generator_class.__name__} 必须继承 BaseImageGenerator")
PROVIDER_REGISTRY[name] = generator_class
def create_generator(provider: str, **config) -> "BaseImageGenerator":
"""根据名称创建 Provider 实例"""
if provider not in PROVIDER_REGISTRY:
available = ", ".join(PROVIDER_REGISTRY.keys())
raise ValueError(f"Unknown provider: '{provider}'. Available: {available}")
return PROVIDER_REGISTRY[provider](**config)
扩展新 Provider 的标准流程:
- 实现
BaseImageGenerator抽象类 - 在对应模块末尾调用
register_provider("your_provider", YourProviderGenerator) - 在配置文件中添加 Provider 名称即可启用
3.2 统一接口契约
文件:src/core/providers/base_generator.py
所有 Provider 必须继承 BaseImageGenerator 并实现以下方法:
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
@dataclass
class ImageGenerationResult:
"""统一返回格式"""
success: bool
error: Optional[str] = None
error_code: Optional[str] = None # 结构化错误码,便于 Fallback 判断
image_path: Optional[str] = None # 本地保存路径
image_data: Optional[bytes] = None # 原始图片数据
image_url: Optional[str] = None # 远程图片 URL
metadata: Dict[str, Any] = field(default_factory=dict) # Provider 原始响应
class BaseImageGenerator(ABC):
"""图片生成器抽象基类"""
@property
@abstractmethod
def provider_name(self) -> str:
"""Provider 标识名称"""
pass
@property
@abstractmethod
def supports_reference_images(self) -> bool:
"""是否支持参考图输入(影响调度策略)"""
pass
@property
@abstractmethod
def requires_async_mode(self) -> bool:
"""是否需要异步轮询模式"""
pass
@abstractmethod
def generate(
self,
prompt: str,
reference_image_paths: Optional[List[str]] = None,
reference_asset_ids: Optional[List[str]] = None,
**kwargs
) -> ImageGenerationResult:
"""同步生成"""
pass
@abstractmethod
async def generate_async(
self,
prompt: str,
reference_image_paths: Optional[List[str]] = None,
reference_asset_ids: Optional[List[str]] = None,
**kwargs
) -> ImageGenerationResult:
"""异步生成(流式/SSE 场景)"""
pass
@abstractmethod
def download_and_save_image(self, url: str, save_path: str) -> bool:
"""下载并保存图片"""
pass
3.3 Primary / Fallback 调度器
文件:src/core/services/image_generation_service.py
import asyncio
from typing import Optional, List, Dict, Any
class ImageGenerationService:
"""
统一调度器:自动选择 Primary / Fallback Provider
"""
def __init__(
self,
primary_provider: str,
fallback_provider: str,
providers_config: Dict[str, Dict[str, Any]],
):
self.primary = create_provider(primary_provider, providers_config.get(primary_provider, {}))
self.fallback = create_provider(fallback_provider, providers_config.get(fallback_provider, {}))
async def generate(
self,
prompt: str,
reference_image_paths: Optional[List[str]] = None,
reference_asset_ids: Optional[List[str]] = None,
**kwargs
) -> ImageGenerationResult:
# ── 调度策略 ──
# 1. 有本地参考图 → 优先 Primary
if reference_image_paths and self.primary.supports_reference_images:
result = await self._try_primary(prompt, reference_image_paths, **kwargs)
if result.success:
return result
# 2. Primary 重试耗尽 → 切换 Fallback
if self._is_retry_exhausted(result):
if self.fallback.requires_async_mode:
return await self._try_fallback(prompt, reference_asset_ids, **kwargs)
else:
return await self._try_fallback(prompt, reference_image_paths, **kwargs)
return result
# 3. 无本地参考图 + 有资产ID → 直接走 Fallback
if reference_asset_ids and self.fallback.requires_async_mode:
return await self._try_fallback(prompt, reference_asset_ids, **kwargs)
# 4. 默认走 Primary
return await self._try_primary(prompt, reference_image_paths, **kwargs)
async def _try_primary(self, prompt: str, assets, **kwargs) -> ImageGenerationResult:
# 实际调用 + 重试逻辑(指数退避)
pass
async def _try_fallback(self, prompt: str, assets, **kwargs) -> ImageGenerationResult:
# Fallback 调用
pass
def _is_retry_exhausted(self, result: ImageGenerationResult) -> bool:
"""判断是否应该触发 Fallback"""
# 429 限流、内容审核拒绝、网络超时 → 应切换
RETRYABLE_CODES = {"RATE_LIMIT", "CONTENT_FILTER", "TIMEOUT", "NETWORK_ERROR"}
return result.error_code in RETRYABLE_CODES
3.4 并发任务池
文件:src/core/services/image_task_pool.py
采用滑动窗口模式,保证容器中始终有 N 个并发任务运行:
import asyncio
from typing import List, Callable, Any, Coroutine
class ImageGenerationTaskPool:
"""
并发任务池(滑动窗口)
- 控制最大并发数,避免 Provider 限流
- 失败自动重试,自动入队
"""
def __init__(self, max_workers: int = 5, max_retries: int = 2):
self.max_workers = max_workers
self.max_retries = max_retries
self.running: int = 0
self.pending_queue: asyncio.Queue = asyncio.Queue()
self.retry_counts: Dict[int, int] = {}
async def submit_batch(
self,
tasks: List[Coroutine],
on_progress: Callable[[int, int, Any], None] = None,
) -> List[Any]:
"""
提交一批任务,自动控制并发
Args:
tasks: 任务列表
on_progress: 进度回调 (idx, total, result)
"""
results = [None] * len(tasks)
for idx, task in enumerate(tasks):
await self.pending_queue.put((idx, task))
async def worker():
while True:
try:
idx, task_coro = self.pending_queue.get_nowait()
except asyncio.QueueEmpty:
break
self.running += 1
try:
result = await task_coro
results[idx] = result
if not self._is_success(result) and self.retry_counts.get(idx, 0) < self.max_retries:
self.retry_counts[idx] = self.retry_counts.get(idx, 0) + 1
await self.pending_queue.put((idx, task_coro))
if on_progress:
await on_progress(idx, len(tasks), result)
finally:
self.running -= 1
self.pending_queue.task_done()
workers = [asyncio.create_task(worker()) for _ in range(self.max_workers)]
await asyncio.gather(*workers)
return results
def _is_success(self, result: Any) -> bool:
return getattr(result, "success", False)
3.5 提示词管理器(按场景类型)
文件:src/core/prompts/prompt_manager.py
每个 Provider 维护独立的 YAML 提示词目录,按场景类型精准匹配:
yaml_prompts/
provider_a/ ← Provider A 专用提示词
封面.yaml
背景页.yaml
内容页.yaml
数据图表.yaml
...
provider_b/ ← Provider B 专用提示词
封面.yaml
背景页.yaml
内容页.yaml
...
使用方式:
class PromptManager:
def __init__(self, prompts_dir: str = "yaml_prompts"):
self.prompts_dir = prompts_dir
self._cache: Dict[str, Dict[str, str]] = {}
def get_prompt(self, provider: str, page_type: str) -> str:
cache_key = f"{provider}:{page_type}"
if cache_key not in self._cache:
path = Path(self.prompts_dir) / provider / f"{page_type}.yaml"
self._cache[cache_key] = yaml.safe_load(path.read_text())
return self._cache[cache_key]["prompt"]
4. 调度决策树
用户请求
│
├── 有本地参考图路径?
│ │
│ ├─ 是 ──→ 检查 Primary 是否支持参考图
│ │ │
│ │ ├─ 支持 ──→ 调用 Primary
│ │ │ ├─ 成功 ──→ 返回结果
│ │ │ ├─ 限流/超时/审核拒绝 ──→ 切换 Fallback
│ │ │ └─ 其他错误 ──→ 返回错误
│ │ │
│ │ └─ 不支持 ──→ 直接调用 Fallback
│ │
│ └─ 否
│ │
│ ├── 有远端资产 ID 列表?
│ │ │
│ │ ├─ 是 ──→ 调用 Fallback(支持资产引用)
│ │ └─ 否 ──→ 调用 Primary(无参考图模式)
│ │
└── (任务池层处理并发、重试、进度推送)
5. Provider 配置说明
5.1 Provider A(Primary)
适用于:需要图片理解/风格迁移、支持本地参考图输入的场景。
| 配置项 | 说明 |
|---|---|
| API 端点 | https://api.provider-a.example/v1/chat/completions |
| 鉴权方式 | Bearer Token(通过环境变量注入) |
| 参考图输入 | 本地文件路径 → base64 压缩后内嵌请求体 |
| 输出格式 | 流式 SSE,内含 base64 编码图片数据 |
| 重试策略 | 429 限流 / 内容审核拒绝:指数退避,最多 3 次 |
5.2 Provider B(Fallback)
适用于:国内直连、成本较低、支持资产 ID 引用的场景。
| 配置项 | 说明 |
|---|---|
| API 端点 | 国内直连域名(无需代理) |
| 鉴权方式 | 云服务商 SecretId / SecretKey(通过配置文件注入) |
| 参考图输入 | 预上传后获得的资产 ID 列表(0~3 个) |
| 输出格式 | 异步任务 → 轮询获取结果 → 下载图片 URL |
| 轮询策略 | 每 3 秒轮询一次,最多 120 次(约 6 分钟超时) |
6. 如何接入新的 Provider
以接入一个新 Provider 为例,完整步骤如下:
6.1 实现 Provider 类
文件:src/core/providers/generator_openai.py
from .base_generator import BaseImageGenerator, ImageGenerationResult, register_provider
from .network_helper import download_image_bytes
import requests, base64, os
class OpenAIImageGenerator(BaseImageGenerator):
def __init__(self, api_key: str, base_url: str = "https://api.openai.com", **kwargs):
self.api_key = api_key
self.base_url = base_url
self.max_retries = kwargs.get("max_retries", 3)
@property
def provider_name(self) -> str:
return "openai"
@property
def supports_reference_images(self) -> bool:
return True # 支持图片参考
@property
def requires_async_mode(self) -> bool:
return False # 同步返回
def _prepare_reference_images(self, paths: List[str]) -> List[Dict]:
"""将本地图片转为 base64 格式"""
images = []
for path in paths:
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode()
images.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{data}"}})
return images
async def generate_async(
self,
prompt: str,
reference_image_paths: List[str] = None,
**kwargs
) -> ImageGenerationResult:
try:
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
body = {"prompt": prompt, "n": 1, "size": "1024x1024"}
if reference_image_paths:
body["images"] = self._prepare_reference_images(reference_image_paths)
resp = requests.post(
f"{self.base_url}/v1/images/generations",
headers=headers, json=body, timeout=60
)
if resp.status_code == 200:
image_url = resp.json()["data"][0]["url"]
image_data = download_image_bytes(image_url)
return ImageGenerationResult(success=True, image_data=image_data, metadata=resp.json())
else:
return ImageGenerationResult(
success=False,
error=resp.text,
error_code=self._classify_error(resp.status_code)
)
except Exception as e:
return ImageGenerationResult(success=False, error=str(e), error_code="NETWORK_ERROR")
def generate(self, prompt: str, **kwargs) -> ImageGenerationResult:
return asyncio.run(self.generate_async(prompt, **kwargs))
def download_and_save_image(self, url: str, save_path: str) -> bool:
data = download_image_bytes(url)
if data:
os.makedirs(os.path.dirname(save_path), exist_ok=True)
with open(save_path, "wb") as f:
f.write(data)
return True
return False
def _classify_error(self, status_code: int) -> str:
mapping = {429: "RATE_LIMIT", 400: "BAD_REQUEST", 401: "AUTH_ERROR", 500: "SERVER_ERROR"}
return mapping.get(status_code, "UNKNOWN_ERROR")
# 注册到注册中心
register_provider("openai", OpenAIImageGenerator)
6.2 添加配置文件
# config/image_providers.yaml
providers:
openai:
enabled: true
api_key: ${OPENAI_API_KEY}
base_url: "https://api.openai.com"
max_retries: 3
timeout: 60
siliconflow: # Fallback Provider
enabled: true
api_key: ${SILICONFLOW_API_KEY}
base_url: "https://api.siliconflow.cn/v1"
max_retries: 3
timeout: 120
6.3 启用 Provider
# 在应用初始化时
from src.core.providers import create_generator
primary = create_generator("openai", config={"api_key": os.getenv("OPENAI_API_KEY")})
fallback = create_generator("siliconflow", config={"api_key": os.getenv("SILICONFLOW_API_KEY")})
service = ImageGenerationService(primary=primary, fallback=fallback)
7. 网络路由策略
| 流量类型 | 路由方式 | 说明 |
|---|---|---|
| Provider A(海外 API) | VPN / 代理 | 海外 endpoint,需配置出口代理 |
| Provider B(国内 API) | 直连 | 国内云服务,直接访问 |
| 结果图片下载 | 直连 | CDN / 云存储,均为国内域名 |
推荐实现: 在系统代理层(如 Clash Verge TUN 模式)按域名规则分流,业务代码无感知。详见:多网络环境隔离方案
8. 目录结构
src/
├── core/
│ ├── providers/
│ │ ├── provider_registry.py ← Provider 注册中心
│ │ ├── base_generator.py ← 统一接口契约(抽象基类)
│ │ ├── generator_provider_a.py ← Provider A 实现(示例)
│ │ ├── generator_provider_b.py ← Provider B 实现(示例)
│ │ ├── generator_openai.py ← OpenAI Provider 示例
│ │ └── network_helper.py ← 网络请求辅助工具
│ └── services/
│ ├── image_generation_service.py ← 调度器(Primary/Fallback)
│ └── image_task_pool.py ← 并发任务池
├── prompts/
│ └── page_type_manager.py ← 提示词管理器
└── config/
└── image_providers.yaml ← Provider 配置文件
9. 已知限制与改进方向
9.1 当前版本已知限制
| 编号 | 限制描述 | 改进建议 | 状态 |
|---|---|---|---|
| ✅ 已修复 | |||
| ✅ 已修复 | |||
| ✅ 已修复 | |||
| L4 | 部分 Provider 接受语义尺寸(如"2K"),不支持精确像素映射 | 在调度层统一归一化为像素尺寸 | 待处理 |
9.2 可选增强方向
| 增强项 | 说明 |
|---|---|
| 指标采集 | 接入 Prometheus,采集每个 Provider 的成功率、平均耗时、成本 |
| 熔断器 | 当某个 Provider 连续失败 N 次后,自动熔断降级,不再尝试 |
| 灰度切换 | 支持按用户、按场景类型选择 Provider,逐步放量 |
| 成本优化 | 增加用量统计,高峰期自动切换到更经济的 Provider |
| 多级兜底 | 从 Primary → Fallback1 → Fallback2,支持更长的降级链 |
10. 安全注意事项
- 密钥不硬编码:所有 API Key / Secret 通过环境变量注入,不提交到代码仓库
- 文件路径校验:
reference_image_paths必须为已存在的本地文件,防止路径穿越攻击 - 资产 ID 隔离:资产 ID 仅来自可信数据源(数据库/Redis),不接受用户直接传入
- 错误信息脱敏:向上层暴露的错误信息不含内部路径、堆栈、Key 信息
11. 部署检查清单
- 所有 Provider 的 API Key / Secret 已正确注入环境变量
- 海外 Provider 的 VPN 代理已启动并验证连通性
- 国内 Provider 的云端配额充足,未达上限
- 参考图预上传流程已验证可用
-
max_workers根据服务器 CPU/内存调整(建议 CPU 核数 / 2) - 任务池并发数与 Provider 侧 Rate Limit 匹配
- 图片输出目录已创建且有写权限
- 域名分流规则已配置(详见网络隔离方案)