AI 后端网关设计:大模型服务限流、熔断与多供应商容错实践

一、大模型调用链路的稳定性困境

当企业内部超过 30 个业务线同时接入大模型能力时,后端架构面临的核心挑战不再是单次调用的正确性,而是调用链路的稳定性保障。生产环境中常见的故障模式包括:上游大模型供应商 API 突发限流返回 429、推理延迟从 500ms 飙升至 30s 导致线程池耗尽、单供应商宕机引发全业务线不可用。这些问题的本质是:大模型服务具有高延迟、高成本、低可靠性的特征,传统的 HTTP 调用模式无法直接承载。需要引入网关层实现限流、熔断与多供应商容错,将大模型调用从"裸调用"升级为"受控服务"。

二、AI 网关分层架构与请求流转机制

AI 网关的核心设计理念是将大模型调用抽象为标准化的服务治理对象,在网关层完成限流、熔断、重试、供应商切换等横切关注点,业务层只关心请求与响应。

flowchart TB
    A[业务服务 A] --> B[AI 网关 - 统一入口]
    C[业务服务 B] --> B
    D[业务服务 C] --> B

    B --> E[限流层 - 令牌桶+优先级队列]
    E --> F[熔断器 - 半开/全开/关闭]
    F --> G{供应商路由策略}
    G -->|权重+健康度| H[供应商 A - OpenAI]
    G -->|权重+健康度| I[供应商 B - Azure]
    G -->|权重+健康度| J[供应商 C - 本地模型]

    H -->|429/超时| K[降级策略]
    I -->|429/超时| K
    J -->|429/超时| K
    K -->|自动切换供应商| G
    K -->|所有供应商不可用| L[兜底响应 - 缓存/规则回复]

    style B fill:#339af0,color:#fff
    style K fill:#ff922b,color:#fff
    style L fill:#ff6b6b,color:#fff

限流层采用双层令牌桶设计:外层按业务线分配配额,防止单一业务线占用全部资源;内层按供应商分配配额,防止超出供应商的 QPS 上限。熔断器基于滑动窗口统计错误率,当 1 分钟内错误率超过 50% 时自动熔断,每 30 秒放行一个探测请求判断供应商是否恢复。

三、生产级 AI 网关核心实现

/**
 * AI 服务网关 - 多供应商容错路由器
 * 设计目的:将大模型调用封装为可路由、可降级、可观测的受控服务
 * 为什么用 Resilience4j 而非 Hystrix:
 *   Hystrix 已停止维护,Resilience4j 原生支持函数式编排,
 *   且熔断器状态机基于 ConcurrentRingBuffer,无锁性能更优
 */
@Service
@Slf4j
public class AiGatewayRouter {

    private final Map<String, LlmProviderClient> providerClients;
    private final Map<String, CircuitBreaker> circuitBreakers;
    private final AiGatewayProperties properties;
    private final LlmResponseCache responseCache;

    public AiGatewayRouter(List<LlmProviderClient> clients,
                           CircuitBreakerRegistry cbRegistry,
                           AiGatewayProperties properties,
                           LlmResponseCache responseCache) {
        // 按供应商名称索引客户端,支持运行时动态增减供应商
        this.providerClients = clients.stream()
            .collect(Collectors.toMap(LlmProviderClient::getProviderName, c -> c));
        // 每个供应商独立熔断器,避免单供应商故障影响其他供应商的熔断判断
        this.circuitBreakers = new ConcurrentHashMap<>();
        for (LlmProviderClient client : clients) {
            circuitBreakers.put(client.getProviderName(),
                cbRegistry.circuitBreaker("llm-" + client.getProviderName()));
        }
        this.properties = properties;
        this.responseCache = responseCache;
    }

    /**
     * 带容错的大模型调用入口
     * 核心逻辑:按优先级遍历供应商,熔断状态自动跳过,全部不可用时降级到缓存
     */
    public LlmResponse chat(LlmRequest request) {
        // 按权重+健康度排序供应商列表
        List<String> sortedProviders = getSortedProviders(request.getModel());

        LlmResponse fallbackResponse = null;
        for (String provider : sortedProviders) {
            CircuitBreaker cb = circuitBreakers.get(provider);
            if (cb == null) {
                log.warn("供应商 {} 无可用熔断器,跳过", provider);
                continue;
            }

            // 熔断器处于打开状态时直接跳过,避免无意义的等待
            if (cb.getState() == CircuitBreaker.State.OPEN) {
                log.info("供应商 {} 熔断中,尝试下一个供应商", provider);
                continue;
            }

            try {
                LlmProviderClient client = providerClients.get(provider);
                // 通过熔断器包装调用,自动统计失败率
                LlmResponse response = cb.executeSupplier(() ->
                    client.chat(request)
                );

                // 调用成功,异步写入缓存供降级使用
                if (response != null && response.isCacheable()) {
                    responseCache.putAsync(request.cacheKey(), response);
                }
                return response;

            } catch (CallNotPermittedException e) {
                // 熔断器在调用过程中打开,切换供应商
                log.warn("供应商 {} 调用被熔断拒绝: {}", provider, e.getMessage());
            } catch (LlmRateLimitException e) {
                // 429 限流:记录该供应商限流,但不计入熔断失败率
                // 为什么不计入熔断:限流是供应商的正常保护机制,不代表服务不可用
                log.warn("供应商 {} 触发限流,切换供应商", provider);
            } catch (LlmTimeoutException e) {
                log.error("供应商 {} 超时: {}ms", provider, e.getTimeoutMs());
            } catch (Exception e) {
                log.error("供应商 {} 调用异常", provider, e);
            }
        }

        // 所有供应商不可用,降级到缓存响应
        fallbackResponse = responseCache.get(request.cacheKey());
        if (fallbackResponse != null) {
            log.info("所有供应商不可用,降级到缓存响应,请求ID: {}", request.getRequestId());
            fallbackResponse.setFromCache(true);
            return fallbackResponse;
        }

        // 缓存也没有,返回兜底规则回复
        log.error("所有供应商不可用且无缓存,请求ID: {}", request.getRequestId());
        return LlmResponse.fallback("服务暂时不可用,请稍后重试");
    }

    /**
     * 供应商排序:权重优先,熔断状态降权
     * 为什么不固定顺序:供应商可用性是动态变化的,
     * 固定顺序会导致高权重供应商故障时仍被优先尝试
     */
    private List<String> getSortedProviders(String model) {
        return providerClients.values().stream()
            .filter(c -> c.supportsModel(model))
            .sorted((a, b) -> {
                int scoreA = a.getWeight();
                int scoreB = b.getWeight();
                CircuitBreaker cbA = circuitBreakers.get(a.getProviderName());
                CircuitBreaker cbB = circuitBreakers.get(b.getProviderName());
                // 熔断中的供应商权重降至最低,排在末尾
                if (cbA != null && cbA.getState() == CircuitBreaker.State.OPEN) scoreA = -1;
                if (cbB != null && cbB.getState() == CircuitBreaker.State.OPEN) scoreB = -1;
                return Integer.compare(scoreB, scoreA);
            })
            .map(LlmProviderClient::getProviderName)
            .collect(Collectors.toList());
    }
}

四、AI 网关方案的架构权衡与边界分析

缓存降级的一致性代价:缓存响应可能包含过时的模型输出。对于实时性要求高的场景(如代码生成、数据分析),过时响应的误导风险远大于服务不可用。因此缓存降级必须按场景配置开关,关键业务线应选择"快速失败"而非"缓存兜底"。

多供应商切换的模型一致性风险:不同供应商对同一模型的实现存在差异。例如 OpenAI 和 Azure OpenAI 的 GPT-4 在系统提示词处理上存在细微差别,切换供应商可能导致输出风格突变。解决方案是在路由层注入供应商适配器,统一提示词格式和输出解析逻辑。

熔断器参数的调优困境:熔断窗口过短(如 10 秒)容易因瞬时波动误触发熔断,窗口过长(如 5 分钟)则故障恢复慢。生产实践表明,1 分钟统计窗口 + 30 秒半开探测间隔是比较合理的起点,但仍需根据供应商的 SLA 特性微调。

网关层的性能损耗:网关引入了额外的序列化/反序列化、熔断器状态检查和路由计算开销。在 P99 延迟已经达到数秒的大模型调用场景中,网关层增加的 1~3ms 延迟可以忽略。但如果未来模型推理延迟降至百毫秒级,网关层的开销占比将显著上升,需要考虑将路由逻辑下沉到客户端 SDK。

五、总结

AI 后端网关的核心价值在于将大模型调用从不可控的外部依赖转化为可治理的内部服务。通过分层限流保护供应商配额、独立熔断器隔离故障传播、多供应商路由实现自动容错、缓存降级保障可用性底线,可以构建出满足企业级 SLA 要求的大模型调用链路。但每层机制都有其代价:缓存降级牺牲一致性、供应商切换引入输出差异、熔断参数需要持续调优。落地建议:先从单供应商 + 熔断器起步,建立基线指标;再引入第二供应商实现容错路由;最后根据业务场景决定是否启用缓存降级。网关层的复杂度应与业务对稳定性的实际需求匹配,避免过度设计。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐