当前位置: 首页 > article >正文

Spring Boot SSE流式输出+AI消息持久化升级实践:从粗暴到优雅的跃迁

在 AI 应用落地过程中,我们常常需要将用户和 AI 的对话以“完整上下文”的形式持久化到数据库中。但当 AI 回复非常长,甚至接近上万字时,传统的单条消息保存机制就会出问题。

在本篇文章中,我将深入讲解一次实际项目中对 对话持久化+SSE流式响应机制 的全面升级,核心围绕 SeekController.java 控制器类改造展开,对比旧代码与新方案,解释其设计思路、实现细节和优化点。

场景背景:

假设你正在开发一个 AI 小学辅导应用,用户与 AI 进行对话,后端通过 SSE(Server-Sent Events)协议流式返回 AI 回复。同时你需要将所有对话保存在数据库中,供后续查看和继续。

原始版本的逻辑存在以下问题:

  • AI回复过长,数据库字段溢出

  • 保存逻辑未考虑拆分或异常

  • 用户消息也可能过长,直接保存风险大

  • SSE流式输出与持久化耦合度过高

  • 报错无提示,仅日志记录

改造目标:

  1. 使用分块机制安全持久化超长消息;
  2. 提供中英文断句逻辑,尽可能自然地分段;
  3. 保留流式体验,同时异步、稳健保存数据;
  4. 出错时降级处理,保留关键信息;
  5. 增强可维护性和日志追踪能力。

新旧对比:设计与核心区别一览

功能点原实现(假设)新实现(本代码)
SSE流式可能返回整段数据基于 DeepSeek 接口流式返回 JSON
持久化整体写入,一次提交分块处理、格式化保存
长文本处理可能直接截断中文标点 + 段落智能断点
错误处理仅 try-catch加入错误消息保存入库
用户体验容易失败不提示“内容被截断”“部分消息”明确反馈

核心升级一:AI回复和用户消息智能分块保存

private static final int MAX_DB_CHUNK_LENGTH = 16000;
private static final int PREFERRED_CHUNK_LENGTH = 8000;

系统限制数据库字段为 16000 字符以内,为避免过长内容保存失败,我们:

  1.     定义“首选分块长度”(8000)和“最大字段长度”;
  2.     优先尝试在中文句号(。)、感叹号、问号等自然断句处断开;
  3.     找不到就退而求其次,在段落分隔符(\n\n)断开;
  4.     保存时附加头部说明和结尾标注,如 [第1块/共3块];
  5.     超出字段限制则尾部加 "[内容被截断]" 明确提示。

分块格式如下:

[第1块/共2块] 这是一段很长很长的回复内容,适合分块保存。[此为分块消息的一部分]

核心升级二:AI 响应内容通过 SSE 流式返回 + 动态拼接

RealEventSource realEventSource = new RealEventSource(request, new EventSourceListener() {@Overridepublic void onEvent(EventSource eventSource, String id, String type, String data) {if (DONE.equals(data)) return;String content = getContent(data);if (content != null) {// 累积回复aiResponseRef.set(aiResponseRef.get() + content);// SSE 推送pw.write("data:" + JsonUtils.convertObj2Json(new ContentDto(content)) + "\n\n");pw.flush();}}
});

核心亮点:

  1.     DeepSeek 的接口通过 SSE 返回 delta 内容;
  2.     每次内容片段都会立即返回前端,极大提升响应速度和流畅性;
  3.     同时我们使用 AtomicReference 变量拼接全量回复,供后续入库。

核心升级三:用户消息也不再“盲目乐观”

别只考虑 AI 长文本,用户输入如果是整段阅读理解、文章、作文题目,也可能超长!

private void saveUserMessage(Integer conversationId, String userContent) {List<String> chunks = splitContentIntoChunks(userContent);for (int i = 0; i < chunks.size(); i++) {String chunkContent = formatChunkContent(chunks.get(i), i, chunks.size());if (chunkContent.length() > MAX_DB_CHUNK_LENGTH) {chunkContent = chunkContent.substring(0, MAX_DB_CHUNK_LENGTH - 100) + "...[内容被截断]";}Message userMsg = new Message();userMsg.setConversationId(conversationId);userMsg.setSenderType(Message.SenderType.USER);userMsg.setContent(chunkContent);messageService.add(userMsg);}
}

用户输入同样 优先尝试智能分段 + 分块保存,并在失败时记录错误提示。

核心升级四:AI系统消息加入“角色注入”

为了让 AI 更符合目标受众(小学生),我们默认在每次对话中插入一条系统 prompt:

systemMessage.put("content", "你是一个经验丰富的小学学习辅导 AI 助手...");

这段 prompt 会 始终被添加为上下文第一条消息,确保风格和角色固定一致。

技术细节亮点汇总

技术点用法说明
SSE协议使用 EventSourceRealEventSource 实现流式对话
OkHttp使用 OkHttp 发送带事件监听器的 POST 请求
CountDownLatch阻塞主线程直到 SSE 结束
Jackson ObjectMapper精准地处理 JsonNode 和字符串互转
分块处理断句处理中文标点、英文标点、段落符
分块标注提供块序号、块总数、尾注辅助前端识别
降级容错保存失败时 fallback 记录错误提示消息

用户体验提升细节

  1. AI语气风格:符合儿童认知水平和心理预期;
  2. 前端流畅呈现:每条回复几乎“实时可见”;
  3. 对话记录清晰分层:块头/块尾标注简洁明确;
  4. 出错时不报错白屏,保留重要提示信息。

小结与反思

本次升级看似只是对“长文本保存”的功能增强,但背后牵涉到了数据结构设计、AI接口集成、用户体验控制、系统健壮性等多个维度的系统性思考。

最关键的不是“写对代码”,而是能预判潜在问题,并留出足够冗余空间处理边界异常。

结语

如果你也在构建 AI 应用系统,强烈建议你:

  1.     使用 流式返回机制 提升响应体验;
  2.     加入 分块策略 处理高可变长度的输入/输出;
  3.     异常可见化,让错误被看到、被记录、被挽救;
  4.     提前考虑 前后端协作约定,如 chunk 标注语法。

希望本篇文章能为你带来实用灵感!

效果展示:

相关文章:

Spring Boot SSE流式输出+AI消息持久化升级实践:从粗暴到优雅的跃迁

在 AI 应用落地过程中&#xff0c;我们常常需要将用户和 AI 的对话以“完整上下文”的形式持久化到数据库中。但当 AI 回复非常长&#xff0c;甚至接近上万字时&#xff0c;传统的单条消息保存机制就会出问题。 在本篇文章中&#xff0c;我将深入讲解一次实际项目中对 对话持久…...

camera功能真的那么难用吗

背景 Android开发工作过程中&#xff0c;经常需要用到camera相关能力&#xff0c;比如&#xff1a;人脸识别&#xff0c;ai识别&#xff0c;拍照预览&#xff0c;摄像头录制等等需求。都需要使用到camera&#xff0c;且需要拿到camera的预览数据。但是每次开发这块代码都比较繁…...

Model Context Protocol (MCP) 是一个前沿框架

微软发布了 Model Context Protocol (MCP) 课程&#xff1a;mcp-for-beginners。 Model Context Protocol (MCP) 是一个前沿框架&#xff0c;涵盖 C#、Java、JavaScript、TypeScript 和 Python 等主流编程语言&#xff0c;规范 AI 模型与客户端应用之间的交互。 MCP 课程结构 …...

SQL Server 日期时间类型全解析:从精确存储到灵活转换

SQL Server 日期时间类型全解析&#xff1a;从精确存储到灵活转换 一、引言&#xff1a;日期时间处理的核心挑战 在数据管理中&#xff0c;日期时间类型是最常用却最容易出错的数据类型之一。不同业务场景对时间精度、时区感知、存储效率的需求差异极大&#xff1a; 金融交易…...

Android Test3 获取的ANDROID_ID值不同

Android Test3 获取的ANDROID_ID值不同 这篇文章来说明上一篇文章中说到的一个现象&#xff1a;在同一个项目中&#xff0c;创建不同的 app module&#xff0c;运行同一段测试代码&#xff0c;获取到的 ANDROID_ID 的值不同。 我也是第一次认真研究这个现象&#xff0c;这个还…...

[蓝桥杯 2024 国 B] 立定跳远

问题描述 在运动会上&#xff0c;小明从数轴的原点开始向正方向立定跳远。项目设置了 n 个检查点 a1,a2,...,an且 ai≥ai−1>0。小明必须先后跳跃到每个检查点上且只能跳跃到检查点上。同时&#xff0c;小明可以自行再增加 m 个检查点让自己跳得更轻松。在运动会前&#xf…...

内容力重塑品牌增长:开源AI大模型驱动下的智能名片与S2B2C商城赋能抖音生态种草范式

摘要&#xff1a;内容力已成为抖音生态中品牌差异化竞争的核心能力&#xff0c;通过有价值、强共鸣的内容实现产品"种草"与转化闭环。本文基于"开源AI大模型AI智能名片S2B2C商城小程序源码"技术架构&#xff0c;提出"技术赋能内容"的新型种草范式…...

手机号在网状态查询接口如何用PHP实现调用?

一、什么是手机号在网状态查询接口 通过精准探测手机号的状态&#xff0c;帮助平台减少此类问题的发生&#xff0c;提供更个性化的服务或进行地域性营销 二、应用场景 1. 金融风控 通过运营商在网态查询接口&#xff0c;金融机构可以核验贷款申请人的手机状态&#xff0c;拦…...

【Java微服务组件】分布式协调P4-一文打通Redisson:从API实战到分布式锁核心源码剖析

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 有很多很多不足的地方&#xff0c;欢迎评论交流&#xff0c;感谢您的阅读和评论&#x1f604;。 目录 引言Redisson基本信息Redisson网站 Redisson应用…...

一个简单的德劳内三角剖分实现

德劳内&#xff08;Delaunay&#xff09;三角剖分是一种经典的将点集进行三角网格化预处理的手段&#xff0c;在NavMesh、随机地牢生成等场景下都有应用。 具体内容百度一大堆&#xff0c;就不介绍了。 比较知名的算法是Bowyer-Watson算法&#xff0c;也就是逐点插入法。 下雨闲…...

Python入门手册:异常处理

在编程过程中&#xff0c;异常处理是一个非常重要的环节。它可以帮助我们处理程序运行时可能出现的错误和异常情况&#xff0c;确保程序的稳定性和可靠性。Python提供了强大的异常处理机制&#xff0c;使得我们能够优雅地处理各种异常情况。今天&#xff0c;就让我们一起深入学…...

C#子线程更新主线程UI及委托回调使用示例

1.声明线程方法 2.线程中传入对象 3.声明委托与使用 声明委托对象 委托作为参数传入方法 4.在线程中传入委托 5.调用传入的委托...

使用VuePress2.X构建个人知识博客,并且用个人域名部署到GitHub Pages中

使用VuePress2.X构建个人知识博客&#xff0c;并且用个人域名部署到GitHub Pages中 什么是VuePress VuePress 是一个以 Markdown 为中心的静态网站生成器。你可以使用 Markdown 来书写内容&#xff08;如文档、博客等&#xff09;&#xff0c;然后 VuePress 会帮助你生成一个…...

手写Promise.all

前言 之前在看远方os大佬直播的时候看到有让手写的Promise.all的问题&#xff0c;然后心血来潮自己准备手写一个 开始 首先&#xff0c;我们需要明确原本js提供的Promise.all的特性 Promise.all返回的是一个Promise如果传入的数据中有一个reject即整个all返回的就是reject&…...

调试器基本原理

调试器基本原理 前言 调试器(debugger)&#xff0c;是一种用于控制其他程序执行流程、监控和修改其他程序状态的软件工具。 调试器通过实时分析程序的执行状态&#xff0c;协助开发者定位代码错误、了解程序工作原理、性能调优及逆向工程等。 1. 调试器核心功能 1.1 控制程…...

2025年6月|注意力机制|面向精度与推理速度提升的YOLOv8模型结构优化研究:融合ACmix的自研改进方案

版本&#xff1a; 8.3.143(Ultralytics YOLOv8框架) ACmix模块原理 在目标检测任务中&#xff0c;小目标&#xff08;如裂缝、瑕疵、零件边缘等&#xff09;由于其尺寸较小、纹理信息稀疏&#xff0c;通常更容易受到图像中复杂背景或噪声的干扰&#xff0c;从而导致漏检或误检…...

JAVA开发代码小工具集合

目录 前言编号生成工具EasyExcel 工具断言工具HTTP 工具字符串 工具验证码生成工具Excel 工具Class 工具Enum 工具分页工具断言工具2IP 地址工具Map 工具 前言 这些工具都是日常开发中能用到的&#xff0c;前后端都有&#xff0c;觉得好用就拿过来了… 编号生成工具 import j…...

利用qcustomplot绘制曲线图

本文详细介绍了qcustomplot绘制曲线图的流程&#xff0c;一段代码一段代码运行看效果。通过阅读本文&#xff0c;读者可以了解到每一项怎么用代码进行配置&#xff0c;进而实现自己想要的图表效果。&#xff08;本文只针对曲线图&#xff09; 1 最简单的图形&#xff08;入门&…...

【基础算法】枚举(普通枚举、二进制枚举)

文章目录 一、普通枚举1. 铺地毯(1) 解题思路(2) 代码实现 2. 回文日期(1) 解题思路思路一&#xff1a;暴力枚举思路二&#xff1a;枚举年份思路三&#xff1a;枚举月日 (2) 代码实现 3. 扫雷(2) 解题思路(2) 代码实现 二、二进制枚举1. 子集(1) 解题思路(2) 代码实现 2. 费解的…...

智能对联网页小程序的仓颉之旅

#传统楹联遇上AI智能体&#xff1a;我的Cangjie Magic开发纪实 引言&#xff1a;一场跨越千年的数字对话 "云对雨&#xff0c;雪对风&#xff0c;晚照对晴空"。昨天晚上星空璀璨&#xff0c;当我用仓颉语言写下第一个智能对联网页小程序的Agent DSL代码时&#xff0…...

Go字符串切片操作详解:str1[:index]

在Go语言中&#xff0c;return str1[:index] 是一个​​字符串切片操作​​&#xff0c;它截取字符串的一部分。让我们深入解析这个操作的含义和原理&#xff1a; 基本语法和含义 str1&#xff1a;原始字符串[:index]&#xff1a;切片操作符str1[:index]&#xff1a; ​​起始…...

JavaScript 本地存储 (localStorage) 完全指南

文章目录 JavaScript 本地存储 (localStorage) 完全指南 &#x1f510;一、什么是 localStorage&#xff1f;&#x1f4a1;二、如何使用 localStorage&#xff1f;&#x1f527;1. 存储数据2. 读取数据3. 删除数据4. 清空所有数据 三、存储对象和数组的技巧 &#x1f3a8;1. 存…...

从golang的sync.pool到linux的slab分配器

最近学习golang的时候&#xff0c;看到golang并发编程中有一个sync.pool&#xff0c;即对象池&#xff0c;猛地一看这不跟linux的slab分配器类似嘛&#xff0c;赶紧学习记录下 这里先总结下设计sync.pool和slab的目的 sync.pool 为了缓解特定类型的对象频繁创建和销毁&#x…...

Python分形几何可视化—— 复数迭代、L系统与生物分形模拟

Python分形几何可视化—— 复数迭代、L系统与生物分形模拟 本节将深入探索分形几何的奇妙世界&#xff0c;实现Mandelbrot集生成器和L系统分形树工具&#xff0c;并通过肺部血管分形案例展示分形在医学领域的应用。我们将使用Python的NumPy进行高效计算&#xff0c;结合Matplo…...

【超详细】英伟达Jetson Orin NX-YOLOv8配置与TensorRT测试

文章主要内容如下&#xff1a; 1、基础运行环境配置 2、Torch-GPU安装 3、ultralytics环境配置 4、Onnx及TensorRT导出详解 5、YOLOv8推理耗时分析 基础库版本&#xff1a;jetpack5.1.3, torch-gpu2.1.0, torchvision0.16.0, ultralytics8.3.146 设备的软件开发包基础信息 需…...

Go语言学习-->项目中引用第三方库方式

Go语言学习–&#xff1e;项目中引用第三方库方式 1 执行 go mod tidy 分析引入的依赖有没有正常放在go.mod里面 找到依赖的包会自动下载到本地 并添加在go.mod里面 执行结果&#xff1a; 2 执行go get XXXX&#xff08;库的名字&#xff09;...

Vue Fragment vs React Fragment

文章目录 前言&#x1f9e9; 一、概念对比&#xff1a;Vue Fragment vs React Fragment&#x1f4e6; 二、使用示例对比✅ Vue 3 中使用 Fragment✅ React 中使用 Fragment &#x1f50d; 三、差异解析1. **使用方式**2. **传递属性&#xff08;如 key&#xff09;**3. **插槽系…...

【LRU】 (最近最少使用)

LRU (最近最少使用) 文章目录 LRU (最近最少使用)一、LRU是什么&#xff1f;二、实现1.常规算法2.双栈更替总结 一、LRU是什么&#xff1f; LRU&#xff08;Least Recently Used&#xff09;是一种常见的缓存淘汰策略&#xff0c;核心思想是 “淘汰最长时间未被使用的缓存数据…...

每日Prompt:云朵猫

提示词 仰视&#xff0c;城镇的天空&#xff0c;一片形似猫咪的云朵&#xff0c;用黑色的简笔画&#xff0c;勾勒出猫咪的形状&#xff0c;可爱&#xff0c;俏皮&#xff0c;极简...

AI浪潮下的IT行业:威胁、转变与共生之道

目录 前言1 AI在IT行业的具体应用场景1.1 软件开发中的AI助手1.2 运维与监控的智能化1.3 测试自动化与质量保障1.4 安全防护中的智能威胁识别 2 AI对IT从业者的实际影响2.1 工作内容的结构性变化2.2 技能结构的再平衡 3 IT从业者不可替代的能力与价值3.1 复杂系统的架构与抽象能…...