AI 每日学习 — Spring Boot 中基于 SSE 的大模型流式响应:从协议到生产落地
2026-07-15 每日技术学习今日主题Spring Boot 中基于 SSE 的大模型流式响应从协议到生产落地所属领域Java AI / 网络为什么后端架构师需要掌握它流式输出Streaming是所有 Chat 类、AI 助手、代码助手产品的标配体验。从协议层面HTTP/1.1 chunked → SSE → WebSocket到 Java 侧Servlet 3.1 async → WebFlux → WebClient再到生产问题首 Token 延迟、背压、用户断网、超时控制、Token 计量、可观测性任何一个细节没处理好都会让 AI 产品的体验从惊艳变成难用。这是把调用大模型 API变成生产级 AI 应用的第一道工程门槛。1. 核心问题学完后需要回答以下 5 个问题为什么 LLM 推理必须用流式响应一次性返回和流式返回在体验、成本、架构上有什么本质差异SSEServer-Sent Events和 WebSocket 在大模型场景下分别适合什么场景为什么大多数 Chat 产品默认选 SSESpring MVCServlet 3.1 async和 Spring WebFlux 两种栈实现 SSE 有什么区别在团队技术栈统一时该如何选择流式响应中的半截响应“用户断网”“服务端超时”Token 超限这些异常如何在 Java 侧被优雅处理流式场景下Token 计量、日志追踪、限流、熔断该怎么设计和普通 HTTP 接口的差异在哪里2. 前置知识理解本主题只需掌握以下基础本文不再展开HTTP/1.1 基础请求/响应模型、Content-Type、Transfer-Encoding: chunked。Servlet 3.1 异步处理AsyncContext、startAsync()的基本概念不要求精通。Spring Boot Web知道RestController、GetMapping即可。大模型 API 基本概念知道 Chat Completion 有streamtrue参数即可不需要了解训练原理。响应式编程基础理解 Mono/Flux 是数据流而不是集合即可。3. 原理讲解3.1 为什么必须流式大模型推理是逐 Token 自回归生成每生成一个 Token都依赖之前所有 Token。70B 级别模型生成 1000 字回答可能需要 5~15 秒。如果走一次性返回客户端 ──请求── 服务端 ──[5~15 秒沉默]── 返回完整 JSON用户在前 5 秒看不到任何东西感知延迟 总耗时。一旦网络中断前面 12 秒的等待全部作废。服务端必须缓存完整结果内存压力 并发数 × 单次响应大小。无法做打字机效果体验差。如果走流式返回客户端 ──请求── 服务端 ──data: xxxx\n\n── data: yyyy\n\n── ... ── data: [DONE]\n\n ↑ 首 Token 延迟通常 200~800ms用户几乎立刻看到第一个字感知延迟 ≈ 首 Token 延迟。中途断网只损失未送达部分可重连续传SSE 协议支持Last-Event-ID但 LLM 场景一般不续传而是允许重发。服务端边生成边推送内存占用低。支持打字机效果符合人类对话习惯。3.2 SSE vs WebSocket维度SSEWebSocket协议HTTP/1.1 长连接独立升级协议Upgrade: websocket方向单向服务端 → 客户端双向浏览器支持全部现代浏览器EventSourceAPI全部现代浏览器自动重连浏览器原生支持需手写代理/防火墙友好非常友好仍是 HTTP一般部分代理不支持 Upgrade中间件支持Nginx 默认支持需关闭缓冲需显式配置 Upgrade 头适合场景服务端单向推送AI 回答、通知、日志双向实时交互IM、协作编辑、游戏架构师结论Chat 类、AI 助手、代码补全 99% 用 SSE 就够了。只有AI 协作白板语音实时对话这种双向高频才需要 WebSocket。3.3 SSE 协议格式SSE 非常简洁本质是带特殊前缀的 chunked 响应HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive X-Accel-Buffering: no data: {choices:[{delta:{content:你}}]} data: {choices:[{delta:{content:好}}]} data: {choices:[{delta:{content:}}]} data: [DONE]要点每个事件以data:开头两个换行\n\n表示事件结束。data: [DONE]是 OpenAI 风格的结束标记不同厂商可能不同。X-Accel-Buffering: no是告诉 Nginx不要缓冲否则客户端要等缓冲区满才看到内容。Cache-Control: no-cache防止 CDN 缓存。Content-Type 必须是text/event-stream。3.4 Java 侧实现路线在 Spring Boot 中实现 SSE 有两条路线路线 ASpring MVC Servlet 3.1 异步推荐用于已有 Spring MVC 项目使用SseEmitterSpring 内置零额外依赖。兼容团队现有技术栈、线程模型、过滤器链路。适合从 0 到 1 接入 LLM。路线 BSpring WebFlux推荐用于新建项目或高并发 AI 网关使用FluxServerSentEventString返回值。天然背压、线程开销低、适合高并发。学习曲线较陡团队需要熟悉响应式编程。架构选择标准团队没有响应式经验 →选 A。系统已经是 WebFlux 栈 → 选 B。并发要求 5000 持续流式连接 → 认真评估 B并压测验证。混合场景同一项目里既有传统接口又有流式接口→ 选 A统一 MVC 即可。3.5 容易踩的坑Nginx 缓冲默认proxy_buffering on会把 SSE 攒满 4KB 才发给客户端 → 关闭proxy_buffering off;。反向代理超时默认proxy_read_timeout 60s流式响应经常超时 → 改为300s或更大。Spring 线程池耗尽Tomcat 默认最大 200 线程每个 SSE 连接占一个线程直到结束 → 必须配置spring.mvc.async.request-timeout和合理的线程池。客户端断网不感知浏览器关闭页面时SSE 连接不会立即断开需要心跳保活。首 Token 延迟被埋总耗时看着正常但首 Token 1 秒后才到原因是 prompt 太大或服务端在拼上下文。4. Java 工程实践4.1 完整代码示例Spring MVC SseEmitter WebClientMaven 依赖Spring Boot 3.2dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-webflux/artifactId!-- 用 WebClient --/dependencyController把大模型流式响应原样转发给前端RestControllerRequestMapping(/ai)RequiredArgsConstructorpublicclassChatController{privatefinalChatStreamServicechatStreamService;/** * 客户端浏览器/APP调用此接口 * 接收POST application/json返回text/event-stream */PostMapping(value/chat/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstreamChat(RequestBodyChatRequestrequest,RequestHeader(valueX-User-Id)StringuserId){// 1. 设置超时5 分钟覆盖最长推理时间SseEmitteremitternewSseEmitter(5*60*1000L);// 2. 客户端断开时清理资源关键emitter.onCompletion(()-log.info(SSE completed, userId{},userId));emitter.onTimeout(()-{log.warn(SSE timeout, userId{},userId);emitter.complete();});emitter.onError(ex-log.error(SSE error, userId{},userId,ex));// 3. 异步调用大模型并推送chatStreamService.streamToEmitter(request,userId,emitter);returnemitter;}}Service调用 OpenAI 兼容协议并流式推送ServiceRequiredArgsConstructorpublicclassChatStreamService{privatefinalWebClientopenAIClient;// 注入配置好的 WebClientprivatefinalTokenMetertokenMeter;// 你的 Token 计量组件privatefinalChatHistoryRepositoryhistoryRepository;// 持久化对话历史publicvoidstreamToEmitter(ChatRequestrequest,StringuserId,SseEmitteremitter){// 1. 构造大模型请求MapString,ObjectbodyMap.of(model,request.getModel(),stream,true,messages,request.getMessages());// 2. 累积完整回答用于持久化 Token 统计StringBuilderfullAnswernewStringBuilder();// 3. 订阅大模型的 FluxString每个元素是一个 SSE data 行openAIClient.post().uri(/v1/chat/completions).bodyValue(body).retrieve().bodyToFlux(String.class).filter(line-line.startsWith(data:)).map(line-line.substring(5).trim()).filter(data-![DONE].equals(data)).doOnNext(data-{try{// 3.1 解析 delta.contentJsonNodedeltaobjectMapper.readTree(data).path(choices).path(0).path(delta).path(content);if(!delta.isMissingNode()!delta.isNull()){Stringtokendelta.asText();fullAnswer.append(token);// 3.2 推送到前端emitter.send(SseEmitter.event().name(message).data(token));}}catch(Exceptione){log.error(Failed to parse stream chunk,e);emitter.completeWithError(e);}}).doOnComplete(()-{// 4. 完整响应结束持久化 Token 计量try{emitter.send(SseEmitter.event().name(done).data());emitter.complete();historyRepository.save(userId,request,fullAnswer.toString());tokenMeter.record(userId,request.getModel(),fullAnswer.length()/2);// 粗估}catch(Exceptione){log.error(Failed to finalize stream,e);}}).doOnError(ex-{log.error(LLM stream error, userId{},userId,ex);try{emitter.send(SseEmitter.event().name(error).data(AI 服务暂时不可用));emitter.completeWithError(ex);}catch(IOExceptionignored){}}).subscribe();// 必须订阅才会触发}}WebClient 配置重要ConfigurationpublicclassWebClientConfig{BeanpublicWebClientopenAIClient(Value(${llm.base-url})StringbaseUrl,Value(${llm.api-key})StringapiKey){returnWebClient.builder().baseUrl(baseUrl).defaultHeader(HttpHeaders.AUTHORIZATION,Bearer apiKey).clientConnector(newReactorClientHttpConnector(HttpClient.create().responseTimeout(Duration.ofMinutes(5))// 推理可能很久.keepAlive(true))).codecs(c-c.defaultCodecs().maxInMemorySize(1024*1024)).build();}}4.2 关键配置项配置默认值推荐值风险spring.mvc.async.request-timeout30s300000 (5min)太短会提前断流server.tomcat.threads.max200400~800SSE 占用线程需按并发量估算proxy_buffering(Nginx)onoff不关则客户端看不到打字机效果proxy_read_timeout(Nginx)60s300s太短会中途断开responseTimeout(WebClient)无5min覆盖 LLM 最长推理时间4.3 推荐的异常处理与日志traceId 贯穿用 MDC 把traceId注入到大模型请求和大模型响应日志方便排查。分级日志首 Token 延迟、每 100 Token 一次进度日志、结束时的总耗时和 Token 数。结构化日志JSON 格式包含userId、model、promptTokens、completionTokens、durationMs、firstTokenLatencyMs。客户端断网通过 SseEmitter 的onCompletion/onError回调清理大模型侧的连接调用其 cancel。超时分层连接超时10s、首 Token 超时30s、整体超时5min三者必须独立设置。5. 生产环境案例案例AI 客服上线首日“打字机效果变成整段刷新”业务背景某电商接入 GPT-4 做智能客服前端是 Web 页面。问题表现用户输入问题后页面空白 8~10 秒然后突然一次性显示整段回答。部分用户反馈页面卡死。服务端 CPU 不高但 Tomcat 线程数被打满。可能原因Nginx 开了proxy_buffering把 SSE 攒满 4KB 才下发。Tomcat 线程被打满导致后续请求排队。大模型本身不是流式调用忘记传streamtrue。排查步骤直接curl -N调用 AI 接口 → 发现数据是分批返回的 → 排除大模型侧。浏览器 F12 看 Network → 发现响应头是Transfer-Encoding: chunked但 content 不是text/event-stream→SseEmitter 没生效仔细看代码发现 Controller 写的是produces MediaType.APPLICATION_JSON_VALUE→SSE 退化成普通 chunked JSON 数组。同时检查 Nginx 配置 →proxy_buffering on是默认开启的。检查 Tomcat 线程数 → 200 个线程被 200 个未释放的 SSE 连接占满。解决方案修正produces MediaType.TEXT_EVENT_STREAM_VALUE。Nginx 关闭缓冲proxy_buffering off; proxy_read_timeout 300s;。Tomcat 线程数从 200 提到 500。加入心跳每 15 秒发一个: ping\n\nSSE 注释行浏览器会忽略防止中间网络设备超时断开。验证方式curl -N看到一行一行返回。浏览器 F12 看 Network → Response 是event-stream类型且 0.5 秒内出现第一条数据。压测 500 并发持续流式Tomcat 线程数稳定在 500 附近且无 OOM。如何避免再次发生接入 AI 流式接口的 checklist 必须包含 SSE 头部、Nginx 配置、超时配置。性能测试用例必须包含持续 30 分钟 100 并发流式场景。监控指标必须包含ai_first_token_latency_ms、ai_stream_active_connections。6. 技术选型与权衡6.1 优点流式 SSE用户体验优秀首 Token 延迟低感知流畅。资源利用率高边生成边推送内存压力小。协议简单HTTP 即可调试方便curl -N 就能看。浏览器支持好原生EventSourceAPI无需第三方库。6.2 缺点服务端线程模型挑战每个连接长期占用一个线程。中间件兼容需要 Nginx、网关、CDN 都正确配置。双向通信不行客户端中途想取消或修改问题需要走另外的 HTTP 接口。重连后无法续传LLM 是无状态推理重连通常意味着从头开始不像视频流可以续传字节。6.3 适用场景Chat 类应用网页聊天、客服、AI 助手。代码补全、AI 搜索、文档问答。任何长文本生成 用户等待的场景。6.4 不适用场景极短响应 200ms流式开销不划算普通 JSON 即可。双向实时交互语音对讲、协作编辑用 WebSocket。移动端弱网且需要离线缓存流式不利于缓存。6.5 替代方案WebSocket需要双向时才考虑。HTTP/2 gRPC streaming微服务内部、跨服务流式调用比 SSE 更高效但前端集成成本高。长轮询Long Polling老式兼容方案移动端老系统才用。6.6 AI 主题额外权衡云端模型 vs 私有化私有化部署 Ollama/vLLM 时Nginx 反向代理配置同样关键。大模型 vs 小模型小模型首 Token 更快200ms 以内流式的相对收益小一些但用户感知差异小。是否保留人工确认客服、医生等高风险场景必须在 SSE 推送完毕后再由人工审核。7. 常见错误错误认知“SSE 就是 WebSocket 的简化版”——SSE 是 HTTP 之上的单向协议WebSocket 是独立协议选哪个取决于是否需要双向。Nginx 默认配置上线SSE 必须关闭proxy_buffering、调大proxy_read_timeout否则等于没优化。默认配置就是错的。不设置超时SseEmitter不设超时默认 30 秒推理稍长就断开。每个值都要根据业务显式设置。忘记清理资源客户端断开后大模型侧的连接没取消 → Token 仍然在扣费 → 资损。必须在onCompletion/onError中 cancel 上游。没有首 Token 延迟监控只监控总耗时首 Token 5 秒才到用户看不到体验崩塌但 P99 看着正常。必须独立打点。线程池配置不足SSE 连接长期占线程Tomcat 默认 200 线程根本不够。按峰值并发 × 平均连接时长估算。缺乏背压机制客户端网络慢时WebFlux 背压会丢数据。需要监控onBackpressureBuffer警告。8. 架构师视角总结什么时候用任何 LLM 长文本输出场景。团队能接受 SSE 的中间件配置和线程模型。什么时候不用响应时间 200ms 的短请求。双向实时交互用 WebSocket。客户端是不支持 SSE 的老 IE/老 App极少。评审方案时重点问流式响应的超时设置是多少分几层中间件Nginx/网关/CDN的缓冲和超时配置改了吗线程池容量是否按峰值并发 × 平均连接时长估算首 Token 延迟如何监控告警阈值是什么客户端断网时大模型侧的资源如何释放谁来 cancel故障时先检查大模型侧是否真的流式streamtrue→curl -N验证。Nginx/网关是否关闭缓冲Tomcat 线程是否被打满→/actuator/metrics/executor.active。首 Token 延迟是不是过高→ 看埋点。大模型供应商是否限流或故障→ 多供应商切换。必须监控的指标ai_first_token_latency_msP50/P95/P99ai_stream_total_duration_msai_stream_active_connectionsai_stream_error_rate按错误类型分类超时/解析失败/网络中断/限流ai_stream_token_usage按 user/model 维度必须提前设计兜底大模型供应商故障 → 多供应商 智能路由。客户端断网 → 大模型侧连接清理 不重试。长推理超时 → 渐进式超时首 Token 30s整体 5min。用户输入超长 Prompt → 提前截断 提示用户。AI 主题额外该功能是否真的需要大模型短回答规则引擎是否更便宜稳定模型失败后系统如何降级→ 静态兜底文案 / 传统检索 / 人工客服。如何控制模型不确定性→ 输出后做格式校验、敏感词过滤。如何控制成本→ 按用户/租户限流、缓存相似问题、上下文压缩。如何控制数据权限→ Prompt 注入过滤、用户身份携带、知识库租户隔离。9. 今日练习题目先不要看答案1. 概念理解题为什么 ChatGPT 网页版使用 SSE 而不是 WebSocket反过来什么场景下你应该选 WebSocket请列出至少 2 个判断维度。2. 生产故障分析题某天 AI 助手的 P99 首 Token 延迟从 800ms 飙升到 6 秒但 P99 总耗时只增加了 1 秒从 8 秒到 9 秒。最可能的 3 个原因是什么分别如何排查3. 系统设计题请设计一个统一 AI 流式网关要求支持接入多家大模型供应商OpenAI、DeepSeek、Anthropic、自研模型支持 SSE 透传到前端支持按用户/租户限流如每用户 60 req/min支持失败自动切换供应商记录首 Token 延迟、Token 用量等关键指标请画出架构图组件 数据流并说明关键设计取舍。4. Java 实践题在 Spring Boot 3 项目中实现一个/ai/chat/stream接口输入{model:gpt-4,messages:[{role:user,content:你好}]}输出SSE 格式逐 Token 推送大模型回答要求处理客户端断连、处理大模型超时、记录首 Token 延迟到 Micrometer完成后用curl -N -X POST http://localhost:8080/ai/chat/stream -H Content-Type: application/json -d {...}验证。参考答案先独立思考再来看答案 1概念理解判断维度通信方向单向服务端→客户端选 SSE双向选 WebSocket。LLM 场景是问一次、答一次SSE 足够。协议兼容SSE 是 HTTP无需特殊代理支持WebSocket 需要 Upgrade 头部分企业代理会拦截。SSE 更适合 ToB 场景。自动重连浏览器原生EventSource自动重连WebSocket 需要手写重连逻辑。服务器实现复杂度SSE 用普通 HTTP 框架即可WebSocket 需要异步 I/O。选 WebSocket 的场景多人协作编辑、实时语音/视频、AI 协作白板用户和 AI 互相打断。答案 2故障分析最可能 3 个原因大模型供应商侧拥塞上游推理服务排队。→ 排查看供应商 dashboard、看其他调用方是否也慢、对比不同供应商的延迟。Prompt 上下文过大每次请求带了过多历史消息导致 prefill 阶段慢。→ 排查打点prompt_tokensP95看是否暴增检查是否有上下文压缩逻辑失效。本地线程池/连接池耗尽WebClient 连接池或 Tomcat 线程池不够新请求在排队。→ 排查看http.client.requestsactive 数、看 Tomcatthreads.busy。答案 3系统设计架构图┌────────────┐ │ Browser │ └─────┬──────┘ │ SSE ▼ ┌──────────────────┐ │ API Gateway │ (限流、鉴权、灰度) └─────────┬────────┘ │ ▼ ┌──────────────────┐ │ AI Gateway │ ← 统一 SSE 输出 │ ┌──────────────┐ │ │ │ 限流(Redis) │ │ │ │ 路由(规则) │ │ → 供应商 A (OpenAI) │ │ 熔断(Resilience4j) │ │ → 供应商 B (DeepSeek) │ │ 监控(Micrometer) │ │ → 供应商 C (自研) │ └──────────────┘ │ └──────────────────┘关键取舍路由策略默认按模型名路由gpt-4 → OpenAI故障时按错误率切换按用户等级分配VIP 走更好的模型。限流位置网关层 Redis 滑动窗口按 userIdtenantId 限流。熔断Resilience4j5 秒内错误率 50% 触发熔断 30 秒。成本控制Token 计量写到 Kafka下游对账。统一 SSE无论后端是什么协议HTTP chunked、SSE、WebSocket网关对外统一 SSE。答案 4Java 实践参考上面的 Controller Service 代码关键点produces TEXT_EVENT_STREAM_VALUE不能少。SseEmitter必须设置超时和onCompletion/onError回调。WebClient 的responseTimeout要 ≥ SseEmitter 超时。首 Token 延迟通过System.nanoTime()在第一个doOnNext中打点。客户端断连时emitter.onCompletion触发需要调用 WebClient 的 subscription dispose。10. 延伸学习后续推荐学习按依赖顺序Spring AI Streaming API官方文档Spring 官方对 LLM 流式响应的封装比手写 WebClient 更优雅。掌握StreamingChatClient、ChatResponse流式版本。WebFlux 响应式 SSESpring 官方文档理解FluxServerSentEventT的背压机制对比 MVC 的SseEmitter。Nginx 反向代理与 SSE 调优Nginx 官方文档深入理解proxy_buffering、proxy_cache、gzip对 SSE 的影响gzip 会破坏 SSE 格式。Resilience4j 熔断在流式场景的应用官方文档流式响应的中途失败如何被熔断器识别和处理。关联说明Spring AI 是今日手写 WebClient 的工业级封装学完能少写很多样板代码。WebFlux 是更高并发的替代方案10K 并发以上必须评估。Nginx 调优是上线的最后一公里本地测试没问题不代表生产没问题。熔断是 AI 网关的必备能力明天会进一步展开。11. 今日知识卡片≤200 字核心原理LLM 自回归生成是逐 Token 长任务SSE 让服务端边生成边推送把感知延迟从总耗时降到首 Token 延迟通常 200~800ms。生产实践建议SSE 三件套必做——(1)produces text/event-stream不能少(2) Nginx 关闭proxy_buffering并调大proxy_read_timeout到 300s(3) 客户端断连必须在onCompletion中 cancel 大模型侧连接否则 Token 仍扣费。常见风险Tomcat 线程数默认 200被持续 SSE 连接耗尽导致全站 502。必须按峰值并发 × 平均连接时长估算线程池。架构设计判断标准单向长文本推送选 SSE只有双向实时才选 WebSocket。SSE 不是弱化版 WebSocket而是为单向流式而生的 HTTP 协议。