一句话结论
多模型配置不能只做“每次换一个模型”。真正可用于生产的方案需要把负载分配、故障切换、重试、熔断和质量监控分开:轮询负责分流,可重试错误触发有限重试,连续失败触发熔断,模型输出还必须经过业务校验。
如果你还没有完成单模型连通,可以先看站内的 GLM-4.7-Flash API 配置与实测。本文默认每个模型已经能独立返回正常响应。
轮询和故障切换不是一回事
轮询(round-robin)按顺序把新请求交给不同模型,主要解决流量分配问题。故障切换(failover)则是在当前模型无法完成任务时,选择另一个健康模型继续请求。只实现轮询会有三个问题:
- 已经故障的模型仍会周期性收到请求;
- 同一个请求失败后不会自动换模型;
- 不同模型的输出质量和参数兼容性没有被处理。
因此,模型池至少要记录 enabled、失败次数、熔断截止时间和最近一次错误,而轮询游标不应该写入长期配置。
一个可运行的 Node.js 模型池
下面的实现使用 OpenAI 兼容的 Chat Completions 接口。API Key 从环境变量读取,模型配置中不保存明文密钥。
const models = [
{
id: 'glm-flash',
model: 'glm-4.7-flash',
url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
apiKey: process.env.ZHIPU_API_KEY,
enabled: true,
failures: 0,
openUntil: 0,
},
{
id: 'backup',
model: process.env.BACKUP_MODEL,
url: process.env.BACKUP_URL,
apiKey: process.env.BACKUP_API_KEY,
enabled: true,
failures: 0,
openUntil: 0,
},
];
let cursor = 0;
function healthyModels() {
const now = Date.now();
return models.filter(m => m.enabled && m.apiKey && m.url && m.openUntil <= now);
}
function orderedCandidates() {
const healthy = healthyModels();
if (!healthy.length) throw new Error('没有可用模型');
const start = cursor++ % healthy.length;
return [...healthy.slice(start), ...healthy.slice(0, start)];
}
function isRetryable(status) {
return status === 408 || status === 429 || status >= 500;
}
async function callOne(config, messages) {
const response = await fetch(config.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
messages,
temperature: 0.2,
max_tokens: 800,
}),
signal: AbortSignal.timeout(30_000),
});
const raw = await response.text();
if (!response.ok) {
const error = new Error(`${config.id} returned ${response.status}`);
error.status = response.status;
error.retryable = isRetryable(response.status);
error.detail = raw.slice(0, 300);
throw error;
}
const payload = JSON.parse(raw);
const content = payload.choices?.[0]?.message?.content?.trim();
if (!content) throw new Error(`${config.id} returned empty content`);
return { content, usage: payload.usage, responseModel: payload.model };
}
export async function generate(messages) {
const errors = [];
for (const model of orderedCandidates()) {
try {
const result = await callOne(model, messages);
model.failures = 0;
return { ...result, configuredModel: model.id };
} catch (error) {
errors.push({ model: model.id, message: error.message });
model.failures += 1;
if (model.failures >= 3) {
model.openUntil = Date.now() + 60_000;
}
if (!error.retryable && error.status) break;
}
}
throw new AggregateError(errors, '所有候选模型均调用失败');
}
这段代码展示的是最小骨架。多进程或多实例部署时,内存中的游标和熔断状态彼此不可见,应改用 Redis 或数据库保存共享状态。
哪些错误应该重试
智谱官方错误码说明中,401 通常表示鉴权问题,429 可能代表并发超额、余额不足或账户异常,500 表示服务端错误。因此不能看到 429 就无限重试。
| 状态 | 建议处理 |
|---|---|
| 400 | 参数或输入错误,不换模型盲目重试 |
| 401 | 停止请求并告警,检查对应模型密钥 |
| 408/网络超时 | 退避后重试一次,再切备用模型 |
| 429 | 读取业务错误;并发超额可退避,余额或账户问题应熔断 |
| 500/502/503 | 短暂退避,失败后切换健康模型 |
重试会增加请求量,因此每个业务请求建议最多尝试两到三个模型,并设置总时间预算。例如总预算 45 秒时,不应让每个候选模型都等待 30 秒。
指数退避要加随机抖动
多个任务同时收到 429 后,如果都在固定的一秒后重试,会再次形成流量尖峰。可以使用带随机抖动的退避:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function backoff(attempt) {
const base = Math.min(500 * 2 ** attempt, 8_000);
await wait(base + Math.random() * 300);
}
退避只适用于可能自行恢复的错误。错误密钥、非法参数和内容校验失败需要分别处理。
熔断器如何设计
一个实用的轻量规则是:同一模型连续失败三次后熔断 60 秒;熔断结束只放行少量探测请求;探测成功关闭熔断,失败则延长冷却时间。生产环境还应区分:
- 接口可用性故障:超时、5xx、连接错误;
- 配额故障:并发、余额或套餐问题;
- 内容质量故障:空输出、JSON 无法解析、字段缺失;
- 安全故障:密钥失效、输入或输出触发策略。
这些问题的恢复方式不同,不应只累计一个模糊的失败数字。
轮询策略怎么选
固定选中模型
适合需要结果稳定、便于评测和排查的摘要、日报生成。默认只使用一个经过验证的模型,故障时才切换备用模型。
普通轮询
适合能力、价格和输出结构相近的模型。每个请求轮换一次,但仍需跳过熔断中的节点。
加权轮询
模型成本或容量不同的时候更合适。例如权重 3:1 表示主模型大约承担四分之三请求。权重应基于实际成功率、延迟和成本调整,而不是只看模型宣传参数。
按任务路由
通常比纯轮询更有价值:分类与摘要使用快速模型,复杂代码审查使用强模型,敏感任务走人工审核。先路由任务,再在同一等级的健康模型之间轮询。
输出校验决定切换是否真的有效
HTTP 200 不代表任务成功。文章分类至少要检查分类是否属于允许集合;JSON 摘要要验证必填字段、类型和条数;代码任务可以运行语法检查或测试。校验失败后是否切模型,取决于失败原因:提示词导致所有模型都错时,轮询只会浪费费用。
建议把结果分为 transport_ok、parse_ok、schema_ok 和 quality_ok 四层指标。这样才能区分接口稳定但内容不合格,还是网络本身失败。
日志和监控至少记录什么
- 内部请求 ID 与任务类型;
- 配置模型、响应模型和供应商;
- HTTP 状态、业务错误码和结束原因;
- 首字延迟、总延迟、输入与输出 Token;
- 重试次数、切换链路和熔断状态;
- JSON/Schema 校验结果;
- 不含密钥和敏感正文的错误摘要。
最值得建立的看板不是“调用次数”,而是各模型的业务成功率、P95 延迟、单次成功成本和人工返工率。
上线前故障演练
- 临时填入错误密钥,确认 401 不会无限重试;
- 把主模型地址改到不可达端口,确认超时后切换备用模型;
- 模拟 429,确认退避和总时间预算生效;
- 返回 HTTP 200 但空 content,确认业务校验会判失败;
- 连续失败三次,确认模型从候选池中暂时移除;
- 所有模型失败,确认任务进入可重跑队列且不会写入半成品。
如果多模型用于日报或文章处理,建议结合 Node.js AI 总结管线设计,把模型切换放在可重跑的总结阶段,而不是与抓取逻辑耦合。
最终建议
内容生成任务优先采用“固定主模型 + 健康检查 + 有限故障切换”,因为输出一致性比平均分流更重要。只有同等级模型经过固定样例评测后,才启用轮询或加权轮询。无论使用哪种策略,熔断、时间预算、结构校验和可观测性都比轮询算法本身更重要。
常见问题
多模型轮询能代替故障切换吗?
不能。轮询只负责分配新请求;故障切换还需要识别可重试错误、跳过熔断模型,并为当前失败任务选择健康候选。
429 是否都应该重试?
不是。并发或临时限流可以退避重试,余额不足、账户异常和密钥问题应停止重试并告警。
熔断状态应该保存在哪里?
单进程可以临时保存在内存;多实例部署应使用 Redis 或数据库共享,否则各实例会重复向故障模型发送请求。