近期DeepSeek等国产大模型热度持续攀升,其关注度甚至超过了OpenAI(被戏称为CloseAI)。在SpringBoot3.x环境中,可以使用官方的Spring AI轻松接入,但对于仍在使用JDK8SpringBoot2.7.3的企业级应用来说,往往需要自定义实现。特别是当大模型团队返回的数据格式不符合标准SSE规范时,更需要灵活处理。本文将分享我们的实战解决方案。


引入Gradle依赖

核心依赖说明:

  • spring-boot-starter-web:基础Web支持
  • spring-boot-starter-webflux:响应式编程支持(WebClient所在模块)
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

WebClient配置要点

初始化时特别注意Header配置:

@Bean
public WebClient init() {
return WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openAi)
// ️ 必须设置为JSON格式
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}

关键踩坑点:初始设置MediaType.TEXT_EVENT_STREAM_VALUE会导致请求失败,必须使用APPLICATION_JSON_VALUE


核心处理逻辑

流式请求入口

@GetMapping(value = "/stream/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatEnhanced(@RequestParam("prompt") String prompt) {
// 请求体构建
String requestBody = String.format("""
{
"model": "%s",
"messages": [{"role": "user", "content": "%s"}],
"stream": true
}
""", model, prompt); return webClient.post()
// 请求配置
.uri("/v1/chat/completions")
.bodyValue(requestBody)
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(DataBuffer.class) // 关键配置点
.transform(this::processStream)
// 重试和超时配置
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.timeout(Duration.ofSeconds(180));
// 错误处理
.doOnError(e -> log.error("Stream error", e))
.doFinally(signal -> log.info("Stream completed: {}", signal));
}

技术原理说明

当使用bodyToFlux(DataBuffer.class)时:

  • 获得原始字节流控制权
  • 避免自动SSE格式解析(适用于非标准响应)
  • 动态数据流处理:类似Java Stream,但数据持续追加

非标准SSE数据处理

核心处理流程

private Flux<String> processStream(Flux<DataBuffer> dataBufferFlux) {
return dataBufferFlux
.transform(DataBufferUtils::join) // 字节流合并
.map(buffer -> { // 字节转字符串
String content = buffer.toString(StandardCharsets.UTF_8);
DataBufferUtils.release(buffer);
return content;
})
.flatMap(content -> // 处理粘包问题
Flux.fromArray(content.split("\\r?\\n\\r?\\n")))
.filter(event -> !event.trim().isEmpty()) // 过滤空事件
.map(event -> { // 格式标准化处理
String trimmed = event.trim();
if (trimmed.startsWith("data:")) {
String substring = trimmed.substring(5);
return substring.startsWith(" ") ? substring.substring(1) : substring;
}
return trimmed;
})
.filter(event -> !event.startsWith("data:")); // 二次过滤
}

三大关键技术点

  1. 粘包处理

    通过split("\\r?\\n\\r?\\n")解决网络传输中的消息边界问题,示例原始数据:

    data:{response1}\n\ndata:{response2}\n\n
  2. 格式兼容处理

    自动去除服务端可能返回的data:前缀,同时保留Spring自动添加SSE前缀的能力

  3. 双重过滤机制

    确保最终输出不包含任何残留的SSE格式标识


️ 特别注意

当接口设置produces = MediaType.TEXT_EVENT_STREAM_VALUE时:

  • Spring WebFlux会自动添加data: 前缀

  • 前端收到的格式示例:

    data: {实际内容}
  • 若手动添加

    data:

    前缀会导致重复:

    data: data: {错误内容}  //  错误格式

️ 完整实现代码

// 包声明和导入...

@Service
@Slf4j
public class OpenAiService {
// 配置项和初始化
private String openAiApiKey = "sk-xxxxxx"; private String baseUrl = "https://openai.com/xxxx"; private String model = "gpt-4o"; private WebClient webClient; @PostConstruct
public void init() {
webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
} @GetMapping(value = "/stream/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatEnhanced(@RequestParam("prompt") String prompt) {
// 构建请求体
String requestBody = String.format("""
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "%s"}],
"stream": true
}
""", prompt); // 发送流式请求
return webClient.post()
.uri("/v1/chat/completions")
.bodyValue(requestBody)
.retrieve()
.onStatus(HttpStatusCode::isError, response ->
response.bodyToMono(String.class)
.flatMap(error -> Mono.error(new RuntimeException("API Error: " + error)))
)
.bodyToFlux(DataBuffer.class)
.transform(this::processStream)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.timeout(Duration.ofSeconds(180))
.doOnError(e -> log.error("Stream error", e))
.doFinally(signal -> log.info("Stream completed: {}", signal));
} private Flux<String> processStream(Flux<DataBuffer> dataBufferFlux) {
return dataBufferFlux
// 使用字节流处理
.transform(DataBufferUtils::join)
.map(buffer -> {
String content = buffer.toString(StandardCharsets.UTF_8);
DataBufferUtils.release(buffer);
return content;
})
// 按 SSE 事件边界,防止粘包的问题
.flatMap(content -> Flux.fromArray(content.split("\\r?\\n\\r?\\n")))
// 过滤空事件
.filter(event -> !event.trim().isEmpty())
// 规范 SSE 事件格式
.map(event -> {
String trimmed = event.trim(); // 由于webflux设置了"produces = MediaType.TEXT_EVENT_STREAM_VALUE",
// 所以在返回数据时会自动添加“data:”,因此如果返回的格式带了“data:”需要手动去除
if (trimmed.startsWith("data:")) {
trimmed = trimmed.replaceFirst("data:","").trim();
}
return trimmed;
})
.filter(event -> !event.startsWith("data:"));
}
}

SpringBoot 2.x 接入非标准SSE格式大模型流式响应实践 🚀的更多相关文章

  1. SQL 将非标准日期格式转换成标准格式,进行条件判断

    a.JLDate为非标准日期格式: 例: 2011-8-28 0:00:000011-8-28 0:00:000111-8-4 0:00:00 select CONVERT(varchar(50),C ...

  2. 关于非标准json格式转变为json对象

    eval('(' + tempData + ')') 只需要这一句

  3. 用 #include “filename.h” 格式来引用非标准库的头文件

    用 #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索) #include <iostream> /* run this progr ...

  4. 3.非标准的NDEF格式数据解析--IsoDep

    1.使用目的:正常开发是针对NDEF格式数据进行开发,但实际情况并非如此,以厦门公交卡为例,厦门公交卡保存的是非NDEF格式数据.其类型是IsoDep类型. 2.非标准的NDEF格式数据流程:当厦门公 ...

  5. IEEE754标准浮点格式

    两种基本浮点格式:单精度和双精度.IEEE单精度格式具有24位有效数字,并总共占用32 位.IEEE双精度格式具有53位有效数字精度,并总共占用64位 两种扩展浮点格式:单精度扩展和双精度扩展.此标准 ...

  6. Linux BSP非标准HDMI分辨率

    Linux BSP非标准HDMI分辨率 Intrinsyc公司发布了它的一个新的Linux BSP软件的发布 打开-Q820 开发套件基于Linux内核版本.支持的软件功能包括HDMI输出,可以支持标 ...

  7. [FFMpeg] 非标准分辨率视频Dump YUV注意事项

    背景 做视频编解码相关开发的过程中我们经常会遇到要把视频原始YUV数据保存下来查看的情况. 使用FFMpeg对视频解码之后原始图片数据都是保存在AVFrame这一结构中,很多时候我们都按照图像的长宽来 ...

  8. Python解析非标准JSON(Key值非字符串)

    采集数据的时候经常碰到一些JSON数据的Key值不是字符串,这些数据在JavaScript的上下文中是可以解析的,但在Python中,没有该部分数据的上下文,无法采用json.loads(JSON)的 ...

  9. 标准 DateTime 格式字符串

    标准 DateTime 格式字符串 MSDN 标准 DateTime 格式字符串包含一个标准 DateTime 格式说明符字符,该字符表示自定义 DateTime 格式字符串.格式字符串最终定义由格式 ...

  10. [Effective JavaScript 笔记]第29条:避免使用非标准的栈检查属性

    许多js环境都提供检查调用栈的功能.调用栈是指当前正在执行的活动函数链.在某些旧的宿主环境中,每个arguments对象含有两个额外的属性:arguments.callee和arguments.cal ...

随机推荐

  1. C# Redis 的基本使用

    C# Redis 的基本使用 -迷恋自留地 Redis 概述 在我们日常的开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题,可是一 ...

  2. OS之《内存管理》

    程序装入方式 绝对装入:程序逻辑地址和物理地址是完全对应的.不现实 可重定位装入:装入的时候重新 计算内存地址.程序中的实际地址加上程序载入的起始地址:但是解决不了进程挂起 后重新唤醒的问题.唤醒的后 ...

  3. 解读GaussDB的BTree索引和UBTree索引,如何带来更强并发能力

    本文分享自华为云社区<[GaussTech技术专栏]GaussDB的BTree索引和UBTree索引>,作者:GaussDB 数据库. 1. 简介 数据库通常使用索引来提高业务查询的速度. ...

  4. ruoyi若依前端验证码不显示的终极解决方法-20230721

    ​搞了3天啊,查了各种资料啊. 然后使劲的看log啊,总算搞定了啊. 一般情况,本地开发环境测试没问题,部署到服务器就各种不适应,就是服务器配置的问题了. 本次这种验证码不显示,典型的nginx的配置 ...

  5. 05C++数据类型

    一.单精度实数float 教学视频A 例程1:金字塔的底是正方形,侧面由四个大小相等的等腰三角形构成.试编一程序,输入底和高,输出三角形的面积. #include <iostream> / ...

  6. 一个.NET开源、易于使用的屏幕录制工具

    前言 一款高效.易用的屏幕录制工具能够极大地提升我们的工作效率和用户体验,今天大姚给大家分享一个.NET开源.免费.易于使用的屏幕录制工具:Captura. 工具介绍 Captura是一款基于.NET ...

  7. Qt音视频开发06-海康sdk内核linux客户端

    一.前言 海康sdk的示例在官方是提供了的,但是无论UI还是交互简直是宇宙无敌的垃圾,猜测应该是初学者编写的,估计练手用的,所以老早就想把这个linux支持集成到自己的示例中,既然已经支持了windo ...

  8. 微信团队分享:微信后端海量数据查询从1000ms降到100ms的技术实践

    本文由微信技术团队仇弈彬分享,原题"微信海量数据查询如何从1000ms降到100ms?",本文进行了内容修订和排版优化. 1.引言 微信的多维指标监控平台,具备自定义维度.指标的监 ...

  9. 小程序IOS系统input设置maxlength时,输入到最后如果输入汉字的拼音长度超过限制会直接中断输入(bug bug)

    我的解决办法:不在输入框限制长度,在提交表单的时候判断长度,欢迎大家有好的解决方法分享一下

  10. 网站架构核心技disruptor

    一 序:本章业务场景:队列在数据结构中是一种线性表,从一端插入数据,然后从另一端删除数据.作者举例的场景有:进行异步处理.系统解耦.数据同步.流量削峰.缓冲.限流等. 前面的比较浅,总结起来,核心知识 ...