📌 专栏:SpringBoot高阶实战
🎯 适用场景:AI连续对话、智能客服多轮答疑、私人AI助手、流式对话留存系统
🛠 技术栈:SpringBoot2.7 + WebSocket + Redis上下文缓存 + AI流式SSE + 断线自动重连
💡 阅读收益:解决单轮对话无记忆、用户对话串频、断线会话丢失、多用户上下文混淆等生产痛点,掌握企业级多轮AI流式对话完整方案
一、前言:单轮AI流式问答的业务短板
上一篇我们实现了SpringBoot+WebSocket真实AI流式逐字问答,解决了伪流式、接口超时、线程阻塞等核心问题,能够完美实现单轮AI打字机效果。
但落地到真实AI聊天、智能客服、连续问答场景时,基础流式版本存在三大致命业务缺陷,完全无法商用:
-
无上下文记忆能力:每轮提问独立生效,AI无法理解上文对话,不能实现连续追问
-
断线对话清零:网络波动、页面刷新后,所有对话记录丢失,需要重新发起问答
-
多用户上下文混淆:全局会话无隔离,高并发下不同用户的对话内容、上下文数据互相串扰
针对以上问题,本文进行全面升级,从零实现带上下文记忆的多轮AI流式对话系统,新增Redis会话缓存、多用户隔离、断线重连、对话清空等商用核心能力,完全对标主流AI对话产品。
二、多轮对话核心设计思路
2.1 核心升级点(区别单轮流式版本)
-
对话上下文持久化:基于Redis缓存单用户多轮对话记录,自定义上下文长度,避免token超限
-
用户会话完全隔离:以唯一会话ID区分用户,每个用户独享独立对话上下文,杜绝数据串扰
-
断线重连续聊:页面刷新、网络重连后,自动加载历史对话,无需重新对话
-
手动清空对话:支持前端主动清空上下文,重置对话状态,灵活适配业务场景
2.2 完整业务交互流程
- 前端生成唯一用户会话ID,建立WebSocket长连接,绑定会话标识; 2. 用户发起提问,后端从Redis读取该用户历史对话,拼接上下文参数; 3. 携带完整上下文调用AI流式接口,实时接收Token并逐字推送前端; 4. 每轮问答结束后,自动更新Redis上下文缓存,留存对话记录; 5. 网络断开自动重连,重连后加载历史对话,支持持续多轮追问; 6. 支持主动清空上下文,重置对话状态,释放缓存资源。
三、项目依赖与配置
3.1 Maven核心依赖
在原有流式问答依赖基础上,新增Redis依赖,用于上下文会话缓存:
<!-- WebSocket核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Redis缓存 存储对话上下文 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 异步HTTP SSE流式调用 --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.11.0</version> </dependency> <!-- JSON解析 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson2</artifactId> <version>2.0.48</version> </dependency>
3.2 Redis基础配置
配置Redis连接参数,设置上下文缓存过期时间,自动清理无效会话数据,application.yml配置如下:
spring: # Redis配置 redis: host: localhost port: 6379 password: database: 0 # 连接超时时间 timeout: 10000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 2 # 自定义AI对话配置 ai: # 上下文缓存过期时间 30分钟 context-expire: 1800 # 最大保存10轮对话,防止token溢出 max-turn: 10
四、核心工具类封装
4.1 对话上下文Redis工具类
封装上下文新增、获取、清空、截断方法,自动控制对话轮数,防止大模型Token超限:
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.TypeReference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * AI多轮对话上下文缓存工具类 * 实现对话留存、自动截断、过期清理 */ @Component public class AiContextRedisUtil { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${ai.context-expire}") private Integer contextExpire; @Value("${ai.max-turn}") private Integer maxTurn; // 对话上下文缓存key前缀 private static final String AI_CONTEXT_KEY = "ai:chat:context:"; /** * 获取用户完整对话上下文 */ public List<ChatMessage> getUserContext(String sessionId) { String key = AI_CONTEXT_KEY + sessionId; String json = stringRedisTemplate.opsForValue().get(key); if (json == null || json.isEmpty()) { return new ArrayList<>(); } return JSON.parseObject(json, new TypeReference<List<ChatMessage>>() {}); } /** * 追加单轮对话到上下文 */ public void appendContext(String sessionId, String userMsg, String aiMsg) { String key = AI_CONTEXT_KEY + sessionId; List<ChatMessage> contextList = getUserContext(sessionId); // 新增用户提问、AI应答 contextList.add(new ChatMessage("user", userMsg)); contextList.add(new ChatMessage("assistant", aiMsg)); // 截断超出最大轮数的对话,防止token溢出 if (contextList.size() > maxTurn * 2) { contextList = contextList.subList(contextList.size() - maxTurn * 2, contextList.size()); } // 更新缓存并设置过期时间 stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(contextList), contextExpire, TimeUnit.SECONDS); } /** * 清空用户对话上下文 */ public void clearContext(String sessionId) { String key = AI_CONTEXT_KEY + sessionId; stringRedisTemplate.delete(key); } /** * 对话消息实体 */ public static class ChatMessage { private String role; private String content; public ChatMessage(String role, String content) { this.role = role; this.content = content; } // getter/setter public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } }
4.2 AI流式请求工具类(适配多轮上下文)
改造原有SSE流式工具类,支持传入上下文对话记录,实现连续问答:
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import okhttp3.*; import okhttp3.sse.EventSource; import okhttp3.sse.EventSources; import org.springframework.stereotype.Component; import javax.websocket.Session; import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; @Component public class AiStreamChatUtil { private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); // 替换为你的AI流式接口地址 private static final String AI_STREAM_API = "https://xxx.xxx.com/chat/stream"; /** * 带上下文的多轮流式问答 */ public void chatStream(String question, List<AiContextRedisUtil.ChatMessage> contextList, Session session) { JSONObject requestJson = new JSONObject(); requestJson.put("prompt", question); requestJson.put("stream", true); requestJson.put("temperature", 0.7); // 传入历史上下文,实现多轮对话 requestJson.put("history", contextList); RequestBody body = RequestBody.create(MediaType.get("application/json; charset=utf-8"), requestJson.toString()); Request request = new Request.Builder().url(AI_STREAM_API).post(body).build(); EventSource.Factory factory = EventSources.createFactory(HTTP_CLIENT); factory.newEventSource(request, new EventSourceListener() { @Override public void onEvent(EventSource eventSource, String id, String type, String data) { try { if ("[DONE]".equals(data)) { session.getBasicRemote().sendText("【STREAM_END】"); eventSource.cancel(); return; } JSONObject json = JSON.parseObject(data); String content = json.getString("content"); if (content != null && content.length() > 0) { session.getBasicRemote().sendText(content); } } catch (Exception e) { e.printStackTrace(); } } @Override public void onFailure(EventSource eventSource, Throwable t, Response response) { try { session.getBasicRemote().sendText("❌ AI问答异常,请重试!"); session.getBasicRemote().sendText("【STREAM_END】"); } catch (IOException e) { e.printStackTrace(); } eventSource.cancel(); } }); } }
五、多轮对话WebSocket核心服务
核心业务类,整合会话隔离、上下文读取、流式问答、对话缓存、清空重置能力,完全适配生产多用户并发场景:
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Component @ServerEndpoint("/ai/multi/chat/{sessionId}") public class AiMultiChatWebSocket { // 在线会话缓存 private static final ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>(); private static AiStreamChatUtil aiStreamChatUtil; private static AiContextRedisUtil contextRedisUtil; @Autowired public void setAiStreamChatUtil(AiStreamChatUtil aiStreamChatUtil) { AiMultiChatWebSocket.aiStreamChatUtil = aiStreamChatUtil; } @Autowired public void setContextRedisUtil(AiContextRedisUtil contextRedisUtil) { AiMultiChatWebSocket.contextRedisUtil = contextRedisUtil; } /** * 建立连接,绑定用户会话ID */ @OnOpen public void onOpen(@PathParam("sessionId") String sessionId, Session session) { SESSION_MAP.put(sessionId, session); log.info("【多轮AI对话】会话{}连接成功,在线数:{}", sessionId, SESSION_MAP.size()); } /** * 接收用户消息,区分清空对话/正常提问 */ @OnMessage public void onMessage(String msg, @PathParam("sessionId") String sessionId) { // 清空上下文指令 if ("clear_context".equals(msg)) { contextRedisUtil.clearContext(sessionId); log.info("【多轮AI对话】会话{}清空上下文成功", sessionId); return; } log.info("【多轮AI对话】会话{}提问:{}", sessionId, msg); // 异步处理多轮流式问答 handleMultiChat(sessionId, msg, SESSION_MAP.get(sessionId)); } /** * 异步多轮对话处理 */ @Async public void handleMultiChat(String sessionId, String question, Session session) { // 1. 获取历史上下文 var contextList = contextRedisUtil.getUserContext(sessionId); // 2. 执行流式问答 aiStreamChatUtil.chatStream(question, contextList, session); // 3. 问答结束后缓存对话记录(简化处理,生产可在流结束后回调存储) contextRedisUtil.appendContext(sessionId, question, ""); } /** * 连接关闭 */ @OnClose public void onClose(@PathParam("sessionId") String sessionId) { SESSION_MAP.remove(sessionId); log.info("【多轮AI对话】会话{}断开连接", sessionId); } /** * 连接异常 */ @OnError public void onError(Session session, Throwable error) { log.error("【多轮AI对话】连接异常:{}", error.getMessage()); } }
六、前端页面(多轮对话+断线重连+清空上下文)
前端新增唯一会话ID生成、历史对话渲染、主动清空对话、3秒自动断线重连功能,完整复刻商用AI对话交互:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>多轮上下文AI流式问答系统</title> <style> * {margin: 0;padding: 0;box-sizing: border-box;font-family: "微软雅黑";} .container {width: 900px;margin: 40px auto;} .title {text-align: center;color: #00b96b;margin-bottom: 20px;} .chat-box {height: 550px;border: 1px solid #eee;border-radius: 10px;padding: 20px;overflow-y: auto;background: #fbfbfb;margin-bottom: 20px;} .tool-bar {margin-bottom: 15px;text-align: right;} .clear-btn {padding: 6px 15px;background: #f53f3f;color: #fff;border: none;border-radius: 4px;cursor: pointer;} .input-box {display: flex;gap: 10px;} #input {flex: 1;padding: 12px;border: 1px solid #ddd;border-radius: 6px;} #send {padding: 12px 30px;background: #00b96b;color: #fff;border: none;border-radius: 6px;cursor: pointer;} .user-msg {text-align: right;color: #1989fa;margin: 15px 0;line-height: 1.8;} .ai-msg {text-align: left;color: #333;margin: 15px 0;line-height: 1.8;white-space: pre-wrap;} </style> </