Spring Boot 3.x + Flux + WebClient 实现大模型 SSE 调用

前言

在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);
    }
}

代码解析:

  1. @RestController:声明该类为 Spring Boot REST 控制器。
  2. @RequestMapping("/v1/chat"):定义 API 路径前缀。
  3. @PostMapping("/completions"):定义 SSE 接口,客户端发送请求后,服务器将持续推送消息。
  4. 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;
    }

}

代码解析:

  1. 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 数据。
  2. 处理流式响应

    • 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("解析失败"));
    }
}

代码解析:

  1. [DONE] 处理 :当 SSE 返回 [DONE] 时,说明流式请求结束,保存完整消息到数据库。

  2. JSON 解析

    • 如果 request.getStream()true,则获取 delta.content(SSE 方式)。
    • 否则,获取 message.content(普通 JSON 方式)。
  3. 返回 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 格式。核心亮点:

  1. 基于 WebFlux 的 WebClient 实现非阻塞 SSE 调用。
  2. 支持 OpenAI 通用格式 ,适配 stream 和非 stream 模式。
  3. 完整的 SSE 连接管理,支持客户端取消和流终止处理。
  4. 数据持久化,在流式请求结束后保存完整对话内容。

适用于大模型对话、实时推送等场景,助力高效 AI 应用开发!🚀

相关推荐
brzhang2 分钟前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
EnCi Zheng19 分钟前
JPA 连接 PostgreSQL 数据库完全指南
java·数据库·spring boot·后端·postgresql
brzhang25 分钟前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
LucianaiB1 小时前
从玩具到工业:基于 CodeBuddy code CLI 构建电力变压器绕组短路智能诊断系统
后端
武子康2 小时前
大数据-118 - Flink 批处理 DataSet API 全面解析:应用场景、代码示例与优化机制
大数据·后端·flink
不要再敲了2 小时前
Spring Security 完整使用指南
java·后端·spring
IT_陈寒2 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端
brzhang3 小时前
高通把Arduino买了,你的“小破板”要变“AI核弹”了?
前端·后端·架构
程序猿阿越4 小时前
Kafka源码(六)消费者消费
java·后端·源码阅读
Terio_my4 小时前
Spring Boot 热部署配置
java·spring boot·后端