业务背景用户在页面输入提示词后端调用大模型并实时流式输出生成内容前端边接收边渲染显著提升“可感知进度”的体验。技术核心SSE/流式输出一旦穿过Nginx → Gateway → 后端服务的多级转发链路就会遇到超时、缓冲、断流、重连与可观测性等工程问题。本文目标用“问题画像 → 根因拆解 → 分层治理 → 验收清单”的结构把这类问题沉淀成可复用的解决方案。1. 为什么选 SSE流式输出大模型生成天然是一个“逐 token/逐片段产出”的过程。如果后端等全部生成完再返回用户体感会变成“长时间无响应 → 突然出现一大段”失败/超时也更难提前感知SSE/流式输出的优势体验更好边生成边展示用户知道系统在工作实现成本可控基于 HTTP不必引入完整 WebSocket 体系更容易做中途取消用户点击停止即可中止生成注意本文的“流式”既包含标准 SSEtext/event-stream也包含“按 SSE 协议格式输出但前端用fetch ReadableStream读取”的实现方式。2. 真实链路长什么样哪里最容易出问题flowchart LR A[Browserbr/fetch/ReadableStream 或 EventSource] --|/api/...| N[Nginx] N -- G[Spring Cloud Gateway] G -- B[Backend Servicebr/LLM Streaming] B --|stream| G --|stream| N --|stream| A“断流/卡顿”通常出在Nginx/Gateway 的默认超时与缓冲而不是业务代码本身。3. 常见故障3.1 固定时间必断比如 60s/300s/600s现象连接建立后规律性断开刷新/重试立刻恢复高概率根因某一层的read_timeout / response-timeout / idle timeout生效3.2 后端日志在写前端不刷新过一会儿突然一大段一起出现现象生成“看似卡住”但最终一次性刷出高概率根因缓冲buffering尤其是 Nginx 默认缓冲上游响应3.3 偶发断流 自动重连后出现重复/缺字/顺序错乱现象用户看到内容重复、缺片段或乱序高概率根因缺少事件id/断点续传语义或前端没有做幂等去重4. 关键结论流式稳定性取决于“分层治理”建议按Nginx → Gateway → 后端 → 前端 → 可观测的顺序落地。5. Nginx 层治理最关键关闭缓冲 拉长超时下面这段是典型 SSE location 的关键配置要点IP/域名需替换为你们环境的真实值location ^~ /api/agent/sse/ { proxy_pass http://gateway_or_upstream; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 避免错误的 Connection 头影响长连接 proxy_set_header Connection ; # 关键关闭缓冲与缓存否则会“攒一大段才吐给前端” proxy_buffering off; proxy_cache off; add_header X-Accel-Buffering no; # 关键读写超时拉长SSE 可能持续很久 proxy_read_timeout 3600s; proxy_send_timeout 3600s; send_timeout 3600s; keepalive_timeout 3600s; # 是否忽略客户端断开业务上要谨慎评估 proxy_ignore_client_abort on; }5.1 为什么proxy_buffering off必配因为 SSE/流式输出的价值就是“实时刷新”。一旦 Nginx 缓冲上游虽然在持续写入但数据会被积累在 Nginx 里前端只能“延迟批量看到”体验直接退化。5.2proxy_ignore_client_abort on的取舍它会让 Nginx 在客户端断开后仍继续读上游数据。对 LLM 生成类场景优点避免偶发网络抖动导致上游生成被意外打断风险客户端已经不需要了上游仍在耗费算力/资源建议做法如果开启该项后端要配合实现“取消”能力Abort/Stop并设置合理的最大生成时长/最大 token避免资源泄漏。6. Gateway 层治理为 SSE 单独开路由策略6.1 给 SSE 路由配置“无限响应超时”网关通常对响应有超时保护但 SSE 是“持续响应”对 SSE 必须放开。spring: cloud: gateway: routes: - id: agent-backend-sse uri: ${BACKEND_URL:http://localhost:8085} predicates: - Path/api/agent/sse/** metadata: response-timeout: -1 # 关键避免网关超时切断流6.2 避免在 SSE 路由上使用“读 body 的过滤器”凡是需要完整响应体才能工作的功能在 SSE 上都可能造成直接阻塞永远等不到响应结束缓冲导致不实时内存不断增长典型风险点统一响应封装 / 响应体 rewrite记录完整响应 body 的日志需要聚合/压缩的过滤器需谨慎评估是否引入缓冲7. 后端服务层SSE 的“三件套”7.1 正确的输出格式最容易被忽略建议输出符合 SSE 约定的帧每条事件以空行结束data: {text:第一段} data: {text:第二段} data: [DONE]并确保响应头正确Content-Type: text/event-stream; charsetutf-8Cache-Control: no-cache7.2 心跳heartbeat防止“空闲超时”很多中间层按“连接空闲”回收连接。解决办法每 10–30 秒发一次心跳注释帧或业务帧均可7.3 断点续传与幂等可选但强烈建议更稳定的方式是给每条事件加idid: 101 data: {text:...}浏览器/客户端重连后会携带Last-Event-ID服务端可从该位置继续推。即便做不到精确续传也建议前端按chunkId/tokenIndex做去重后端在同一次生成会话中保持输出顺序与唯一性8. 前端实现为什么很多团队不用 EventSource而用 fetch stream在真实项目里常见约束是请求需要带Authorization、X-Tenant-ID、刷新 token 等自定义 Header需要POST提交用户提示词与上下文需要可控的Abort中止生成标准EventSource不支持自定义 header也不支持 POST。因此很多团队会采用fetch(POST)ReadableStream.getReader()自己读流一个最小的读取逻辑示意const res await fetch(url, { method: POST, headers, body }); const reader res.body!.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() ?? ; for (const line of lines) { if (!line.startsWith(data:)) continue; const payload line.slice(5).trim(); if (payload [DONE]) return; // 解析 JSON 或当纯文本追加 } }8.1 前端需要补齐的“稳定性能力”仅能读流还不够建议补齐重试/重连带指数退避与上限避免抖动时雪崩幂等去重重连后避免重复输出中止AbortController 立刻停止并通知后端取消9. 可观测性不要记录每个 token要记录“连接生命周期”SSE 最常见的排障痛点是断了但不知道谁断的、为什么断的。9.1 建议的日志/指标在网关和后端都建议记录连接建立requestId/traceId、path、tenant脱敏、用户脱敏连接断开持续时长、事件数/字节数、最后一个 chunk id若有、断开原因分类9.2 脱敏底线不要在日志出现token、真实用户输入提示词原文、模型输出全文、真实 IP/域名、真实租户/客户名称 可以保留字符数、耗时、事件数、错误码、阶段标记10. 验收清单不断流连续 30–120 分钟稳定不断实时性后端持续写入时前端能稳定“边来边显示”不成批刷出取消生效前端点击停止后链路能及时释放资源网关/后端都不继续耗算力异常可定位能回答“是谁断的、为什么断、断前推了多少”高并发可控连接数上升时网关与后端 CPU/内存/连接数/FD 不爆11. 常见坑速查只放开了 Gateway 超时Nginx 仍在 buffering → 仍然“卡住不刷新”没有心跳 → 线上按固定 idle timeout 断只做了前端重连没做幂等 → 断了之后重复输出开启proxy_ignore_client_abort on但后端不支持取消 → 算力被“幽灵请求”吃掉日志打印 token/提示词/输出全文 → 成本爆炸 安全风险12. 小结LLM 流式输出的体验提升很大但前提是把 SSE 当成“基础设施能力”治理Nginx 关闭缓冲 超时拉长、Gateway 对 SSE 路由放开 response-timeout、后端补齐心跳/结束语义、前端支持中止与重试、再加上连接生命周期可观测才能在生产长期稳定运行。