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 ...
随机推荐
- 记一次vsix安装异常
vsix是vs扩展安装的包,里面包含了vs扩展功能的相关内容和元数据. 由于个人需求,我通过zip工具,对其中的许可文件进行了更新,将EULA.rtf替换为eula.txt.但在安装过程中报错,日志提 ...
- MATLAB R2024a免费+破解版本(含密钥)
强大功能 在使用2024a之前,我一直在使用2018b,最近数学建模+学年论文的摧残,让我看到了matlab新增的强大功能: Deep Learning Toolbox:新增支持 Transforme ...
- 前端跨平台调试代理神器Whistle
概述 抓包一直是平时开发中经常要做的,有一款好用的抓包工具可以让开发体验更棒,以前一直用fiddler,fiddler虽然强悍,但是入手有一定难度,而且较笨重,今天介绍另一款抓包工具:Whistle. ...
- Podman与docker兼容性问题
使用Podman最好的地方就是支持rootless,也就是说用户不需要为root权限即可进行容器的管理操作.因此现在在CentOS 8及以后的版本中,默认使用Podman替代Docker,如果使用do ...
- Qt/C++编写的mqtt调试助手使用说明
一.使用说明 第一步,选择协议前缀,可选mqtt://.mqtts://.ws://.wss://四种,带s结尾的是走ssl通信,ws表示走websocket通信.一般选默认的mqtt://就好. 第 ...
- JVM实战—3.JVM垃圾回收的算法和全流程
大纲 1.JVM内存中的对象何时会被垃圾回收 2.JVM中的垃圾回收算法及各算法的优劣 3.新生代和老年代的垃圾回收算法 4.避免本应进入S区的对象直接升入老年代 5.Stop the World问题 ...
- Windows 配置自动更新重启策略
I. 打开策略编辑器 [Win + R]打开 "运行" 窗口,输入: gpedit.msc 打开"本地组策略编辑器". II. 设置不自动重启 启用策略,选择在 ...
- .NET 9 new features-分布式追踪支持、HTTP/3 改进以及更好的容器镜像支持
.NET 9 针对云原生开发进行了显著优化,重点改进了分布式追踪.HTTP/3 支持和容器镜像优化等方面. 这些特性极大地提升了 .NET 在现代云原生应用中的适配性与开发效率. 1. 设计原理 1. ...
- Windows安全加固(四)
七.服务安全 1.禁用TCP/IP上的NetBIOS(协议所用端口139) 作用:禁用TCP/IP上的NetBIOS协议,可以关闭监听的UDP137.UDP138.UDP139端口. (1)使用快捷键 ...
- Solution Set -「LOCAL」冲刺省选 Round XXIX
\(\mathscr{Summary}\) 啊--说老实话,早上昏昏欲睡的,起码浪费了一个多小时.比赛打麻了 qwq. A 题类似费用提前计算,回忆起这个 trick 之后就简单了.B 题又错 ...