SpringBoot 2.x 接入非标准SSE格式大模型流式响应实践 🚀
近期DeepSeek等国产大模型热度持续攀升,其关注度甚至超过了OpenAI(被戏称为CloseAI)。在SpringBoot3.x环境中,可以使用官方的Spring AI轻松接入,但对于仍在使用JDK8和SpringBoot2.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:")); // 二次过滤
}
三大关键技术点
粘包处理
通过split("\\r?\\n\\r?\\n")解决网络传输中的消息边界问题,示例原始数据:data:{response1}\n\ndata:{response2}\n\n
格式兼容处理
自动去除服务端可能返回的data:前缀,同时保留Spring自动添加SSE前缀的能力双重过滤机制
确保最终输出不包含任何残留的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格式大模型流式响应实践 🚀的更多相关文章
- SQL 将非标准日期格式转换成标准格式,进行条件判断
a.JLDate为非标准日期格式: 例: 2011-8-28 0:00:000011-8-28 0:00:000111-8-4 0:00:00 select CONVERT(varchar(50),C ...
- 关于非标准json格式转变为json对象
eval('(' + tempData + ')') 只需要这一句
- 用 #include “filename.h” 格式来引用非标准库的头文件
用 #include “filename.h” 格式来引用非标准库的头文件(编译器将 从用户的工作目录开始搜索) #include <iostream> /* run this progr ...
- 3.非标准的NDEF格式数据解析--IsoDep
1.使用目的:正常开发是针对NDEF格式数据进行开发,但实际情况并非如此,以厦门公交卡为例,厦门公交卡保存的是非NDEF格式数据.其类型是IsoDep类型. 2.非标准的NDEF格式数据流程:当厦门公 ...
- IEEE754标准浮点格式
两种基本浮点格式:单精度和双精度.IEEE单精度格式具有24位有效数字,并总共占用32 位.IEEE双精度格式具有53位有效数字精度,并总共占用64位 两种扩展浮点格式:单精度扩展和双精度扩展.此标准 ...
- Linux BSP非标准HDMI分辨率
Linux BSP非标准HDMI分辨率 Intrinsyc公司发布了它的一个新的Linux BSP软件的发布 打开-Q820 开发套件基于Linux内核版本.支持的软件功能包括HDMI输出,可以支持标 ...
- [FFMpeg] 非标准分辨率视频Dump YUV注意事项
背景 做视频编解码相关开发的过程中我们经常会遇到要把视频原始YUV数据保存下来查看的情况. 使用FFMpeg对视频解码之后原始图片数据都是保存在AVFrame这一结构中,很多时候我们都按照图像的长宽来 ...
- Python解析非标准JSON(Key值非字符串)
采集数据的时候经常碰到一些JSON数据的Key值不是字符串,这些数据在JavaScript的上下文中是可以解析的,但在Python中,没有该部分数据的上下文,无法采用json.loads(JSON)的 ...
- 标准 DateTime 格式字符串
标准 DateTime 格式字符串 MSDN 标准 DateTime 格式字符串包含一个标准 DateTime 格式说明符字符,该字符表示自定义 DateTime 格式字符串.格式字符串最终定义由格式 ...
- [Effective JavaScript 笔记]第29条:避免使用非标准的栈检查属性
许多js环境都提供检查调用栈的功能.调用栈是指当前正在执行的活动函数链.在某些旧的宿主环境中,每个arguments对象含有两个额外的属性:arguments.callee和arguments.cal ...
随机推荐
- uni-app下载文件在ios下失败
标签: js uni-app 前情 uni-app是我很喜欢的跨平台框架,它能开发小程序,H5,APP(安卓/iOS),对前端开发很友好,自带的IDE让开发体验也很棒,公司项目就是主推uni-app. ...
- 百度地图 自定义弹窗 InfoBox
infoBox文档地址: https://api.map.baidu.com/library/InfoBox/1.2/docs/symbols/BMapLib.InfoBox.html infobox ...
- 哪家开源 ChatGPT 生成代码好
试玩 poe 上的代码生成器,Sage,Claud-instant,Dragonfly 效果都不错,太省事了.于是想找一下开源平替哪家强,回头在自己的显卡上跑起来. 我试验的内容是 根据下面的函数编写 ...
- Redis应用—9.简单应用汇总
大纲 1.基于Redis实现的简单缓存机制(String数据结构) 2.实现一个最简单的分布式锁(String数据结构) 3.博客网站的文章发布与查看(String数据结构) 4.博客字数统计与文章预 ...
- [solon]Solon开发实战之权限认证
本项目采用权限认证框架sa-token(sa-token-solon-plugin) pom.xml <!-- 鉴权--> <dependency> <groupId&g ...
- mysql5.7以后group by 报错 sql_mode=only_full_group_by的解决方法
一.发现问题 1.查询语句 SELECT * from class group by class_name; 2.报错结果 ..... this is incompatible with sql_mo ...
- mysql restart
bin/mysqld --defaults-file=/etc/mysql.cnf --user=mysql bin/mysqld --user=app --basedir=/opt/mysql-5. ...
- Qt/C++音视频开发74-合并标签图形/生成yolo运算结果图形/文字和图形合并成一个/水印滤镜
一.前言 在使用yolo做人工智能运算后,运算结果除了一个方框,还可能需要增加文字显示在对应方框上,以便标记是何种物体,比如显示是人还是动物,或者还有可能追踪人员,显示该人员的姓名.这种应用场景非常普 ...
- Qt音视频开发35-Onvif图片参数
一.前言 视频中的图片的配置参数一般有亮度.饱和度.对比度.锐度等,以前一直以为这些需要通过厂家的私有协议SDK来设置才行,后面通过研究Onvif Device Manager 和 Onvif Dev ...
- Spring-RetryTemplate-RestTemplate的使用
------------------------------------------------------------------------------------ 1.基本概念 1.1应用场景 ...