AI Gateway 模型路由器设计

BodySense 多 API 提供商的统一抽象层设计:AIService → ModelRouter → ProviderAdapter → QuotaManager,业务层不感知厂商差异。

#tech / ai #tech / architecture #bodysense #type / note #domain / software

[!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

常见陷阱

  1. 不要在业务代码里写 if (provider === "mimo") — 会很快失控
  2. 不要只用 .env 手动切换 — 做不到自动路由、自动降级
  3. 不要把厂商 SSE 格式直接给前端 — 前端会被各种格式污染
  4. 不要假设所有模型都支持 tool calling / JSON schema / vision — 必须显式声明能力
  5. 不要只在失败后重试 — 429、TPM、余额不足都应进入状态管理

现成方案对比

方案适合场景优势劣势
自己写轻量 Router学习 + 项目成长理解全链路边界前期基础设施多
LiteLLM快速接多模型100+ provider,统一 OpenAI 格式自定义路由受限
Vercel AI SDKReact/Next.js结构化输出、流式 UIPython 项目不适用
OpenRouter快速试模型聚合层、自带 fallback不应让业务直接依赖
创建于 2026/6/27 更新于 2026/6/27