调用大模型 API 时,你有没有遇到过这些问题:某个模型突然限流、响应变慢、甚至直接挂掉?或者 不同模型价格差异大,想根据任务复杂度选择合适的模型?如果你的服务只依赖单一模型,这些问题就是单点故障。解决方案很简单:多模型 + 负载均衡。这篇就聊用 Go 实现 AI 多模型负载均衡的思路和代码。
为什么需要多模型负载均衡?
假设你的应用只调用 OpenAI 的 GPT-4,某天 OpenAI 服务波动,你的应用就跟着「躺平」。更现实的问题是:
- 单点风险:一个模型挂了,整个服务不可用
- 成本优化:简单任务用 GPT-4 太贵,用 GPT-3.5 又怕效果不够
- 限流问题:单个 API 有速率限制,高峰期容易触发
- 响应速度:不同模型响应时间不同,想选最快的
多模型负载均衡的核心思路:把多个 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()
}
}
}
负载均衡器在选择模型时,只从 healthy 为 true 的模型中选。
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 的接口、并发原语让实现变得简洁。
在实际项目中,都是一点点做起来的,从简单开始:先用轮询 + 重试,跑起来后再加健康检查、熔断器。不要一上来就搞复杂架构,够用就好。