本课目标

用 WebFlux 实现 SSE 流式输出,掌握流式与工具调用、记忆的配合。


模型生成一段 500 字的回答可能要 15 秒。15 秒盯着空白屏幕是什么体验?用户会以为系统卡死了,疯狂点刷新,然后投诉你。

流式输出把"一次性交付"变成"逐字吐字"——模型每生成几个字就推给前端。用户看到字在屏幕上长出来,感知等待时间几乎归零。本讲不讲原理,讲落地的三个关键点:怎么写、跟工具调用怎么配合、生产环境要注意什么。

核心内容

SSE 流式:WebFlux + Flux

Spring AI 的流式基于 Reactor Flux,天然非阻塞:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .content();
}

前端用 EventSourcefetch 流式读取,逐字渲染。关键点:produces = TEXT_EVENT_STREAM_VALUE 告诉浏览器"这是一个会持续推送的响应",浏览器就不会在收到第一个 chunk 后关闭连接。

流式 + 结构化输出:两个世界的融合

流式是逐字推文本,结构化要拿到完整 JSON 才能解析。怎么办?

生产里的常见做法:流式文本展示给用户(快),流结束后后台异步调一次非流式请求拿结构化对象落库(稳)。两个请求,各自干各自擅长的事。别试图在流中间截断文本做 JSON 解析——流中途的 token 不代表最终答案。

流式 + 工具调用:模型在说话时突然"停一下"

当模型决定调用工具,文本流会中断。框架自动执行工具后继续生成。对前端来说,就是文本停了几秒然后继续——你需要在 UI 上处理这个"卡顿"(比如显示"正在查询数据…"而不是让用户以为卡死了)。

JDK 21 的虚拟线程是流式场景的绝配。流式是典型的 IO 密集——一个连接挂着等模型推数据,不占 CPU。虚拟线程让单机并发连接数轻松上万。我在灵枢 OS 的实践中,这是性价比最高的基础设施优化。

生产环境的三个坑

  1. 网关超时:Nginx 默认 proxy_read_timeout 只有 60 秒,流式长回答会被掐断。至少调到 300 秒。
  2. 前端断连处理:用户关了浏览器标签页,SSE 连接断开,但后端的 Flux 还在跑。用 Sinks 或超时机制兜底。
  3. 异常恢复:模型服务抖动时流中断,前端要有重试按钮,而不是卡死。Flux.onErrorResume() 是你的朋友。

动手练习

  1. 实现 SSE 流式接口,用 curl -N 观察逐块返回
  2. 在流式请求中加入工具调用,观察流的暂停与恢复