本文记录从零搭建一个支持GPT、Claude、Gemini、DeepSeek、Qwen的多模型聊天后端的全过程。包含项目结构设计、SDK选型、流式处理、错误重试、成本控制的完整代码。
一、项目背景
团队需要一个内部AI助手,要求:
- 支持多个模型自由切换
- 统一的API接口,前端不用关心后端用的是哪个模型
- 流式输出,用户体验好
- 成本可控,能按模型分别统计用量
技术栈:Python + FastAPI + OpenAI SDK(兼容协议)
二、项目结构
multi-model-chat/
├── app/
│ ├── main.py # FastAPI 入口
│ ├── config.py # 配置管理
│ ├── models.py # 模型路由配置
│ ├── router.py # 请求路由与转发
│ ├── billing.py # 用量统计
│ └── utils.py # 工具函数
├── requirements.txt
└── .env
三、第1步:配置管理
不同模型的API地址和Key分开管理。这里我们通过环境变量加载,支持多个中转站和直连方式:
python
# app/config.py
import os
from dataclasses import dataclass
from typing import Dict
@dataclass
class ProviderConfig:
name: str
base_url: str
api_key: str
models: list
# 从环境变量加载配置
# 支持多种接入方式:
# 1. 直连官方API(如 OpenAI 官方)
# 2. 通过中转站接入(如 OpenRouter、硅基流动、魔芋AI 等)
# 3. 自建中转层(如 one-api、new-api)
def load_providers() -> Dict[str, ProviderConfig]:
providers = {}
# OpenAI 直连
if os.getenv("OPENAI_API_KEY"):
providers["openai"] = ProviderConfig(
name="OpenAI 直连",
base_url="https://api.openai.com/v1",
api_key=os.getenv("OPENAI_API_KEY"),
models=["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"]
)
# OpenRouter 中转(国际主流中转站)
# 官网:https://openrouter.ai
if os.getenv("OPENROUTER_API_KEY"):
providers["openrouter"] = ProviderConfig(
name="OpenRouter",
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
models=["gpt-4o", "claude-3.5-sonnet", "gemini-2.0-flash"]
)
# 硅基流动(国内中转站,开源模型为主)
# 官网:https://siliconflow.cn
if os.getenv("SILICONFLOW_API_KEY"):
providers["siliconflow"] = ProviderConfig(
name="硅基流动",
base_url="https://api.siliconflow.cn/v1",
api_key=os.getenv("SILICONFLOW_API_KEY"),
models=["deepseek-v3", "qwen2.5-72b", "yi-34b"]
)
# 魔芋AI(国内中转站,性价比方案)
# 官网:https://www.moyu.info/register?aff=CRB8
if os.getenv("MOYU_API_KEY"):
providers["moyu"] = ProviderConfig(
name="魔芋AI",
base_url="https://api.moyu.info/v1",
api_key=os.getenv("MOYU_API_KEY"),
models=["gpt-4o", "claude-3.5-sonnet", "deepseek-chat"]
)
return providers
配置说明:
- OpenAI直连:适合对延迟要求极高的场景,但国内访问需要代理
- OpenRouter(openrouter.ai):国际主流中转站,模型覆盖最全,支持200+模型
- 硅基流动(siliconflow.cn):国内中转站,开源模型为主,价格便宜
- 魔芋AI(moyu.info):国内中转站,支持主流闭源模型,新用户有免费额度
- 自建方案:用 one-api 或 new-api 开源项目自建中转层,适合对数据隐私要求高的场景
四、第2步:模型路由
根据用户请求的模型名称,自动路由到对应的中转站:
python
# app/router.py
from openai import AsyncOpenAI
from .config import ProviderConfig
from typing import Optional
import logging
logger = logging.getLogger(__name__)
class ModelRouter:
def __init__(self, providers: dict):
self.providers = providers
# 建立模型到Provider的反向索引
self.model_index = {}
for provider_name, config in providers.items():
for model in config.models:
self.model_index[model] = provider_name
logger.info(f"已加载 {len(self.model_index)} 个模型路由")
def get_client(self, model: str) -> Optional[tuple]:
"""根据模型名获取对应的AsyncOpenAI客户端"""
provider_name = self.model_index.get(model)
if not provider_name:
logger.error(f"模型 {model} 未找到可用的Provider")
return None
config = self.providers[provider_name]
client = AsyncOpenAI(
api_key=config.api_key,
base_url=config.base_url
)
return client, provider_name
def list_models(self) -> list:
"""列出所有可用模型"""
return list(self.model_index.keys())
核心思路:所有中转站都兼容OpenAI协议,所以用同一个AsyncOpenAI客户端,只是base_url不同。路由表负责把模型名映射到对应的中转站。
五、第3步:流式聊天接口
实现一个统一的流式聊天接口,前端通过SSE接收:
python
# app/main.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from .router import ModelRouter
from .config import load_providers
from .billing import UsageTracker
import json
import logging
logging.basicConfig(level=logging.INFO)
app = FastAPI(title="多模型AI聊天后端")
# 初始化
providers = load_providers()
router = ModelRouter(providers)
tracker = UsageTracker()
class ChatRequest(BaseModel):
model: str
messages: list
temperature: float = 0.7
max_tokens: int = 2000
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
"""流式聊天接口"""
result = router.get_client(req.model)
if not result:
raise HTTPException(404, f"模型 {req.model} 不可用")
client, provider_name = result
async def generate():
total_tokens = 0
try:
stream = await client.chat.completions.create(
model=req.model,
messages=req.messages,
temperature=req.temperature,
max_tokens=req.max_tokens,
stream=True
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
total_tokens += 1 # 粗略统计
# SSE 格式返回
yield f"data: {json.dumps({'content': content, 'model': req.model})}\n\n"
# 流结束,发送统计信息
yield f"data: {json.dumps({'done': True, 'tokens': total_tokens, 'provider': provider_name})}\n\n"
except Exception as e:
logger.error(f"聊天出错: {e}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
@app.get("/models")
async def list_models():
"""列出所有可用模型"""
return {"models": router.list_models()}
六、第4步:错误处理与重试
网络请求难免失败,需要实现自动重试和Provider降级:
python
# app/utils.py
import asyncio
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def retry_with_backoff(max_retries=3, base_delay=1.0):
"""指数退避重试装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
last_error = None
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_error = e
delay = base_delay * (2 ** attempt)
logger.warning(
f"第 {attempt+1}/{max_retries} 次重试,"
f"等待 {delay}s,错误: {e}"
)
await asyncio.sleep(delay)
raise last_error
return wrapper
return decorator
class FallbackChain:
"""Provider降级链:主Provider失败时自动切换到备用"""
def __init__(self, router):
self.router = router
async def chat_with_fallback(self, model, messages, **kwargs):
"""带降级的聊天"""
# 获取主Provider
result = self.router.get_client(model)
if not result:
raise ValueError(f"模型 {model} 不可用")
primary_client, primary_provider = result
try:
# 尝试主Provider
response = await primary_client.chat.completions.create(
model=model, messages=messages, **kwargs
)
return response, primary_provider
except Exception as e:
logger.warning(f"主Provider {primary_provider} 失败: {e}")
# 尝试备用Provider(同模型在其他中转站)
for provider_name, config in self.router.providers.items():
if provider_name == primary_provider:
continue
if model in config.models:
try:
backup_client = AsyncOpenAI(
api_key=config.api_key,
base_url=config.base_url
)
response = await backup_client.chat.completions.create(
model=model, messages=messages, **kwargs
)
logger.info(f"降级到 {provider_name} 成功")
return response, provider_name
except Exception as e2:
logger.warning(f"备用 {provider_name} 也失败: {e2}")
continue
raise Exception(f"所有Provider均失败,最后错误: {e}")
七、第5步:用量统计
按模型和Provider分别统计Token用量,方便成本分析:
python
# app/billing.py
import time
from collections import defaultdict
import logging
logger = logging.getLogger(__name__)
class UsageTracker:
def __init__(self):
# {model: {provider: {"calls": int, "tokens": int}}}
self.stats = defaultdict(lambda: defaultdict(lambda: {"calls": 0, "tokens": 0}))
def record(self, model: str, provider: str, tokens: int):
"""记录一次调用"""
self.stats[model][provider]["calls"] += 1
self.stats[model][provider]["tokens"] += tokens
logger.info(
f"用量记录: model={model}, provider={provider}, "
f"tokens={tokens}, total_calls={self.stats[model][provider]['calls']}"
)
def get_summary(self) -> dict:
"""获取用量汇总"""
summary = {}
for model, providers in self.stats.items():
summary[model] = {
p: {"calls": d["calls"], "tokens": d["tokens"]}
for p, d in providers.items()
}
return summary
def estimate_cost(self, model: str, tokens: int) -> float:
"""估算单次调用成本(美元)"""
# 简化版价格表,实际应从配置读取
price_table = {
"gpt-4o": 0.0025, # $2.5/1M
"gpt-4o-mini": 0.00015, # $0.15/1M
"claude-3.5-sonnet": 0.003, # $3/1M
"deepseek-chat": 0.00027, # $0.27/1M
"gemini-2.0-flash": 0.0001, # $0.1/1M
}
rate = price_table.get(model, 0.001) # 默认 $1/1M
return tokens * rate / 1000
八、第6步:启动与测试
python
# 启动服务
# uvicorn app.main:app --host 0.0.0.0 --port 8000
# 测试请求
import requests
# 1. 查看可用模型
resp = requests.get("http://localhost:8000/models")
print(resp.json())
# 2. 流式聊天
import httpx
async with httpx.AsyncClient() as client:
async with client.stream(
"POST",
"http://localhost:8000/chat/stream",
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "你好"}]
}
) as resp:
async for line in resp.aiter_lines():
if line.startswith("data: "):
import json
data = json.loads(line[6:])
if "content" in data:
print(data["content"], end="", flush=True)
elif "done" in data:
print(f"\n\n完成: {data}")
九、踩坑记录
坑1:不同中转站的SSE格式略有差异
大部分中转站的SSE格式和OpenAI官方一致,但少数中转站会在每个chunk后多加一个空行,或者finish_reason的值不同。建议在generate()函数里做兼容处理:
python
# 兼容不同中转站的SSE格式
if chunk.choices:
choice = chunk.choices[0]
if hasattr(choice, 'delta') and choice.delta.content:
content = choice.delta.content
elif hasattr(choice, 'message') and choice.message.content:
content = choice.message.content
else:
continue
坑2:Anthropic模型的system消息处理
Claude的system消息是独立字段,不是放在messages里。好的中转站会自动处理这个转换,但部分中转站不做转换,导致Claude收不到system提示。接入前用相同prompt测试一下。
坑3:Token统计不一致
不同中转站用的Tokenizer可能不同,同一个请求在不同中转站计费Token数有1-3%偏差。建议以模型官方的Tokenizer为准,中转站的统计仅作参考。
十、总结
搭建多模型AI应用的核心是统一接口 + 路由分发。因为OpenAI兼容协议已经成为事实标准,所有主流中转站都支持,所以我们只需要:
- 用同一个SDK(OpenAI Python SDK)
- 不同的
base_url指向不同中转站 - 路由表负责模型名到中转站的映射
这种架构的好处是扩展性极强------新增一个模型只需要在配置里加一行,路由表自动生效。如果你刚开始搭建,可以参考文中提到的几个中转站(OpenRouter、硅基流动、魔芋AI等),选择适合自己场景的组合。
完整代码已开源,有问题欢迎评论区交流。