AI Gateway 模型路由器设计
BodySense 多 API 提供商的统一抽象层设计:AIService → ModelRouter → ProviderAdapter → QuotaManager,业务层不感知厂商差异。
[!info] related notes
AI Gateway 模型路由器设计
最优雅的多模型设计不是”在业务代码里切换不同 API Key”,而是做一个内部 AI Gateway / Model Router。业务层永远只调用一个统一接口:
await ai.generate({
useCase: "consultation.reply",
messages,
stream: true,
tools,
responseFormat,
})
底层用 MiMo、OpenRouter、Qwen、Gemini、DeepSeek、OpenAI,由路由层根据能力、免费额度、限流、可用性、成本自动决定。
推荐架构
业务代码 / Agent Workflow
↓
AIService 统一调用接口
↓
Prompt / UseCase 层
↓
Model Router 模型路由器
↓
Quota / RateLimit / Health 状态层
↓
Provider Adapter 适配器层
↓
MiMo / OpenAI / Qwen / Gemini / DeepSeek / OpenRouter ...
核心思想:业务不认识任何厂商 SDK,只认识你自己的 AIService。 切模型只改配置或路由策略,不改业务逻辑。
抽象”任务能力”而非”模型”
不要 callMimo() / callOpenAI(),而是按能力抽象:
ai.run({ useCase: "chat.fast", messages })
ai.run({ useCase: "agent.reasoning", messages, tools })
ai.run({ useCase: "summary.long_context", messages })
同一”聊天”请求可能有完全不同的模型需求:
| 任务 | 模型需求 |
|---|---|
| 简单问答 | 便宜 / 免费 / 快速模型 |
| 复杂推理 | reasoning 模型 |
| 长文档总结 | 长上下文模型 |
| Function Calling | 支持工具调用的模型 |
| 结构化输出 | 支持 JSON schema 的模型 |
| 多模态理解 | vision / omni 模型 |
4 层核心设计
1. 统一 Request / Response 协议
定义内部协议,不直接暴露 OpenAI、Anthropic、MiMo 的原始格式:
export type AiUseCase =
| "chat.fast" | "chat.smart"
| "agent.reasoning" | "summary.long"
| "embedding" | "vision.analyze";
export interface AiRequest {
useCase: AiUseCase;
messages: Array<{ role: string; content: string | Array<any> }>;
stream?: boolean;
tools?: ToolDefinition[];
responseFormat?: "text" | "json" | { type: "json_schema"; schema: unknown };
temperature?: number;
maxTokens?: number;
metadata?: { userId?: string; sessionId?: string; traceId?: string };
}
export interface AiResponse {
text: string;
model: string;
provider: string;
usage?: { inputTokens: number; outputTokens: number; totalTokens: number };
finishReason?: string;
}
2. Provider Adapter 适配器层
每个厂商一个 Adapter,实现统一接口:
export interface AiProvider {
id: string;
supports: {
stream: boolean; tools: boolean;
structuredOutput: boolean; vision: boolean;
embedding: boolean; longContext: boolean;
};
generate(req: AiRequest, model: string): Promise<AiResponse>;
stream?(req: AiRequest, model: string): AsyncIterable<AiStreamEvent>;
healthCheck?(): Promise<boolean>;
}
很多厂商兼容 OpenAI 格式,可复用 OpenAICompatibleProvider,只需换 baseURL / apiKey / model。
3. Model Router 模型路由层
整个系统最值钱的部分——根据请求决定用哪个模型:
class ModelRouter {
async select(req: AiRequest): Promise<ModelCandidate> {
const candidates = this.getCandidatesByUseCase(req.useCase);
return candidates
.filter(model => this.matchCapability(model, req))
.filter(model => this.quota.hasAvailableQuota(model))
.filter(model => this.health.isHealthy(model.provider))
.sort((a, b) => this.score(b, req) - this.score(a, req))[0];
}
}
选择逻辑:按 useCase 找候选 → 过滤不支持的能力 → 过滤额度用完的 key → 过滤 429/故障 provider → 按成本/速度/质量打分 → 调用第一候选 → 失败 fallback。
4. Quota / RateLimit / Health 层
多 key 薙免费额度场景,必须单独设计额度层:
interface ModelQuotaState {
provider: string; model: string; apiKeyId: string;
rpmLimit?: number; tpmLimit?: number;
dailyTokenLimit?: number; monthlyTokenLimit?: number;
usedTokensToday: number; usedTokensThisMonth: number;
last429At?: Date; disabledUntil?: Date;
consecutiveErrors: number;
}
建议用 Redis 做 token bucket(RPM/TPM)、daily/monthly counter、circuit breaker。
配置驱动
用 models.yaml + routes.yaml,切模型只改配置:
routes:
chat.fast:
strategy: fallback
models:
- provider: mimo
model: mimo-v2-flash
- provider: qwen
model: qwen-plus
- provider: openrouter
model: deepseek/deepseek-chat
agent.reasoning:
strategy: fallback
models:
- provider: mimo
model: mimo-v2.5-pro
- provider: openrouter
model: openai/gpt-oss-120b
流式输出统一事件协议
不要把不同厂商的 SSE 原样透传到前端。统一为自己的事件格式:
type AiStreamEvent =
| { type: "text_delta"; text: string }
| { type: "tool_call_delta"; toolCallId: string; name?: string; argumentsDelta?: string }
| { type: "tool_call_done"; toolCallId: string; name: string; arguments: unknown }
| { type: "usage"; usage: TokenUsage }
| { type: "done"; finishReason: string }
| { type: "error"; error: AiError };
免费额度优先路由打分
score = qualityScore * 0.4
+ remainingFreeQuotaScore * 0.3
+ latencyScore * 0.2
+ stabilityScore * 0.1
- costPenalty
策略:免费额度充足优先 → 快用完降优先级 → 429 短时熔断 → 重要任务允许 fallback 到稳定模型。
落地路径
第一阶段:自己写轻量 AIService + ProviderAdapter + ModelRouter
第二阶段:接入 OpenAI-Compatible Provider(MiMo / Qwen / OpenRouter / DeepSeek)
第三阶段:加 QuotaManager(记录 token、429、错误率、免费额度)
第四阶段:抽象统一 StreamEvent,前端不感知厂商差异
第五阶段:provider 太多时引入 LiteLLM 做 gateway
目录结构
src/ai/
core/ types.ts, errors.ts, stream-events.ts, usage.ts
service/ ai-service.ts
router/ model-router.ts, route-policy.ts, scoring.ts
quota/ quota-manager.ts, token-bucket.ts, usage-store.ts
providers/ provider.interface.ts, openai-compatible.provider.ts, ...
prompts/ prompt-registry.ts, usecases/
config/ models.yaml, routes.yaml
telemetry/ ai-logger.ts, trace.ts, cost-reporter.ts
常见陷阱
- 不要在业务代码里写
if (provider === "mimo")— 会很快失控 - 不要只用
.env手动切换 — 做不到自动路由、自动降级 - 不要把厂商 SSE 格式直接给前端 — 前端会被各种格式污染
- 不要假设所有模型都支持 tool calling / JSON schema / vision — 必须显式声明能力
- 不要只在失败后重试 — 429、TPM、余额不足都应进入状态管理
现成方案对比
| 方案 | 适合场景 | 优势 | 劣势 |
|---|---|---|---|
| 自己写轻量 Router | 学习 + 项目成长 | 理解全链路边界 | 前期基础设施多 |
| LiteLLM | 快速接多模型 | 100+ provider,统一 OpenAI 格式 | 自定义路由受限 |
| Vercel AI SDK | React/Next.js | 结构化输出、流式 UI | Python 项目不适用 |
| OpenRouter | 快速试模型 | 聚合层、自带 fallback | 不应让业务直接依赖 |