资讯中心

LangChain4j全集-16-springboot返回流的方式

📅 2026/7/23 15:13:29
LangChain4j全集-16-springboot返回流的方式
Spring Boot 后台返回流式内容常见有两种做法Spring MVC SseEmitterSpring WebFlux Flux / ServerSentEvent它们都可以实现类似 ChatGPT 那种“边生成边返回”的效果但底层模型、适合场景、复杂度不太一样。一句话结论如果你现在是普通 Spring Boot MVC 项目想快速实现 AI 流式输出优先用 SseEmitter。简单、直接、够用。如果你的项目本身就是 WebFlux 技术栈或者你要做高并发、全链路非阻塞用 WebFlux Flux / ServerSentEvent 更合适。1. SseEmitter 是什么SseEmitter是 Spring MVC 里的流式响应工具。它适合这种项目spring-boot-starter-web也就是传统 Spring MVC 项目底层一般是Tomcat / Jetty / Undertow用法大概是GetMapping(value/chat,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchat(Stringmessage){SseEmitteremitternewSseEmitter(0L);streamingChatModel.chat(message,newStreamingChatResponseHandler(){OverridepublicvoidonPartialResponse(StringpartialResponse){try{emitter.send(partialResponse);}catch(IOExceptione){emitter.completeWithError(e);}}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){emitter.complete();}OverridepublicvoidonError(Throwableerror){emitter.completeWithError(error);}});returnemitter;}核心思想是后端拿到一段模型输出就调用emitter.send(...)推给前端。2. WebFlux 是什么WebFlux 是 Spring 的响应式编程框架。它适合这种项目spring-boot-starter-webflux底层通常是Netty它通过Flux表示一串连续的数据流。比如GetMapping(value/chat,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxServerSentEventStringchat(Stringmessage){returnFlux.create(sink-{streamingChatModel.chat(message,newStreamingChatResponseHandler(){OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event(answer).build());}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder([DONE]).event(done).build());sink.complete();}OverridepublicvoidonError(Throwableerror){sink.error(error);}});});}核心思想是Controller 直接返回一个Flux前端不断接收这个 Flux 里面的数据。3. 它俩都能做 SSE这一点要先明确SseEmitter和WebFlux都可以返回 SSE 流。SSE 的本质是 HTTP 长连接服务端不断往客户端推送文本事件。前端都可以这样接收consteventSourcenewEventSource(/api/ai/chat);eventSource.addEventListener(answer,event{console.log(event.data);});eventSource.addEventListener(done,event{eventSource.close();});区别主要在后端实现方式和底层线程模型。4. 核心区别对比对比项SseEmitterWebFlux所属技术栈Spring MVCSpring WebFlux常用依赖spring-boot-starter-webspring-boot-starter-webflux编程模型命令式、回调式响应式、声明式返回类型SseEmitterFluxT/FluxServerSentEventT底层服务器Tomcat 常见Netty 常见线程模型Servlet 异步事件循环 非阻塞学习成本低较高适合项目普通 Spring MVC 项目响应式项目高并发能力可以但资源占用相对高更适合大量长连接背压支持弱更好和传统业务代码结合很自然需要响应式思维5. 最大区别线程模型不同5.1 SseEmitter传统 MVC 异步流SseEmitter虽然是异步的但它还是属于 Spring MVC 体系。你可以理解为一个请求进来 ↓ Spring MVC 创建 SseEmitter ↓ 请求线程先释放 ↓ 后台有数据时调用 emitter.send() ↓ 不断写回浏览器它比普通接口好因为请求线程不会一直阻塞着等 AI 完整生成。但是它仍然是传统 Servlet 体系整体不是完全响应式的。5.2 WebFlux响应式非阻塞流WebFlux 的设计理念是数据来了就推 没数据就不占线程 通过事件驱动处理它更适合大量连接长期挂着的场景。比如大量用户同时和 AI 聊天每个回答持续几十秒后端需要维持很多 SSE 长连接你的数据库、Redis、HTTP Client 也都是响应式的这时候 WebFlux 的资源利用率通常更好。6. 开发体验区别6.1 SseEmitter 更符合普通 Spring Boot 开发习惯如果你平时写的是RestControllerServiceMapper然后 Controller 返回StringResultVOListUser那SseEmitter会更自然。你只需要掌握emitter.send(...)emitter.complete()emitter.completeWithError(...)就可以了。示例GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(){SseEmitteremitternewSseEmitter(0L);newThread(()-{try{emitter.send(第一段);Thread.sleep(1000);emitter.send(第二段);Thread.sleep(1000);emitter.send(第三段);emitter.complete();}catch(Exceptione){emitter.completeWithError(e);}}).start();returnemitter;}简单直接。6.2 WebFlux 需要适应 Flux / MonoWebFlux 代码更像这样GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxStringstream(){returnFlux.just(第一段,第二段,第三段).delayElements(Duration.ofSeconds(1));}看起来很简洁但真实业务里你要理解MonoFluxsubscribesinkbackpressureSchedulers非阻塞调用响应式链路如果只是为了 AI 流式输出而引入 WebFlux学习成本会高一些。7. LangChain4j 结合时有什么区别LangChain4j 的低层流式接口本身是回调式的newStreamingChatResponseHandler(){OverridepublicvoidonPartialResponse(StringpartialResponse){}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){}OverridepublicvoidonError(Throwableerror){}}所以它天然就很适合和SseEmitter搭配onPartialResponse(...)↓ emitter.send(...)非常直观。如果你用 WebFlux需要把回调式 API 转换成FluxonPartialResponse(...)↓ sink.next(...)例如GetMapping(value/chat,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxServerSentEventStringchat(RequestParamStringmessage){returnFlux.create(sink-{streamingChatModel.chat(message,newStreamingChatResponseHandler(){OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){sink.next(ServerSentEvent.builder(partialThinking.text()).event(thinking).build());}OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event(answer).build());}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder([DONE]).event(done).build());sink.complete();}OverridepublicvoidonError(Throwableerror){sink.error(error);}});});}也不难但比SseEmitter多了一层响应式封装。8. 带思考内容时两种方式怎么写8.1 SseEmitter 写法GetMapping(value/chat-sse,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterchatSse(RequestParamStringmessage){SseEmitteremitternewSseEmitter(0L);streamingChatModel.chat(message,newStreamingChatResponseHandler(){OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){send(emitter,thinking,partialThinking.text());}OverridepublicvoidonPartialResponse(StringpartialResponse){send(emitter,answer,partialResponse);}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){send(emitter,done,[DONE]);emitter.complete();}OverridepublicvoidonError(Throwableerror){send(emitter,error,error.getMessage());emitter.completeWithError(error);}});returnemitter;}privatevoidsend(SseEmitteremitter,StringeventName,Stringdata){try{emitter.send(SseEmitter.event().name(eventName).data(data));}catch(IOExceptione){emitter.completeWithError(e);}}8.2 WebFlux 写法GetMapping(value/chat-flux,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxServerSentEventStringchatFlux(RequestParamStringmessage){returnFlux.create(sink-{streamingChatModel.chat(message,newStreamingChatResponseHandler(){OverridepublicvoidonPartialThinking(PartialThinkingpartialThinking){sink.next(ServerSentEvent.builder(partialThinking.text()).event(thinking).build());}OverridepublicvoidonPartialResponse(StringpartialResponse){sink.next(ServerSentEvent.builder(partialResponse).event(answer).build());}OverridepublicvoidonCompleteResponse(ChatResponsecompleteResponse){sink.next(ServerSentEvent.builder([DONE]).event(done).build());sink.complete();}OverridepublicvoidonError(Throwableerror){sink.error(error);}});sink.onCancel(()-{System.out.println(前端断开连接);});});}9. 用哪个好推荐一普通 Spring Boot 项目用 SseEmitter如果你的项目是这种dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency那建议用SseEmitter原因简单和 MVC 项目兼容好不用引入 WebFlux 思维和 LangChain4j 回调模型搭配自然对大多数 AI 聊天系统已经够用适合公司内部 AI 助手知识库问答普通客服机器人后台管理系统里的 AI 功能用户量不是特别夸张的应用推荐二项目本来就是 WebFlux用 Flux如果你的项目本身是spring-boot-starter-webflux那就直接用FluxServerSentEventString不要为了用SseEmitter再切回 MVC。适合全链路响应式项目高并发长连接大量用户同时流式聊天网关层、BFF 层响应式数据库、响应式 Redis、响应式 HTTP Client 都已经在用推荐三不要为了“看起来高级”强行上 WebFlux很多人会觉得WebFlux 高性能这个理解不完全对。WebFlux 的优势要发挥出来需要你的整个调用链路都尽量非阻塞。如果你 WebFlux 里面还是大量调用阻塞代码比如MyBatis 阻塞查询普通 RedisTemplate阻塞 HTTP Client本地文件阻塞读写同步调用第三方接口那 WebFlux 的优势会被削弱甚至代码还更复杂。10. 性能角度怎么选简单理解SseEmitter中小并发开发简单够用比如几十个并发流几百个并发流公司内部使用业务系统内嵌 AI 功能SseEmitter一般没问题。WebFlux大量长连接更适合比如上千甚至更多并发 SSE每个连接持续几十秒甚至几分钟资源利用率要求高你愿意使用响应式技术栈可以考虑 WebFlux。11. 一个重要提醒不要随便混用 MVC 和 WebFluxSpring Boot 里如果同时引入spring-boot-starter-web spring-boot-starter-webflux默认情况下Spring Boot 通常会优先以 Spring MVC 模式启动。这会导致有些人以为自己用了 WebFlux但其实应用还是 MVC 模式。如果你想真正使用 WebFlux一般只引入spring-boot-starter-webflux如果你是普通 MVC 项目一般只引入spring-boot-starter-web不要两个都乱加。12. 最实际的建议你现在的情况是Spring Boot 后台通过接口返回 AI 流式内容前端接收并显示还包括思考内容。我的建议是第一阶段先用 SseEmitter先把功能跑通LangChain4j StreamingChatModel StreamingChatResponseHandler SseEmitter 前端 EventSource这样最容易理解整个流程。第二阶段如果遇到性能瓶颈再考虑 WebFlux如果后面发现并发连接很多Tomcat 线程压力大SSE 长连接很多需要更强的非阻塞能力再升级为WebFlux FluxServerSentEventString13. 最终选择表你的情况推荐普通 Spring Boot MVC 项目SseEmitter已经用了spring-boot-starter-webSseEmitter想快速实现 ChatGPT 式流式输出SseEmitter刚学习 LangChain4jSseEmitter项目本来就是 WebFluxFluxServerSentEvent?大量长连接高并发WebFlux团队熟悉响应式编程WebFlux项目里大量阻塞调用不建议强行 WebFlux14. 简单总结可以这么记SseEmitter 传统 Spring MVC 里的 SSE 工具简单直接适合大多数业务系统。 WebFlux 响应式流式方案更适合高并发、全链路非阻塞但学习和维护成本更高。对于你当前学习 LangChain4j 和 Spring Boot AI 流式输出先用 SseEmitter 更合适。等你把流式响应、思考内容、正式回答、前端 EventSource 都跑通之后再学习 WebFlux。