原创

Node.js 多模型轮询与故障切换:重试、熔断和监控实现

面向生产环境的 Node.js 多模型调用方案:区分轮询与故障切换,给出可运行模型池代码,并讲清 401、429、5xx 的处理、指数退避、熔断、任务路由、输出校验和监控指标。

GLM API 实战专题 · 第 2/3 篇查看专题目录 →

一句话结论

多模型配置不能只做“每次换一个模型”。真正可用于生产的方案需要把负载分配、故障切换、重试、熔断和质量监控分开:轮询负责分流,可重试错误触发有限重试,连续失败触发熔断,模型输出还必须经过业务校验。

如果你还没有完成单模型连通,可以先看站内的 GLM-4.7-Flash API 配置与实测。本文默认每个模型已经能独立返回正常响应。

轮询和故障切换不是一回事

轮询(round-robin)按顺序把新请求交给不同模型,主要解决流量分配问题。故障切换(failover)则是在当前模型无法完成任务时,选择另一个健康模型继续请求。只实现轮询会有三个问题:

  1. 已经故障的模型仍会周期性收到请求;
  2. 同一个请求失败后不会自动换模型;
  3. 不同模型的输出质量和参数兼容性没有被处理。

因此,模型池至少要记录 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 延迟、单次成功成本和人工返工率。

上线前故障演练

  1. 临时填入错误密钥,确认 401 不会无限重试;
  2. 把主模型地址改到不可达端口,确认超时后切换备用模型;
  3. 模拟 429,确认退避和总时间预算生效;
  4. 返回 HTTP 200 但空 content,确认业务校验会判失败;
  5. 连续失败三次,确认模型从候选池中暂时移除;
  6. 所有模型失败,确认任务进入可重跑队列且不会写入半成品。

如果多模型用于日报或文章处理,建议结合 Node.js AI 总结管线设计,把模型切换放在可重跑的总结阶段,而不是与抓取逻辑耦合。

最终建议

内容生成任务优先采用“固定主模型 + 健康检查 + 有限故障切换”,因为输出一致性比平均分流更重要。只有同等级模型经过固定样例评测后,才启用轮询或加权轮询。无论使用哪种策略,熔断、时间预算、结构校验和可观测性都比轮询算法本身更重要。

常见问题

多模型轮询能代替故障切换吗?

不能。轮询只负责分配新请求;故障切换还需要识别可重试错误、跳过熔断模型,并为当前失败任务选择健康候选。

429 是否都应该重试?

不是。并发或临时限流可以退避重试,余额不足、账户异常和密钥问题应停止重试并告警。

熔断状态应该保存在哪里?

单进程可以临时保存在内存;多实例部署应使用 Redis 或数据库共享,否则各实例会重复向故障模型发送请求。

实测与内容说明

实测记录

  • 基于本站已上线的多模型配置与轮询实现整理
  • 错误处理建议逐项对照智谱官方 API 错误码
  • 示例覆盖轮询、有限故障切换和连续失败熔断

参考资料

内容版本 1.1 · 审核:推荐智能手记 · 计划复审:2026-09-12