Spring AI 中的 Flux 与 SSE:流式输出完全解析
深入理解 Spring AI 如何实现大模型的流式响应
目录
一、核心概念
1.1 为什么需要流式输出?
在大模型(LLM)对话场景中,流式输出(Streaming)至关重要:
- 实时性:用户可以立即看到部分结果,而不是等待完整响应
- 体验优化:类似打字机的效果,提升用户体验
- 效率:对于长文本生成,可以边生成边返回
对比:
| 模式 | 等待时间 | 用户体验 |
|---|---|---|
| 传统模式 | 5-10 秒后一次性显示 | 用户需要长时间等待 |
| 流式模式 | 0.5 秒开始显示,持续更新 | 立即看到内容,体验流畅 |
1.2 三个关键角色
理解 Spring AI 的流式输出,需要明白三个关键角色的分工:
┌─────────────────┐
│ Flux │ ← 内存中的数据流(响应式编程)
│ (Project │
│ Reactor) │
└─────────────────┘
↓
┌─────────────────┐
│ SSE │ ← 网络传输协议(HTTP 之上)
│ (Server-Sent │
│ Events) │
└─────────────────┘
↓
┌─────────────────┐
│ Spring AI │ ← 自动转换和封装
│ (WebFlux) │
└─────────────────┘
二、Flux 是什么?
2.1 Flux 的来源
Flux 不是 Spring AI 特有的 ,它来自 Project Reactor 库。
- 所属库 :
io.projectreactor:reactor-core - 包路径 :
reactor.core.publisher.Flux - 地位:Spring WebFlux 的基石
2.2 Flux 的定义
Flux 是一个响应式流(Reactive Stream) ,表示一个异步的、0 到 N 个元素的序列。
简单理解:
Flux<T>= 一个会陆续吐出 T 类型数据的"管道"- 基于观察者模式 + 异步非阻塞
2.3 Flux 的核心特性
(1)懒加载(Lazy Execution)
java
Flux<String> flux = Flux.just("A", "B", "C");
// 此时代码还未执行,只是定义了数据流
// 只有订阅时才会执行
flux.subscribe(item -> System.out.println("收到:" + item));
(2)背压(Backpressure)
消费者可以告诉生产者:"慢点发,我跟不上了"
java
// 订阅者可以指定每次只请求 n 个元素
flux.subscribe(new Subscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
s.request(1); // 每次只请求 1 个
}
@Override
public void onNext(String item) {
System.out.println(item);
request(1); // 处理完再要下一个
}
});
(3)丰富的操作符
java
Flux.range(1, 10)
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> "数字:" + n) // 转换格式
.take(3) // 只取前 3 个
.subscribe(System.out::println);
// 输出:数字:2, 数字:4, 数字:6
2.4 Flux 的使用场景
✅ 不仅限于 Spring AI,Flux 可以用于任何异步数据流场景:
- 文件逐行读取
- 数据库游标遍历
- 消息队列消费
- WebSocket 消息流
- 大模型流式输出 ← Spring AI 使用场景
三、SSE 是什么?
3.1 SSE 的定义
SSE(Server-Sent Events) 是一个基于 HTTP 的协议,允许服务器向浏览器推送实时数据。
- 规范:W3C 标准
- API :浏览器原生支持
EventSource - 方向:单向(服务器 → 客户端)
3.2 SSE 的数据格式
SSE 使用简单的文本格式:
data: 你好\n\n
data: 世界\n\n
data: [DONE]\n\n
完整示例:
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"text":"你"}
data: {"text":"好"}
data: {"text":","}
data: [DONE]
3.3 前端接收 SSE
浏览器使用 EventSource API 接收:
javascript
const eventSource = new EventSource('/api/chat/stream');
eventSource.onmessage = (event) => {
console.log('收到:', event.data); // "你"、"好"、","
};
eventSource.onerror = () => {
console.log('传输完成或出错');
eventSource.close();
};
3.4 SSE vs WebSocket
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP 之上 | 独立协议(握手后) |
| 数据格式 | 文本 | 文本/二进制 |
| 复杂度 | 简单 | 较复杂 |
| 适用场景 | AI 对话、新闻推送 | 聊天室、在线游戏 |
结论:AI 对话场景,SSE 更简单够用!
四、Flux 与 SSE 的关系
4.1 核心关系
Flux ≠ SSE,它们是两个不同层面的概念:
| 层面 | Flux | SSE |
|---|---|---|
| 本质 | 内存中的数据流抽象 | 网络传输协议 |
| 所属 | Project Reactor | HTTP 协议规范 |
| 作用 | 响应式编程模型 | 数据传输格式 |
| 类比 | 水流 | 水管 |
4.2 它们如何一起工作?
后端代码:
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
return Flux.just("你", "好", "啊");
}
↓ Spring WebFlux 自动转换
HTTP 响应:
Content-Type: text/event-stream
data: 你
data: 好
data: 啊
↓ 通过网络传输
前端接收:
new EventSource('/stream')
onmessage: "你"
onmessage: "好"
onmessage: "啊"
4.3 关键点
✅ Flux 是数据载体
✅ SSE 是传输方式
✅ Spring WebFlux 自动将 Flux 转换为 SSE 格式
重要结论:
- Flux 可以通过 SSE 传输
- Flux 也可以通过 WebSocket 传输
- Flux 甚至可以只在内存中处理(不涉及网络)
五、Spring AI 的流式实现原理
5.1 大模型的流式特性
大模型本身就以流式方式生成数据:
大模型内部处理流程:
时间轴 →
0ms: [开始思考...]
100ms: 生成 Token 1: "你" → 立即返回
200ms: 生成 Token 2: "好" → 立即返回
350ms: 生成 Token 3: "," → 立即返回
450ms: 生成 Token 4: "我" → 立即返回
... 继续生成剩余内容
大模型 API 返回格式(SSE):
data: {"output":{"text":"你"},"request_id":"xxx"}
data: {"output":{"text":"好"},"request_id":"xxx"}
data: {"output":{"text":","},"request_id":"xxx"}
data: [DONE]
5.2 Spring AI 的三层架构
┌─────────────────────────────────────────────────────┐
│ 第 1 层:大模型 API(通义千问/ChatGPT) │
│ │
│ 以 SSE 格式返回流式数据 │
│ data: {"text":"你"} │
│ data: {"text":"好"} │
└─────────────────────────────────────────────────────┘
↓ HTTP Client
┌─────────────────────────────────────────────────────┐
│ 第 2 层:Spring AI ChatClient │
│ │
│ 1. 接收大模型的 SSE 响应 │
│ 2. 解析并封装成 Flux<ChatResponse> │
│ 3. 提供简洁的 API:.stream().content() │
└─────────────────────────────────────────────────────┘
↓ WebFlux 自动转换
┌─────────────────────────────────────────────────────┐
│ 第 3 层:前端浏览器 │
│ │
│ EventSource 接收 Spring AI 转发的 SSE │
│ onmessage: "你" │
│ onmessage: "好" │
└─────────────────────────────────────────────────────┘
5.3 Spring AI 的"魔法"配置
Spring AI 的高明之处在于几乎不需要配置:
java
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
// ✨ 只需要一行配置!
@GetMapping(value = "/chat/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE) // ← 全部配置
public Flux<String> streamChat(@RequestParam String prompt) {
return chatClient.prompt()
.user(prompt)
.stream() // 调用大模型的流式接口
.content(); // 返回 Flux<String>
}
}
5.4 Spring WebFlux 的自动转换机制
当你声明 produces = MediaType.TEXT_EVENT_STREAM_VALUE 时:
Spring WebFlux 检测到:
1. 返回值是 Flux
2. produces = text/event-stream
↓
自动启用 ServerSentEventHttpMessageWriter
↓
自动转换过程:
Flux<String>
↓
每来一个元素 → 包装成 SSE 格式
"你" → "data: 你\n\n"
"好" → "data: 好\n\n"
↓
通过 HTTP 响应流发送给客户端
5.5 与传统手动 SSE 的对比
传统方式(需要显式配置):
java
@GetMapping(value = "/manual", produces = "text/event-stream")
public Flux<ServerSentEvent<String>> manualStream() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "Data " + i)
.map(data -> ServerSentEvent.builder(data) // ← 手动包装
.event("message") // ← 设置事件类型
.id(UUID.randomUUID().toString()) // ← 设置 ID
.build());
}
Spring AI 方式(自动化):
java
@GetMapping(value = "/ai", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> aiStream(@RequestParam String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content(); // ← 直接返回内容,无需包装
}
优势:
- ✅ 约定优于配置
- ✅ 自动类型转换
- ✅ 响应式集成
- ✅ 默认行为合理
六、完整示例
6.1 后端实现(Spring Boot + Spring AI)
java
package com.example.demo.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
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;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
/**
* 流式输出接口
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
}
6.2 前端实现(HTML + JavaScript)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Spring AI 流式对话</title>
<style>
#result {
border: 1px solid #ccc;
padding: 20px;
min-height: 200px;
white-space: pre-wrap;
font-family: Arial, sans-serif;
}
button {
padding: 10px 20px;
margin: 10px 0;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
}
</style>
</head>
<body>
<h1>AI 智能助手</h1>
<input type="text" id="promptInput"
placeholder="请输入问题..."
value="请介绍一下杭州"/>
<button onclick="startChat()" id="sendBtn">发送</button>
<div id="result"></div>
<script>
let eventSource = null;
function startChat() {
const prompt = document.getElementById('promptInput').value;
const resultDiv = document.getElementById('result');
const sendBtn = document.getElementById('sendBtn');
// 清空之前的结果
resultDiv.innerHTML = '';
sendBtn.disabled = true;
// 关闭之前的连接
if (eventSource) {
eventSource.close();
}
// 创建 EventSource 连接到后端
eventSource = new EventSource(`/api/chat/stream?prompt=${encodeURIComponent(prompt)}`);
// 监听消息
eventSource.onmessage = (event) => {
// 追加新内容
resultDiv.innerHTML += event.data;
// 自动滚动到底部
resultDiv.scrollTop = resultDiv.scrollHeight;
};
// 监听错误
eventSource.onerror = (error) => {
console.error('SSE Error:', error);
eventSource.close();
sendBtn.disabled = false;
};
// 监听完成
eventSource.onload = () => {
console.log('流式传输完成');
sendBtn.disabled = false;
};
}
</script>
</body>
</html>
6.3 Vue 组件示例
vue
<template>
<div class="chat-container">
<div class="input-area">
<input v-model="prompt" placeholder="请输入问题..." />
<button @click="startChat" :disabled="isStreaming">
{{ isStreaming ? '生成中...' : '发送' }}
</button>
</div>
<div ref="resultDiv" class="result-area">
{{ responseText }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
prompt: '',
responseText: '',
isStreaming: false,
eventSource: null
};
},
methods: {
startChat() {
this.responseText = '';
this.isStreaming = true;
this.eventSource = new EventSource(
`/api/chat/stream?prompt=${encodeURIComponent(this.prompt)}`
);
this.eventSource.onmessage = (event) => {
this.responseText += event.data;
this.$nextTick(() => {
this.$refs.resultDiv.scrollTop = this.$refs.resultDiv.scrollHeight;
});
};
this.eventSource.onload = () => {
this.isStreaming = false;
this.eventSource.close();
};
this.eventSource.onerror = () => {
this.isStreaming = false;
this.eventSource.close();
};
}
},
beforeDestroy() {
if (this.eventSource) {
this.eventSource.close();
}
}
};
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
}
.input-area {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.result-area {
border: 1px solid #ddd;
padding: 20px;
min-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
</style>
七、总结
7.1 核心要点回顾
(1)Flux 是什么?
- ✅ 来自 Project Reactor,不是 Spring AI 特有
- ✅ 响应式编程模型,表示异步数据流
- ✅ 可以在任何场景中使用,不仅限于 Web
(2)SSE 是什么?
- ✅ HTTP 之上的推送协议
- ✅ 服务器向浏览器推送实时数据
- ✅ 简单易用,适合 AI 对话场景
(3)Flux 与 SSE 的关系?
- ✅ Flux = 数据流(内存中的响应式对象)
- ✅ SSE = 传输协议(网络上的数据格式)
- ✅ Spring WebFlux 自动将 Flux 转换为 SSE
(4)Spring AI 的实现原理?
大模型(SSE)
↓
Spring AI(接收 SSE → 封装成 Flux)
↓
Spring WebFlux(检测 produces → 自动转 SSE)
↓
前端(EventSource 接收)
7.2 为什么 Spring AI 如此简单?
| 特性 | 说明 |
|---|---|
| 约定优于配置 | 只要返回 Flux + TEXT_EVENT_STREAM,就自动用 SSE |
| 自动类型转换 | 不需要手动包装成 ServerSentEvent |
| 响应式集成 | Project Reactor 与 Spring WebFlux 无缝配合 |
| 默认行为合理 | 99% 的场景不需要额外配置 |
7.3 实际应用建议
推荐方案(99% 场景):
java
@GetMapping(value = "/chat/stream",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
何时考虑 WebSocket?
- 需要双向实时通信(如聊天室)
- 需要传输二进制数据
- 对延迟要求极高(如在线游戏)
7.4 知识体系图
┌─────────────────────────────────────────┐
│ Spring AI 流式输出 │
├─────────────────────────────────────────┤
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ Flux │ │ SSE │ │
│ │ (数据流) │────▶│ (传输协议)│ │
│ └───────────┘ └───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Spring WebFlux 自动转换 │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 大模型(通义千问/ChatGPT) │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
7.5 最后的总结
Spring AI 的流式输出 = Flux + SSE + Spring WebFlux 自动转换
- Flux:响应式数据流(内存中)
- SSE:传输协议(网络上)
- Spring WebFlux:自动转换器(幕后英雄)
- 大模型:数据源头(本身就以流式生成)
掌握这些知识,你就能轻松实现任何流式输出场景! 🚀
附录:常见问题
Q1: Flux 只能用于 AI 对话吗?
A: 不是!Flux 可以用于任何异步数据流场景,如文件读取、消息队列等。
Q2: 必须使用 SSE 吗?WebSocket 可以吗?
A: 可以!但 SSE 更简单,适合 AI 对话这种单向推送场景。
Q3: 前端除了 EventSource,还有其他选择吗?
A: 可以用 fetch + ReadableStream,更灵活但代码稍复杂。
Q4: 如何自定义 SSE 的事件类型和 ID?
A: 手动包装成 ServerSentEvent 对象,设置 event 和 id 属性。
Q5: Flux 的性能如何?
A: 非常好!基于响应式流,支持背压控制,不会压垮消费者。
作者 : AI Assistant
创建时间 : 2026 年 4 月 1 日
适用版本: Spring AI 1.x + Spring Boot 3.x