调用大模型 API 时,你有没有遇到过这些问题:某个模型突然限流、响应变慢、甚至直接挂掉?或者 不同模型价格差异大,想根据任务复杂度选择合适的模型?如果你的服务只依赖单一模型,这些问题就是单点故障。解决方案很简单:多模型 + 负载均衡。这篇就聊用 Go 实现 AI 多模型负载均衡的思路和代码。


为什么需要多模型负载均衡?

假设你的应用只调用 OpenAI 的 GPT-4,某天 OpenAI 服务波动,你的应用就跟着「躺平」。更现实的问题是:

  1. 单点风险:一个模型挂了,整个服务不可用
  2. 成本优化:简单任务用 GPT-4 太贵,用 GPT-3.5 又怕效果不够
  3. 限流问题:单个 API 有速率限制,高峰期容易触发
  4. 响应速度:不同模型响应时间不同,想选最快的

多模型负载均衡的核心思路:把多个 AI 模型当作后端服务,用负载均衡策略分发请求。Go 的并发特性和简洁语法,非常适合实现这套逻辑。


一、基础架构:模型适配器模式

1.1 统一接口,屏蔽差异

不同模型 API 的调用方式各异:OpenAI 用 messages 数组,Claude 用 prompt 字符串,国产模型又有自己的格式。第一步是定义统一接口,屏蔽这些差异:

type AIModel interface {
    Name() string
    Generate(ctx context.Context, prompt string) (string, error)
}

每个模型实现这个接口:

type OpenAIModel struct { apiKey string }

func (m *OpenAIModel) Generate(ctx context.Context, prompt string) (string, error) {
    // 调用 OpenAI API,返回结果
}

type ClaudeModel struct { apiKey string }

func (m *ClaudeModel) Generate(ctx context.Context, prompt string) (string, error) {
    // 调用 Claude API,返回结果
}

这样,上层调用者不用关心具体是哪个模型,只管调用 Generate 方法。

1.2 模型注册中心

用一个注册中心管理所有可用模型:

type ModelRegistry struct {
    models map[string]AIModel
}

func (r *ModelRegistry) Register(name string, model AIModel) {
    r.models[name] = model
}

func (r *ModelRegistry) Get(name string) (AIModel, bool) {
    model, ok := r.models[name]
    return model, ok
}

初始化时注册所有模型:

registry := &ModelRegistry{models: make(map[string]AIModel)}
registry.Register("gpt4", &OpenAIModel{apiKey: "sk-xxx"})
registry.Register("gpt35", &OpenAIModel{apiKey: "sk-xxx"})
registry.Register("claude", &ClaudeModel{apiKey: "sk-xxx"})

二、负载均衡策略:从简单到智能

有了统一接口,接下来就是如何选择模型。常见的负载均衡策略有:轮询、加权轮询、随机、最少连接、响应时间优先等,大多类似 Nginx 的负载均衡策略。

2.1 轮询策略

最简单的策略,依次选择模型:

type RoundRobinBalancer struct {
    models []AIModel
    index  int
    mu     sync.Mutex
}

func (b *RoundRobinBalancer) Next() AIModel {
    b.mu.Lock()
    defer b.mu.Unlock()
    model := b.models[b.index]
    b.index = (b.index + 1) % len(b.models)
    return model
}

轮询的优点是简单公平,每个模型机会均等。缺点是不考虑模型性能差异,假如 GPT-4 和 GPT-3.5 响应时间差好几倍,轮询会导致整体响应变慢。

2.2 加权轮询策略

给每个模型分配权重,权重高的被选中概率大:

type WeightedModel struct {
    model  AIModel
    weight int
}

type WeightedRoundRobinBalancer struct {
    weightedModels []WeightedModel
    // ... 加权轮询逻辑
}

使用示例:

balancer := &WeightedRoundRobinBalancer{
    weightedModels: []WeightedModel{
        {model: gpt35, weight: 5},  // 50% 概率
        {model: gpt4, weight: 3},   // 30% 概率
        {model: claude, weight: 2}, // 20% 概率
    },
}

加权轮询适合成本敏感的场景:简单任务多用便宜的模型,复杂任务才用贵的模型。

2.3 最少连接策略

选择当前「最空闲」的模型,适合并发请求多的场景:

type LeastConnectionBalancer struct {
    models      []AIModel
    connections map[AIModel]int  // 每个模型的活跃连接数
    mu          sync.Mutex
}

func (b *LeastConnectionBalancer) Next() AIModel {
    b.mu.Lock()
    defer b.mu.Unlock()
    // 遍历找到连接数最少的模型
    var selected AIModel
    minConn := int(^uint(0) >> 1)
    for _, m := range b.models {
        if b.connections[m] < minConn {
            minConn, selected = b.connections[m], m
        }
    }
    b.connections[selected]++
    return selected
}

func (b *LeastConnectionBalancer) Release(model AIModel) {
    b.mu.Lock()
    b.connections[model]--
    b.mu.Unlock()
}

调用完成后要记得 Release,减少连接计数。这个策略能动态感知负载,避免把请求都打到同一个模型上。

2.4 响应时间优先

选择响应最快的模型,适合追求速度的场景:

type ResponseTimeBalancer struct {
    models       []AIModel
    responseTime map[AIModel]time.Duration
    mu           sync.RWMutex
}

func (b *ResponseTimeBalancer) Next() AIModel {
    b.mu.RLock()
    defer b.mu.RUnlock()
    // 遍历找到响应时间最短的模型
    var selected AIModel
    minTime := time.Hour
    for _, m := range b.models {
        if b.responseTime[m] < minTime {
            minTime, selected = b.responseTime[m], m
        }
    }
    return selected
}

每次调用后更新响应时间统计,下次选择时就能参考历史数据。这个策略需要一段时间预热,积累足够数据后才准确。


三、故障转移:当模型挂了怎么办?

负载均衡解决了「选哪个模型」,但没解决「模型挂了怎么办」。需要加入健康检查故障转移机制。

3.1 简单的重试逻辑

最基础的故障转移:调用失败时,换一个模型重试:

type FailoverBalancer struct {
    balancer LoadBalancer
    maxRetry int
}

func (b *FailoverBalancer) Generate(ctx context.Context, prompt string) (string, error) {
    var lastErr error
    for i := 0; i < b.maxRetry; i++ {
        model := b.balancer.Next()
        result, err := model.Generate(ctx, prompt)
        if err == nil {
            return result, nil
        }
        lastErr = err  // 记录失败,可标记模型不健康
    }
    return "", fmt.Errorf("all models failed: %v", lastErr)
}

3.2 健康检查

定期检查模型是否可用,不健康的模型暂时剔除:

type HealthChecker struct {
    models   []AIModel
    healthy  map[AIModel]bool
    interval time.Duration
}

func (h *HealthChecker) Start() {
    ticker := time.NewTicker(h.interval)
    for range ticker.C {
        for _, m := range h.models {
            ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
            _, err := m.Generate(ctx, "ping")
            h.healthy[m] = (err == nil)
            cancel()
        }
    }
}

负载均衡器在选择模型时,只从 healthytrue 的模型中选。

3.3 熔断器模式

更高级的做法是用熔断器(Circuit Breaker):当某个模型连续失败多次,暂时「熔断」,不再请求它,过一段时间后再尝试恢复:

熔断器能快速失败,避免浪费时间请求一个已经挂掉的模型。


四、成本优化:智能路由

不同模型价格差异很大。GPT-4 可能比 GPT-3.5 贵,但不是所有任务都需要 GPT-4。可以根据任务复杂度选择模型

4.1 基于任务类型的路由

type SmartRouter struct {
    simpleModel  AIModel  // 便宜模型,处理简单任务
    complexModel AIModel  // 贵模型,处理复杂任务
}

func (r *SmartRouter) Generate(ctx context.Context, prompt string) (string, error) {
    if isSimpleTask(prompt) {
        return r.simpleModel.Generate(ctx, prompt)
    }
    return r.complexModel.Generate(ctx, prompt)
}

func isSimpleTask(prompt string) bool {
    // 简单启发式:短 prompt、关键词匹配等
    return len(prompt) < 100 || strings.Contains(prompt, "翻译")
}

4.2 基于 Token 估算的路由

更精细的做法是估算输出 Token 数,选择性价比最高的模型:

func (r *SmartRouter) SelectModel(prompt string) AIModel {
    estimatedTokens := estimateTokens(prompt)
    if estimatedTokens < 500 {
        return r.cheapModel
    } else if estimatedTokens < 2000 {
        return r.mediumModel
    }
    return r.expensiveModel
}

五、完整架构示意

把以上组件组合起来,整体架构如下:

┌─────────────────────────────────────────────┐
│                  业务层                      │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────▼───────────────────────────┐
│              负载均衡器                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │ 轮询策略 │ │加权轮询 │ │最少连接 │    │
│  └──────────┘ └──────────┘ └──────────┘    │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────▼───────────────────────────┐
│              故障转移层                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │ 重试逻辑 │ │健康检查 │ │ 熔断器  │    │
│  └──────────┘ └──────────┘ └──────────┘    │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────▼───────────────────────────┐
│              模型适配器                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │  OpenAI  │ │ Claude  │ │ 国产模型 │    │
│  └──────────┘ └──────────┘ └──────────┘    │
└─────────────────────────────────────────────┘

写在最后

多模型负载均衡不是什么高深技术,本质就是把后端负载均衡的思路应用到 AI 模型调用上。Go 的接口、并发原语让实现变得简洁。

在实际项目中,都是一点点做起来的,从简单开始:先用轮询 + 重试,跑起来后再加健康检查、熔断器。不要一上来就搞复杂架构,够用就好