从零搭建多模型AI应用:6步实现一个支持5大模型的聊天后端

本文记录从零搭建一个支持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直连:适合对延迟要求极高的场景,但国内访问需要代理
  • OpenRouteropenrouter.ai):国际主流中转站,模型覆盖最全,支持200+模型
  • 硅基流动siliconflow.cn):国内中转站,开源模型为主,价格便宜
  • 魔芋AImoyu.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兼容协议已经成为事实标准,所有主流中转站都支持,所以我们只需要:

  1. 用同一个SDK(OpenAI Python SDK)
  2. 不同的base_url指向不同中转站
  3. 路由表负责模型名到中转站的映射

这种架构的好处是扩展性极强------新增一个模型只需要在配置里加一行,路由表自动生效。如果你刚开始搭建,可以参考文中提到的几个中转站(OpenRouter、硅基流动、魔芋AI等),选择适合自己场景的组合。

完整代码已开源,有问题欢迎评论区交流。