Spring AI Alibaba 1.x 系列【67】ReactAgent SSE 流式输出

文章目录

  • [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 底层机制

核心流程

  1. 客户端发起一个长 HTTP 请求(请求头特殊标记)
  2. 服务端接收后不关闭连接,保持连接常开
  3. 服务端有新数据时,按固定格式实时推送给客户端
  4. 连接断开 → 浏览器自动重连
  5. 客户端监听事件,接收并处理数据

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 异步请求处理机制,相关能力如下:

  • 控制器方法可通过返回 DeferredResultCallableWebAsyncTask 实现单个异步结果返回
  • 控制器支持流式输出多条数据,包含服务器推送事件 SSE 与原生二进制数据流
  • 控制器可调用响应式客户端,并返回响应式类型完成响应处理

SSE 属于异步通信,Spring 生态中的 Spring MVCSpring 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 异步支持的单个异步结果

  • DeferredResult
  • Callable
  • WebAsyncTask

如需持续推送多条异步数据,使用以下三类流式返回值:

  • 普通对象流式输出 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 Reactor 官方文档

Spring MVC 虽然是同步阻塞框架,但它兼容响应式类型 ,会自动适配这些响应式类型,把它们转成异步/流式输出

  • 可以在 Spring MVC 里调用 WebClient(WebFlux 客户端)
  • 可以调用 Spring Data 响应式仓库(Redis/Mongo 响应式)
  • 控制器方法可以直接返回 Mono / Flux

Spring MVC 通过 ReactiveAdapterRegistry 支持响应式库

  • 适配 ReactorMono/Flux
  • 适配 RxJava
  • 适配其他响应式库

适配策略:

  • 单元素响应(MonoCompletionStage):自动把它当成 DeferredResult
  • Flux + 流式媒体类型:自动把它当成 SseEmitter / ResponseBodyEmitter
  • Flux <对象>:自动转成 DeferredResult<List<?>>

但底层写响应依然是阻塞的,只是用异步线程池隐藏了阻塞。

2.4 Flux 流式响应

Spring AISpring AI Alibaba 默认都使用 Flux 作为流式响应返回,Spring MVC 支持返回 Flux``/Mono,必须手动引入 Reactor 核心依赖:

xml 复制代码
        <!-- Reactor 核心包(支持 Flux/Mono)-->
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
        </dependency>

引入了 Spring AISpring 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 引入了用于响应式 WebSSE 事件类 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 /runPOST /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:用户消息
  • agentAI 智能体对象,一个应用中,可以包含多个 ReactAgent 实例
  • runnableConfig : 运行配置

完整流式处理逻辑:

  1. 获取Agent原生流式输出流

    • 判断入参UserMessage是否存在,分场景触发 Agent 执行
    • 存在用户消息:调用agent.stream()发起正常对话流式请求
    • 无用户消息:传入空字符串,发起空消息触发的流式请求
    • 最终得到Flux<NodeOutput>类型的原始输出流
  2. 过滤冗余结束事件(前端体验优化)

    • 过滤类型为AGENT_MODEL_FINISHED的重复结束事件
    • 目的:避免前端接收到重复的最终消息,防止展示错乱
  3. 流式数据转换与业务逻辑处理

    • 提取流式输出的基础元数据:执行节点名称、Agent 名称、Token 消耗统计
    • 初始化前端响应对象AgentRunResponse
    • 分支1:处理常规流式输出(StreamingOutput)
      • 无消息内容:直接返回空JSON格式的SSE事件
      • 助手消息(AssistantMessage):区分普通文本消息工具调用消息,分别封装响应对象
      • 其他类型消息(系统/提示消息):直接封装基础响应对象
    • 分支2:处理工具中断(InterruptionMetadata)
      • 针对需要用户确认的工具调用中断场景
      • 将中断信息转换为前端可识别的工具确认消息 DTO
      • 封装为标准响应对象
  4. 序列化为 JSON 并封装标准SSE事件

    • 将封装好的AgentRunResponse序列化为 JSON 字符串
    • 构建ServerSentEvent<String>,将 JSON作为 SSEdata数据
    • 序列化异常:捕获错误并返回格式化失败的错误消息
    • 无有效数据时:返回空 JSONSSE 事件
  5. 全局流式异常兜底处理

    • 捕获 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 对话的核心入口,负责参数校验、配置构建、调用核心执行方法并返回流式数据流。

完整处理逻辑:

  1. 接口基础定义

    • 请求方式:POST,接口路径:/run_sse
    • 响应类型:指定为 text/event-stream;charset=UTF-8标准 SSE 格式 + 强制中文 UTF-8 编码
    • 返回值:Flux<ServerSentEvent<String>>Spring 官方标准响应式 SSE 写法
  2. 接收前端请求参数

    • 接收 @RequestBody 注解的 AgentRunRequest 对象
    • 包含核心参数:应用名、用户ID、会话ID、用户消息、流式开关、状态增量
  3. 第一层参数校验:appName 非空校验

    • 校验应用名称 appName 不能为空/空白字符
    • 校验失败:打印警告日志,返回 400 BAD_REQUEST 响应式异常
    • 作用:保证请求绑定正确的 AI 应用(根据应用名称加载 Agent 实例对象)
  4. 第二层参数校验:threadId 非空校验

    • 校验会话 ID threadId 不能为空/空白字符
    • 校验失败:打印警告日志,返回 400 BAD_REQUEST 响应式异常
    • 作用:threadId 是维持上下文对话的核心,必须校验
  5. 构建 AI Agent 运行配置

    • 创建 RunnableConfig 运行上下文配置
    • 绑定 threadId:实现多用户、多会话隔离,保留对话历史
    • 追加用户 ID 元数据:用于日志追踪、用户维度监控
    • AI Agent 执行的核心上下文参数
  6. 调用核心流式执行方法

    • 将前端 DTO 对象 UserMessageDTO 转为 Spring AI 原生 UserMessage
    • 传入用户消息、AI 智能体、运行配置 ,调用 executeAgent 核心方法
    • 直接返回处理完成的流式 SSE 数据流给前端
  7. 全局异常捕获与处理

    • 捕获配置构建、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":""}
相关推荐
李二。1 小时前
鸿蒙原生ArkTS-系外行星百科AI
人工智能·华为·harmonyos
大模型最新论文速读1 小时前
06-04 · LLM 最新论文速览
论文阅读·人工智能·深度学习·机器学习·自然语言处理
我登哥MVP1 小时前
Spring Boo从“会用”到“精通”:Spring Boot 入门
java·spring boot·后端·spring·maven·intellij-idea·mybatis
清辞8532 小时前
入门大模型工程师第四课----通过RAG增强大模型原本无法回答的问题
大数据·人工智能·学习·语言模型
森诺Alyson2 小时前
前沿技术借鉴研讨-2026.6.4(孕期持续累积高温暴露显著升高妊娠期糖尿病患病风险)
论文阅读·人工智能·经验分享
Urbano2 小时前
夹克制作全流程科普:工艺标准、自动化改造与设备科学选型
人工智能
染翰2 小时前
Java 实现 Git 自动克隆工具,打包成 Windows 独立 EXE(免安装JDK)
java·git·后端
虎冯河2 小时前
AI人工智能技术类文章
人工智能·aigc
AI视觉网奇2 小时前
Bambu Studio 发现 xx个开放边
开发语言·人工智能·python