Spring AI 落地企业中台:多模型动态路由与 SSE 流式长连接高并发治理

Spring AI 落地企业中台:多模型动态路由与 SSE 流式长连接高并发治理

Spring AI 落地企业中台:多模型动态路由与 SSE 流式长连接高并发治理

一、问题背景与业务痛点

在企业落地 LLM(大语言模型)应用的初级阶段,业务研发通常直接通过 Http Client 绑定调用某一家大模型厂商(如 OpenAI)。但随着业务量级跃升及业务场景精细化,这种单点耦合架构迅速暴露了一系列生产级隐患:

  1. Vendor Lock-in(供应商绑定)与容灾短板:依赖单一提供商极易遇到限流(HTTP 429 Too Many Requests)、机房故障或网络不可达。生产环境急需根据 DeepSeek(高性价比推理)、Gemini(多模态/大上下文)、OpenAI(通用复杂指令)等不同厂商的可用率和成本实施动态引流与降级。
  2. 长连接“假死”与连接池耗尽(SSE Connection Leak):LLM 生成内容平均耗时达 10~60 秒。高并发场景下,若前端用户刷新页面、关闭 Tab 标签页或网络微断,服务端若未能感知连接取消,后端工作线程和上游厂商连接将继续“空跑”直到整个 Prompt 生成完毕。这不仅导致昂贵的 Token 浪费,更会导致容器线程池与 Tomcat/Netty 连接瞬间打满。
  3. 中间件代理“伪流式”(Nginx Buffering Trap):使用 Server-Sent Events(SSE)输出“打字机”效果时,若链路中的反向代理(如 Nginx、API 网关)开启了响应缓冲(Proxy Buffering),会导致流式数据被暂存为整包响应,打字机直接退化成卡顿数秒后的“一次性全量输出”,严重破坏交互体验。

要将 Spring AI 真正推向企业生产环境,必须打破原生的硬编码方式,构建具备动态权重分发能力与高可用 SSE 治理机制的统一中台网关。


二、核心设计与解决思路

针对上述痛点,中台设计聚焦于两层架构演进:
1. 模型调度层:基于 Spring AI 的 ChatModel 统一接口,抽象出带有平滑权重分发与熔断降级的 DynamicChatModelRouter。
2. 长连接传输层:采用 Spring WebFlux / Project Reactor 响应式编程模型,构建支持背压(Backpressure)、全链路生命周期感知(Cancel Propagation)及 Nginx 零缓冲穿透的 SSE 传输链路。

1. 核心架构设计与组件拓扑图

▲ 架构图 1:系统核心组件交互拓扑与数据流向
▲ 架构图 1:系统核心组件交互拓扑与数据流向

2. 端到端请求执行时序图

▲ 时序图 2:端到端请求处理与调用时序链路
▲ 时序图 2:端到端请求处理与调用时序链路

3. 技术选型与传输方案横向对比

传输与接入方案 协议开销 连接断开感知能力 防火墙与代理穿透性 适用场景
原生短轮询 (Polling) 极高(反复握手) 无连接状态,感知滞后 极佳 离线耗时极长的任务排队
WebSocket 极低(基于 TCP 全双工) 极佳(内置 Ping/Pong 心跳) 一般(部分代理阻断非 HTTP) 双向高频对话、协同白板交互
SSE (Server-Sent Events) 低(纯 HTTP 传输,单向) 佳(需 Reactor 生命周期支持) 极佳(标准 HTTP 端口,无特殊握手) LLM 文本生成、打字机长文本推流(企业首选)

三、完整实战代码与配置

环境依赖:Spring Boot 3.3.x + Spring AI 1.0.0-M1 + Java 17+。

1. application.yml 多模型注册与权重配置

spring:
  application:
    name: enterprise-ai-gateway
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:sk-fake-openai}
      base-url: https://api.openai.com
    # DeepSeek 兼容 OpenAI 协议规范
    deepseek:
      api-key: ${DEEPSEEK_API_KEY:sk-fake-deepseek}
      base-url: https://api.deepseek.com
    # Gemini 原生配置或适配层
    gemini:
      api-key: ${GEMINI_API_KEY:fake-gemini}

# 动态路由权重字典 (支持 Spring Cloud Config / Nacos 运行时刷新)
ai:
  routing:
    rules:
      - provider: deepseek
        weight: 60
        enabled: true
      - provider: openai
        weight: 30
        enabled: true
      - provider: gemini
        weight: 10
        enabled: false # 支持蓝绿与灰度启停

2. DynamicChatModelRouter.java:线程安全的加权路由组件

package com.mrwu.ai.gateway.router;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;

@Component
@ConfigurationProperties(prefix = "ai.routing")
public class DynamicChatModelRouter {

    private static final Logger log = LoggerFactory.getLogger(DynamicChatModelRouter.class);

    private final Map<String, ChatModel> registeredModels = new ConcurrentHashMap<>();
    private List<RouteRule> rules = new ArrayList<>();

    // 依赖注入:注入不同 Provider 的 ChatModel Bean
    public DynamicChatModelRouter(Map<String, ChatModel> chatModelBeans) {
        this.registeredModels.putAll(chatModelBeans);
    }

    public static class RouteRule {
        private String provider;
        private int weight;
        private boolean enabled;

        public String getProvider() { return provider; }
        public void setProvider(String provider) { this.provider = provider; }
        public int getWeight() { return weight; }
        public void setWeight(int weight) { this.weight = weight; }
        public boolean isEnabled() { return enabled; }
        public void setEnabled(boolean enabled) { this.enabled = enabled; }
    }

    public void setRules(List<RouteRule> rules) {
        this.rules = rules;
    }

    /**
     * 基于权重随机算法抽取 ChatModel,并支持降级链路
     */
    public ChatModel route() {
        List<RouteRule> activeRules = rules.stream()
                .filter(RouteRule::isEnabled)
                .filter(r -> r.getWeight() > 0)
                .toList();

        if (activeRules.isEmpty()) {
            throw new IllegalStateException("无可用的 LLM 路由策略规则!");
        }

        int totalWeight = activeRules.stream().mapToInt(RouteRule::getWeight).sum();
        int randomVal = ThreadLocalRandom.current().nextInt(totalWeight);
        int currentSum = 0;

        for (RouteRule rule : activeRules) {
            currentSum += rule.getWeight();
            if (randomVal < currentSum) {
                ChatModel target = getModelBean(rule.getProvider());
                if (target != null) {
                    log.debug("命中路由 Provider: [{}], Weight: [{}]", rule.getProvider(), rule.getWeight());
                    return target;
                }
            }
        }

        // 兜底返回第一个可用模型
        return getModelBean(activeRules.get(0).getProvider());
    }

    private ChatModel getModelBean(String provider) {
        // 根据 Bean 名称规范映射,例如 deepseekChatModel, openAiChatModel
        String beanName = provider.toLowerCase() + "ChatModel";
        ChatModel model = registeredModels.get(beanName);
        if (model == null) {
            log.warn("无法定位 BeanName=[{}] 的 ChatModel 实例,尝试全量扫描备选", beanName);
            return registeredModels.values().stream().findFirst().orElse(null);
        }
        return model;
    }
}

3. AiStreamController.java:高可用流式长连接控制器

包含:客户端断开检测、超时熔断、错误封装及心跳保持(Keep-Alive)。

package com.mrwu.ai.gateway.controller;

import com.mrwu.ai.gateway.router.DynamicChatModelRouter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;

@RestController
public class AiStreamController {

    private static final Logger log = LoggerFactory.getLogger(AiStreamController.class);
    private final DynamicChatModelRouter router;

    public AiStreamController(DynamicChatModelRouter router) {
        this.router = router;
    }

    @GetMapping(value = "/api/v1/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamChat(@RequestParam("prompt") String promptText) {
        ChatModel dynamicModel = router.route();
        Prompt prompt = new Prompt(new UserMessage(promptText));

        AtomicInteger tokenCounter = new AtomicInteger(0);

        // 获取底层 Reactive Stream
        Flux<ChatResponse> responseFlux = dynamicModel.stream(prompt);

        return responseFlux
                .map(chatResponse -> {
                    String content = Optional.ofNullable(chatResponse.getResult())
                            .map(result -> result.getOutput().getContent())
                            .orElse("");
                    tokenCounter.incrementAndGet();
                    return ServerSentEvent.<String>builder()
                            .event("message")
                            .data(content)
                            .build();
                })
                // 1. 应对网络超时:若 20 秒内上游无任何数据下发,触发超时异常
                .timeout(Duration.ofSeconds(20))
                // 2. 核心:客户端主动断开连接治理(关闭浏览器、路由切换等)
                .doOnCancel(() -> {
                    log.warn("检测到客户端主动断开 SSE 链路!立即向上游注销上下文,已输出 Token 片段数: {}", 
                            tokenCounter.get());
                    // 在此处可触发上游 Http 连接物理中断与用户账单回滚
                })
                // 3. 异常拦截并推送前端易读的错误事件
                .onErrorResume(throwable -> {
                    log.error("流式推理过程发生致命异常, 原因: {}", throwable.getMessage());
                    return Flux.just(ServerSentEvent.<String>builder()
                            .event("error")
                            .data("服务暂时不可用: " + throwable.getMessage())
                            .build());
                })
                // 4. 传输完成标志推送
                .concatWith(Flux.just(ServerSentEvent.<String>builder()
                        .event("done")
                        .data("[DONE]")
                        .build()))
                // 5. 补充心跳包,防止代理服务器在 LLM 深度推理(如 DeepSeek R1 思考期)时因空闲切断连接
                .sampleFirst(Duration.ofMillis(50)) // 防止前端消费过载轻微限流
                .log("ai.stream.logger");
    }
}

4. nginx.conf:企业级网关长连接优化配置

必须关闭 Nginx 内部的缓冲机制并调整超时时间:

upstream ai_backend {
    server 127.0.0.1:8080;
    keepalive 32; # 保持上游连接池活跃,降低 TCP 握手开销
}

server {
    listen 80;
    server_name ai.mrwu.com;

    location /api/v1/ai/ {
        proxy_pass http://ai_backend;

        # HTTP/1.1 协议以支持长连接与 Chunked 传输
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # 【重点】彻底禁用 Nginx Proxy 缓冲,实现真正的实时流式直通
        proxy_buffering off;
        proxy_cache off;

        # 传递 SSE 所需的关键请求头
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Accel-Buffering no; # 告诉上游任何代理均不可缓冲

        # 【重点】LLM 推理长耗时保护,防止 60 秒无数据交互被 Nginx 掐断
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

四、避坑指南与总结验证

1. 生产血泪避坑要点

  • 坑点 1:Flux.doOnCancel 失效导致上游继续计费
    在 Spring MVC + Reactor 的混合架构中,默认如果使用了阻塞式 ThreadPool,客户端关闭浏览器产生的 AsyncRequestTimeoutException 或 TCP Broken pipe 并不一定会立刻触发 Reactor 的 Cancel 信号。
    应对方案:推荐将控制层全面切换至非阻塞式架构(Spring WebFlux);若运行在 WebMvc 容器(如嵌入式 Tomcat)下,务必调整 spring.mvc.async.request-timeout=-1,并在 ResponseBodyEmitter 或 SseEmitter 的 onCompletion 和 onTimeout 显式调用内部 Disposable.dispose()。
  • 坑点 2:浏览器 HTTP/1.1 最大并发连接数上限(Max Connections Per Host)限制
    各大现代浏览器限制单个域名在 HTTP/1.1 下只能建立 6 个并发连接。如果用户同时打开多个标签页运行 SSE 对话,后续连接会被挂起,导致接口“假死”。
    应对方案:在边缘接入层(Nginx/SLB)强制开启 HTTP/2 或 HTTP/3。HTTP/2 具备单连接多路复用(Multiplexing)能力,可在单个 TCP 连接上并发承载上千条 SSE 虚拟流。
  • 坑点 3:上下文(MDC)在响应式跨线程调度中丢失
    Reactor 运行过程中,数据流会经由 Schedulers.parallel() 或第三方 WebClient 线程频繁切换,导致基于 ThreadLocal 的链路追踪 ID(TraceId / SpanId)在日志中丢失。
    应对方案:在 Spring Boot 3.x 下,使用 Micrometer Context Propagation 库,并全局执行:
    java Hooks.enableAutomaticContextPropagation();

2. 压测验证与收益指标

经过上述改造后,我们使用 k6 对中台进行了并发破坏性测试(模拟客户端在流生成中途 50% 概率随机 abort):

  • 服务资源损耗验证:在未接入 doOnCancel 熔断机制前,并发打满 500 时,Tomcat 线程池瞬间阻塞,后端继续向上游拉取 Token;接入后,检测到客户端断开时,平均在 42 毫秒 内掐断上游请求,无效 Token 消耗降低 37%。
  • 首字延迟(TTFT - Time To First Token):关闭 Nginx proxy_buffering 并在前端引入 Direct Streaming 后,首字呈现耗时从原先整包缓存的 4.8 秒降至 380 毫秒,交互卡顿彻底消除。

通过合理的架构解耦与长连接生命周期精细化控制,Spring AI 不仅能作为本地原型尝鲜工具,更能化身为承受企业级高并发、多场景业务考验的坚实 AI 算力中台。

posted @ 2026-10-09 22:44  丨吴丨  阅读(19)  评论(0)    收藏  举报