前言
在AI 应用开发中,流式输出(SSE,Server-Sent Events) 是一种常见的方式,特别适用于大模型的 API 调用,比如 ChatGPT, DeepSeek, GLM .... 接口。SSE 允许服务器向客户端推送数据流,避免长时间阻塞请求,提高用户体验。
本篇文章基于 Spring Boot 3.x ,结合 Flux(Spring WebFlux) 和 WebClient ,实现 大模型 SSE 调用,并支持 OpenAI 通用格式。
技术栈
- Spring Boot 3.x:现代 Java Web 开发框架
- Spring WebFlux:响应式 Web 框架,支持非阻塞 I/O
- WebClient:Spring WebFlux 提供的 HTTP 客户端,支持异步流式请求
- SSE(Server-Sent Events) :服务器推送技术,适用于流式响应
- Lombok:简化 Java 代码
- Jackson:JSON 解析库
1.项目依赖(pom.xml)
在 pom.xml
文件中,需要引入以下依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.51</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
1. 控制层(Controller)
ChatController
作为 RESTful API 入口,提供 SSE 方式的聊天接口。
java
/**
* Created by WeiRan on 2025.03.26
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/chat")
public class ChatController {
private final ChatCompletionService chatCompletionService;
@PostMapping(value = "/completions")
public Flux<ServerSentEvent<JSONObject>> completions(@RequestBody ChatCompletionRequest chatCompletionRequest) {
return chatCompletionService.completions(chatCompletionRequest);
}
}
代码解析:
@RestController
:声明该类为 Spring Boot REST 控制器。@RequestMapping("/v1/chat")
:定义 API 路径前缀。@PostMapping("/completions")
:定义 SSE 接口,客户端发送请求后,服务器将持续推送消息。Flux<ServerSentEvent<JSONObject>>
:返回SSE 事件流,适用于流式响应。
2. 业务逻辑层(Service)
ChatCompletionServiceImpl
通过 WebClient
调用大模型 API,并处理 SSE 数据流。
java
/**
* Created by WeiRan on 2025.03.26
*/
public interface ChatCompletionService {
Flux<ServerSentEvent<JSONObject>> completions(ChatCompletionRequest chatCompletionRequest);
}
java
/**
* Created by WeiRan on 2025.03.26
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ChatCompletionServiceImpl implements ChatCompletionService {
private final WebClient.Builder webClientBuilder;
public Flux<ServerSentEvent<JSONObject>> completions(ChatCompletionRequest request) {
// 使用 WebClient 发送 SSE 请求
WebClient webClient = webClientBuilder
.baseUrl("url") // 设置目标 API 地址
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + "1233133") // 认证信息
.defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE) // 期望接收 SSE 格式的响应
.build();
StringBuilder contentBuilder = new StringBuilder(); // 用于累积 SSE 返回的消息内容
return webClient.post()
.bodyValue(request) // 发送请求体
.retrieve()
.bodyToFlux(String.class) // 解析响应数据为流式字符串
.flatMap(data -> processResponse(data, contentBuilder,request)) // 逐个处理 SSE 数据
.doOnCancel(() -> onClientCancel(contentBuilder)) // 监听前端主动断开连接
.doOnTerminate(this::onStreamTerminate) // 监听 SSE 连接终止(可能是正常结束或异常)
.onErrorResume(this::handleError); // 发生异常时进行错误处理
}
/**
* 处理 SSE 响应数据
*
* @param data SSE 返回的单条数据
* @param contentBuilder 用于累积完整的返回内容
* @return 处理后的 SSE 事件
*/
private Mono<ServerSentEvent<JSONObject>> processResponse(String data, StringBuilder contentBuilder,ChatCompletionRequest request) {
// 判断是否为 SSE 结束标志 "[DONE]"
if ( "[DONE]".equals(data) ) {
String finalContent = contentBuilder.toString();
log.info("SSE 消息接收完成,最终内容: {}", finalContent);
// SSE 完成后,将内容保存到数据库
saveToDatabase(finalContent);
// 返回一个 SSE 事件,表示会话已完成
return Mono.just(ServerSentEvent.<JSONObject>builder()
.event("done")
.id(UUID.randomUUID().toString())
.data(new JSONObject()) // 发送空数据,仅通知前端结束
.build());
}
try {
// 解析 SSE 返回的 JSON 数据
ChatCompletionResponse response = new ObjectMapper().readValue(data, ChatCompletionResponse.class);
String content = "";
if ( request.getStream() ) {
content= response.getChoices().get(0).getDelta().getContent();
}else {
content = response.getChoices().get(0).getMessage().getContent();
}
// 累积返回的消息内容
if ( content != null ) {
contentBuilder.append(content);
}
// 构造 SSE 事件并返回
return Mono.just(ServerSentEvent.<JSONObject>builder()
.event("add") // 事件类型
.id(UUID.randomUUID().toString()) // 生成唯一 ID
.data(new ObjectMapper().convertValue(response, JSONObject.class)) // 发送解析后的 JSON 数据
.build());
} catch (JsonProcessingException e) {
log.error("JSON 解析失败: {}", data, e);
return Mono.just(createErrorEvent("解析失败")); // 解析异常时返回错误事件
}
}
/**
* 将完整的聊天内容保存到数据库
*
* @param content 完整的聊天记录
*/
private void saveToDatabase(String content) {
if ( content.isBlank() ) {
return; // 空内容不保存
}
log.info("消息已成功入库:{}", content);
}
/**
* 处理 SSE 请求中的异常情况
*
* @param e 异常对象
* @return 错误事件的 Mono
*/
private Mono<ServerSentEvent<JSONObject>> handleError(Throwable e) {
log.error("SSE 请求处理异常", e);
return Mono.just(createErrorEvent("服务异常,请联系管理员"));
}
/**
* 创建 SSE 错误事件
*
* @param message 错误信息
* @return SSE 错误事件
*/
private ServerSentEvent<JSONObject> createErrorEvent(String message) {
return ServerSentEvent.<JSONObject>builder()
.event("error") // 事件类型为错误
.id(UUID.randomUUID().toString()) // 生成唯一 ID
.data(ModelMessageUtils.convertModelChatResponse(UUID.randomUUID().toString(), message)) // 发送错误信息
.build();
}
/**
* 监听前端主动关闭 SSE 连接
*/
private void onClientCancel(StringBuilder contentBuilder) {
System.out.println("11111----->" + contentBuilder);
log.info("前端关闭了 SSE 连接");
}
/**
* 监听 SSE 连接终止(包括正常结束或异常)
*/
private void onStreamTerminate() {
log.info("SSE 连接终止");
}
}
2. 实体类定义
ChatCompletionRequest:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatCompletionRequest implements Serializable {
private String model; // 模型名称,指定使用哪个模型进行对话生成。
private List<Message> messages; // 消息列表,包含对话的历史消息。
private String request_id; // 请求ID,用于标识本次请求。
private Boolean do_sample; // 是否进行采样,用于控制生成文本的多样性。
private Boolean stream; // 是否以流式方式返回结果,用于实时获取生成内容。
private Float temperature; // 温度参数,用于控制生成文本的随机性。
private Float top_p; // 核采样概率,用于控制生成文本的多样性。
private Integer max_tokens; // 最大生成令牌数,限制生成文本的长度。
private List<String> stop; // 停止序列,生成过程中遇到这些序列将停止生成。
private List<Tool> tools; // 工具列表,包含可用于对话生成的工具。
private Object tool_choice; // 工具选择,可以是字符串或对象,用于指定使用哪个工具。
private String user_id; // 用户ID,用于标识用户。
}
Message:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Message implements Serializable {
private String role; // 消息角色,表示消息的发送者身份,例如用户、助手等。
private String content; // 消息内容,包含发送者所传递的文本信息。
}
Tool:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Tool implements Serializable {
private String type; // 工具类型,表示工具的种类或分类。
private Object function; // 函数对象,可以是具体的函数对象或者为null,表示工具所关联的函数。
private String knowledge_id; // 知识ID,用于标识与工具相关的知识或数据。
private String prompt_template; // 提示模板,用于生成提示信息的模板字符串。
private Boolean enable; // 是否启用,表示工具是否被启用。
private String search_query; // 搜索查询,用于存储搜索查询的字符串。
private Boolean search_result; // 搜索结果,表示是否获取了搜索结果。
}
ChatCompletionResponse:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatCompletionResponse implements Serializable {
private String id; // 响应ID,唯一标识这次对话生成的响应。
private String object; // 对象类型,通常用于指示这个响应的类型。
private Long created; // 创建时间,表示这个响应何时被创建,通常是一个时间戳。
private String model; // 模型名称,指示生成这个响应所使用的模型。
private String systemFingerprint; // 系统指纹,用于追踪和验证响应的来源或版本。
private List<ChatChoice> choices; // 选择列表,包含多个对话生成的选择,每个选择代表一个可能的回复。
private Usage usage; // 使用情况,包含关于这次对话生成中模型使用情况的统计信息。
}
ChatChoice:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatChoice implements Serializable {
private Integer index; // 索引,表示这个选择在列表中的位置。
/**
* 当请求参数stream为true时,返回的是delta对象,
* 表示消息的增量更新,用于流式传输部分消息内容。
*/
private MessageResponse delta;
/**
* 当请求参数stream为false时,返回的是message对象,
* 表示完整的消息响应,包含整个消息的内容。
*/
private MessageResponse message;
private String finish_reason; // 完成原因,表示为什么生成过程结束,例如正常完成、达到最大令牌数等。
}
MessageResponse:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MessageResponse implements Serializable {
private String role; // 消息角色,表示消息的发送者身份,例如用户、助手等。
private String reasoning_content; //兼容deepseek官方api深度思考字段
public String content; // 消息内容,包含发送者所传递的文本信息。
private String name; // 名称,可能用于标识消息发送者的名称或昵称。
}
Usage:
java
/**
* Created by WeiRan on 2025.03.26
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Usage implements Serializable {
private long prompt_tokens; // 提示tokens数,表示输入文本中使用的tokens数量。
private long completion_tokens; // 完成tokens数,表示生成文本中使用的tokens数量。
private long total_tokens; // 总tokens数,表示整个交互过程中使用的tokens总数,包括提示和完成。
}
ModelMessageUtils
java
/**
* Created by WeiRan on 2025.03.26
*/
public class ModelMessageUtils {
/**
* 转换请求体
*
* @return ChatCompletion
*/
public static ChatCompletion convertModelCompletion(ChatCompletionRequest chatCompletionRequest) {
return null;
}
/**
* 转换给前端返回消息内容
*
* @return JSONObject
*/
public static JSONObject convertModelChatResponse(String id, String content) {
JSONObject jsonObject = new JSONObject();
if ( StrUtil.isNotBlank(id) && StrUtil.isNotBlank(content) ) {
jsonObject.put("id", id);
jsonObject.put("content", content);
return jsonObject;
}
return jsonObject;
}
}
代码解析:
-
WebClient
发送 POST 请求baseUrl("url")
:设置目标 API 地址。defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer xxx")
:设置 API 认证。defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE)
:声明期望返回 SSE 数据。bodyToFlux(String.class)
:解析 API 返回的流式 JSON 数据。
-
处理流式响应
flatMap(data -> processResponse(data, contentBuilder, request))
:解析 SSE 数据并返回事件。doOnCancel(() -> onClientCancel(contentBuilder))
:监听前端主动断开 SSE 连接。doOnTerminate(this::onStreamTerminate)
:监听 SSE 连接终止。onErrorResume(this::handleError)
:异常处理。
3. SSE 响应数据处理
java
private Mono<ServerSentEvent<JSONObject>> processResponse(String data, StringBuilder contentBuilder, ChatCompletionRequest request) {
if ("[DONE]".equals(data)) {
String finalContent = contentBuilder.toString();
log.info("SSE 消息接收完成,最终内容: {}", finalContent);
saveToDatabase(finalContent);
return Mono.just(ServerSentEvent.<JSONObject>builder()
.event("done")
.id(UUID.randomUUID().toString())
.data(new JSONObject())
.build());
}
try {
ChatCompletionResponse response = new ObjectMapper().readValue(data, ChatCompletionResponse.class);
String content = request.getStream() ? response.getChoices().get(0).getDelta().getContent()
: response.getChoices().get(0).getMessage().getContent();
if (content != null) {
contentBuilder.append(content);
}
return Mono.just(ServerSentEvent.<JSONObject>builder()
.event("add")
.id(UUID.randomUUID().toString())
.data(new ObjectMapper().convertValue(response, JSONObject.class))
.build());
} catch (JsonProcessingException e) {
log.error("JSON 解析失败: {}", data, e);
return Mono.just(createErrorEvent("解析失败"));
}
}
代码解析:
-
[DONE]
处理 :当 SSE 返回[DONE]
时,说明流式请求结束,保存完整消息到数据库。 -
JSON 解析:
- 如果
request.getStream()
为true
,则获取delta.content
(SSE 方式)。 - 否则,获取
message.content
(普通 JSON 方式)。
- 如果
-
返回 SSE 事件 :封装
ServerSentEvent
,前端可实时获取消息。
4. SSE 连接管理
java
private void onClientCancel(StringBuilder contentBuilder) {
log.info("前端关闭了 SSE 连接,已接收内容: {}", contentBuilder);
}
private void onStreamTerminate() {
log.info("SSE 连接终止");
}
4. SSE 测试示例
CURL:
curl
curl --location 'http://localhost:8092/api/v1/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "glm-4-flash",
"messages": [
{
"role": "system",
"content": "你是一个智能助手"
},
{
"role": "user",
"content": "你好!"
}
],
"stream": true
}'
响应示例:
json
id:42b2359e-1ad1-41f0-935a-0471264a7fa2
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"你好","name":null},"message":null,"finish_reason":null}],"usage":null}
id:78f1bc01-6ea8-4089-af86-d0a9f5aa5f27
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"","name":null},"message":null,"finish_reason":null}],"usage":null}
id:6f4d470c-676d-45f2-b8f1-0e84a7c8bfb4
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"","name":null},"message":null,"finish_reason":null}],"usage":null}
id:27611478-7b2f-4b64-a03d-9b663f66e180
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"\uD83D\uDC4B","name":null},"message":null,"finish_reason":null}],"usage":null}
id:683a2c71-3b09-46a7-bdbe-56de38b908b5
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"!","name":null},"message":null,"finish_reason":null}],"usage":null}
id:27d9d5b6-c389-4e5f-9a62-6a7af65b2eca
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"很高兴","name":null},"message":null,"finish_reason":null}],"usage":null}
id:c71bac86-1d55-4fca-948b-2120f515a042
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"见到","name":null},"message":null,"finish_reason":null}],"usage":null}
id:4f1932d7-2f9c-43f1-afb3-20eb4899c14b
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"你","name":null},"message":null,"finish_reason":null}],"usage":null}
id:a8abd83c-ceb6-4afd-9b42-6c015740176c
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":",","name":null},"message":null,"finish_reason":null}],"usage":null}
id:d85d29f5-6513-473e-8a98-434ac7d5bd5e
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"欢迎","name":null},"message":null,"finish_reason":null}],"usage":null}
id:e1b7e2e2-9cf2-402e-a0e2-cebffa94c430
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"问我","name":null},"message":null,"finish_reason":null}],"usage":null}
id:2381697d-73e9-41df-bc53-73474d28a9d7
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"任何","name":null},"message":null,"finish_reason":null}],"usage":null}
id:17a39271-5a9b-41b6-8ecb-f48eba0cc9b3
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"问题","name":null},"message":null,"finish_reason":null}],"usage":null}
id:ac6f977e-44b2-429d-97c2-fee53c93f048
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":null,"content":"。","name":null},"message":null,"finish_reason":null}],"usage":null}
id:b924d532-b827-4d6a-a753-1fe9128a7981
event:add
data:{"id":"gen-1742990444401174612","object":"chat.completion.chunk","created":1742990444,"model":"big-model","systemFingerprint":null,"choices":[{"index":0,"delta":{"role":null,"reasoning_content":null,"content":"","name":null},"message":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":15,"total_tokens":15}}
id:1648ceed-8566-42b9-ba3b-d9f8810333c9
event:done
data:{}
代码解析:
onClientCancel
:前端主动关闭 SSE 连接时触发,打印已接收内容。onStreamTerminate
:SSE 连接终止时触发,记录日志。
5. 结论
本教程实现了 Spring Boot 3.x + Flux + WebClient 的 SSE 调用,用于对接大模型 API,并支持 OpenAI 格式。核心亮点:
- 基于 WebFlux 的 WebClient 实现非阻塞 SSE 调用。
- 支持 OpenAI 通用格式 ,适配
stream
和非stream
模式。 - 完整的 SSE 连接管理,支持客户端取消和流终止处理。
- 数据持久化,在流式请求结束后保存完整对话内容。
适用于大模型对话、实时推送等场景,助力高效 AI 应用开发!🚀