1. 为什么需要流式响应第一次对接大模型API时我被返回的完整JSON结果震惊了——用户需要等待十几秒才能看到回复。这种体验就像等待下载完整个视频才能播放显然不符合对话场景的自然交互。后来发现流式响应才是更优雅的解决方案。传统HTTP请求就像点外卖下单后要等全部菜品做好一起送来。而流式响应更像是回转寿司——数据像传送带上的寿司一样持续送达。这种来一点处理一点的模式特别适合大模型生成文本的场景。实测使用SSE协议实现流式传输后响应延迟从原来的5秒降低到300毫秒内。前端不再需要频繁轮询服务器压力降低60%。更重要的是用户看到文字逐个出现的效果就像有人在实时打字交流这种体验提升是质的飞跃。2. WebFlux与SSE技术选型2.1 传统方案的三大痛点早期我用Spring MVCWebSocket实现过类似功能踩过几个深坑线程阻塞每个WebSocket连接占用一个线程200并发时Tomcat直接崩溃协议复杂需要处理握手、心跳等底层细节代码量增加30%过度设计简单的文本推送场景用WebSocket就像用大炮打蚊子相比之下WebFluxSSE组合的优势很明显非阻塞IO单线程可处理上万并发连接轻量协议SSE基于普通HTTP无需特殊处理自动重连浏览器默认支持断线重连机制2.2 关键技术组件解析核心依赖其实非常简单dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency关键对象说明Flux反应式流的核心载体可以想象成会持续冒泡的数据泉水SseEmitterSSE协议的Spring封装自动处理事件流格式WebClient非阻塞的HTTP客户端用于调用大模型API3. 完整实现步骤3.1 后端核心代码实现Controller层的关键配置PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString streamChat(RequestBody ChatRequest request) { return chatService.streamResponse(request); }注意两个关键点必须设置TEXT_EVENT_STREAM_VALUE作为produces类型返回类型必须是FluxT而不是常规的MonoTService层的流式处理public FluxString streamResponse(ChatRequest request) { return WebClient.create() .post() .uri(API_ENDPOINT) .bodyValue(request.toJson()) .retrieve() .bodyToFlux(String.class) .map(this::processChunk); }这里有个实用技巧用map操作符实时处理每个数据块时建议加上超时控制.timeout(Duration.ofSeconds(30)) .onErrorResume(e - Flux.just(服务超时请重试));3.2 前端对接要点现代浏览器原生支持EventSource APIconst eventSource new EventSource(/api/stream); eventSource.onmessage (event) { const data JSON.parse(event.data); // 逐字渲染到页面 outputEl.innerHTML data.content; };但在实际项目中我推荐使用更强大的fetch-event-source库import { fetchEventSource } from microsoft/fetch-event-source; await fetchEventSource(/api/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question: inputValue }), onmessage(event) { // 处理增量数据 } });这个库支持POST请求和自定义header比原生API灵活得多。4. 性能优化实战经验4.1 网关配置避坑指南在Nginx环境中遇到过典型的缓冲问题location /api/ { proxy_pass http://backend; proxy_buffering off; # 关键配置 proxy_cache off; }如果不关闭proxy_buffering会出现数据积压后突然爆发式输出的情况完全破坏了打字机效果。4.2 背压处理技巧当生产速度大于消费速度时需要处理背压问题。这是我常用的处理方案.flatMap(chunk - Mono.fromCallable(() - process(chunk)) .subscribeOn(Schedulers.boundedElastic()), 5) // 最大并发数这个配置可以防止大模型生成速度过快导致的内存溢出。5. 高级应用场景5.1 多数据源合并实际项目中经常需要组合多个流FluxString modelStream getModelResponse(query); FluxString dbStream getDatabaseResults(query); return Flux.merge( modelStream.map(data - 模型 data), dbStream.map(data - 数据库 data) );5.2 断线续传实现通过Last-Event-ID头实现断点续传GetMapping(/resumable) public FluxString resumeStream( RequestHeader(value Last-Event-ID, required false) String lastId) { return repository.findSince(lastId) .map(Event::toString); }前端只需要保存最后收到的IDlet lastId 0; eventSource.onmessage (event) { lastId event.lastEventId; // ...处理数据... };这种机制在移动端网络不稳定的场景特别有用。