文章目录
- [1. Server-Sent Events](#1. Server-Sent Events)
-
- [1.1 核心概念](#1.1 核心概念)
- [1.2 底层机制](#1.2 底层机制)
- [2. Spring MVC 异步](#2. Spring MVC 异步)
-
- [2.1 和 Spring WebFlux 的差异](#2.1 和 Spring WebFlux 的差异)
- [2.2 SEE 支持](#2.2 SEE 支持)
- [2.3 响应式客户端支持](#2.3 响应式客户端支持)
- [2.4 Flux 流式响应](#2.4 Flux 流式响应)
- [2.5 SSE 响应式标准类](#2.5 SSE 响应式标准类)
- [3. ReactAgent](#3. ReactAgent)
-
- [3.1 用户消息对象](#3.1 用户消息对象)
- [3.2 执行请求对象](#3.2 执行请求对象)
- [3.3 执行响应对象](#3.3 执行响应对象)
- [3.4 流式输出对话接口](#3.4 流式输出对话接口)
-
- [3.4.1 处理流式输出](#3.4.1 处理流式输出)
- [3.4.2 AI 聊天控制器](#3.4.2 AI 聊天控制器)
- [3.5 请求测试](#3.5 请求测试)
1. Server-Sent Events
1.1 核心概念
Server-Sent Events 简称 SSE(服务器发送事件),是一种基于 HTTP 协议 ,客户端只连一次 ,服务端持续单向推送数据到前端的实时通信技术。
核心特点:
- 单向推送 :只能服务端 → 客户端(客户端不能发消息给服务端,这是和
WebSocket最大区别) - 基于原生 HTTP :无需额外协议、无需升级连接,兼容所有
Web服务器 - 自动重连:断开后浏览器会自动重连,自带断线重连机制
- 轻量简单 :比
WebSocket代码少、资源占用低 - 文本数据 :只支持推送
UTF-8文本(JSON/纯文本),不支持二进制
适用场景:
- 系统实时通知、消息提醒
- 实时日志展示(后台日志、监控数据)
- 股票/行情数据更新
- 实时排行榜、状态同步
- 不适合:聊天、游戏(需要双向通信)
1.2 底层机制
核心流程:
- 客户端发起一个长 HTTP 请求(请求头特殊标记)
- 服务端接收后不关闭连接,保持连接常开
- 服务端有新数据时,按固定格式实时推送给客户端
- 连接断开 → 浏览器自动重连
- 客户端监听事件,接收并处理数据
SSE 靠响应头 + 固定数据格式实现,这是底层核心:
服务端必须返回的响应头:
json
Content-Type: text/event-stream; charset=utf-8 # 固定类型,告诉浏览器是 SSE
Cache-Control: no-cache # 禁止缓存
Connection: keep-alive # 保持长连接
服务端推送数据的固定格式:
- 每行以
data:开头 - 数据结尾必须用 两个换行符
\n\n表示一条消息结束 - 推送 JSON 时:
data: {"key":"val"}\n\n
SSE 对数据格式有严格要求,必须按这个格式发送,否则前端接收不到:
json
data: 推送的内容\n\n
2. Spring MVC 异步
Spring MVC 深度集成了 Servlet 异步请求处理机制,相关能力如下:
- 控制器方法可通过返回
DeferredResult、Callable、WebAsyncTask实现单个异步结果返回 - 控制器支持流式输出多条数据,包含服务器推送事件 SSE 与原生二进制数据流
- 控制器可调用响应式客户端,并返回响应式类型完成响应处理
SSE属于异步通信,Spring生态中的Spring MVC、Spring WebFlux都提供了完整支持,WebFlux是完全非阻塞的响应式Web框架,但是因为其生态兼容 、学习路线 等问题,除了某些特定场景下,用的还是不多,所以这里只介绍Spring MVC中如何实现SSE。
2.1 和 Spring WebFlux 的差异
底层架构对比:
| 对比项 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 依赖基础 | 基于Servlet API | 完全脱离 Servlet 体系 |
| 异步实现原理 | 借助容器异步机制,挂起响应、二次分发收尾 | 原生 Reactor 响应式模型,全链路内置异步 |
| 运行链路 | 依旧走过滤器+Servlet 调用链 | 独立响应式调度链路 |
IO 模型与线程模型:
| 对比项 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| IO 类型 | 阻塞IO | 非阻塞IO |
| 线程开销 | 数据写出需额外子线程,开销大 | 少量线程支撑高并发,资源利用率高 |
| 线程模型 | 同步线程池+独立异步线程池 | 事件驱动、Reactor 线程模型 |
编程能力与参数支持:
| 对比项 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 响应式返回值 | 支持 Mono、Flux、流式输出、背压 | 全量支持,原生适配 |
| 接口入参 | 不支持响应式类型入参 | 支持响应式入参、响应式模型属性 |
| 编程定位 | 同步接口外挂异步增强 | 全新完整响应式编程范式 |
开发配置与环境要求:
| 对比项 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 容器配置 | 必须开启 Servlet 异步支持 | 无需任何容器异步配置 |
| 额外配置 | 需自定义异步线程池、超时时间 | 开箱即用,默认配置即可 |
| 项目改造 | 老项目低侵入改造 | 需整体切换技术栈 |
异步核心实现方式:
| 技术场景 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 单个异步结果 | DeferredResult、Callable、WebAsyncTask | Mono |
| 普通流式推送 | ResponseBodyEmitter | Flux |
| SSE 服务端推送 | SseEmitter | Flux<ServerSentEvent> |
场景能力与适配业务:
| 对比项 | Spring MVC 异步 | Spring WebFlux |
|---|---|---|
| 连接断开感知 | 无原生回调,需手动心跳探测 | 原生监听断开信号 |
| 适合项目 | 传统SSM/SpringBoot老项目、SSE推送、简单异步接口 | 高并发网关、海量长连接、流式服务、新项目 |
| 学习成本 | 低,兼容原有MVC写法 | 高,需掌握响应式编程思想 |
2.2 SEE 支持
Spring MVC 异步支持的单个异步结果:
DeferredResultCallableWebAsyncTask
如需持续推送多条异步数据,使用以下三类流式返回值:
- 普通对象流式输出
ResponseBodyEmitter - 服务器推送事件
SSE - 原生二进制流
StreamingResponseBody
创建一个 Spring Boot 工程引入 Web 启动器:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
java
@RestController
@RequestMapping("/sse")
public class SseController {
/**
* 日志记录器
*/
private static final Logger log = LoggerFactory.getLogger(SseController.class);
/**
* 纯SSE流式推送:直接返回String,无大模型、无结构体
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public SseEmitter chat(@RequestParam("q") String question) {
// 60秒超时
SseEmitter emitter = new SseEmitter(60_000L);
// 跨线程异步推送(核心)
new Thread(() -> {
try {
// 1. 直接推送第一段字符串
emitter.send("收到你的问题:" + question);
Thread.sleep(500); // 模拟延迟,实现流式效果
// 2. 直接推送第二段字符串
emitter.send("正在处理中,请稍候...");
Thread.sleep(800);
// 3. 直接推送结果字符串
emitter.send("处理完成!这是Spring Boot SSE纯文本流式响应");
Thread.sleep(500);
// 4. 推送结束标识(纯String)
emitter.send("[DONE]");
// 5. 完成关闭连接
emitter.complete();
log.info("SSE推送完成");
} catch (InterruptedException e) {
// 线程中断处理
Thread.currentThread().interrupt();
completeExceptionally(emitter, "推送中断");
} catch (IOException e) {
// 客户端断开连接异常
log.error("客户端已断开连接", e);
completeExceptionally(emitter, "客户端断开");
}
}).start();
// 连接完成回调
emitter.onCompletion(() -> log.info("SSE连接正常关闭"));
// 超时回调
emitter.onTimeout(() -> log.info("SSE连接超时"));
// 异常回调
emitter.onError(e -> log.error("SSE连接异常", e));
return emitter;
}
/**
* 统一异常关闭方法
*/
private void completeExceptionally(SseEmitter emitter, String msg) {
try {
emitter.send("错误:" + msg);
emitter.complete();
} catch (IOException ignored) {
}
}
}
在 Controller 中使用 SseEmitter 服务器推送事件 对象返回,SseEmitter 继承自 ResponseBodyEmitter,严格遵循 W3C SSE 协议格式推送事件流:
java
@RestController
@RequestMapping("/sse")
public class SseController {
/**
* 日志记录器
*/
private static final Logger log = LoggerFactory.getLogger(SseController.class);
/**
* 纯SSE流式推送:直接返回String,无大模型、无结构体
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public SseEmitter chat(@RequestParam("q") String question) {
// 60秒超时
SseEmitter emitter = new SseEmitter(60_000L);
// 跨线程异步推送(核心)
new Thread(() -> {
try {
// 1. 直接推送第一段字符串
emitter.send("收到你的问题:" + question);
Thread.sleep(500); // 模拟延迟,实现流式效果
// 2. 直接推送第二段字符串
emitter.send("正在处理中,请稍候...");
Thread.sleep(800);
// 3. 直接推送结果字符串
emitter.send("处理完成!这是Spring Boot SSE纯文本流式响应");
Thread.sleep(500);
// 4. 推送结束标识(纯String)
emitter.send("[DONE]");
// 5. 完成关闭连接
emitter.complete();
log.info("SSE推送完成");
} catch (InterruptedException e) {
// 线程中断处理
Thread.currentThread().interrupt();
completeExceptionally(emitter, "推送中断");
} catch (IOException e) {
// 客户端断开连接异常
log.error("客户端已断开连接", e);
completeExceptionally(emitter, "客户端断开");
}
}).start();
// 连接完成回调
emitter.onCompletion(() -> log.info("SSE连接正常关闭"));
// 超时回调
emitter.onTimeout(() -> log.info("SSE连接超时"));
// 异常回调
emitter.onError(e -> log.error("SSE连接异常", e));
return emitter;
}
/**
* 统一异常关闭方法
*/
private void completeExceptionally(SseEmitter emitter, String msg) {
try {
emitter.send("错误:" + msg);
emitter.complete();
} catch (IOException ignored) {
}
}
}
注意:IE 浏览器不原生支持
SSE,兼容场景建议使用Spring WebSocket+SockJS降级方案。
2.3 响应式客户端支持
Spring MVC 虽然是同步阻塞框架,但它兼容响应式类型 ,会自动适配这些响应式类型,把它们转成异步/流式输出:
- 可以在
Spring MVC里调用 WebClient(WebFlux 客户端) - 可以调用 Spring Data 响应式仓库(Redis/Mongo 响应式)
- 控制器方法可以直接返回 Mono / Flux
Spring MVC 通过 ReactiveAdapterRegistry 支持响应式库
- 适配
Reactor(Mono/Flux) - 适配
RxJava - 适配其他响应式库
适配策略:
- 单元素响应(
Mono、CompletionStage):自动把它当成DeferredResult Flux+ 流式媒体类型:自动把它当成SseEmitter/ResponseBodyEmitterFlux<对象>:自动转成DeferredResult<List<?>>
但底层写响应依然是阻塞的,只是用异步线程池隐藏了阻塞。
2.4 Flux 流式响应
Spring AI 和 Spring AI Alibaba 默认都使用 Flux 作为流式响应返回,Spring MVC 支持返回 Flux``/Mono,必须手动引入 Reactor 核心依赖:
xml
<!-- Reactor 核心包(支持 Flux/Mono)-->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
引入了
Spring AI和Spring AI Alibaba框架时,无需手动引入,已默认集成
使用 Flux 流式响应:
java
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public Flux<String> chat(@RequestParam("q") String question) {
// 1. 定义要流式推送的文本列表
Flux<String> messageFlux = Flux.just(
"收到你的问题:" + question,
"正在处理中,请稍候...",
"处理完成!这是Spring WebFlux SSE流式响应",
"[DONE]"
)
// 2. 每条消息延迟 500ms,模拟流式打字效果
.delayElements(Duration.ofMillis(500));
// 3. 直接返回 Flux<String> → Spring 自动转 SSE
return messageFlux;
}
2.5 SSE 响应式标准类
Spring 5.0 引入了用于响应式 Web 的 SSE 事件类 ServerSentEvent<T> ,其中 <T> 表示推送的数据类型(String/JSON/对象),
其字段对应 SSE 原生协议:
java
@Nullable private final String id; // 事件ID(断线重连用)
@Nullable private final String event; // 事件类型(前端可监听自定义事件)
@Nullable private final Duration retry; // 重连间隔(毫秒)
@Nullable private final String comment; // 注释(不会推给前端,仅日志)
@Nullable private final T data; // 核心:推送的真实数据
真实开发场景中,推荐使用 ServerSentEvent 标准用法:
java
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public Flux<ServerSentEvent<String>> chat(@RequestParam("q") String question) {
// 1. 构造流式消息
return Flux.just(
"收到你的问题:" + question,
"正在处理中,请稍候...",
"处理完成!标准SSE响应式推送",
"[DONE]"
)
// 2. 每条延迟,模拟流式效果
.delayElements(Duration.ofMillis(500))
// 3. 包装为标准 ServerSentEvent(官方规范)
.map(content -> ServerSentEvent.<String>builder()
.id(String.valueOf(System.currentTimeMillis())) // 可选:SSE事件ID
.data(content) // 核心:推送的字符串数据
.build()
);
}
3. ReactAgent
使用 ReactAgent 进行对话时,可以参考 spring-ai-alibaba-studio 中的相关实现。
3.1 用户消息对象
定义用户消息传输对象,专门用于前后端流式消息交互,作用:
- 对
Spring AI原生UserMessage做序列化友好封装 - 固定消息类型为
user,适配聊天对话格式 - 支持媒体、元数据扩展,是
AI聊天的标准消息体
java
public class UserMessageDTO implements MessageDTO {
@JsonProperty("messageType")
private String messageType = "user";
@JsonProperty("content")
private String content;
@JsonProperty("metadata")
private Map<String, Object> metadata;
@JsonProperty("media")
private List<MediaDTO> media;
/**
* Default constructor for deserialization.
*/
public UserMessageDTO() {
this.metadata = new HashMap<>();
this.media = new ArrayList<>();
}
/**
* Constructor with content.
*/
public UserMessageDTO(String content) {
this();
this.content = content;
}
/**
* Constructor from Spring AI UserMessage.
*/
public UserMessageDTO(UserMessage message) {
this();
this.content = message.getText();
this.metadata = new HashMap<>(message.getMetadata());
// Note: Media extraction is not currently supported
// Spring AI's Media API is not directly accessible in this version
}
/**
* Convert to Spring AI UserMessage.
*/
public UserMessage toUserMessage() {
// UserMessage constructor just takes content as String
return new UserMessage(this.content);
}
// Getters and Setters
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
public List<MediaDTO> getMedia() {
return media;
}
public void setMedia(List<MediaDTO> media) {
this.media = media;
}
/**
* DTO for Media within UserMessage.
* Placeholder for future media support.
*/
public static class MediaDTO {
@JsonProperty("mimeType")
private String mimeType;
@JsonProperty("data")
private Object data;
public MediaDTO() {
}
// Getters and Setters
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
}
3.2 执行请求对象
定义智能体执行请求 DTO :
- 用于接收前端
POST /run和POST /run-sse接口的请求参数 - 封装执行
AI智能体所需的全部核心信息
java
ublic class AgentRunRequest {
/**
* 应用名称
*/
@JsonProperty("appName")
public String appName;
/**
* 用户唯一标识
*/
@JsonProperty("userId")
public String userId;
/**
* 对话线程ID(用于区分不同会话)
*/
@JsonProperty("threadId")
public String threadId;
/**
* 用户发送的新消息
*/
@JsonProperty("newMessage")
public UserMessageDTO newMessage;
/**
* 是否开启流式输出
* true:开启SSE流式推送 false:关闭(默认)
*/
@JsonProperty("streaming")
public boolean streaming = false;
/**
* 会话状态增量
* 用于在执行智能体前,动态合并更新会话状态
* 可用于注入配置、回放模式等场景,无需修改持久化的会话数据
*/
@JsonProperty("stateDelta")
public Map<String, Object> stateDelta;
/**
* 无参构造函数
* 用于JSON反序列化
*/
public AgentRunRequest() {
}
/**
* 获取应用名称
*/
public String getAppName() {
return appName;
}
/**
* 获取用户ID
*/
public String getUserId() {
return userId;
}
/**
* 获取对话线程ID
*/
public String getThreadId() {
return threadId;
}
/**
* 获取用户新消息
*/
public UserMessageDTO getNewMessage() {
return newMessage;
}
/**
* 获取是否开启流式输出
*/
public boolean getStreaming() {
return streaming;
}
/**
* 获取会话状态增量
*/
public Map<String, Object> getStateDelta() {
return stateDelta;
}
}
3.3 执行响应对象
定义 AI 智能体执行响应对象,用于封装 Agent 流式/非流式执行的返回结果,是前端接收 SSE 流式消息的统一响应格式。
java
public class AgentRunResponse {
/**
* 当前执行的节点名称
*/
@JsonProperty("node")
protected String node;
/**
* 执行的智能体名称
*/
@JsonProperty("agent")
protected String agent;
/**
* Token 消耗统计信息
*/
@JsonProperty("tokenUsage")
protected Usage tokenUsage;
/**
* 对话消息 DTO 对象
* 用于序列化传输,避免原生 Message 序列化异常
*/
@JsonProperty("message")
protected MessageDTO message;
/**
* 流式输出文本片段
* SSE 逐字推送的核心字段
*/
@JsonProperty("chunk")
private String chunk;
/**
* 无参构造函数
* 供 Jackson 反序列化使用
*/
AgentRunResponse() {
}
/**
* 构造函数:通过 Spring AI 原生 Message 创建响应对象
* @param node 执行节点
* @param agent 智能体名称
* @param message Spring AI 原生消息
* @param tokenUsage Token 消耗
* @param chunk 流式文本片段
*/
public AgentRunResponse(String node, String agent, Message message, Usage tokenUsage, String chunk) {
this.node = node;
this.agent = agent;
// 将原生消息转为 DTO 对象,保证序列化正常
this.message = message != null ? MessageDTO.MessageDTOFactory.fromMessage(message) : null;
this.tokenUsage = tokenUsage;
this.chunk = chunk;
}
/**
* 构造函数:直接通过 MessageDTO 创建响应对象
* @param node 执行节点
* @param agent 智能体名称
* @param message 消息 DTO
* @param tokenUsage Token 消耗
* @param chunk 流式文本片段
*/
public AgentRunResponse(String node, String agent, MessageDTO message, Usage tokenUsage, String chunk) {
this.node = node;
this.agent = agent;
this.message = message;
this.tokenUsage = tokenUsage;
this.chunk = chunk;
}
// ==================== Getter & Setter ====================
/**
* 获取执行节点名称
*/
public String getNode() {
return node;
}
/**
* 设置执行节点名称
*/
public void setNode(String node) {
this.node = node;
}
/**
* 获取智能体名称
*/
public String getAgent() {
return agent;
}
/**
* 设置智能体名称
*/
public void setAgent(String agent) {
this.agent = agent;
}
/**
* 获取 Token 消耗统计
*/
public Usage getTokenUsage() {
return tokenUsage;
}
/**
* 设置 Token 消耗统计
*/
public void setTokenUsage(Usage tokenUsage) {
this.tokenUsage = tokenUsage;
}
/**
* 获取消息 DTO 对象
*/
public MessageDTO getMessage() {
return message;
}
/**
* 设置消息 DTO 对象
*/
public void setMessage(MessageDTO message) {
this.message = message;
}
/**
* 转换为 Spring AI 原生 Message 对象
* @JsonIgnore 不序列化到前端,仅内部使用
* @return 原生消息对象
*/
@JsonIgnore
public Message getMessageAsSpringAI() {
return message != null ? MessageDTO.MessageDTOFactory.toMessage(message) : null;
}
/**
* 获取流式输出文本片段
*/
public String getChunk() {
return chunk;
}
/**
* 设置流式输出文本片段
*/
public void setChunk(String chunk) {
this.chunk = chunk;
}
}
3.4 流式输出对话接口
3.4.1 处理流式输出
定义执行 AI Agent 并处理流式输出的核心方法,返回 Flux<ServerSentEvent<String>> 流式 SSE 响应。
请求参数:
userMessage:用户消息agent:AI智能体对象,一个应用中,可以包含多个ReactAgent实例runnableConfig: 运行配置
完整流式处理逻辑:
-
获取Agent原生流式输出流
- 判断入参
UserMessage是否存在,分场景触发Agent执行 - 存在用户消息:调用
agent.stream()发起正常对话流式请求 - 无用户消息:传入空字符串,发起空消息触发的流式请求
- 最终得到
Flux<NodeOutput>类型的原始输出流
- 判断入参
-
过滤冗余结束事件(前端体验优化)
- 过滤类型为
AGENT_MODEL_FINISHED的重复结束事件 - 目的:避免前端接收到重复的最终消息,防止展示错乱
- 过滤类型为
-
流式数据转换与业务逻辑处理
- 提取流式输出的基础元数据:执行节点名称、
Agent名称、Token消耗统计 - 初始化前端响应对象
AgentRunResponse - 分支1:处理常规流式输出(StreamingOutput)
- 无消息内容:直接返回空JSON格式的SSE事件
- 助手消息(
AssistantMessage):区分普通文本消息 和工具调用消息,分别封装响应对象 - 其他类型消息(系统/提示消息):直接封装基础响应对象
- 分支2:处理工具中断(InterruptionMetadata)
- 针对需要用户确认的工具调用中断场景
- 将中断信息转换为前端可识别的工具确认消息
DTO - 封装为标准响应对象
- 提取流式输出的基础元数据:执行节点名称、
-
序列化为 JSON 并封装标准SSE事件
- 将封装好的
AgentRunResponse序列化为JSON字符串 - 构建
ServerSentEvent<String>,将JSON作为SSE的data数据 - 序列化异常:捕获错误并返回格式化失败的错误消息
- 无有效数据时:返回空
JSON的SSE事件
- 将封装好的
-
全局流式异常兜底处理
- 捕获
Agent执行全流程的所有异常(网络/执行/序列化失败等) - 构造标准错误
JSON,包含错误类型、错误信息 - 指定SSE事件类型为
error,前端可专门监听错误事件 - 异常构造失败时:返回服务器内部错误兜底消息
- 保证流式连接不会因异常中断,提升前端稳定性
- 捕获
实现代码:
java
/**
* 真正执行 AI Agent 并处理流式输出的核心方法
* 将 Agent 输出转为标准 SSE 事件
*
* @param userMessage 用户消息
* @param agent AI 智能体
* @param runnableConfig 运行配置
* @return Flux<ServerSentEvent<String>> 流式 SSE 响应
* @throws GraphRunnerException Agent 执行异常
*/
@NotNull
private Flux<ServerSentEvent<String>> executeAgent(
UserMessage userMessage,
Agent agent,
RunnableConfig runnableConfig) throws GraphRunnerException {
// 1. 获取 Agent 流式输出流
Flux<NodeOutput> agentStream;
if (userMessage != null) {
// 有用户消息:正常对话
agentStream = agent.stream(userMessage, runnableConfig);
} else {
// 无用户消息:空消息触发
agentStream = agent.stream("", runnableConfig);
}
// 2. 处理流式输出 → 转为前端可识别的 SSE 格式
return agentStream
// 过滤掉重复的结束事件,避免前端重复展示最终消息
.filter(nodeOutput -> !(nodeOutput instanceof StreamingOutput<?> so
&& so.getOutputType() == OutputType.AGENT_MODEL_FINISHED))
// 3. 转换输出为 JSON 格式的 SSE 事件
.map(nodeOutput -> {
// 当前执行的节点名称
String node = nodeOutput.node();
// Agent 名称
String agentName = nodeOutput.agent();
// Token 消耗统计
Usage tokenUsage = nodeOutput.tokenUsage();
// 最终返回给前端的响应对象
AgentRunResponse agentResponse = null;
// ====================== 处理流式输出 ======================
if (nodeOutput instanceof StreamingOutput<?> streamingOutput) {
// 获取 AI 输出的消息对象
Message message = streamingOutput.message();
// 无消息内容时返回空 JSON
if (message == null) {
return ServerSentEvent.<String>builder().data("{}").build();
}
// AI 助手消息(核心输出)
if (message instanceof AssistantMessage assistantMessage) {
// 判断是否包含工具调用(如搜索、画图、查天气等)
if (assistantMessage.hasToolCalls()) {
// 工具调用消息
agentResponse = new AgentRunResponse(
node, agentName, assistantMessage, tokenUsage, "");
} else {
// 普通文本消息(流式打字输出)
agentResponse = new AgentRunResponse(
node, agentName, assistantMessage, tokenUsage, assistantMessage.getText());
}
} else {
// 其他类型消息(系统消息、提示等)
agentResponse = new AgentRunResponse(
node, agentName, message, tokenUsage, "");
}
}
// ====================== 处理工具中断(需要用户确认) ======================
else if (nodeOutput instanceof InterruptionMetadata interruptionMetadata) {
// 转为前端可识别的工具确认消息
ToolRequestConfirmMessageDTO toolRequestMessage =
MessageDTO.MessageDTOFactory.fromInterruptionMetadata(interruptionMetadata);
agentResponse = new AgentRunResponse(
node, agentName, toolRequestMessage, tokenUsage, "");
}
// ====================== 转为 JSON 并返回 SSE 事件 ======================
try {
if (agentResponse != null) {
// 对象转 JSON 字符串
String jsonData = mapper.writeValueAsString(agentResponse);
// 封装成标准 SSE 事件返回
return ServerSentEvent.<String>builder().data(jsonData).build();
}
} catch (Exception e) {
log.error("Agent 响应结果序列化 JSON 失败", e);
return ServerSentEvent.<String>builder()
.data("{\"error\":\"响应数据格式化失败\"}")
.build();
}
// 默认空消息
return ServerSentEvent.<String>builder().data("{}").build();
})
// ====================== 流异常处理 ======================
.onErrorResume(error -> {
log.error("Agent 流式执行过程中发生错误", error);
String errorMsg = error.getMessage() != null ? error.getMessage() : "未知错误";
String errorType = error.getClass().getSimpleName();
try {
// 构造标准错误 JSON
String errorJson = String.format(
"{\"error\":true,\"errorType\":\"%s\",\"errorMessage\":\"%s\"}",
errorType.replace("\"", "\\\""),
errorMsg.replace("\"", "\\\"").replace("\n", "\\n")
);
// 返回错误类型的 SSE 事件
return Flux.just(ServerSentEvent.<String>builder()
.event("error")
.data(errorJson)
.build());
} catch (Exception e) {
log.error("构造错误 SSE 事件失败", e);
return Flux.just(ServerSentEvent.<String>builder()
.event("error")
.data("{\"error\":true,\"errorMessage\":\"服务器内部错误\"}")
.build());
}
});
}
3.4.2 AI 聊天控制器
智能体对话的流式入口接口,基于 Spring MVC 响应式适配实现 SSE 服务器推送,是前端发起流式 AI 对话的核心入口,负责参数校验、配置构建、调用核心执行方法并返回流式数据流。
完整处理逻辑:
-
接口基础定义
- 请求方式:
POST,接口路径:/run_sse - 响应类型:指定为
text/event-stream;charset=UTF-8,标准 SSE 格式 + 强制中文 UTF-8 编码 - 返回值:
Flux<ServerSentEvent<String>>,Spring官方标准响应式SSE写法
- 请求方式:
-
接收前端请求参数
- 接收
@RequestBody注解的AgentRunRequest对象 - 包含核心参数:应用名、用户
ID、会话ID、用户消息、流式开关、状态增量
- 接收
-
第一层参数校验:appName 非空校验
- 校验应用名称
appName不能为空/空白字符 - 校验失败:打印警告日志,返回
400 BAD_REQUEST响应式异常 - 作用:保证请求绑定正确的
AI应用(根据应用名称加载Agent实例对象)
- 校验应用名称
-
第二层参数校验:threadId 非空校验
- 校验会话
IDthreadId不能为空/空白字符 - 校验失败:打印警告日志,返回
400 BAD_REQUEST响应式异常 - 作用:
threadId是维持上下文对话的核心,必须校验
- 校验会话
-
构建 AI Agent 运行配置
- 创建
RunnableConfig运行上下文配置 - 绑定
threadId:实现多用户、多会话隔离,保留对话历史 - 追加用户
ID元数据:用于日志追踪、用户维度监控 - 是
AI Agent执行的核心上下文参数
- 创建
-
调用核心流式执行方法
- 将前端
DTO对象UserMessageDTO转为Spring AI原生UserMessage - 传入用户消息、AI 智能体、运行配置 ,调用
executeAgent核心方法 - 直接返回处理完成的流式 SSE 数据流给前端
- 将前端
-
全局异常捕获与处理
- 捕获配置构建、
Agent初始化等全流程异常 - 打印会话级别的错误日志,方便问题排查
- 返回
500 INTERNAL_SERVER_ERROR响应式异常,封装异常原因 - 保证接口不会因异常直接崩溃,友好返回错误信息
- 捕获配置构建、
完整代码:
java
/**
* AI 聊天控制器
* 提供基于 SSE 流式输出的 AI Agent 对话接口,实现像 ChatGPT 一样的打字机效果
*/
@RestController
@RequestMapping("/api/chat")
public class SseController {
/**
* 日志记录器
*/
private static final Logger log = LoggerFactory.getLogger(SseController.class);
/**
* JSON 序列化工具,用于把响应对象转为 JSON 字符串返回给前端
*/
final ObjectMapper mapper = new ObjectMapper();
/**
* AI 智能体(React 架构 Agent,自带思考 + 调用工具能力)
*/
@Autowired
private ReactAgent chatAgent;
/**
* AI 对话流式接口(SSE 服务器推送事件)
* 功能:接收前端请求 → 运行 AI Agent → 实时逐字返回聊天内容
*
* @param request 前端传入的请求对象,包含应用名、会话ID、用户消息等
* @return Flux<ServerSentEvent<String>> SSE 格式的数据流
*/
@PostMapping(value = "/run_sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=UTF-8")
public Flux<ServerSentEvent<String>> agentRunSse(@RequestBody AgentRunRequest request) {
// 1. 参数校验:appName 不能为空
if (request.appName == null || request.appName.trim().isEmpty()) {
log.warn("SSE 请求中 appName 不能为空,appName:{},会话ID:{}",
request.appName, request.threadId);
return Flux.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, "appName 不能为空"));
}
// 2. 参数校验:threadId 会话ID 不能为空
if (request.threadId == null || request.threadId.trim().isEmpty()) {
log.warn("SSE 请求中 threadId 不能为空,appName:{},会话ID:{}",
request.appName, request.threadId);
return Flux.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, "threadId 不能为空"));
}
try {
// 3. 构建 Agent 运行配置
// 包含会话ID、用户ID,用于维持上下文对话
RunnableConfig runnableConfig = RunnableConfig.builder()
.threadId(request.threadId) // 会话ID:区分不同用户的对话
.addMetadata("user_id", request.userId) // 附加用户ID,便于日志追踪
.build();
// 4. 执行 AI Agent,并返回流式数据给前端
return executeAgent(request.newMessage.toUserMessage(), chatAgent, runnableConfig);
} catch (Exception e) {
// 5. 全局异常捕获:Agent 启动/运行失败
log.error("会话 {} 的 Agent 运行异常", request.threadId, e);
return Flux.error(new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "Agent 运行失败", e));
}
}
}
3.5 请求测试
执行请求:

流式返回:
java
data:{"node":"_AGENT_MODEL_","agent":"email-chat-agent","tokenUsage":{"promptTokens":43,"totalTokens":265,"completionTokens":222,"nativeUsage":{"output_tokens":222,"input_tokens":43,"total_tokens":265,"prompt_tokens_details":{"cached_tokens":0}}},"message":{"messageType":"assistant","messageType":"assistant","content":"方案,请帮我起草","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"","id":"f4f78ca8-fd55-949e-8506-579e9d36b17f","reasoningContent":""},"toolCalls":[]},"chunk":"方案,请帮我起草"}
data:{"node":"_AGENT_MODEL_","agent":"email-chat-agent","tokenUsage":{"promptTokens":43,"totalTokens":271,"completionTokens":228,"nativeUsage":{"output_tokens":228,"input_tokens":43,"total_tokens":271,"prompt_tokens_details":{"cached_tokens":0}}},"message":{"messageType":"assistant","messageType":"assistant","content":"回复"\n\n随时告诉我你的需求","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"","id":"f4f78ca8-fd55-949e-8506-579e9d36b17f","reasoningContent":""},"toolCalls":[]},"chunk":"回复"\n\n随时告诉我你的需求"}
data:{"node":"_AGENT_MODEL_","agent":"email-chat-agent","tokenUsage":{"promptTokens":43,"totalTokens":276,"completionTokens":233,"nativeUsage":{"output_tokens":233,"input_tokens":43,"total_tokens":276,"prompt_tokens_details":{"cached_tokens":0}}},"message":{"messageType":"assistant","messageType":"assistant","content":",我们马上开始","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"","id":"f4f78ca8-fd55-949e-8506-579e9d36b17f","reasoningContent":""},"toolCalls":[]},"chunk":",我们马上开始"}
data:{"node":"_AGENT_MODEL_","agent":"email-chat-agent","tokenUsage":{"promptTokens":43,"totalTokens":277,"completionTokens":234,"nativeUsage":{"output_tokens":234,"input_tokens":43,"total_tokens":277,"prompt_tokens_details":{"cached_tokens":0}}},"message":{"messageType":"assistant","messageType":"assistant","content":" 👇","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"","id":"f4f78ca8-fd55-949e-8506-579e9d36b17f","reasoningContent":""},"toolCalls":[]},"chunk":" 👇"}
data:{"node":"_AGENT_MODEL_","agent":"email-chat-agent","tokenUsage":{"promptTokens":43,"totalTokens":277,"completionTokens":234,"nativeUsage":{"output_tokens":234,"input_tokens":43,"total_tokens":277,"prompt_tokens_details":{"cached_tokens":0}}},"message":{"messageType":"assistant","messageType":"assistant","content":"","metadata":{"search_info":"","role":"ASSISTANT","messageType":"ASSISTANT","finishReason":"STOP","id":"f4f78ca8-fd55-949e-8506-579e9d36b17f","reasoningContent":""},"toolCalls":[]},"chunk":""}