Spring AI 中的 Flux 与 SSE:流式输出完全解析

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

相关推荐
星梦清河2 小时前
Java并发编程
java·开发语言
SimonKing2 小时前
IntelliJ IDEA AI Assistant 携带OpenCode保姆级安装教程来了
java·后端·程序员
XiYang-DING2 小时前
【Java SE】sealed关键字
java·开发语言·python
Flittly2 小时前
【SpringAIAlibaba新手村系列】(8)持久化会话与 Redis 内存管理
java·人工智能·spring boot·spring·ai
东离与糖宝2 小时前
Java 干掉 Python 垄断!LangChain4j + PgVector 本地知识库开发全流程
java·人工智能
东离与糖宝2 小时前
OpenClaw 企业级实战:Java 微服务集成 AI 智能体,自动处理业务流
java·人工智能
半瓶榴莲奶^_^2 小时前
优先级队列(堆)
java·数据结构·算法
东离与糖宝2 小时前
成本砍半!Java 生产环境 INT4/INT8 模型量化 + 提示词缓存落地
java·人工智能
Lyyaoo.2 小时前
Spring中Bean的作用域与生命周期
java·后端·spring