调用 AI 模型时,如何实现一个简单的熔断机制

适合场景:RAG 应用、智能客服、知识库问答、内容生成、Embedding 检索、Rerank 精排等需要调用 AI 模型服务的系统。

前言

在 AI 应用里,我们经常会调用不同类型的模型服务,例如:

Chat 模型:负责对话、问答、总结、生成
Embedding 模型:负责把文本转换成向量
Rerank 模型:负责对检索结果做精排

这些模型可能来自云厂商,也可能是本地部署的推理服务,比如 Ollama、vLLM、Text Embeddings Inference、Rerank Server 等。

在理想情况下,模型服务稳定可用,请求来了就调用模型,模型返回结果。但真实线上环境通常没这么顺利。

常见问题包括:

模型接口超时
供应商限流
网络连接异常
API Key 配置错误
本地模型服务正在重启
GPU 显存不足
模型接口返回异常响应

如果某个模型已经连续失败,系统还继续把请求打过去,就会带来几个问题:

用户请求响应变慢
失败请求持续堆积
下游模型服务压力更大
备用模型没有及时接管
刚恢复的模型可能被瞬间打爆

这时就需要一个简单的熔断机制。

它的核心思想很朴素:

模型正常时,继续调用;
模型连续失败时,暂时跳过;
过一段时间后,放一个请求试探它是否恢复;
如果恢复成功,重新启用;
如果仍然失败,继续熔断。

本文介绍一种轻量级实现方式,重点讲清楚逻辑,不依赖复杂框架。

技术原理

1. 熔断机制解决什么问题

假设系统里配置了两个 Rerank 模型:

主模型:远程 Rerank 模型,效果更好
备用模型:Noop Rerank,只做简单截断

正常情况下,请求优先调用主模型:

用户问题 + 候选文档
  -> 主 Rerank 模型
  -> 返回相关性排序后的结果

如果主模型接口异常,系统可以切换到备用模型:

主模型调用失败
  -> 记录失败
  -> 尝试备用模型
  -> 返回降级结果

如果主模型连续失败多次,就不要每次都继续尝试它,而是让它冷却一段时间:

主模型连续失败
  -> 进入熔断状态
  -> 一段时间内跳过主模型
  -> 直接尝试备用模型

这就是熔断机制的价值:减少无效请求,保护下游服务,并让系统具备更稳定的降级能力。

2. 熔断器的三个状态

一个简单熔断器通常只需要三个状态:

CLOSED     正常状态,可以调用模型
OPEN       熔断状态,暂时不调用模型
HALF_OPEN  半开状态,允许一个请求试探模型是否恢复

状态流转如下:

CLOSED
  -> 连续失败达到阈值
  -> OPEN

OPEN
  -> 熔断时间结束
  -> HALF_OPEN

HALF_OPEN
  -> 探测成功:恢复 CLOSED
  -> 探测失败:重新 OPEN

可以把它理解成一个比较谨慎的保护策略:

CLOSED:我认为模型是好的,正常调用。
OPEN:我认为模型短时间内不可用,先别调用。
HALF_OPEN:我不确定模型是否恢复了,先放一个请求试试。

3. 需要保存哪些健康状态

对每个模型,只需要维护少量状态:

class HealthState {
    int consecutiveFailures;
    long openUntil;
    boolean halfOpenInFlight;
    State state;
}

字段含义如下:

consecutiveFailures  连续失败次数
openUntil            熔断结束时间
halfOpenInFlight     半开状态下是否已有探测请求正在执行
state                当前状态

这些状态可以按模型 ID 保存:

modelId -> HealthState

例如:

chat-model      -> CLOSED
embedding-model -> CLOSED
rerank-model    -> OPEN

这样设计有一个好处:每个模型的健康状态互不影响。

Chat 模型失败,不影响 Embedding 模型;Rerank 模型失败,也不影响 Chat 模型。

4. 调用模型前:判断是否允许调用

每次调用模型前,先判断一次:

这个模型当前还能不能调用?

核心逻辑如下:

如果是 CLOSED:
  允许调用

如果是 OPEN:
  如果还没到熔断结束时间:
    不允许调用
  如果已经到了熔断结束时间:
    切换到 HALF_OPEN
    允许一个请求试探

如果是 HALF_OPEN:
  如果已经有探测请求在执行:
    不允许调用
  否则:
    允许一个探测请求

简化代码:

boolean allowCall(String modelId) {
    HealthState h = getState(modelId);

    if (h.state == State.CLOSED) {
        return true;
    }

    if (h.state == State.OPEN) {
        if (System.currentTimeMillis() < h.openUntil) {
            return false;
        }

        h.state = State.HALF_OPEN;
        h.halfOpenInFlight = true;
        return true;
    }

    if (h.state == State.HALF_OPEN) {
        if (h.halfOpenInFlight) {
            return false;
        }

        h.halfOpenInFlight = true;
        return true;
    }

    return false;
}

这里的 halfOpenInFlight 很关键。

它的作用是控制半开探测数量。模型刚从熔断期出来时,不应该让大量请求同时打过去,而是先放一个请求试探。

如果这个请求成功,说明模型恢复了;如果失败,说明模型还不可用。

5. 调用成功后:恢复健康状态

如果模型调用成功,就认为它当前是健康的。

这时可以执行 markSuccess

void markSuccess(String modelId) {
    HealthState h = getState(modelId);

    h.state = State.CLOSED;
    h.consecutiveFailures = 0;
    h.openUntil = 0;
    h.halfOpenInFlight = false;
}

成功后的处理很直接:

状态恢复为 CLOSED
连续失败次数清零
熔断结束时间清零
半开探测标记清除

也就是说,只要一次调用成功,就认为模型可以重新进入正常服务状态。

6. 调用失败后:累计失败并触发熔断

如果模型调用失败,就记录一次失败。

当连续失败次数达到阈值时,进入熔断状态:

void markFailure(String modelId) {
    HealthState h = getState(modelId);
    long now = System.currentTimeMillis();

    if (h.state == State.HALF_OPEN) {
        h.state = State.OPEN;
        h.openUntil = now + openDurationMs;
        h.consecutiveFailures = 0;
        h.halfOpenInFlight = false;
        return;
    }

    h.consecutiveFailures++;

    if (h.consecutiveFailures >= failureThreshold) {
        h.state = State.OPEN;
        h.openUntil = now + openDurationMs;
        h.consecutiveFailures = 0;
    }
}

比如配置为:

failureThreshold = 2
openDurationMs = 30000

含义是:

连续失败 2 次后熔断
熔断持续 30 秒

半开状态下如果调用失败,不需要继续累计失败次数,而是直接重新熔断。

因为半开请求本来就是恢复性探测。探测失败,就说明模型还没恢复。

7. 和模型路由结合

熔断机制通常不是单独使用,而是和模型路由、故障转移一起使用。

假设有一组候选模型:

候选 1:主模型
候选 2:备用模型
候选 3:本地兜底模型

调用流程可以设计成:

按优先级遍历候选模型
  -> 检查当前模型是否允许调用
  -> 如果正在熔断,跳过
  -> 如果允许调用,尝试请求模型
  -> 成功则返回结果
  -> 失败则记录失败,并尝试下一个模型

简化代码:

for (ModelTarget target : targets) {
    if (!healthStore.allowCall(target.id())) {
        continue;
    }

    try {
        Result result = callModel(target);
        healthStore.markSuccess(target.id());
        return result;
    } catch (Exception ex) {
        healthStore.markFailure(target.id());
    }
}

throw new RuntimeException("All model candidates failed");

这样,当主模型不可用时,请求不会一直卡在主模型上,而是会继续尝试后面的候选模型。

这就是一个简单的故障转移过程。

8. 这是被动健康监测

需要注意的是,这种方案属于被动健康监测。

它不会主动定时请求模型接口,也不会后台 ping 模型服务。

它只根据真实业务调用结果更新健康状态:

真实调用成功 -> 认为模型健康
真实调用失败 -> 记录失败
连续失败过多 -> 进入熔断
熔断时间结束 -> 用下一次真实请求做半开探测

这种方式的优点是简单、轻量,不需要额外线程,也不依赖专门的健康检查接口。

缺点也很明确:如果模型已经恢复,但一直没有新请求进来,系统不会主动发现它恢复。只有下一次真实请求到来时,才会触发半开探测。

9. 单机和分布式场景

如果应用只有一个实例,健康状态可以直接保存在内存里。

例如:

Map<String, HealthState> healthMap = new ConcurrentHashMap<>();

这已经能满足很多简单场景。

但如果应用是多实例部署,并且多个实例调用的是同一个模型服务,那么只保存在本地内存里就不够了。

例如:

应用实例 A 发现模型失败,并进入熔断
应用实例 B 不知道,仍然继续调用
应用实例 C 也不知道,仍然继续调用

这种情况下,熔断状态是不一致的。

如果希望多个应用实例共享同一个模型健康状态,可以把状态放到 Redis 里:

model-health:rerank-model -> OPEN
model-health:chat-model   -> CLOSED

分布式场景还需要特别注意半开探测。

如果多个实例同时发现熔断时间到了,可能会一起把请求打到刚恢复的模型上。更稳妥的做法是用 Redis 锁控制:

同一时间,只允许一个实例执行半开探测

可以用类似下面的方式实现:

SET model-health:rerank-model:probe-lock 1 NX PX 30000

谁抢到锁,谁执行探测;其他实例继续跳过。

一个完整调用过程示例

以 Rerank 模型为例:

1. 请求进入系统,需要对检索结果做精排
2. 系统优先选择主 Rerank 模型
3. 调用前执行 allowCall(modelId)
4. 如果模型处于 CLOSED,允许调用
5. 模型调用失败,执行 markFailure(modelId)
6. 失败次数达到阈值,模型进入 OPEN
7. 后续请求在 30 秒内跳过该模型
8. 系统自动尝试备用模型
9. 30 秒后,下一次请求触发 HALF_OPEN 探测
10. 探测成功,模型恢复 CLOSED
11. 探测失败,模型重新进入 OPEN

这个过程不复杂,但能让模型调用链路稳定很多。

总结

一个简单的 AI 模型熔断机制,可以总结为几句话:

调用前判断是否允许访问;
调用成功后恢复健康;
调用失败后累计失败;
连续失败达到阈值后熔断;
熔断时间结束后允许一个请求探测;
探测成功则恢复,失败则继续熔断。

它的优势是:

实现简单
不依赖复杂框架
适合模型路由和故障转移
可以快速提升系统稳定性

它的边界是:

本地内存状态不适合多实例共享
被动监测无法主动发现模型恢复
半开探测需要控制并发
失败原因需要谨慎区分,避免误熔断

对于大多数 AI 应用来说,可以先实现一个轻量级的内存熔断器。

当系统进入多实例部署,或者多个服务共享同一个本地模型服务时,再把健康状态迁移到 Redis 这类共享存储中,是比较自然的演进方向。

总结

AI 应用在调用 Chat、Embedding、Rerank 等模型服务时,经常会遇到接口超时、供应商限流、网络异常、本地模型重启等问题。本文介绍了一种简单的熔断机制:通过 CLOSEDOPENHALF_OPEN 三种状态管理模型健康,在真实调用成功或失败后被动更新状态,并结合模型路由实现失败后的自动切换,从而提升 AI 模型调用链路的稳定性。

Logo

免费领 100 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐