From 79661c4f6578e146158da591ada2a668c1d30651 Mon Sep 17 00:00:00 2001 From: yuoon Date: Sun, 18 Jan 2026 19:12:43 +0800 Subject: [PATCH] chat-stream --- springboot-chat-stream/README.md | 355 ++++++++++ springboot-chat-stream/pom.xml | 48 ++ .../example/chat/ChatStreamApplication.java | 15 + .../chat/controller/StreamChatController.java | 36 + .../chat/service/StreamChatService.java | 77 ++ .../src/main/resources/application.yml | 13 + .../src/main/resources/static/index.html | 667 ++++++++++++++++++ 7 files changed, 1211 insertions(+) create mode 100644 springboot-chat-stream/README.md create mode 100644 springboot-chat-stream/pom.xml create mode 100644 springboot-chat-stream/src/main/java/com/example/chat/ChatStreamApplication.java create mode 100644 springboot-chat-stream/src/main/java/com/example/chat/controller/StreamChatController.java create mode 100644 springboot-chat-stream/src/main/java/com/example/chat/service/StreamChatService.java create mode 100644 springboot-chat-stream/src/main/resources/application.yml create mode 100644 springboot-chat-stream/src/main/resources/static/index.html diff --git a/springboot-chat-stream/README.md b/springboot-chat-stream/README.md new file mode 100644 index 0000000..7566bbb --- /dev/null +++ b/springboot-chat-stream/README.md @@ -0,0 +1,355 @@ +# 像 ChatGPT 一样丝滑:Spring Boot 如何实现大模型流式(Streaming)响应? + +## 一、为什么需要流式响应? + +同样的 HTTP 请求,为什么像 ChatGPT 这类模型的回答能像打字机一样逐字输出,而我们平时写的接口却要等全部处理完才返回? + +问题的核心在于 **响应模式**: + +| 传统模式 | 流式模式 | +|---------|---------| +| 服务器处理完成 → 一次性返回 | 生成一部分 → 立即推送 | +| 客户端等待总时长 = 服务器处理时间 | 客户端首字等待时间通常很短 | +| 适合快速查询 | 适合耗时生成 | + +对于大模型这种 **生成式 AI**,一个响应可能需要几秒甚至几十秒。如果用传统模式,用户体验就是: + +``` +提问 → (10秒空白) → 答案全部出现 +``` + +而流式响应的体验是: + +``` +提问 → 0.1秒后 → "我" → "认" → "为" → ... → 逐字呈现 +``` + +实现这种效果有多种技术方案,本文将介绍基于 Spring Boot WebFlux + SSE 的实现方式。 + +--- + +## 二、核心技术选型 + +实现流式响应主要有以下几种方案: + +| 方案 | 优点 | 缺点 | 适用场景 | +|------|------|------|----------| +| **SSE** | 单向推送、HTTP协议、实现简单 | 不支持双向通信 | 服务端主动推送 | +| **WebSocket** | 双向通信、实时性强 | 实现复杂、需要额外协议 | 聊天、游戏 | +| **长轮询** | 兼容性好 | 资源消耗大 | 低频数据更新 | + +**本文选择 SSE 方案**,原因如下: +- Spring Boot 原生支持 `ResponseEntity>` +- 基于标准 HTTP,无需额外协议协商 +- 代码简洁,易于理解和维护 + +--- + +## 三、项目依赖配置 + +### 3.1 Maven 依赖 + +```xml + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.example + springboot-chat-stream + 1.0.0 + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.projectlombok + lombok + true + + + +``` + +### 3.2 关键依赖说明 + +- **spring-boot-starter-webflux**:提供响应式 Web 支持,核心是 Reactor 的 `Flux` 类型 +- **Reactor**:响应式编程库,`Flux` 表示 0-N 个元素的异步序列 + +--- + +## 四、核心代码实现 + +### 4.1 Controller 层:流式响应入口 + +```java +package com.example.chat.controller; + +import com.example.chat.service.StreamChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +@RestController +@RequestMapping("/api/chat") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") // 开发环境允许跨域 +public class StreamChatController { + + private final StreamChatService chatService; + + /** + * 流式聊天接口 + * @param prompt 用户输入的问题 + * @return 流式响应,text/event-stream 格式 + */ + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseEntity> streamChat(@RequestParam String prompt) { + return ResponseEntity.ok() + .header("Cache-Control", "no-cache") + .header("Connection", "keep-alive") + .body(chatService.streamResponse(prompt)); + } +} +``` + +**关键点解析:** + +1. `produces = MediaType.TEXT_EVENT_STREAM_VALUE`:声明返回 SSE 格式 +2. `Flux`:响应式流,可以发送多个数据块 +3. `Cache-Control: no-cache`:禁用缓存,确保实时推送 + +### 4.2 Service 层:模拟大模型流式生成 + +```java +package com.example.chat.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.Duration; + +@Slf4j +@Service +public class StreamChatService { + + /** + * 模拟大模型流式生成响应 + * @param prompt 用户问题 + * @return 按字符/词汇流式输出的响应 + */ + public Flux streamResponse(String prompt) { + log.info("收到用户提问: {}", prompt); + + // 模拟大模型生成的回复内容 + String response = mockLLMResponse(prompt); + + // 将响应拆分为字符流,每 50ms 发送一个字符 + return Flux.fromArray(response.split("")) + .delayElements(Duration.ofMillis(50)) + .doOnNext(chunk -> log.debug("发送数据块: {}", chunk)) + .doOnComplete(() -> log.info("流式响应完成")) + .doOnError(e -> log.error("流式响应异常", e)); + } + + /** + * 模拟大模型生成内容(实际项目可接入 OpenAI/通义千问等) + */ + private String mockLLMResponse(String prompt) { + return """ + 【Spring Boot 流式响应】 + 您的问题是:%s + + 这是一个模拟大模型流式输出的示例。 + 在实际应用中,你可以: + 1. 接入 OpenAI API 使用 GPT-4 + 2. 接入阿里云通义千问 API + 3. 接入本地部署的大模型 + + 流式响应的核心是: + - 使用 Spring WebFlux 的 Flux + - 返回 text/event-stream 格式 + - 前端使用 EventSource 或 fetch 接收 + + 这样就能实现像 ChatGPT 一样的丝滑体验! + """.formatted(prompt); + } +} +``` + +**核心逻辑:** + +1. `Flux.fromArray(response.split(""))`:将字符串拆分为字符数组转为流 +2. `.delayElements(Duration.ofMillis(50))`:每个字符延迟 50ms 发送 +3. `.doOnNext()/.doOnComplete()/.doOnError()`:生命周期钩子,用于日志记录 + +### 4.3 接入真实大模型 API(扩展) + +```java +// 接入 OpenAI Streaming API 的示例(伪代码) +public Flux streamOpenAI(String prompt) { + WebClient webClient = WebClient.builder() + .baseUrl("https://api.openai.com/v1") + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer YOUR_API_KEY") + .build(); + + return webClient.post() + .uri("/chat/completions") + .bodyValue(Map.of( + "model", "gpt-4", + "messages", List.of(Map.of("role", "user", "content", prompt)), + "stream", true + )) + .retrieve() + .bodyToFlux(String.class) + .map(this::extractContentFromSSE); // 解析 SSE 格式提取 content +} +``` + +### 4.4 启动类 + +```java +package com.example.chat; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ChatStreamApplication { + public static void main(String[] args) { + SpringApplication.run(ChatStreamApplication.class, args); + } +} +``` + +### 4.5 配置文件 + +```yaml +server: + port: 8080 + +spring: + application: + name: chat-stream-demo + +# 日志配置 +logging: + level: + com.example.chat: DEBUG +``` + +--- + +## 五、前端对接示例 + +### 5.1 使用 EventSource 接收流 + +```html + + + + + Spring Boot 流式响应示例 + + + +

Spring Boot 流式聊天

+ + +
+ + + + +``` + +### 5.2 使用 Fetch API(推荐) + +```javascript +async function streamChat(prompt) { + const response = await fetch(`/api/chat/stream?prompt=${encodeURIComponent(prompt)}`); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + console.log('收到数据:', chunk); + // 更新 UI + } +} +``` + +--- + +## 六、运行效果 + +启动项目后,访问 `http://localhost:8080`(需添加静态页面支持),输入问题,你会看到: + +``` +【Spring Boot 流式响应】 +您的问题是:如何学习 Spring Boot? + +这是一个模拟大模型流式输出的示例。 +... +``` + +文字像打字机一样逐字出现,体验丝滑! + + +--- + +## 七、总结 + +本文介绍了如何使用 Spring Boot WebFlux 实现 SSE 流式响应。核心是通过 `Flux` + `TEXT_EVENT_STREAM_VALUE` 将数据分块推送,配合前端 `EventSource` 实现逐字显示效果。相比传统一次性返回,流式响应能显著降低用户等待感知,特别适合大模型对话等耗时生成场景。 diff --git a/springboot-chat-stream/pom.xml b/springboot-chat-stream/pom.xml new file mode 100644 index 0000000..f7fa439 --- /dev/null +++ b/springboot-chat-stream/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.example + springboot-chat-stream + 1.0.0 + Spring Boot Chat Stream Demo + 流式响应演示项目 - 像 ChatGPT 一样丝滑 + + + 17 + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/springboot-chat-stream/src/main/java/com/example/chat/ChatStreamApplication.java b/springboot-chat-stream/src/main/java/com/example/chat/ChatStreamApplication.java new file mode 100644 index 0000000..deb5369 --- /dev/null +++ b/springboot-chat-stream/src/main/java/com/example/chat/ChatStreamApplication.java @@ -0,0 +1,15 @@ +package com.example.chat; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Spring Boot 流式响应演示应用 + */ +@SpringBootApplication +public class ChatStreamApplication { + + public static void main(String[] args) { + SpringApplication.run(ChatStreamApplication.class, args); + } +} diff --git a/springboot-chat-stream/src/main/java/com/example/chat/controller/StreamChatController.java b/springboot-chat-stream/src/main/java/com/example/chat/controller/StreamChatController.java new file mode 100644 index 0000000..e245810 --- /dev/null +++ b/springboot-chat-stream/src/main/java/com/example/chat/controller/StreamChatController.java @@ -0,0 +1,36 @@ +package com.example.chat.controller; + +import com.example.chat.service.StreamChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +/** + * 流式聊天控制器 + * 提供 SSE 流式响应接口 + */ +@RestController +@RequestMapping("/api/chat") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class StreamChatController { + + private final StreamChatService chatService; + + /** + * 流式聊天接口(SSE) + * + * @param prompt 用户输入的问题 + * @return SSE 流式响应 + */ + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamChat(@RequestParam String prompt) { + return chatService.streamResponse(prompt); + } +} diff --git a/springboot-chat-stream/src/main/java/com/example/chat/service/StreamChatService.java b/springboot-chat-stream/src/main/java/com/example/chat/service/StreamChatService.java new file mode 100644 index 0000000..81f1053 --- /dev/null +++ b/springboot-chat-stream/src/main/java/com/example/chat/service/StreamChatService.java @@ -0,0 +1,77 @@ +package com.example.chat.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.Duration; + +/** + * 流式聊天服务 + * 模拟大模型流式生成响应 + */ +@Slf4j +@Service +public class StreamChatService { + + /** + * 模拟大模型流式生成响应 + * + * @param prompt 用户问题 + * @return 按字符/词汇流式输出的响应 + */ + public Flux streamResponse(String prompt) { + log.info("收到用户提问: {}", prompt); + + // 模拟大模型生成的回复内容 + String response = mockLLMResponse(prompt); + + // 将文本拆分成小块,使用响应式延迟模拟打字效果 + int chunkSize = 2; // 每次发送 2 个字符 + + return Flux.fromArray(splitIntoChunks(response, chunkSize)) + .delayElements(Duration.ofMillis(30)) // 每 30ms 发送一个块 + .doOnComplete(() -> log.info("流式响应完成")); + } + + /** + * 将字符串拆分成固定大小的块 + */ + private String[] splitIntoChunks(String text, int chunkSize) { + int length = (text.length() + chunkSize - 1) / chunkSize; + String[] chunks = new String[length]; + for (int i = 0; i < length; i++) { + int start = i * chunkSize; + int end = Math.min(start + chunkSize, text.length()); + chunks[i] = text.substring(start, end); + } + return chunks; + } + + /** + * 模拟大模型生成内容 + * 实际项目可接入 OpenAI/通义千问等 API + * + * @param prompt 用户问题 + * @return 模拟的回复内容 + */ + private String mockLLMResponse(String prompt) { + return """ + 【Spring Boot 流式响应】 + 您的问题是:%s + + 这是一个模拟大模型流式输出的示例。 + 在实际应用中,你可以: + 1. 接入 OpenAI API 使用 GPT-4 + 2. 接入阿里云通义千问 API + 3. 接入本地部署的大模型 + + 流式响应的核心是: + - 使用 Spring WebFlux 的 Flux + - 返回 text/event-stream 格式 + - 前端使用 EventSource 或 fetch 接收 + + 这样就能实现像 ChatGPT 一样的丝滑体验! + """.formatted(prompt); + } +} diff --git a/springboot-chat-stream/src/main/resources/application.yml b/springboot-chat-stream/src/main/resources/application.yml new file mode 100644 index 0000000..3c2bc6b --- /dev/null +++ b/springboot-chat-stream/src/main/resources/application.yml @@ -0,0 +1,13 @@ +server: + port: 8080 + +spring: + application: + name: chat-stream-demo + +# 日志配置 +logging: + level: + com.example.chat: DEBUG + pattern: + console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/springboot-chat-stream/src/main/resources/static/index.html b/springboot-chat-stream/src/main/resources/static/index.html new file mode 100644 index 0000000..b7ae4f1 --- /dev/null +++ b/springboot-chat-stream/src/main/resources/static/index.html @@ -0,0 +1,667 @@ + + + + + + AI 流式响应演示 + + + + + + + +
+ +
+
+
+ 在线 +
+
+
+ + +
+ +
+

👋 欢迎体验 AI 流式响应

+

基于 Spring Boot WebFlux + SSE 实现实时流式输出
输入任意问题,感受像 ChatGPT 一样的逐字显示效果

+
+ + +
+
+ +
+
🤖
+
+
+ AI 助手 + 现在 +
+
+ 你好!我是基于 Spring Boot 的流式响应助手。你可以问我任何问题,我会逐字回复,让你体验流畅的打字效果! +
+
+
+
+ + +
+
+
+ +
+ +
+
+
+
+ + + +