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 应用开发!🚀

相关推荐
Asthenia0412几秒前
面试复盘:Java String 源码分析与不可变类设计原理
后端
Asthenia04121 分钟前
面试复盘:synchronized 锁与 ReentrantLock 锁的区别及 AQS 认知完善
后端
joker学java37 分钟前
java基础快速入门07
后端
uhakadotcom38 分钟前
了解Pulumi:基础设施即代码的新选择
后端·面试·github
fliter1 小时前
性能比拼: TCP vs UDP(重大改进)
后端
林川的邹1 小时前
如何根据场景判断是使用ArrayList还是LinkedList?
java·后端
Postkarte不想说话1 小时前
ZLMediaKit搭建直播平台
后端
用户86178277365181 小时前
营销邮件
后端
fliter1 小时前
性能比拼: TCP vs UDP(延迟和吞吐量)
后端
Bohemian1 小时前
LeetCode39 组合总和(带扩展)
后端·面试