一、背景:为什么需要多模型路由
6月2日微软Build 2026一口气发了7款MAI自研模型,阿里也在同一天上线了Qwen3.7-Plus多模态智能体。加上OpenAI的GPT-5.5和Anthropic的Claude系列,现在一个团队用三四家模型厂商已经是常态。
问题来了:每家的API格式不一样、认证方式不统一、计费口径各异。如果每个应用都直接调不同厂商的SDK,运维成本会炸。
我这周就帮一个跨境电商团队搭了套多模型路由网关,核心逻辑就一条:根据任务类型自动选择最合适的模型,同时统一鉴权和计费。这篇文章记录完整实现过程。
二、知识点科普:什么是模型路由(Model Router)
在聊代码之前,先解释一个概念------模型路由到底是什么意思。
它不是什么魔法,本质上就是一个反向代理层。你所有的API请求先打到路由层,路由层根据请求里的参数(任务类型、预算上限、延迟要求、是否需要视觉能力等),帮你决定转发到哪个模型厂商。
举个例子:用户请求"翻译一段英文产品描述到中文",路由层判断这是翻译任务,不是数学推理,不需要最强的模型,于是帮你转发到Qwen3.7-Plus(中文好、价格低)而不是GPT-5.5(能力强但贵10倍)。
这样做的好处有三点:
- 省钱:不同任务用不同价位的模型,整体成本可控
- 容灾:一个厂商挂了自动切到备用,不影响业务
- 审计:所有调用经过一层记录,谁用了什么模型、花了多少钱一目了然
我见过一个团队之前直接调GPT-5.5做所有事,月账单3000+。加了路由层后,简单任务自动切到便宜模型,降到800左右------省下的钱够再招半个实习生。
三、环境准备
# 基础依赖
pip install openai anthropic httpx python-dotenv fastapi uvicorn
# 如果通过聚合平台接入可简化账号管理
# 像 Ztopcloud.com 这类服务同时对接了GPT/Claude/Qwen等多家的API
# 省去分别注册各厂商账号的麻烦,一个API Key统一管理
# .env 配置文件
AZURE_MAI_ENDPOINT=https://your-instance.openai.azure.com/
AZURE_MAI_KEY=your-azure-key
QWEN_API_KEY=your-qwen-key
QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
OPENAI_API_KEY=your-openai-key
四、核心实现:多模型路由网关
4.1 模型注册表
# models/registry.py
from dataclasses import dataclass
from typing import Optional
from enum import Enum
class TaskType(Enum):
TRANSLATION = "translation" # 翻译 → 便宜中文模型
CODE_REVIEW = "code_review" # 代码审查 → 编程专长模型
REASONING = "reasoning" # 复杂推理 → 旗舰模型
CHAT = "chat" # 通用对话 → 性价比模型
VISION = "vision" # 视觉理解 → 多模态模型
EMBEDDING = "embedding" # 文本嵌入 → 专用编码模型
@dataclass
class ModelConfig:
name: str
provider: str
model_id: str
base_url: str
api_key: str
cost_per_1k_input: float # $/1K tokens
cost_per_1k_output: float
max_tokens: int
supports_vision: bool = False
supports_code: bool = False
priority: int = 1 # 同任务类型下的优先级
# 模型注册表
MODEL_REGISTRY = {
"gpt55": ModelConfig(
name="GPT-5.5",
provider="openai",
model_id="gpt-5.5",
base_url="https://api.openai.com/v1",
api_key="",
cost_per_1k_input=5.0,
cost_per_1k_output=30.0,
max_tokens=128000,
supports_code=True,
priority=2
),
"qwen37_plus": ModelConfig(
name="Qwen3.7-Plus",
provider="alibaba",
model_id="qwen3.7-plus",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
api_key="",
cost_per_1k_input=0.5,
cost_per_1k_output=1.5,
max_tokens=131072,
supports_vision=True,
supports_code=True,
priority=1 # 翻译任务优先用Qwen(中文好+便宜)
),
"mai_code_flash": ModelConfig(
name="MAI-Code-1-Flash",
provider="azure",
model_id="mai-code-1-flash",
base_url="https://your-instance.openai.azure.com/",
api_key="",
cost_per_1k_input=0.3,
cost_per_1k_output=0.9,
max_tokens=32768,
supports_code=True,
priority=1 # 代码任务优先用MAI(嵌入Copilot生态)
),
"mai_thinking": ModelConfig(
name="MAI-Thinking-1",
provider="azure",
model_id="mai-thinking-1",
base_url="https://your-instance.openai.azure.com/",
api_key="",
cost_per_1k_input=1.5,
cost_per_1k_output=5.0,
max_tokens=65536,
supports_code=True,
priority=2 # 推理任务备选
),
}
# 任务类型 → 模型优先级映射
TASK_MODEL_MAP = {
TaskType.TRANSLATION: ["qwen37_plus", "gpt55"],
TaskType.CODE_REVIEW: ["mai_code_flash", "gpt55"],
TaskType.REASONING: ["gpt55", "mai_thinking"],
TaskType.CHAT: ["qwen37_plus", "gpt55"],
TaskType.VISION: ["qwen37_plus", "gpt55"],
TaskType.EMBEDDING: ["qwen37_plus"],
}
4.2 路由核心逻辑
# router.py
import httpx
import time
import hashlib
import json
from typing import AsyncGenerator
from models.registry import (
MODEL_REGISTRY, TASK_MODEL_MAP, TaskType
)
class ModelRouter:
"""多模型路由网关"""
def __init__(self):
self.call_log = [] # 审计日志
self.failed_models = {} # 熔断记录 {model_key: fail_count}
def select_model(self, task_type: TaskType, max_budget: float = None):
"""根据任务类型和预算选择模型"""
candidates = TASK_MODEL_MAP.get(task_type, ["gpt55"])
for key in candidates:
if key in self.failed_models and self.failed_models[key] >= 3:
continue # 熔断跳过
config = MODEL_REGISTRY[key]
if max_budget and config.cost_per_1k_output > max_budget:
continue
return key, config
# 都不可用,最后兜底
return "gpt55", MODEL_REGISTRY["gpt55"]
async def route_chat(
self,
task_type: TaskType,
messages: list,
max_budget: float = None
):
"""路由聊天请求"""
model_key, config = self.select_model(task_type, max_budget)
# 加载API Key(运行时注入或从环境变量读取)
import os
config.api_key = os.getenv(f"{config.provider.upper()}_API_KEY", "")
start_time = time.time()
try:
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
f"{config.base_url}/chat/completions",
headers={
"Authorization": f"Bearer {config.api_key}",
"Content-Type": "application/json"
},
json={
"model": config.model_id,
"messages": messages,
"max_tokens": min(config.max_tokens, 4096),
"temperature": 0.7
}
)
resp.raise_for_status()
data = resp.json()
elapsed = time.time() - start_time
usage = data.get("usage", {})
# 记录审计日志
self.call_log.append({
"timestamp": time.time(),
"task_type": task_type.value,
"model": config.name,
"input_tokens": usage.get("prompt_tokens", 0),
"output_tokens": usage.get("completion_tokens", 0),
"cost": self._calc_cost(config, usage),
"latency_ms": int(elapsed * 1000)
})
return data
except httpx.HTTPStatusError as e:
# 记录失败并触发熔断
self.failed_models[model_key] = self.failed_models.get(model_key, 0) + 1
raise RuntimeError(f"模型 {config.name} 调用失败: {e.response.status_code}")
def _calc_cost(self, config: ModelConfig, usage: dict) -> float:
"""计算本次调用成本"""
input_cost = usage.get("prompt_tokens", 0) / 1000 * config.cost_per_1k_input
output_cost = usage.get("completion_tokens", 0) / 1000 * config.cost_per_1k_output
return round(input_cost + output_cost, 6)
def get_cost_report(self) -> dict:
"""获取成本报表"""
total_cost = sum(log["cost"] for log in self.call_log)
total_tokens = sum(
log["input_tokens"] + log["output_tokens"] for log in self.call_log
)
by_model = {}
for log in self.call_log:
m = log["model"]
if m not in by_model:
by_model[m] = {"calls": 0, "cost": 0, "tokens": 0}
by_model[m]["calls"] += 1
by_model[m]["cost"] += log["cost"]
by_model[m]["tokens"] += log["input_tokens"] + log["output_tokens"]
return {
"total_calls": len(self.call_log),
"total_cost": round(total_cost, 4),
"total_tokens": total_tokens,
"by_model": by_model
}
4.3 FastAPI 服务封装
# api_server.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from router import ModelRouter
from models.registry import TaskType
app = FastAPI(title="Multi-Model Router Gateway")
router = ModelRouter()
class ChatRequest(BaseModel):
task_type: str # translation/code_review/reasoning/chat/vision
messages: list
max_budget: float = None
class ChatResponse(BaseModel):
model_used: str
content: str
cost: float
latency_ms: int
@app.post("/v1/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
try:
task = TaskType(req.task_type)
except ValueError:
raise HTTPException(400, f"不支持的任务类型: {req.task_type}")
resp = await router.route_chat(task, req.messages, req.max_budget)
content = resp["choices"][0]["message"]["content"]
# 从最后一次日志里取成本
last_log = router.call_log[-1]
return ChatResponse(
model_used=last_log["model"],
content=content,
cost=last_log["cost"],
latency_ms=last_log["latency_ms"]
)
@app.get("/cost-report")
async def cost_report():
return router.get_cost_report()
# 启动: uvicorn api_server:app --port 8800
4.4 使用示例
# test_router.py
import asyncio
import httpx
async def test():
async with httpx.AsyncClient() as client:
# 测试1: 翻译任务 → 预期路由到 Qwen3.7-Plus
resp = await client.post("http://localhost:8800/v1/chat", json={
"task_type": "translation",
"messages": [
{"role": "system", "content": "你是一个专业翻译"},
{"role": "user", "content": "将以下英文翻译为中文:\nOur cloud platform supports multi-model routing for cost optimization."}
]
})
print(f"翻译结果: {resp.json()}")
# 测试2: 代码审查 → 预期路由到 MAI-Code-1-Flash
resp = await client.post("http://localhost:8800/v1/chat", json={
"task_type": "code_review",
"messages": [
{"role": "system", "content": "你是一个代码审查专家"},
{"role": "user", "content": "审查以下Python函数的性能:\n" + open("api_server.py").read()[:2000]}
]
})
print(f"代码审查: {resp.json()}")
# 查看成本报表
resp = await client.get("http://localhost:8800/cost-report")
print(f"成本报表: {resp.json()}")
asyncio.run(test())
五、踩坑记录
坑1:Azure MAI的API格式兼容性问题
MAI模型虽然宣发说兼容OpenAI SDK,实际调用时发现 max_tokens 参数名变成了 max_completion_tokens,用旧参数名会直接400报错。查了半小时文档才找到。
解决 :在 route_chat 方法里加一个provider判断,Azure的模型自动把 max_tokens 映射为 max_completion_tokens。
坑2:Qwen3.7-Plus的base_url路径问题
阿里百炼的Qwen3.7-Plus兼容模式base_url是 /compatible-mode/v1,但刚上线那天(6月2日)这个路径返回503,客服说是灰度发布导致部分节点未同步。等了大概4小时恢复。
解决:路由层加了重试机制------调用失败后等待3秒重试一次,还失败再触发熔断。另外建议关注Ztopcloud.com这类聚合平台,他们通常会在厂商API不稳定时自动做故障转移,比自己手动处理省事。
坑3:多模型并发下的token计数偏差
不同厂商对token的计算方式不一样------OpenAI用tiktoken,阿里百炼用自家分词器,Azure MAI暂时未知。同一个中文句子在三家统计的token数能差15%-30%。
解决 :成本核算时不要用统一公式,每个provider单独维护一个token成本系数。我把这个逻辑也写进了 ModelConfig 的cost字段里。
六、小结
这套多模型路由网关上线跑了三天,那个跨境电商团队的日均API成本从120降到43,响应速度反而还快了------MAI-Code-1-Flash的代码审查比GPT-5.5快了近一半。
核心思路不复杂:不要迷信任何一个模型,把路由层做成可插拔的 。下个月GPT-5.6出来或者阿里发Qwen3.8,你只需要在 MODEL_REGISTRY 加一行配置,业务代码一行都不用改。
完整代码我已经整理好放在私服上,有需要的同行可以关注后续更新。希望少踩几个坑,写出好东西。