用 Python 5 分钟搭一个多模型 AI API 网关(附完整代码)

还在手动切换 OpenAI、Claude、DeepSeek 的 API?太蠢了。本文教你用 Python 搭一个统一的 AI 网关,一个接口调所有模型,自动故障转移,代码不到 100 行。

为什么需要 AI API 网关?

如果你在做 AI 应用开发,大概率遇到过这些问题:

  • OpenAI 抽风了,服务挂了 2 小时,你的应用跟着挂
  • DeepSeek 发布新版本,API 过载,请求全超时
  • 老板突然说"我们试试 Claude",你得改一堆代码
  • 不同模型的 API 格式不一样,适配起来头疼

这些问题的本质是:你的应用直接耦合了某个具体的模型 API。

解决方案很简单------在你的应用和模型之间加一层网关,统一接口、自动路由、故障转移。

完整代码:100 行的 AI 网关

直接上代码,Python + FastAPI,能跑的那种。

python 复制代码
import os
import time
import httpx
import asyncio
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI(title="AI API Gateway")

# 模型配置:按优先级排列
MODEL_PROVIDERS = [
    {
        "name": "deepseek",
        "base_url": "https://api.deepseek.com/v1",
        "api_key": os.getenv("DEEPSEEK_API_KEY", ""),
        "model": "deepseek-chat",
        "timeout": 30,
        "max_retries": 2,
    },
    {
        "name": "openai",
        "base_url": "https://api.openai.com/v1",
        "api_key": os.getenv("OPENAI_API_KEY", ""),
        "model": "gpt-4o-mini",
        "timeout": 30,
        "max_retries": 1,
    },
    {
        "name": "anthropic",
        "base_url": "https://api.anthropic.com/v1",
        "api_key": os.getenv("ANTHROPIC_API_KEY", ""),
        "model": "claude-3-5-sonnet-20241022",
        "timeout": 30,
        "max_retries": 1,
    },
]

# 健康状态追踪
provider_health = {p["name"]: {"healthy": True, "last_fail": 0} for p in MODEL_PROVIDERS}
COOLDOWN_SECONDS = 60  # 故障冷却时间


async def call_provider(provider: dict, messages: list) -> dict:
    """调用单个模型供应商"""
    headers = {
        "Authorization": f"Bearer {provider['api_key']}",
        "Content-Type": "application/json",
    }

    # Anthropic 的 API 格式略有不同
    if provider["name"] == "anthropic":
        headers = {
            "x-api-key": provider["api_key"],
            "anthropic-version": "2023-06-01",
            "Content-Type": "application/json",
        }
        payload = {
            "model": provider["model"],
            "max_tokens": 4096,
            "messages": messages,
        }
        url = f"{provider['base_url']}/messages"
    else:
        payload = {
            "model": provider["model"],
            "messages": messages,
            "max_tokens": 4096,
        }
        url = f"{provider['base_url']}/chat/completions"

    async with httpx.AsyncClient(timeout=provider["timeout"]) as client:
        resp = await client.post(url, json=payload, headers=headers)
        resp.raise_for_status()
        return resp.json()


def normalize_response(provider_name: str, raw: dict) -> dict:
    """统一不同供应商的响应格式"""
    if provider_name == "anthropic":
        content = raw.get("content", [{}])[0].get("text", "")
        model = raw.get("model", "")
    else:
        content = raw["choices"][0]["message"]["content"]
        model = raw.get("model", "")

    return {
        "content": content,
        "model": model,
        "provider": provider_name,
    }


@app.post("/v1/chat")
async def chat(request: Request):
    """统一聊天接口------自动路由到可用的模型"""
    body = await request.json()
    messages = body.get("messages", [])
    preferred = body.get("provider", None)  # 可选:指定供应商

    errors = []

    for provider in MODEL_PROVIDERS:
        name = provider["name"]
        health = provider_health[name]

        # 跳过指定供应商以外的
        if preferred and name != preferred:
            continue

        # 跳过冷却中的供应商
        if not health["healthy"] and time.time() - health["last_fail"] < COOLDOWN_SECONDS:
            continue

        try:
            raw = await call_provider(provider, messages)
            provider_health[name]["healthy"] = True
            return JSONResponse(normalize_response(name, raw))

        except Exception as e:
            provider_health[name] = {"healthy": False, "last_fail": time.time()}
            errors.append(f"{name}: {str(e)}")
            continue

    return JSONResponse(
        {"error": "All providers failed", "details": errors},
        status_code=503,
    )


@app.get("/health")
async def health():
    """查看各供应商健康状态"""
    return {name: info for name, info in provider_health.items()}

怎么用

1. 安装依赖

bash 复制代码
pip install fastapi uvicorn httpx

2. 设置 API Key

bash 复制代码
export DEEPSEEK_API_KEY="sk-xxx"
export OPENAI_API_KEY="sk-xxx"
export ANTHROPIC_API_KEY="sk-xxx"

3. 启动

bash 复制代码
uvicorn gateway:app --port 8000

4. 调用

bash 复制代码
# 自动路由(优先 DeepSeek,失败自动切换)
curl -X POST http://localhost:8000/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "你好"}]}'

# 指定供应商
curl -X POST http://localhost:8000/v1/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "你好"}], "provider": "openai"}'

# 查看健康状态
curl http://localhost:8000/health

核心设计思路

1. 优先级路由

MODEL_PROVIDERS 数组的顺序就是优先级。DeepSeek 排第一是因为性价比最高,OpenAI 和 Claude 作为备用。

2. 自动故障转移

某个供应商报错后,标记为"不健康"并进入 60 秒冷却期。在此期间跳过该供应商,直接尝试下一个。冷却结束后自动恢复。

3. 响应格式统一

不管后端用的是哪个模型,返回格式都是统一的 {content, model, provider},调用方完全不需要关心底层用的是谁。

进阶优化方向

这个 100 行版本是最小可用版。生产环境中你可能还需要:

功能 说明
流式响应 SSE 支持,实现打字机效果
请求日志 记录每次请求的模型、耗时、token 数
成本追踪 按 token 计费,统计各模型花费
负载均衡 多个 API Key 轮询,避免单 Key 限流
缓存层 相同请求命中缓存,省钱又快
鉴权 给网关加 API Key,防止滥用

如果你不想自己造轮子,市面上也有现成的模型聚合方案:

  • OpenRouter:国外主流,模型多,但国内访问不稳定
  • Ofox.ai:国内可直接用,支持主流模型,适合国内开发者
  • One-API / New-API:开源自建方案,需要自己部署维护

总结

搭一个 AI API 网关并不难,核心就三件事:统一接口、自动路由、故障转移

100 行代码就能解决 90% 的场景。剩下的 10%(流式、缓存、监控),要么自己加,要么用现成的聚合平台。

重点是:别让你的应用跟任何一个模型供应商绑死。今天 DeepSeek 最强,明天可能是别人。保持灵活,才能永远用上最好的。


觉得有用?点个赞收个藏,这是我继续写的最大动力。有问题评论区见。

相关推荐
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
山塘小鱼儿9 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
码说AI10 小时前
python快速绘制走势图对比曲线
开发语言·python
wait_luky10 小时前
python作业3
开发语言·python
Python大数据分析@11 小时前
tkinter可以做出多复杂的界面?
python·microsoft
大黄说说11 小时前
新手选语言不再纠结:Java、Python、Go、JavaScript 四大热门语言全景对比与学习路线建议
java·python·golang
小小张说故事12 小时前
SQLAlchemy 技术入门指南
后端·python
我是章汕呐12 小时前
拆解Libvio.link爬虫:从动态页面到反爬对抗的实战解析
爬虫·python