大模型流式输出与 SSE 推送:Spring Boot 中的实时响应架构设计
大模型流式输出与 SSE 推送Spring Boot 中的实时响应架构设计一、等待焦虑与超时风险大模型实时响应的工程困境大模型推理的延迟通常在数秒到数十秒之间传统 HTTP 请求-响应模式下客户端必须等待模型完成全部生成后才能收到响应。这种黑盒等待带来三个核心问题其一用户面对长时间白屏体验极差对话类场景的容忍阈值通常在 3 秒以内其二网关和负载均衡器的超时配置通常为 30-60 秒长文本生成极易触发超时断连其三服务端在生成期间持有连接资源高并发下线程池和连接池压力巨大。流式输出Streaming是解决上述问题的标准方案——模型每生成一个 Token 就立即推送给客户端用户看到逐字打印的效果。但流式输出在工程实现上并非简单的边生成边返回它涉及 SSE 协议适配、背压控制、异常恢复和资源回收等一系列问题。二、SSE 协议与流式推理架构从模型输出到客户端渲染Server-Sent EventsSSE是基于 HTTP 的单向推送协议与 WebSocket 相比更轻量天然适配大模型的服务端生成、客户端消费模式。Spring Boot 中通过SseEmitter或 WebFlux 的FluxServerSentEvent实现流式推送。sequenceDiagram participant C as 客户端 participant G as Spring Gateway participant S as LLM Service participant M as 大模型 API C-G: POST /chat/stream (SSE) G-S: 转发请求 S-M: 发起流式调用 loop Token 生成循环 M--S: Token Chunk S--G: SSE: data: {token} G--C: SSE: data: {token} end M--S: [DONE] 信号 S--G: SSE: data: [DONE] G--C: SSE: data: [DONE] C-C: 关闭 EventSource关键设计点在于流式透传——中间层不应缓冲完整响应再转发而应逐 Chunk 推送。Spring WebFlux 的响应式模型天然支持这种模式而传统 Servlet 模型需要借助异步 Servlet 或SseEmitter实现。三、生产级代码实现流式调用、背压控制与异常恢复3.1 基于 WebFlux 的流式调用Service public class StreamingLlmService { private final WebClient llmClient; private final TokenMeter tokenMeter; // 构造器注入省略 public FluxServerSentEventChatChunk streamChat(ChatRequest request) { return llmClient.post() .uri(/v1/chat/completions) .bodyValue(buildStreamRequest(request)) .retrieve() .bodyToFlux(String.class) .takeUntil(data - [DONE].equals(data)) .map(this::parseChunk) .map(chunk - ServerSentEvent.ChatChunkbuilder() .data(chunk) .build()) // 背压控制限制内存中缓冲的 Token 数 .onBackpressureBuffer(64, () - log.warn(背压缓冲区溢出丢弃最旧数据), BufferOverflowStrategy.DROP_OLDEST) .doOnNext(chunk - { if (chunk.data() ! null) { tokenMeter.record(chunk.data().getTokens()); } }) .doOnError(e - log.error(流式调用异常: {}, e.getMessage())) .doFinally(signal - log.info(流结束: signal{}, signal)); } }3.2 Servlet 模式下的 SseEmitter 方案RestController RequestMapping(/api/chat) public class ChatStreamController { private final StreamingLlmService llmService; GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamChat(RequestParam String message) { // 超时设置为 5 分钟覆盖长文本生成场景 SseEmitter emitter new SseEmitter(300_000L); llmService.streamChat(new ChatRequest(message)) .subscribe( event - { try { emitter.send(event); } catch (IOException e) { // 客户端断连取消上游订阅 emitter.completeWithError(e); } }, emitter::completeWithError, emitter::complete ); // 客户端主动断连时的清理逻辑 emitter.onCompletion(() - log.info(SSE 连接正常关闭)); emitter.onTimeout(() - { log.warn(SSE 连接超时); emitter.complete(); }); return emitter; } }3.3 异常恢复与重试public FluxServerSentEventChatChunk streamWithRetry(ChatRequest request) { return llmService.streamChat(request) // 单个 Chunk 解析失败不中断整个流 .onErrorResume(ParseException.class, e - { log.warn(Chunk 解析失败跳过: {}, e.getMessage()); return Flux.empty(); }) // 上游连接断开时携带上下文重试 .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) .filter(e - e instanceof WebClientException) .doBeforeRetry(signal - log.info( 流式重试: attempt{}, signal.totalRetries())) ); }四、流式架构的隐性代价背压、一致性与资源回收背压控制的精度权衡onBackpressureBuffer的缓冲区大小直接影响内存占用和延迟。缓冲区过小如 16会导致频繁丢数据客户端看到的文本出现跳跃缓冲区过大如 1024则在高并发场景下内存压力显著。生产环境建议根据下游消费速度动态调整或采用onBackpressureDrop策略配合客户端重连补全。SSE 的断连恢复局限SSE 协议本身不支持断点续传。一旦网络中断客户端只能重新发起请求而大模型无法从断点继续生成。工程上可以通过已生成文本回放策略缓解——客户端重连时携带已接收的 Token 列表服务端将其作为上下文前缀重新发起调用。但这会增加 Token 消耗和重复计算成本。Servlet 模式的线程占用SseEmitter虽然是异步的但在 Tomcat 默认配置下仍占用一个 Servlet 线程。当并发流式连接数超过线程池容量时新请求会被拒绝。WebFlux 基于事件循环模型单线程可处理数千并发连接是流式场景的首选方案。Token 计量的精度问题流式场景下Token 计量需要逐 Chunk 累加而非一次性获取。部分大模型 API 在流式响应中不返回 Token 使用量只能在流结束后通过单独的 API 查询增加了计量延迟和一致性风险。五、总结大模型流式输出将等待完整响应转化为逐 Token 推送本质上是将服务端的生成延迟分散到客户端的渐进渲染中。本文方案的核心链路为WebFlux 流式调用 → 背压控制 → SSE 推送 → 异常恢复。落地时需重点关注三个参数SseEmitter 超时时间建议 5 分钟起、背压缓冲区大小建议 64-128、重试次数建议 3 次。推荐优先采用 WebFlux 方案Servlet 模式仅用于无法升级的技术栈。上线前务必进行断连恢复的混沌测试验证客户端重连和服务端资源回收的可靠性。