Spring AI + Flux/FluxSink + SSE 实战技术笔记

说明:本篇笔记基于真实企业级 Spring AI 流式聊天接口整理,采用 SSE 长连接 + Spring WebFlux(Flux/FluxSink)架构。内容会先讲整体项目架构与代码结构,再讲 Flux 核心知识点,最后解答开发中最容易踩坑的疑问,全程图文结合,新手也能看懂。

一、整体架构说明(先看懂全貌)

我们这套服务是标准 SSE 流式服务端架构,用于实现 AI 回答实时逐字输出,核心技术栈为 Spring WebFlux(Flux/FluxSink)+ SSE,整体采用"Controller + Service + Agent 服务"的分层架构,所有代码均围绕这套架构运行。

1. 整体分层架构

复制代码
前端请求
    ↓
【 Controller 层 】SSE 接口入口,返回 SseEmitter
    ↓
【 Service 层 】业务编排 + Flux 数据流创建 + FluxSink 生产数据
    ↓
【 Agent 服务层 】各类 AI 能力实现,返回 Flux 流式数据
    ↓
数据通过 Flux 传输
    ↓
【 Controller 层 】doOnNext 监听 → SSE 推送给前端

2. 核心代码结构(极简版)

① 对外接口:SSE Controller
复制代码
/**
 * SSE 流式聊天接口(对外提供服务)
 * 前端通过 POST 请求建立 SSE 连接,实时接收 AI 流式响应
 */
@PostMapping("/agent/call/stream")
public SseEmitter agentChatStream(@Validated @RequestBody ChatRequestDto chatRequestDto) {
    // 调用 Service 层方法,获取流式处理结果(内部封装 Flux 数据流)
    return chatService.managerChatStream(chatRequestDto);
}

作用:接收前端请求,建立 SSE 长连接,返回流式响应。

② 业务层:Service(返回 Flux)
复制代码
// 核心 Service 方法,返回 Flux 数据流,供 Controller 层监听
public Flux<StreamResponse> processUserMessageStream(UserInputMessage userInputMessage) {
    // 手动创建 Flux 数据流,通过 FluxSink 生产和转发数据
    return Flux.create(sink -> {
        try {
            // 1. 发送处理进度事件
            sink.next(StreamResponse.progress(new HashMap<String, String>(){{
                        put("sessionId", userInputMessage.getSessionId());
                        put("content","开始处理您的请求..." );
                    }}));
            
            // 2. 特殊逻辑处理,发送 content 和 complete 事件
            if (特殊业务类型) {
                sink.next(StreamResponse.builder().eventType(EventType.CONTENT.getName()).build());
                sink.next(StreamResponse.builder().eventType(EventType.COMPLETE.getName()).build());
                sink.complete(); // 终止流
                return;
            }
            
            // 3. 意图分类,调用对应 Agent 服务(返回 Flux 数据流)
            Flux<StreamResponse> agentFlux = faqChatStreamService.chatFAQAgentStream(userInputMessage);
            // 订阅 Agent 流,转发数据到当前 Flux
            agentFlux
                .doOnNext(streamResponse -> sink.next(streamResponse))
                .doOnComplete(() -> sink.complete())
                .doOnError(error -> sink.error(error))
                .subscribe();
        } catch (Exception e) {
            // 异常处理,发送错误事件
            sink.next(StreamResponse.error(...));
            sink.complete();
        }
    });
}
③ SSE 管理方法
复制代码
public SseEmitter managerChatStream(ChatRequestDto chatRequestDto) {
    // 1. 创建 SSE 连接,设置超时时间(避免连接异常)
    SseEmitter emitter = new SseEmitter(30_000L);
    // 2. 转换请求参数,调用核心方法获取 Flux 数据流
    UserInputMessage userInputMessage = convertToUserInput(chatRequestDto);
    Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);
    
    // 3. 监听 Flux 数据流,推送给前端
    responseFlux
        .doOnNext(streamResponse -> {
            // 每接收一条数据,通过 SseEmitter 推送给前端
            String jsonData = objectMapper.writeValueAsString(streamResponse);
            emitter.send(SseEmitter.event().name(eventName).data(jsonData));
        })
        .doOnComplete(() -> {
            // 数据流结束,关闭 SSE 连接
            emitter.complete();
        })
        .doOnError(error -> {
            // 异常处理,发送错误信息并关闭连接
            emitter.send(StreamResponse.error(...));
            emitter.complete();
        })
        .subscribe(); // 启动 Flux 流,否则所有逻辑不执行
    
    return emitter;
}
④ 业务层:Agent 服务层(具体业务实现)

核心作用:根据 Service 层的意图分类结果(FAQ、订单、商品等),提供具体的 AI 流式响应逻辑,返回 Flux<StreamResponse> 数据流,供 Service 层转发。

示例(自身代码中 Agent 服务):

  • faqChatStreamService.chatFAQAgentStream:FAQ 意图对应的流式服务

  • orderChatStreamService.chatOrderAgentStream:订单意图对应的流式服务

  • productChatStreamService.chatProductAgentStream:商品意图对应的流式服务

3. 核心技术关联(Flux + SSE 如何协同工作)

整套服务的核心是"Flux 数据流 + SSE 长连接",两者协同实现"实时流式响应",核心关联逻辑:Flux 负责"异步生产、传输数据",SseEmitter 负责"维护长连接、推送数据给前端",Service 层封装业务逻辑生产数据,Controller 层监听数据并推送,形成完整的流式响应链路。

二、完整流程图(经典版)

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                               【 Spring AI 流式完整流程图 】                   │
└─────────────────────────────────────────────────────────────────────────────┘

┌───────────────────────────────┐        ┌─────────────────────────────────────┐
│  【生产端:业务 Service】        │        │                                     │
│   Flux.create(sink -> { ... })│───────▶│   FluxSink  【水龙头】                │
│                               │        │                                     │
│  作用:创建一条数据流            │        │  • sink.next(数据)   发一条消息       │
└───────────────────────────────┘        │  • sink.complete()   正常结束        │
                                         │  • sink.error(异常)    异常报错       │
                                         └───────────────┬─────────────────────┘
                                                         │
                                                         ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                                   Flux                                      │
│                            【 数据流 / 管道 】                                │
│                                                                             │
│  特点:异步、被动、只传输、不执行、不订阅就不动                                     │
└───────────────────────────────┬─────────────────────────────────────────────┘
                                │
            ┌───────────────────┼───────────────────┬───────────────────┐
            │                   │                   │                   │
            ▼                   ▼                   ▼                   │
┌────────────────────┐  ┌────────────────┐  ┌────────────────┐          │
│    doOnNext        │  │  doOnComplete  │  │   doOnError    │          │
│  每条数据来就执行     │  │  流结束时执行    │  │  流异常时执行    │          │
└────────────────────┘  └────────────────┘  └────────────────┘          │
            │                   │                   │                   │
            └───────────────────┼───────────────────┴───────────────────┘
                                │
                                ▼
                 ┌───────────────────────────────┐
                 │      .subscribe()             │
                 │    【 🔥 点火开关 】            │
                 │  不调用 → 前面所有代码不执行      │
                 └───────────────┬───────────────┘
                                 │
                                 ▼
               ┌─────────────────────────────────────┐
               │      SseEmitter.send() 推送给前端     │
               └─────────────────────────────────────┘

三、核心疑问前置(对应自身代码困惑)

结合上面的整体架构和代码结构,下面梳理开发中最常遇到的疑问,后续将结合知识点和代码逐一解答:

  • 疑问1:代码中 doOnNext 里调用 emitter.send(),会不会重复发送消息?

  • 疑问2:subscribe() 到底有什么作用?为什么必须调用?

  • 疑问3:Flux 和 FluxSink 是什么关系?在代码中各自扮演什么角色?

  • 疑问4:代码中 if ("content") {send; return;},return 能阻止后面的兜底 send 吗?

  • 疑问5:双层 Flux(Service 层 Flux.create + Controller 层 responseFlux)为什么要嵌套?

  • 疑问6:SSE Controller 接口如何与 Flux 结合,实现前端实时接收流式响应?

四、Flux 核心知识点(必记)

1. 核心概念(对应自身代码)

  • Flux:异步数据流容器,可发送 0~N 条数据、1 个完成信号、1 个错误信号,仅负责"传输数据",不生产、不主动执行,没人订阅就处于"休眠"状态。

  • 自身代码中:`Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);` 就是一个 Flux,用于传输 AI 流式响应数据。

  • FluxSink:手动生产 Flux 数据的工具,通过 `Flux.create(sink -> { ... })` 获取,是 Flux 的"数据生产者"。

  • 自身代码中:Service 层 `Flux.create(sink -> { ... })` 里的 sink 就是 FluxSink,通过 `sink.next()` 发送进度、内容、完成等事件。

  • doOnNext:Flux 的事件监听器,每有一条数据从 Flux 中流过,就执行一次,仅监听、不修改数据,不影响数据流本身。

  • 自身代码中:Controller 层通过 doOnNext 监听 AI 流式数据,调用 `emitter.send()` 推送给前端 SSE。

  • subscribe():Flux 的"点火开关",只有调用 subscribe(),Flux 才会启动,数据流才会开始传输,否则前面所有逻辑(doOnNext、FluxSink 生产数据)都不执行。

  • 自身代码中:Controller 层最后 `.subscribe()` 启动外层流,Service 层内部调用 Agent 流后 `.subscribe()` 启动内层流,双层流都需启动。

2. Flux 核心方法(高频使用,结合自身代码)

|--------------------------------------------|-----------------------------------------------|---------------------------------------------------------------------------------------|
| 方法 | 作用 | 自身代码中的使用场景 |
| Flux.create(sink -> {}) | 手动创建 Flux 数据流,获取 FluxSink 用于生产数据 | Service 层 `processUserMessageStream` 方法中,创建内层流,生产进度、内容等事件 |
| flux.doOnNext(Cons<T> consumer) | 监听 Flux 中的每一条数据,数据流过时执行一次 consumer 逻辑 | 1. Controller 层监听 Flux 数据,调用 emitter.send() 推送给前端;2. Service 层监听 Agent 流数据,转发到自身 Flux |
| flux.doOnComplete(Runnable runnable) | Flux 数据流正常结束时,执行一次 runnable 逻辑 | 1. Controller 层流结束时,关闭 SSE 连接;2. Service 层转发 Agent 流结束信号,终止自身 Flux |
| flux.doOnError(Cons<Throwable> consumer) | Flux 数据流异常时,执行一次 consumer 逻辑,处理异常 | 1. Controller 层异常时,发送错误信息并关闭 SSE 连接;2. Service 层转发 Agent 流异常信号,处理错误 |
| flux.subscribe() | 启动 Flux 数据流,触发所有监听逻辑(doOnNext/doOnComplete 等) | 1. Controller 层启动外层流;2. Service 层启动 Agent 流,确保数据正常生产和转发 |
| sink.next(T data) | FluxSink 生产一条数据,发送到 Flux 管道中 | Service 层发送进度、内容、完成等事件,Agent 流生产数据后也通过该方法发送 |
| sink.complete() | 发送 Flux 数据流结束信号,终止数据流传输 | Service 层特殊逻辑处理完成、Agent 流结束后,终止自身 Flux 流 |
| sink.error(Throwable error) | 发送 Flux 数据流异常信号,终止数据流并抛出异常 | Service 层初始化失败、转发 Agent 流异常时,发送异常信号 |

3. 关键区别(避免混淆)

  • Flux vs FluxSink:Flux 是"管道"(传输),FluxSink 是"水龙头"(生产数据),没有 FluxSink,Flux 就是空管道;没有 Flux,FluxSink 生产的数据无处传输。

  • doOnNext vs map:doOnNext 只监听、不修改数据;map 用于修改、转换数据(自身代码中未用到 map,仅用 doOnNext 监听推送)。

  • subscribe() vs doOnNext:subscribe() 负责启动流,doOnNext 负责监听流中的数据,没有 subscribe(),doOnNext 永远不执行。

五、自身代码中 Flux 的实际用法(核心流程)

1. 代码整体架构(双层 Flux 嵌套)

结合开篇的架构说明,自身代码采用"双层 Flux"架构,对应 AI 流式响应的生产、传输、消费全流程,流程拆解(对应代码):

  1. Controller 层:接收前端请求,调用 Service 层方法,获取 Flux<StreamResponse>(外层流)。

  2. Service 层:通过 `Flux.create(sink -> { ... })` 创建内层流,用 FluxSink 生产数据(进度、内容、完成等)。

  3. Service 层:根据意图分类,调用 FAQ/订单/商品等 Agent 流(也是 Flux),订阅 Agent 流后,将其数据转发到内层流(sink.next())。

  4. Controller 层:给外层流绑定 doOnNext(监听数据)、doOnComplete(流结束)、doOnError(异常),调用 subscribe() 启动外层流。

  5. Controller 层:doOnNext 中通过 SseEmitter 将数据推送给前端,完成流式输出。

2. 核心代码片段解析(Flux 相关)

(1)Service 层:Flux.create 手动创建流 + FluxSink 生产数据
复制代码
// 自身代码 Service 层核心方法
public Flux<StreamResponse> processUserMessageStream(UserInputMessage userInputMessage) {
    // 手动创建 Flux,获取 FluxSink(sink)
    return Flux.create(sink -> {
        try {
            // 1. 用 FluxSink 发送进度事件(生产数据)
            sink.next(StreamResponse.progress(new HashMap<String, String>(){{
                        put("sessionId", userInputMessage.getSessionId());
                        put("content","开始处理您的请求..." );
                    }}));
            
            // 2. 特殊逻辑处理,发送 content 和 complete 事件
            if (特殊业务类型) {
                sink.next(StreamResponse.builder().eventType(EventType.CONTENT.getName()).build());
                sink.next(StreamResponse.builder().eventType(EventType.COMPLETE.getName()).build());
                sink.complete(); // 终止流
                return;
            }
            
            // 3. 调用 Agent 流(内层嵌套的 Flux)
            Flux<StreamResponse> responseFlux = faqChatStreamService.chatFAQAgentStream(userInputMessage);
            // 订阅 Agent 流,将其数据转发到当前 Flux(sink.next())
            responseFlux
                .doOnNext(streamResponse -> sink.next(streamResponse)) // 转发数据
                .doOnComplete(() -> sink.complete()) // 转发结束信号
                .doOnError(error -> sink.error(error)) // 转发异常信号
                .subscribe(); // 启动 Agent 流
            
        } catch (Exception e) {
            // 异常时,用 FluxSink 发送错误事件
            sink.next(StreamResponse.error(...));
            sink.complete();
        }
    });
}
(2)Controller 层:监听 Flux 数据 + 启动流
复制代码
// 自身代码 Controller 层核心逻辑(managerChatStream 方法)
public SseEmitter managerChatStream(ChatRequestDto chatRequestDto) {
    // 1. 创建 SSE 连接,设置超时时间
    SseEmitter emitter = new SseEmitter(30_000L);
    // 2. 转换请求参数
    UserInputMessage userInputMessage = convertToUserInput(chatRequestDto);
    // 3. 获取 Service 层返回的 Flux(外层流)
    Flux<StreamResponse> responseFlux = aiChatManagerService.processUserMessageStream(userInputMessage);

    // 4. 监听 Flux 数据,推送给前端 SSE
    responseFlux
        .doOnNext(streamResponse -> {
            // 监听每一条数据,发送给前端
            String jsonData = objectMapper.writeValueAsString(streamResponse);
            emitter.send(SseEmitter.event().name(eventName).data(jsonData));
        })
        .doOnComplete(() -> {
            // 流结束时,关闭 SSE 连接
            emitter.complete();
        })
        .doOnError(error -> {
            // 异常时,发送错误信息并关闭连接
            emitter.send(StreamResponse.error(...));
            emitter.complete();
        })
        .subscribe(); // 启动外层流,否则所有逻辑不执行
    
    return emitter;
}

六、核心疑问解答(结合代码 + 原理)

疑问1:doOnNext 里调用 emitter.send(),会不会重复发送消息?

答:不会!核心原因:1 条数据 → Flux 传输 → 触发 1 次 doOnNext → 执行 1 次 emitter.send(),一对一执行,不会重复。

自身代码中,通过 return 截断逻辑,进一步避免重复。补充:自身代码逻辑完全正确,仅"其他未匹配事件"走兜底 send,content/progress/complete 都会被 return 截断,不会走到兜底 send。

疑问2:subscribe() 到底有什么作用?为什么必须调用?

答:subscribe() 是 Flux 的"点火开关",Flux 是"声明式"数据流,仅定义流程(生产、传输、监听),不主动执行,必须调用 subscribe() 才能启动整个流程。

自身代码中,需要两次 subscribe():

  • Controller 层 subscribe():启动外层流,让 Service 层的 Flux 开始生产数据。

  • Service 层 subscribe():启动 Agent 流,让 Agent 开始生产数据,才能转发到外层流。

类比:Flux 是铺好的水管,FluxSink 是水龙头,subscribe() 是拧开水龙头的动作,不拧开水龙头,水管里永远没有水。

疑问3:Flux 和 FluxSink 是什么关系?

答:互补关系,Flux 是"管道",FluxSink 是"水龙头"。自身代码中:FluxSink 生产的数据(progress、content 等),必须通过 Flux 管道传输,才能被 Controller 层的 doOnNext 监听并推送给前端。

疑问4:if ("content") {send; return;},return 能阻止后面的兜底 send 吗?

答:能!return 会直接退出当前 doOnNext 方法,后面的所有代码(包括兜底 send)都不会执行。

验证(对应自身代码):

  • 当 eventName = content:进入 if 分支,send 后 return → 方法结束,兜底 send 不执行。

  • 当 eventName = progress:进入 if 分支,直接 return → 方法结束,兜底 send 不执行。

  • 当 eventName = complete:进入 if 分支,send 后 return → 方法结束,兜底 send 不执行。

  • 其他事件:不进入任何 if 分支,执行兜底 send → 仅发送 1 次。

疑问5:双层 Flux 为什么要嵌套?

答:为了实现"业务逻辑封装 + 数据转发",符合分层开发思想:

  • 内层 Flux(Service 层):封装 AI 意图分类、特殊逻辑处理、Agent 流调用,负责生产和转发核心业务数据。

  • 外层 Flux(Controller 层):负责监听数据、推送前端 SSE、处理连接生命周期(结束、异常),与前端交互。

好处:业务逻辑与前端交互解耦,便于维护和扩展(如修改 Agent 流,不影响 Controller 层的 SSE 推送逻辑)。

疑问6:SSE Controller 接口如何与 Flux 结合,实现前端实时接收流式响应?

答:核心是"Flux 生产传输数据 + SseEmitter 维护长连接推送数据",结合自身代码的完整流程:

  1. 前端发送 POST 请求到 /agent/call/stream,Controller 层创建 SseEmitter 对象(维护长连接)。

  2. Controller 层调用 Service 层方法,获取 Flux<StreamResponse> 数据流(外层流)。

  3. 给 Flux 绑定 doOnNext 监听器,每有一条数据流过,就通过 SseEmitter.send() 推送给前端。

  4. 调用 subscribe() 启动 Flux 流,Service 层开始生产数据(FluxSink.next()),数据通过 Flux 传输到 Controller 层。

  5. 流结束(doOnComplete)或异常(doOnError)时,关闭 SseEmitter 连接,完成整个流式响应。

七、实战注意事项(结合自身代码)

    1. 双层 Flux 必须都调用 subscribe(),否则数据流不会启动(自身代码已实现,无需修改)。
    1. doOnNext 中需先判断 closed.get(),避免连接关闭后继续发送数据,导致异常(自身代码已实现)。
    1. FluxSink.send() 后,需及时调用 sink.complete(),避免流一直处于"活跃"状态,造成资源泄露(自身代码特殊逻辑中已处理)。
    1. 异常处理需完整:doOnError 中需发送错误事件并关闭 SSE 连接,避免前端一直等待(自身代码已实现,可补充心跳机制优化)。
    1. 避免重复判断 closed.get():自身代码中 doOnNext 里两次判断 closed.get(),可精简为 1 次(不影响功能,仅优化代码整洁度)。
    1. SseEmitter 需设置超时时间,避免连接长期闲置导致异常(自身代码可补充 `emitter.setTimeout(30_000L)`)。

八、终极总结(口诀 + 核心流程)

1. 核心口诀(背会即通透)

  • Flux = 数据流管道,只传不产不执行

  • FluxSink = 水龙头,手动生产数据

  • doOnNext = 监听器,来一条处理一条

  • subscribe = 点火开关,不调用不执行

  • return 能截断,不会多发消息

  • SSE + Flux = 实时流式响应

2. 自身代码核心流程(一句话概括)

前端请求 SSE Controller 接口 → Controller 层创建 SseEmitter 并调用 Service 层 → Service 层通过 Flux.create() 创建流,用 FluxSink 生产数据并转发 Agent 流数据 → Controller 层监听 Flux 数据,通过 SseEmitter 推送给前端 → subscribe() 启动整个流程,实现 AI 流式响应的实时接收。

相关推荐
龙文浩_2 小时前
AI深度学习中的张量计算理论与实践
人工智能·神经网络
软件算法开发2 小时前
基于霸王龙优化算法的LSTM网络模型(TROA-LSTM)的一维时间序列预测matlab仿真
人工智能·matlab·lstm·一维时间序列预测·霸王龙优化·troa-lstm
专业发呆业余科研2 小时前
从“炼金术”到“建筑学”:深度学习结构设计的五大范式
人工智能·深度学习·神经网络·机器学习
s石有八九2 小时前
LLM评分集中化偏差:从人类评分者到LLM智能体的系统性综述
人工智能·语言模型
rainy雨2 小时前
精益生产管理八大浪费的系统化拆解:如何利用精益生产管理八大浪费分析功能解决多品种小批量生产难题
大数据·人工智能·智能手机·精益工程
SeatuneWrite2 小时前
AI仿真人剧供应商2025推荐,高效内容创作与分发解决方案
人工智能·python
爱丽_2 小时前
Redis 持久化与高可用:RDB/AOF、主从复制、哨兵与一致性取舍
java·后端·spring
数智工坊2 小时前
【深度学习基础】Focal Loss、Dice Loss、组合损失函数
人工智能·深度学习
伯远医学2 小时前
如何判断提取的RNA是否可用?
java·开发语言·前端·javascript·人工智能·eclipse·创业创新