一、引言:告别等待!AI 实时对话的流式解决方案
1.1 传统 AI 对话的痛点与技术瓶颈
在传统的 AI 对话系统中,我们通常采用的是 "请求 - 等待 - 完整响应" 的模式。当用户发送一个问题后,客户端会向服务器发起请求,服务器接收请求后,将其转发给 AI 模型进行处理。AI 模型经过复杂的计算和推理,生成完整的回复内容后,再将其返回给服务器,最后由服务器传递给客户端。
这种模式在面对简单问题时,响应速度尚可接受。但一旦涉及到长文本的生成或复杂的语义理解,问题就会凸显出来。用户需要等待 AI 模型生成全部内容后才能获取回复,这期间可能会经历数秒甚至数十秒的等待时间。例如,当用户询问 "请详细介绍一下人工智能从诞生到现在的发展历程,并分析其未来的发展趋势" 这样的问题时,模型需要对大量的知识进行检索、整合和生成,整个过程耗时较长。对于用户来说,长时间的等待会极大地影响交互体验,使其感觉与 AI 的对话不够流畅和自然,仿佛在与一个反应迟钝的伙伴交流。
此外,这种模式还存在延迟高的问题。在网络传输过程中,完整的响应数据量越大,传输所需的时间就越长。而且,服务器在处理请求时,可能会因为资源紧张或负载过高而导致处理速度变慢,进一步增加了响应延迟。在一些对实时性要求较高的场景,如在线客服、智能聊天机器人等,这种延迟是难以接受的,可能会导致用户流失或业务效率下降。
1.2 本文核心内容与技术栈说明
为了解决传统 AI 对话的痛点,本文将引入基于 Server-Sent Events(SSE)的流式解决方案。SSE 是一种基于 HTTP 协议的服务器向客户端推送数据的技术,它允许服务器在无需客户端明确请求的情况下,将实时更新的数据发送到客户端。通过 SSE,AI 模型生成的回复内容可以逐字逐句地实时推送给用户,就像打字机一样,每生成一个字符或一个片段,就立即发送给客户端展示,让用户能够第一时间看到回复的进展,大大提升了交互的流畅度和实时性。
在技术实现上,本文将结合 Java SpringBoot 框架进行开发。SpringBoot 是一个基于 Spring 框架的快速开发框架,它提供了丰富的功能和便捷的配置,能够帮助我们快速搭建稳定、高效的后端服务。我们将利用 SpringBoot 的强大功能,实现 SSE 的集成、AI 接口的调用以及连接管理、消息推送等核心功能。同时,我们还会涉及到前端的开发,使用 JavaScript 的 EventSource 对象来接收 SSE 推送的消息,并在页面上进行实时渲染,为用户呈现出流畅的 AI 实时对话界面。
本文还会对比 SSE 与 WebSocket 这两种实时通信技术的适用场景,帮助读者更好地理解和选择合适的技术方案。在代码实战部分,将逐步展示如何搭建一个完整的 AI 实时对话系统,从后端的配置和实现,到前端的交互设计,让读者能够通过实际操作掌握基于 SSE 和 SpringBoot 的 AI 实时对话系统的开发技巧。
二、原理篇:SSE 为何是 AI 流式对话的最优解
2.1 SSE 核心原理:服务器单向推送的长连接技术
SSE,即 Server-Sent Events,是一种基于 HTTP 协议的服务器向客户端单向推送数据的技术 。在传统的 HTTP 通信中,通常是客户端发起请求,服务器响应请求后关闭连接。而 SSE 打破了这种常规模式,它允许客户端通过一次 HTTP 请求与服务器建立起一个长连接。在这个长连接保持期间,服务器可以主动地、持续地向客户端推送数据,形成一个事件流。
具体来说,当客户端想要接收服务器推送的数据时,会创建一个EventSource对象,并向服务器发起一个 HTTP GET 请求,这个请求的Accept头会设置为text/event-stream,以此告知服务器客户端期望接收的是 SSE 数据。服务器在接收到这个请求后,会设置特定的响应头,其中关键的是Content-Type: text/event-stream,这表明服务器返回的数据是符合 SSE 规范的事件流数据;同时设置Cache-Control: no-cache,防止客户端缓存数据,确保每次都能获取到最新的推送;Connection: keep-alive则维持 HTTP 连接的持久性 。
服务器向客户端推送的数据遵循特定的格式,每一条消息都以data:开头,后面跟着实际的数据内容,并且消息结束时用两个换行符\n\n分隔。例如:
Plain
data: 这是第一条推送消息\n\n
data: 这是第二条推送消息\n\n
SSE 还支持一些可选的字段,如id用于标记事件 ID,方便客户端在重连时确定从哪个事件继续接收;event可以指定事件类型,客户端可以根据不同的事件类型执行不同的操作;retry则用于设置重连时间,当连接意外断开时,客户端会按照指定的时间尝试重新连接 。
在 SpringBoot 中,实现 SSE 主要依赖于SseEmitter组件。SseEmitter负责管理 SSE 连接的生命周期,包括发送数据、处理连接关闭、超时等情况。通过它,我们可以很方便地在 SpringBoot 应用中创建 SSE 端点,向客户端推送实时数据。例如,下面是一个简单的 SpringBoot 中使用SseEmitter的示例:
java
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@RestController
public class SseController {
private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter sse() {
SseEmitter emitter = new SseEmitter();
executor.scheduleAtFixedRate(() -> {
try {
String data = "这是实时推送的数据: " + System.currentTimeMillis();
emitter.send(ServerSentEvent.builder(data).build());
} catch (IOException e) {
emitter.completeWithError(e);
}
}, 0, 5, TimeUnit.SECONDS);
emitter.onTimeout(() -> emitter.complete());
emitter.onError((e) -> emitter.completeWithError(e));
return emitter;
}
}
在上述代码中,/sse端点返回一个SseEmitter实例,通过scheduleAtFixedRate方法定时向客户端发送数据。onTimeout和onError方法分别处理连接超时时和发生错误时的情况。
2.2 SSE vs WebSocket:技术选型的关键考量
在实时通信领域,除了 SSE,WebSocket 也是一种常用的技术。WebSocket 是一种基于 TCP 的全双工通信协议,它允许客户端和服务器在同一个连接上进行双向数据传输 。与 SSE 相比,两者在很多方面存在差异,这些差异也决定了它们在不同场景下的适用性。
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 全双工双向(服务器⇔客户端) |
| 协议 | 基于 HTTP 协议 | 独立的 WebSocket 协议 |
| 实现复杂度 | 较低,基于 HTTP,无需复杂的协议升级 | 较高,需要专门的握手过程来升级协议 |
| 兼容性 | 现代浏览器基本都支持,兼容性较好 | 广泛支持,但在一些旧版本浏览器中可能存在兼容性问题 |
| 重连机制 | 浏览器内置自动重连机制 | 需要手动实现心跳检测和重连逻辑 |
| 适用场景 | 适用于服务器单向推送数据的场景,如新闻推送、实时监控、AI 对话等 | 适用于需要双向实时通信的场景,如在线聊天、实时协作、游戏等 |
| 在 AI 对话场景中,主要是服务器将 AI 生成的回复推送给客户端,客户端很少需要向服务器反向传输数据。因此,SSE 的单向推送特性完全能够满足需求,而且其轻量级、兼容性好、自带重连机制等优势,使得开发和维护成本更低。如果在 AI 对话系统中使用 WebSocket,就如同 "杀鸡用牛刀",不仅增加了不必要的复杂性,还可能导致资源浪费 。 |
例如,在一个简单的 AI 聊天机器人项目中,使用 SSE 实现实时对话,客户端只需要通过EventSource对象监听服务器推送的消息,无需关心复杂的连接管理和双向通信逻辑,代码简洁明了:
javascript
const eventSource = new EventSource('/ai/chat/stream');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
// 将接收到的数据显示在聊天界面
document.getElementById('chat-content').innerHTML += data.message + '<br>';
};
eventSource.onerror = (err) => {
console.error('SSE 错误:', err);
};
而如果使用 WebSocket,除了要处理消息接收,还需要实现心跳检测、重连等功能,代码量会显著增加,复杂度也会提升。
2.3 AI 流式对话与 SSE 的契合点
AI 大模型在生成文本时,通常采用流式输出的方式。所谓流式输出,就是模型不是一次性生成完整的回复内容,而是逐词、逐句,或者说逐 token 地生成文本。例如,当用户询问 "请介绍一下中国的四大发明" 时,AI 模型可能先生成 "中国的四大发明包括",然后接着生成 "造纸术",再生成 "印刷术" 等等,随着时间的推移逐步输出完整的回复 。
SSE 的持续推送能力与 AI 的这种流式输出特性高度契合。当服务器调用 AI 的流式 API 时,每获取到 AI 模型生成的一个文本片段,就可以立即通过 SSE 将这个片段推送给前端客户端。在客户端,这些片段会按照顺序依次显示,就像打字机打字一样,一个字一个字地呈现给用户,形成 "边生成边显示" 的效果。
从技术层面来看,这种契合大大缩短了用户感知的延迟。在传统的完整响应模式下,用户需要等待 AI 模型生成全部回复后才能看到内容,而在 SSE 与 AI 流式输出结合的模式下,用户可以在模型生成的过程中就开始阅读部分回复,感觉 AI 的响应速度更快,交互更加流畅自然。同时,这种实时的反馈也能让用户更好地理解 AI 的思考过程,增强用户与 AI 之间的互动感,提升用户体验。
三、实战篇:SpringBoot 整合 SSE 构建 AI 对话系统
3.1 环境准备:SpringBoot 项目搭建与依赖配置
首先,我们来搭建 SpringBoot 项目。可以使用 IDEA 的 Spring Initializr 快速创建项目,这是一种高效便捷的方式。在创建项目时,确保选择 Maven 构建工具,它能够帮助我们管理项目的依赖和构建过程。
在依赖配置方面,我们主要引入spring-boot-starter-web依赖,它是 SpringBoot 用于 Web 开发的核心依赖,包含了 SpringMVC 等 Web 开发所需的组件。值得注意的是,SpringBoot 原生支持 SSE,所以我们无需额外引入 SSE 相关的包,这大大简化了依赖管理。
在application.properties配置文件中,我们还需要配置异步请求的超时时间,以确保 SSE 的长连接不会被提前断开。例如,可以设置如下配置:
properties
spring.mvc.async.request-timeout=60000
上述配置将异步请求的超时时间设置为 60 秒,在实际应用中,你可以根据需求调整这个值。同时,为了确保项目的兼容性和稳定性,建议使用 JDK 1.8 及以上版本,并使用 IDEA 作为开发工具,它提供了丰富的功能和便捷的操作,能够大大提高开发效率。
3.2 核心组件 1:SSE 连接管理器(SSEServer)
3.2.1 连接管理的核心需求
为了实现对 SSE 连接的有效管理,我们需要设计一个SSEServer工具类。这个类需要具备以下核心功能:
-
用户连接注册 :当用户发起 SSE 连接请求时,将用户 ID 与对应的
SseEmitter实例进行关联并存储,以便后续进行消息推送。 -
定向消息推送:根据用户 ID,能够准确地将消息推送给对应的用户连接。
-
异常连接关闭:当连接出现异常、超时或完成时,能够及时关闭连接并清理相关资源,避免资源浪费和内存泄漏。
为了实现这些功能,我们采用ConcurrentHashMap来存储用户 ID 与SseEmitter的映射关系。ConcurrentHashMap是线程安全的,能够在多线程环境下保证数据的一致性和安全性。同时,我们还需要注册连接超时、完成、异常的回调函数,以便在这些事件发生时能够进行相应的处理。例如,当连接超时时,我们可以主动关闭连接并移除对应的映射关系;当连接完成时,同样移除映射关系;当连接发生异常时,记录异常信息并关闭连接。
3.2.2 关键方法实现(connect/sendMsg/close)
下面是SSEServer工具类中关键方法的实现:
java
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSEServer {
// 存放所有用户的SseEmitter连接
private static final Map<String, SseEmitter> sseClients = new ConcurrentHashMap<>();
// 建立连接
public static SseEmitter connect(String userId) {
// 设置超时时间为0,即不超时,默认是30秒,超时未完成任务则会抛出异常
SseEmitter sseEmitter = new SseEmitter(0L);
// 注册连接完成、超时、异常时的回调函数
sseEmitter.onTimeout(timeoutCallback(userId));
sseEmitter.onCompletion(completionCallback(userId));
sseEmitter.onError(errorCallback(userId));
sseClients.put(userId, sseEmitter);
System.out.println("SSE connect, userId: " + userId);
return sseEmitter;
}
// 发送消息
public static void sendMsg(String userId, String message, SSEMsgType msgType) {
if (CollectionUtils.isEmpty(sseClients)) {
return;
}
if (sseClients.containsKey(userId)) {
SseEmitter sseEmitter = sseClients.get(userId);
sendEmitterMessage(sseEmitter, userId, message, msgType);
}
}
private static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType) {
// 指定事件名称(name),前端根据这个名称监听
SseEmitter.SseEventBuilder msgEvent = SseEmitter.event().id(userId).data(message).name(msgType.type);
try {
sseEmitter.send(msgEvent);
} catch (IOException e) {
System.out.println("SSE send message error, userId: " + userId + ", error: " + e.getMessage());
close(userId); // 发送异常时,移除该连接
}
}
// 关闭连接
public static void close(String userId) {
SseEmitter emitter = sseClients.get(userId);
if (emitter != null) {
emitter.complete(); // 这会触发 onCompletion 回调,回调中已经包含了 remove 操作
}
}
// 连接超时的回调函数
private static Runnable timeoutCallback(String userId) {
return () -> {
System.out.println("SSE connection timeout, userId: " + userId);
close(userId);
};
}
// 连接完成的回调函数
private static Runnable completionCallback(String userId) {
return () -> {
System.out.println("SSE connection completed, userId: " + userId);
sseClients.remove(userId);
};
}
// 连接错误的回调函数
private static Consumer<Throwable> errorCallback(String userId) {
return throwable -> {
System.out.println("SSE connection error, userId: " + userId + ", error: " + throwable.getMessage());
close(userId);
};
}
}
在connect方法中,我们创建一个SseEmitter实例,并设置其超时时间为 0,即永不超时。然后注册超时、完成和异常的回调函数,并将SseEmitter存入sseClients中。
sendMsg方法用于向指定用户发送消息。首先检查sseClients是否为空以及是否包含指定用户的连接,如果存在则调用sendEmitterMessage方法发送消息。在sendEmitterMessage方法中,构建包含事件名称、数据和 ID 的 SSE 消息,并通过sseEmitter.send方法发送。如果发送过程中出现异常,记录错误信息并调用close方法关闭连接。
close方法用于主动关闭连接,并从sseClients中移除对应的用户连接。
3.3 核心组件 2:消息类型枚举(SSEMsgType)
为了区分不同类型的消息推送,我们定义一个SSEMsgType枚举类:
java
public enum SSEMsgType {
MESSAGE("message", "单次发送的普通信息"),
ADD("add", "消息追加,适用于流式stream推送"),
FINISH("finish", "消息发送完成");
public final String type;
public final String value;
SSEMsgType(String type, String value) {
this.type = type;
this.value = value;
}
}
其中,MESSAGE表示普通消息,通常用于一次性发送完整的消息内容;ADD表示流式追加消息,适用于 AI 模型逐字逐句生成回复内容时的推送,前端可以根据这个类型将接收到的消息片段逐行追加显示;FINISH表示消息发送完成信号,当 AI 模型生成完成后,服务器发送这个类型的消息告知前端,前端可以根据这个信号进行一些收尾操作,比如隐藏加载动画等。前端可以通过监听不同的事件名称(type)来区分不同类型的消息,并进行相应的处理,实现差异化渲染,提升用户体验。
3.4 核心组件 3:SSE 控制器(SSEController)
接下来,我们创建 SSE 控制器SSEController,用于处理前端的 SSE 连接请求:
java
import com.example.demo.enums.SSEMsgType;
import com.example.demo.utils.SSEServer;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/sse")
public class SSEController {
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId) {
return SSEServer.connect(userId);
}
}
在上述代码中,@GetMapping注解映射/sse/connect接口,通过produces = MediaType.TEXT_EVENT_STREAM_VALUE指定返回的数据类型为 SSE 事件流。方法接收前端传入的用户 ID,调用SSEServer.connect方法创建并返回SseEmitter对象。SpringBoot 会自动识别返回类型为SseEmitter,从而维持 HTTP 长连接,为后续的消息推送提供通道。前端通过访问这个接口,即可建立与服务器的 SSE 连接,等待接收服务器推送的消息。
3.5 业务层整合:AI 大模型流式 API 调用
3.5.1 AI 流式接口对接(以 DeepSeek 为例)
以 DeepSeek 大模型为例,我们来实现 AI 流式接口的对接。首先,我们需要引入 HTTP 请求工具,这里可以使用RestTemplate或者OkHttp。RestTemplate是 Spring 提供的用于访问 RESTful 服务的客户端工具,使用简单方便;OkHttp则是一个高效的 HTTP 客户端库,具有良好的性能和扩展性。
假设 DeepSeek 提供的流式接口地址为https://api.deepseek.com/stream,请求参数包括prompt(用户输入的问题)、stream(设置为true开启流式输出)等。使用RestTemplate调用接口的示例代码如下:
java
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class DeepSeekClient {
private static final String API_URL = "https://api.deepseek.com/stream";
private final RestTemplate restTemplate;
public DeepSeekClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public void streamChat(String prompt, String userId) {
restTemplate.execute(API_URL, HttpMethod.POST, request -> {
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 构建请求参数
String requestBody = "{\"prompt\":\"" + prompt + "\",\"stream\":true}";
request.getBody().write(requestBody.getBytes());
return request;
}, new ResponseExtractor<Void>() {
@Override
public Void extractData(ClientHttpResponse response) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getBody()));
String line;
while ((line = reader.readLine()) != null) {
// 处理每一行响应数据,这里假设每行是一个完整的文本片段
SSEServer.sendMsg(userId, line, SSEMsgType.ADD);
}
return null;
}
});
}
}
在上述代码中,streamChat方法接收用户输入的问题prompt和用户 IDuserId。通过restTemplate.execute方法发送 POST 请求,设置请求头的内容类型为application/json,并构建请求体。在响应提取器中,使用BufferedReader逐行读取响应流中的文本片段,每读取到一个片段,就调用SSEServer.sendMsg方法,以SSEMsgType.ADD类型推送给指定用户,实现实时的流式消息推送。
3.5.2 异步处理与异常兜底
为了避免 AI 接口调用阻塞主线程,影响系统的响应性能,我们采用@Async注解标记 AI 调用方法,将其放入异步线程池中执行。同时,在调用过程中,需要捕获可能出现的IOException(输入输出异常,例如网络连接中断、读取响应失败等)和TimeoutException(超时异常,当请求在规定时间内未得到响应时抛出)。
当捕获到异常时,通过SSEServer.sendMsg推送错误信息给用户,告知用户发生了错误,并主动关闭用户连接,释放资源。例如:
java
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
@Service
@EnableAsync
public class AIService {
@Autowired
private DeepSeekClient deepSeekClient;
@Async
public void callAI(String prompt, String userId) {
try {
deepSeekClient.streamChat(prompt, userId);
// AI生成完成后,推送消息发送完成信号
SSEServer.sendMsg(userId, "AI生成完成", SSEMsgType.FINISH);
} catch (IOException e) {
SSEServer.sendMsg(userId, "AI调用发生错误:" + e.getMessage(), SSEMsgType.MESSAGE);
SSEServer.close(userId);
} catch (TimeoutException e) {
SSEServer.sendMsg(userId, "AI调用超时,请稍后重试", SSEMsgType.MESSAGE);
SSEServer.close(userId);
}
}
}
在上述代码中,@Async注解将callAI方法标记为异步方法,Spring 会将其放入默认的异步线程池中执行。在方法内部,调用deepSeekClient.streamChat进行 AI 接口调用,当调用完成后,推送SSEMsgType.FINISH类型的消息告知前端 AI 生成完成。如果调用过程中发生异常,根据异常类型推送相应的错误信息,并关闭用户连接,确保系统的稳定性和资源的有效管理。同时,为了处理异步方法执行过程中的异常,我们还可以配置AsyncUncaughtExceptionHandler,在全局层面统一处理未捕获的异步异常 ,提高系统的健壮性。
四、前端实现:基于 EventSource 的实时消息渲染
4.1 EventSource 初始化与连接建立
在前端,我们主要使用 JavaScript 的EventSource对象来实现与后端 SSE 的连接和消息接收。EventSource是浏览器提供的一个用于处理服务器发送事件(SSE)的接口,它使得客户端能够方便地监听服务器推送的实时数据。
首先,我们需要创建一个EventSource实例,并传入后端 SSE 连接的接口地址。假设后端的 SSE 连接接口为/sse/connect,前端代码如下:
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI实时对话</title>
</head>
<body>
<div id="chat-container">
<!-- 聊天内容将显示在这里 -->
</div>
<form id="question-form">
<input type="text" id="question-input" placeholder="请输入问题">
<button type="submit">发送</button>
</form>
<script>
const userId = "user123";// 假设用户ID
const eventSource = new EventSource(`/sse/connect?userId=${userId}`);
eventSource.onopen = function() {
console.log('SSE连接已建立');
};
eventSource.onerror = function(error) {
console.error('SSE连接错误:', error);
// 处理连接异常,例如自动重连
setTimeout(() => {
eventSource.close();
eventSource = new EventSource(`/sse/connect?userId=${userId}`);
}, 5000);
};
</script>
</body>
</html>
在上述代码中,new EventSource('/sse/connect?userId=${userId}')创建了一个EventSource实例,它会向指定的后端接口发起 HTTP GET 请求,建立 SSE 长连接。onopen事件在连接成功建立时触发,我们可以在这个回调函数中记录连接成功的日志,以便调试和监控。onerror事件则在连接发生错误时触发,比如网络断开、服务器响应错误等情况。在onerror回调中,我们通过setTimeout实现了简单的自动重连机制,每 5 秒尝试重新建立连接,以确保用户能够持续接收服务器推送的消息。
4.2 流式文本渲染与交互优化
当EventSource接收到服务器推送的消息时,会触发onmessage事件。在onmessage的回调函数中,我们可以获取到服务器推送的文本片段,并将其追加到对话容器中,实现逐字显示的打字机效果。同时,为了提升交互体验,我们还需要处理一些细节,比如在接收到FINISH类型的消息时,停止加载动画并更新对话状态。
首先,我们来完善onmessage事件的处理:
javascript
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
const chatContainer = document.getElementById('chat-container');
const messageElement = document.createElement('p');
if (data.type === 'ADD') {
// 模拟打字机效果,逐字显示
const text = data.message;
let index = 0;
const interval = setInterval(() => {
messageElement.textContent += text[index];
index++;
if (index >= text.length) {
clearInterval(interval);
}
}, 50);
chatContainer.appendChild(messageElement);
} else if (data.type === 'FINISH') {
// 停止加载动画(假设之前有显示加载动画)
const loadingElement = document.getElementById('loading');
if (loadingElement) {
loadingElement.style.display = 'none';
}
// 可以在这里更新对话状态,比如显示对话结束提示
const endMessage = document.createElement('p');
endMessage.textContent = 'AI回复已完成';
chatContainer.appendChild(endMessage);
}
};
在上述代码中,当接收到ADD类型的消息时,通过setInterval实现逐字显示效果,每 50 毫秒显示一个字符,模拟打字机打字的过程。当接收到FINISH类型的消息时,首先隐藏加载动画元素(假设页面中有一个id为loading的元素用于显示加载动画),然后添加一条对话结束提示消息到聊天容器中。
为了区分用户提问与 AI 回复的气泡样式,我们可以通过 CSS 来实现。例如:
css
#chat-container p {
margin: 5px;
padding: 5px 10px;
border-radius: 10px;
max-width: 80%;
}
#chat-container p.user-question {
background-color: #e1f5fe;
align-self: flex-start;
}
#chat-container p.ai-answer {
background-color: #dcedc8;
align-self: flex-end;
}
在 HTML 中,当添加用户提问和 AI 回复的消息时,为相应的p元素添加对应的类名:
javascript
// 添加用户提问
const questionForm = document.getElementById('question-form');
questionForm.addEventListener('submit', function(event) {
event.preventDefault();
const questionInput = document.getElementById('question-input');
const question = questionInput.value;
const userQuestionElement = document.createElement('p');
userQuestionElement.textContent = `用户: ${question}`;
userQuestionElement.classList.add('user-question');
const chatContainer = document.getElementById('chat-container');
chatContainer.appendChild(userQuestionElement);
// 发送问题到后端
// 这里假设通过其他方式(如fetch)将问题发送到后端
questionInput.value = '';
});
javascript
// 添加AI回复
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
const chatContainer = document.getElementById('chat-container');
const messageElement = document.createElement('p');
if (data.type === 'ADD') {
// 模拟打字机效果,逐字显示
const text = data.message;
let index = 0;
const interval = setInterval(() => {
messageElement.textContent += text[index];
index++;
if (index >= text.length) {
clearInterval(interval);
}
}, 50);
messageElement.textContent = `AI: `;
messageElement.classList.add('ai-answer');
chatContainer.appendChild(messageElement);
} else if (data.type === 'FINISH') {
// 停止加载动画(假设之前有显示加载动画)
const loadingElement = document.getElementById('loading');
if (loadingElement) {
loadingElement.style.display = 'none';
}
// 可以在这里更新对话状态,比如显示对话结束提示
const endMessage = document.createElement('p');
endMessage.textContent = 'AI回复已完成';
chatContainer.appendChild(endMessage);
}
};
通过上述 CSS 和 JavaScript 代码,用户提问和 AI 回复将以不同的气泡样式显示,用户提问的气泡在左侧,背景色为浅蓝色;AI 回复的气泡在右侧,背景色为浅绿色,从而提升对话界面的可视化效果和用户体验。
4.3 前端兼容性处理
虽然EventSource在现代浏览器中得到了广泛支持,但对于一些老旧浏览器(如 IE 浏览器),可能并不支持该接口。为了确保在这些浏览器中也能实现类似的功能,我们可以使用fetch-event-source库作为替代方案。fetch-event-source是一个轻量级的库,它基于fetch API 实现了对 SSE 的支持,并且支持 POST 请求和自定义请求头。
首先,安装fetch-event-source库:
bash
npm install @microsoft/fetch-event-source
然后,在前端代码中引入并使用该库:
javascript
import { fetchEventSource } from '@microsoft/fetch-event-source';
const userId = "user123";// 假设用户ID
const controller = new AbortController();
const signal = controller.signal;
fetchEventSource(`/sse/connect?userId=${userId}`, {
method: 'GET',
signal: signal,
headers: {
'Accept': 'text/event-stream'
},
onopen: async (response) => {
if (response.ok) {
console.log('SSE连接已建立');
} else {
console.error('连接失败:', response.status);
}
},
onmessage: (event) => {
const data = JSON.parse(event.data);
// 处理消息,同EventSource的onmessage处理
},
onerror: (err) => {
console.error('SSE连接错误:', err);
// 处理连接异常,例如自动重连
setTimeout(() => {
controller.abort();
fetchEventSource(`/sse/connect?userId=${userId}`, {
method: 'GET',
signal: new AbortController().signal,
headers: {
'Accept': 'text/event-stream'
},
onopen: async (response) => {
if (response.ok) {
console.log('SSE连接已重新建立');
} else {
console.error('重新连接失败:', response.status);
}
},
onmessage: (event) => {
const data = JSON.parse(event.data);
// 处理消息,同EventSource的onmessage处理
},
onerror: (err) => {
console.error('重新连接时错误:', err);
}
});
}, 5000);
}
});
在上述代码中,fetchEventSource方法用于建立 SSE 连接,通过method指定请求方法为 GET,signal用于控制请求的取消,headers设置请求头以表明期望接收 SSE 事件流数据。onopen、onmessage和onerror回调函数的功能与EventSource中的类似,分别处理连接建立、消息接收和连接错误的情况。在onerror回调中,同样实现了自动重连机制。
此外,为了提升用户体验,我们还可以添加连接状态提示。例如,在页面中添加一个状态提示元素,当连接建立时显示 "已连接",连接断开时显示 "连接已断开,正在重试...":
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI实时对话</title>
</head>
<body>
<div id="connection-status">连接中...</div>
<div id="chat-container">
<!-- 聊天内容将显示在这里 -->
</div>
<form id="question-form">
<input type="text" id="question-input" placeholder="请输入问题">
<button type="submit">发送</button>
</form>
<script>
// 引入fetch-event-source库的代码...
const connectionStatusElement = document.getElementById('connection-status');
fetchEventSource(`/sse/connect?userId=${userId}`, {
// 其他配置...
onopen: async (response) => {
if (response.ok) {
connectionStatusElement.textContent = '已连接';
console.log('SSE连接已建立');
} else {
connectionStatusElement.textContent = '连接失败';
console.error('连接失败:', response.status);
}
},
onerror: (err) => {
connectionStatusElement.textContent = '连接已断开,正在重试...';
console.error('SSE连接错误:', err);
// 自动重连代码...
}
});
</script>
</body>
</html>
通过以上兼容性处理和连接状态提示,我们的 AI 实时对话系统能够在不同浏览器环境下稳定运行,为用户提供一致的交互体验,即使在网络不稳定或浏览器不支持原生EventSource的情况下,用户也能及时了解连接状态,并尽可能保持与 AI 的正常对话。
五、测试与验证:一键运行实时对话系统
5.1 功能测试步骤
在完成上述后端和前端的开发后,我们需要对基于 SSE 的 AI 实时对话系统进行全面的功能测试,以确保其能够正常运行并满足预期的功能需求。
首先,启动 SpringBoot 项目。可以通过在 IDEA 中点击运行按钮,或者在项目根目录下执行mvn spring-boot:run命令来启动项目。启动成功后,在浏览器中访问前端页面。假设前端页面是一个简单的 HTML 文件,我们可以直接在浏览器中打开该文件,也可以将其部署到 Web 服务器上进行访问。
在前端页面中,输入用户 ID,这里我们可以随意输入一个唯一标识用户的字符串,比如 "user123"。然后,在输入框中输入问题,例如 "今天天气怎么样?",点击发送按钮,此时前端会向服务器发起对话请求。
在请求发送后,我们需要密切观察前端对话容器的变化。正常情况下,应该能够看到 AI 的回复内容逐字显示在对话容器中,就像打字机打字一样,一个字符一个字符地出现,这是因为后端通过 SSE 将 AI 模型生成的回复内容逐片段推送给前端,前端接收到消息后进行逐字渲染。在这个过程中,我们可以验证不同类型消息的推送效果。例如,当后端推送ADD类型的消息时,前端是否正确地将消息追加显示在对话容器中;当后端推送FINISH类型的消息时,前端是否能够正确地识别并停止加载动画,显示对话结束提示。
当对话结束后,检查前端是否收到FINISH信号。可以通过查看浏览器的开发者工具,在控制台中查看是否有相关的日志输出,或者在前端代码中添加调试信息来确认。同时,确认 SSE 连接是否正常关闭。在浏览器的开发者工具中,查看 Network 面板,找到 SSE 连接的请求,检查其状态码是否为正常关闭的状态码(通常为 200),并且在连接关闭后,后端是否正确地清理了相关的资源,如移除用户连接的映射关系等。
5.2 常见问题排查与解决方案
在测试过程中,可能会遇到一些问题,下面我们来分析一些常见问题及其解决方案。
连接超时问题 :如果在测试过程中出现连接超时的情况,首先检查 SSE 连接的超时时间设置。在SSEServer工具类中,我们设置了SseEmitter的超时时间为 0,即不超时。但如果在其他地方配置了相关的超时时间,可能会导致连接被提前断开。例如,在 SpringBoot 的配置文件application.properties中,spring.mvc.async.request-timeout配置了异步请求的超时时间,如果这个时间设置得过短,可能会导致 SSE 连接超时。此时,我们需要根据实际情况调整这个配置,适当增大超时时间,比如将其设置为 60 秒或更长时间,以确保 SSE 连接能够稳定保持。
properties
spring.mvc.async.request-timeout=60000
消息推送丢失问题 :当出现消息推送丢失的情况时,需要确保消息推送的线程安全。在SSEServer中,我们使用了ConcurrentHashMap来存储用户连接,这在一定程度上保证了线程安全。但在消息推送过程中,如果出现异常,可能会导致消息丢失。例如,在sendMsg方法中,如果sendEmitterMessage方法发送消息时出现IOException,我们会关闭连接并移除用户连接的映射关系,但可能会导致部分消息未成功推送。此时,我们可以在异常处理中添加更详细的日志记录,以便定位问题。同时,确保异常回调中的关闭逻辑正确执行,避免资源泄漏。
java
private static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType) {
SseEmitter.SseEventBuilder msgEvent = SseEmitter.event().id(userId).data(message).name(msgType.type);
try {
sseEmitter.send(msgEvent);
} catch (IOException e) {
System.out.println("SSE send message error, userId: " + userId + ", error: " + e.getMessage());
close(userId); // 发送异常时,移除该连接
}
}
前端未接收消息问题 :如果前端未接收到消息,首先排查前端监听的事件名称是否与后端枚举的SSEMsgType中的事件名称一致。在前端的EventSource事件处理中,通过event.type来判断消息类型,如果前后端的事件名称不一致,前端将无法正确处理消息。例如,后端发送的ADD类型消息,前端在onmessage事件中判断data.type === 'ADD'来进行处理,如果后端的事件名称拼写错误或者前端判断逻辑有误,就会导致前端无法接收和处理消息。此时,仔细检查前后端的代码,确保事件名称的一致性。
javascript
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'ADD') {
// 处理消息
} else if (data.type === 'FINISH') {
// 处理消息
}
};
通过以上的测试与验证步骤,以及常见问题的排查与解决,我们可以确保基于 SSE 的 AI 实时对话系统能够稳定、可靠地运行,为用户提供流畅的实时对话体验。
六、进阶优化:提升系统稳定性与扩展性
6.1 连接池优化与分布式适配
在高并发场景下,大量的 SSE 连接可能会对服务器资源造成较大压力。为了避免服务器资源耗尽,我们可以引入连接池机制。连接池可以限制最大连接数,当连接数达到上限时,新的连接请求会被暂时阻塞,直到有可用的连接被释放。
在 Java 中,我们可以使用HikariCP等连接池框架来实现 SSE 连接池。HikariCP是一个高性能的连接池,具有快速的连接获取和释放速度,以及低资源消耗的特点。首先,在pom.xml文件中添加HikariCP的依赖:
xml
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>
然后,在 SpringBoot 的配置文件application.properties中配置连接池参数:
properties
# 最大连接数
hikari.maximum-pool-size=100
# 最小空闲连接数
hikari.minimum-idle=10
# 连接超时时间
hikari.connection-timeout=30000
在SSEServer类中,我们可以通过HikariDataSource来管理 SseEmitter 的创建和释放:
java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSEServer {
private static final HikariDataSource dataSource;
private static final Map<String, SseEmitter> sseClients = new ConcurrentHashMap<>();
static {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(100);
config.setMinimumIdle(10);
config.setConnectionTimeout(30000);
dataSource = new HikariDataSource(config);
}
public static SseEmitter connect(String userId) {
SseEmitter sseEmitter = new SseEmitter(dataSource.getConnection());
sseEmitter.onTimeout(timeoutCallback(userId));
sseEmitter.onCompletion(completionCallback(userId));
sseEmitter.onError(errorCallback(userId));
sseClients.put(userId, sseEmitter);
return sseEmitter;
}
// 发送消息和关闭连接等方法不变
}
在分布式场景下,为了实现跨服务实例的 SSE 消息推送,我们可以结合 Redis 的发布订阅功能。当一个服务实例需要向用户推送消息时,它将消息发布到 Redis 的指定频道,其他服务实例通过订阅该频道,获取到消息后推送给对应的用户。这样,无论用户连接到哪个服务实例,都能接收到消息。
首先,在pom.xml文件中添加 Redis 的依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.properties中配置 Redis 连接信息:
properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
然后,创建一个 Redis 消息发布者:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisPublisher {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
}
在SSEServer中,修改sendMsg方法,将消息发布到 Redis:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSEServer {
// 省略其他代码
@Autowired
private RedisPublisher redisPublisher;
public static void sendMsg(String userId, String message, SSEMsgType msgType) {
// 构建消息对象
SSEMessage sseMessage = new SSEMessage(userId, message, msgType);
redisPublisher.publish("sse-channel", sseMessage);
}
}
同时,创建一个 Redis 消息监听器,在接收到消息后推送给用户:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisSubscriber implements MessageListener {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
SSEMessage sseMessage = (SSEMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
String userId = sseMessage.getUserId();
String messageContent = sseMessage.getMessage();
SSEMsgType msgType = sseMessage.getMsgType();
SSEServer.sendMsg(userId, messageContent, msgType);
}
}
最后,在 Spring 的配置类中注册 Redis 消息监听器:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
@Configuration
public class RedisConfig {
@Autowired
private RedisSubscriber redisSubscriber;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisSubscriber);
container.addMessageListener(messageListenerAdapter, new PatternTopic("sse-channel"));
return container;
}
}
通过上述优化,我们不仅提高了 SSE 连接的管理效率,还实现了分布式环境下的消息推送,增强了系统的稳定性和扩展性。
6.2 消息重试与幂等性保障
在消息推送过程中,由于网络波动、服务器负载过高等原因,可能会出现消息推送失败的情况。为了确保消息能够成功送达客户端,我们可以添加消息重试机制。当消息推送失败时,系统可以自动进行有限次数的重试,提高消息送达的成功率。
我们可以使用Spring Retry框架来实现消息重试功能。首先,在pom.xml文件中添加Spring Retry的依赖:
xml
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
然后,在SSEServer的sendMsg方法上添加重试注解@Retryable:
java
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSEServer {
// 省略其他代码
@Retryable(value = IOException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public static void sendMsg(String userId, String message, SSEMsgType msgType) {
if (CollectionUtils.isEmpty(sseClients)) {
return;
}
if (sseClients.containsKey(userId)) {
SseEmitter sseEmitter = sseClients.get(userId);
sendEmitterMessage(sseEmitter, userId, message, msgType);
}
}
private static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType) throws IOException {
SseEmitter.SseEventBuilder msgEvent = SseEmitter.event().id(userId).data(message).name(msgType.type);
sseEmitter.send(msgEvent);
}
}
在上述代码中,@Retryable注解表示当方法抛出IOException异常时,进行重试,最大重试次数为 3 次,每次重试的间隔时间为 1000 毫秒。
为了避免客户端在重连时重复接收相同的消息,我们需要实现消息的幂等性。可以通过为每条消息分配一个唯一的事件 ID(Event ID)来实现。当客户端重连时,服务器可以根据事件 ID 判断哪些消息已经被推送过,从而避免重复推送。
在SSEServer中,我们可以在构建 SSE 消息时,为其添加唯一的事件 ID:
java
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
public class SSEServer {
// 省略其他代码
public static void sendMsg(String userId, String message, SSEMsgType msgType) {
if (CollectionUtils.isEmpty(sseClients)) {
return;
}
if (sseClients.containsKey(userId)) {
SseEmitter sseEmitter = sseClients.get(userId);
String eventId = UUID.randomUUID().toString();
sendEmitterMessage(sseEmitter, userId, message, msgType, eventId);
}
}
private static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType, String eventId) throws IOException {
SseEmitter.SseEventBuilder msgEvent = SseEmitter.event().id(eventId).data(message).name(msgType.type);
sseEmitter.send(msgEvent);
}
}
在前端,当EventSource重连时,通过Last-Event-ID头将上次接收到的最后一个事件 ID 发送给服务器:
javascript
const eventSource = new EventSource('/sse/connect?userId=${userId}', {
withCredentials: true,
headers: {
'Last-Event-ID': lastEventId // lastEventId为上次接收到的最后一个事件ID
}
});
在服务器端,我们可以在SSEController中获取Last-Event-ID,并根据它判断是否需要重新推送消息:
java
import com.example.demo.enums.SSEMsgType;
import com.example.demo.utils.SSEServer;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/sse")
public class SSEController {
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId, @RequestHeader(value = "Last-Event-ID", required = false) String lastEventId) {
SseEmitter sseEmitter = SSEServer.connect(userId);
if (lastEventId != null) {
// 根据lastEventId判断是否需要重新推送消息
// 这里可以实现具体的逻辑,例如查询已推送消息记录等
}
return sseEmitter;
}
}
通过消息重试和幂等性保障机制,我们进一步提升了系统的可靠性,确保消息能够准确、稳定地推送给客户端。
6.3 多模型集成与动态切换
为了满足不同用户的需求和应用场景,我们可以设计一个 AI 模型适配层,支持多种 AI 模型的流式接口,如 OpenAI、DeepSeek、本地大模型等,并实现模型的动态切换。
首先,定义一个统一的 AI 模型接口,例如AIModel接口,该接口包含一个流式对话的方法:
java
public interface AIModel {
void streamChat(String prompt, String userId, SseEmitter emitter) throws IOException;
}
然后,针对不同的 AI 模型,实现该接口。以 OpenAI 和 DeepSeek 为例:
java
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class OpenAIModel implements AIModel {
private static final String API_URL = "https://api.openai.com/v1/chat/completions";
private static final String API_KEY = "your-openai-api-key";
@Override
public void streamChat(String prompt, String userId, SseEmitter emitter) throws IOException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(API_KEY);
// 构建请求体
String requestBody = "{\"model\":\"gpt-3.5-turbo\",\"messages\":[{\"role\":\"user\",\"content\":\"" + prompt + "\"}],\"stream\":true}";
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
// 发送请求并处理响应
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class);
// 处理流式响应,将内容通过SseEmitter发送给客户端
// 这里省略具体的处理逻辑,可参考之前的DeepSeekClient实现
}
}
java
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
public class DeepSeekModel implements AIModel {
private static final String API_URL = "https://api.deepseek.com/stream";
private static final String API_KEY = "your-deepseek-api-key";
@Override
public void streamChat(String prompt, String userId, SseEmitter emitter) throws IOException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(API_KEY);
// 构建请求体
String requestBody = "{\"prompt\":\"" + prompt + "\",\"stream\":true}";
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
// 发送请求并处理响应
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class);
// 处理流式响应,将内容通过SseEmitter发送给客户端
// 这里省略具体的处理逻辑,可参考之前的DeepSeekClient实现
}
}
接下来,创建一个模型工厂类AIModelFactory,用于根据配置文件中的模型名称创建对应的模型实例:
java
import java.util.HashMap;
import java.util.Map;
public class AIModelFactory {
private static final Map<String, AIModel> modelMap = new HashMap<>();
static {
modelMap.put("openai", new OpenAIModel());
modelMap.put("deepseek", new DeepSeekModel());
// 可以继续添加其他模型
}
public static AIModel getModel(String modelName) {
return modelMap.get(modelName);
}
}
在AIService中,修改callAI方法,根据配置文件中的模型名称动态选择模型:
java
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
@Service
@EnableAsync
public class AIService {
@Value("${ai.model.name}")
private String modelName;
@Async
public void callAI(String prompt, String userId, SseEmitter emitter) {
AIModel model = AIModelFactory.getModel(modelName);
try {
model.streamChat(prompt, userId, emitter);
// AI生成完成后,推送消息发送完成信号
SSEServer.sendMsg(userId, "AI生成完成", SSEMsgType.FINISH);
} catch (IOException e) {
SSEServer.sendMsg(userId, "AI调用发生错误:" + e.getMessage(), SSEMsgType.MESSAGE);
SSEServer.close(userId);
} catch (Exception e) {
SSEServer.sendMsg(userId, "AI调用发生未知错误", SSEMsgType.MESSAGE);
SSEServer.close(userId);
}
}
}
在application.properties配置文件中,添加模型名称的配置:
properties
ai.model.name=deepseek
通过这种方式,我们可以通过修改配置文件中的ai.model.name属性,轻松切换使用的 AI 模型,实现了多模型集成与动态切换,为用户提供了更加灵活和多样化的 AI 服务。
七、总结与展望
7.1 本文核心内容总结
本文深入探讨了如何利用 SpringBoot 与 SSE 技术构建 AI 实时流式对话系统,通过对传统 AI 对话模式痛点的剖析,引出 SSE 技术在解决实时性和交互流畅性方面的独特优势。在原理部分,详细阐述了 SSE 的核心原理,对比了 SSE 与 WebSocket 在技术选型上的差异,明确了 SSE 在 AI 流式对话场景中的适用性。
实战环节,从 SpringBoot 项目的搭建与依赖配置入手,逐步实现了 SSE 连接管理器、消息类型枚举、SSE 控制器等核心组件。通过对接 AI 大模型的流式 API,实现了异步调用和异常兜底处理,确保系统在高并发和复杂网络环境下的稳定性。前端部分基于 EventSource 实现了实时消息渲染,通过优化流式文本显示和兼容性处理,提升了用户体验。
在测试与验证阶段,对系统的功能进行了全面测试,排查并解决了常见问题,保证了系统的正常运行。进阶优化部分,通过连接池优化、消息重试、多模型集成等手段,进一步提升了系统的稳定性、可靠性和扩展性。
7.2 技术拓展方向
后续可从以下几个方面对系统进行拓展:
-
用户鉴权机制:集成 JWT(JSON Web Token)验证机制,在用户发起 SSE 连接请求时,前端将 JWT 令牌发送到后端,后端通过解析令牌来验证用户 ID 的合法性和有效性。只有验证通过的用户才能建立 SSE 连接,从而有效防止非法访问,提高系统的安全性。
-
对话历史记录存储:引入数据库(如 MySQL、MongoDB 等)来存储用户的对话历史记录。每次用户与 AI 进行对话时,将对话内容、时间戳等信息存储到数据库中。这样,用户可以随时查看历史对话,方便回顾和分析,同时也有助于系统进行数据分析和优化。
-
优化大模型调用成本:引入缓存机制,如使用 Redis 缓存常用的对话结果。当用户提出相同或相似的问题时,先从缓存中查找答案,如果命中则直接返回,避免重复调用大模型,从而降低调用成本,提高响应速度。同时,可以根据实际业务场景,合理调整大模型的参数配置,如温度、最大令牌数等,在保证回答质量的前提下,降低成本。
-
结合 WebRTC 实现语音流式对话:WebRTC(Web Real - Time Communication)是一种实时通信技术,允许浏览器之间进行实时语音和视频通信。将 WebRTC 与现有的 AI 实时对话系统相结合,可以实现语音流式对话功能。用户通过语音输入问题,前端将语音转换为文本发送到后端,后端调用 AI 模型生成回复,再将回复以语音的形式实时返回给用户,为用户提供更加便捷和自然的交互方式。
八、附录:完整代码仓库与资源链接
8.1 参考资料与学习链接
-
SSE 官方文档 :https://html.spec.whatwg.org/multipage/server-sent-events.html,官方文档详细介绍了 SSE 的规范和用法,是深入了解 SSE 技术的重要参考。
-
SpringBoot 异步请求文档 :https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.task-execution-and-scheduling,该文档阐述了 SpringBoot 中异步请求的配置和使用方法,有助于理解和优化本文中 AI 调用的异步处理机制。
-
OpenAI 官方文档 :https://platform.openai.com/docs/api-reference,如果想要对接 OpenAI 的大模型,这份文档提供了详细的 API 接口说明和使用示例。
-
DeepSeek 官方文档 :https://www.deepseek.com/docs/,对于使用 DeepSeek 大模型的开发者,官方文档是了解其流式接口和参数配置的重要资源。
-
MDN Web Docs - EventSource :https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource,介绍了 JavaScript 中
EventSource对象的详细用法,帮助前端开发者更好地实现 SSE 连接和消息处理。