AI对话流式输出实战:SSE、Markdown渲染与Nginx防粘连配置
1. 从“打字机”说起AI 对话流式输出的核心体验如果你用过 ChatGPT、Claude、文心一言或者任何一个 AI 对话产品你一定注意过那个细节AI 的回答不是“啪”一下整段蹦出来的而是一个字一个字往外“吐”像老式打字机一样。这个效果在圈内通常叫“打字机效果”也有人叫“流式输出”。它看起来只是个视觉小花招但背后牵扯的东西其实不少——前端怎么接、后端怎么推、中间代理怎么不捣乱、Markdown 怎么边收边渲染每一环都有坑。我最近刚好完整地做了一套 AI 对话的前后端链路从 SSE 流式接口到 Markdown 组件渲染再到 Nginx 反向代理的防粘连配置踩了一圈坑之后觉得有必要把这套东西完整地梳理一遍。这篇文章适合谁看如果你正在做 AI 对话类产品或者你只是好奇“为什么 AI 回答能一个字一个字出来”又或者你已经写了 SSE 接口但发现消息总是攒一批才到、Markdown 渲染总是闪烁、Nginx 一代理就变成“一次性返回”那这篇内容应该能帮你省下不少排查时间。核心关键词先摆出来SSE、Markdown、Nginx、流式、fetch-event-source。这几个词基本覆盖了整条链路的关键节点。我会从整体设计思路讲起然后逐个拆解核心细节再给出一套可以直接抄的实操方案最后把我遇到的那些“玄学问题”整理成排查表。全程说人话不堆术语尽量让刚接触这块的人也能跟着做出来。2. 整体设计与思路拆解为什么是 SSE而不是 WebSocket2.1 流式输出的本质把“一次性响应”拆成“连续小包”传统 HTTP 请求是这样的客户端发一个请求服务端处理完把完整结果一次性返回连接关闭。整个过程客户端只能等等到最后才知道结果是什么。AI 对话如果也这么干用户问一个问题可能要等十几秒甚至几十秒屏幕上一直转圈体验非常差。流式输出要解决的就是这个问题服务端每生成一小段内容就立刻推给客户端客户端收到一段就渲染一段。用户看到文字在“长出来”心理上会觉得“它在思考、它在回答”等待感大幅降低。这就像你去餐厅点菜传统方式是厨房全做完了一起端上来流式方式是做好一道先上一道你边吃边等体感完全不一样。那怎么实现“服务端主动推、客户端持续收”呢常见方案有两个WebSocket 和 SSE。这两个经常被拿来比较我当初也纠结过后来想清楚了它们的本质区别。2.2 SSE 与 WebSocket 的选型对比WebSocket 是全双工协议客户端和服务端可以随时互相发消息适合聊天室、协同编辑、游戏这类需要双向实时通信的场景。SSEServer-Sent Events是单向的只能服务端推给客户端基于普通 HTTP 协议浏览器原生支持EventSource。对于 AI 对话来说交互模式其实是“客户端发一次请求服务端持续推回答”本质上是单向的。用户不会在 AI 回答的过程中反复给服务端发消息除了“停止生成”这种控制指令那个用普通请求就能做。所以 SSE 在语义上更贴合而且它有几个 WebSocket 比不了的好处基于 HTTP不需要额外的协议升级走标准的 80/443 端口Nginx、网关、负载均衡器对它的兼容性更好配置成本低。自动重连浏览器原生EventSource自带断线重连机制虽然我们后面会用fetch-event-source替代它但这个设计思路值得借鉴。实现简单服务端就是一个普通的 HTTP 接口设置好响应头然后往响应流里写数据就行不需要维护复杂的连接状态。当然 SSE 也有短板它是纯文本协议只能传 UTF-8 文本二进制数据得自己编码浏览器对同域名下的 SSE 连接数有限制HTTP/1.1 下通常是 6 个不过 HTTP/2 下这个限制基本没了。对于 AI 对话场景这些都不是问题。所以我的结论是AI 对话用 SSE不用 WebSocket。除非你的产品还需要在对话过程中做复杂的双向实时交互那才考虑 WebSocket。2.3 为什么不用原生 EventSource而用 fetch-event-source浏览器原生有个EventSourceAPI用起来很简单const es new EventSource(/api/chat); es.onmessage (e) { console.log(e.data); };但它有几个硬伤在实际项目里很致命第一它只支持 GET 请求。AI 对话通常要把对话历史、模型参数、用户输入打包成 JSON 发给服务端这些数据用 GET 的 query string 传很不优雅长度也受限。虽然可以用一些 hack 手段但很别扭。第二它不能自定义请求头。很多接口需要带Authorization、Content-Type这些头EventSource做不到。第三它的重连机制是“无脑重连”。断线之后它会自动重连但你没法控制重连时带什么参数、重连几次、什么条件下停止。对于 AI 对话如果服务端已经生成了一半重连可能导致重复请求或者状态错乱。所以实际项目里大家普遍用microsoft/fetch-event-source这个库。它基于fetch实现支持 POST、支持自定义请求头、支持手动控制重连同时保留了 SSE 的解析能力。这就是为什么热词里fetch-event-source会和 SSE 绑在一起出现。2.4 整条链路的全景图在动手之前先把整条链路在脑子里过一遍这样后面每个环节你都知道它在干什么前端用户输入问题前端用fetch-event-source发起 POST 请求请求体里带上对话历史和参数。Nginx请求先到 NginxNginx 做反向代理把请求转发给后端服务。这里必须配置“不缓冲”否则 Nginx 会把流式数据攒起来流式就废了。后端后端接到请求调用大模型接口拿到流式响应然后一段一段地通过 SSE 格式写给前端。前端接收fetch-event-source每收到一段数据就触发回调前端把这段文本追加到消息列表里。Markdown 渲染追加的文本要实时渲染成 Markdown但因为是“半截”内容渲染时要处理未闭合的标签、代码块、表格等问题。滚动与状态消息更新后要自动滚到底部同时处理“停止生成”“重新生成”等交互。这条链路里Nginx 的缓冲配置和Markdown 的半截渲染是两个最容易翻车的地方后面会重点讲。3. 核心细节解析与实操要点SSE 协议、Markdown 渲染、Nginx 配置3.1 SSE 协议格式别小看那几个换行符SSE 的协议格式看起来很简单但细节不对前端就是收不到消息。它的基本格式是这样的data: 这里是消息内容\n\n注意几个关键点每条消息以data:开头后面跟内容然后以两个换行符\n\n结尾。这两个换行符是消息的分隔符少一个都不行。如果内容本身包含换行需要把每一行都单独用data:开头。比如要发送第一行\n第二行实际写出来是data: 第一行 data: 第二行除了data:还有event:指定事件类型、id:消息 ID、retry:重连时间等字段AI 对话场景一般只用data:就够了。响应头必须设置Content-Type: text/event-stream否则浏览器不会按 SSE 解析。还要设置Cache-Control: no-cache和Connection: keep-alive避免中间层缓存或提前关闭连接。我当初第一次写 SSE 接口就是因为只写了一个\n前端死活收不到消息排查了半天。后来才明白两个换行符是消息边界一个换行只是行内分隔。这个坑很基础但真的很常见。后端如果用 Java 的 Spring 框架返回SseEmitter或者直接写HttpServletResponse的 output stream 都可以。用 Node.js 的话设置好响应头然后res.write()就行。关键是要及时 flush不能等缓冲区满了才发出去。3.2 fetch-event-source 的正确用法microsoft/fetch-event-source的用法和原生EventSource不太一样它是基于fetch的所以要用fetchEventSource函数import { fetchEventSource } from microsoft/fetch-event-source; const controller new AbortController(); fetchEventSource(/api/chat, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer token, }, body: JSON.stringify({ messages: history, model: gpt-4, }), signal: controller.signal, onopen(response) { if (response.ok response.headers.get(content-type)?.includes(text/event-stream)) { return; } throw new Error(连接失败); }, onmessage(event) { // event.data 就是服务端推过来的一段内容 appendToMessage(event.data); }, onerror(err) { // 返回 undefined 表示不重连抛出异常表示重连 throw err; }, onclose() { // 服务端关闭连接时触发 }, });几个实操要点signal用于中断请求。用户点“停止生成”时调用controller.abort()就能中断。这个很重要否则用户点了停止后端还在生成白白浪费资源。onopen里要校验响应。如果服务端返回的不是text/event-stream说明出错了要主动抛异常否则库会一直等。onerror的返回值决定是否重连。返回undefined表示不重连抛出异常表示重连。AI 对话场景我一般选择不自动重连因为重连可能导致重复内容让用户手动点“重新生成”更可控。onmessage里拿到的event.data是字符串如果服务端发送的是 JSON需要自己JSON.parse。还有一个细节fetch-event-source默认会解析data:字段但如果服务端发送的是多行data:它会用换行符拼接起来。这个行为和原生EventSource一致。3.3 Markdown 实时渲染半截内容怎么处理AI 返回的内容通常是 Markdown 格式包含标题、列表、代码块、表格、链接等。前端要实时渲染但问题是流式输出时内容是不完整的。比如代码块可能只收到了python开头还没收到结尾的表格可能只收到了表头还没收到分隔行。如果直接把半截 Markdown 丢给渲染器会出现各种奇怪的问题代码块没闭合导致后面的内容全被当成代码、表格没渲染出来、列表缩进错乱。我试过几种方案最后总结出一套比较稳的做法。方案一直接渲染接受短暂的不完美。用marked或markdown-it这类库每次收到新内容就重新渲染整个消息。缺点是每次都要重新解析全文内容长了性能会下降而且半截代码块会导致闪烁。优点是实现简单。方案二增量渲染 补全。每次渲染前检查内容是否“完整”。如果代码块没闭合就临时补一个如果表格没结束就临时补上分隔行。渲染完再把补的内容去掉。这样能避免大部分闪烁问题。方案三分段渲染。把消息按“完整段落”切分已经完整的段落用 Markdown 渲染最后一段不完整的用纯文本渲染。等它完整了再升级成 Markdown。这个方案体验最好但实现最复杂。我实际项目里用的是方案二因为它在实现复杂度和体验之间平衡得比较好。具体做法是写一个normalizeMarkdown函数在渲染前对内容做预处理function normalizeMarkdown(text) { // 统计代码块标记数量奇数说明没闭合 const codeBlockCount (text.match(//g) || []).length; if (codeBlockCount % 2 ! 0) { text \n; } // 处理未闭合的行内代码 const inlineCodeCount (text.match(//g) || []).length; if (inlineCodeCount % 2 ! 0) { text ; } return text; }这个函数不完美但能解决 80% 的闪烁问题。剩下的 20% 主要是表格和复杂嵌套需要更精细的处理。另外Markdown 渲染的性能也要注意。如果每次收到一个字符就重新渲染全文内容长了会卡。我的做法是用requestAnimationFrame做节流或者设置一个最小更新间隔比如 50ms把这段时间内收到的内容攒一起再渲染。这样既保证了“打字机”的流畅感又不会过度渲染。3.4 Nginx 防粘连为什么你的流式变成了一次性返回这是整条链路里最容易被忽略、也最容易翻车的地方。我当初本地开发一切正常部署到服务器上走 Nginx 之后流式效果直接没了——所有内容攒在一起等 AI 全部生成完才一次性返回。排查了半天最后发现是Nginx 的缓冲机制在作怪。Nginx 作为反向代理默认会开启proxy_buffering它会把后端返回的数据先攒到缓冲区攒够一定大小或者后端响应结束才转发给客户端。对于普通接口这没问题但对于 SSE 流式这就等于把流式变成了“攒批”。解决办法是关闭缓冲并设置相关参数location /api/chat { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 关键关闭缓冲 proxy_buffering off; proxy_cache off; # 关键不限制响应超时 proxy_read_timeout 3600s; proxy_send_timeout 3600s; # 关键告诉 Nginx 不要缓冲 chunked_transfer_encoding on; }几个参数的解释proxy_buffering off这是最关键的。关闭之后Nginx 收到后端数据就立刻转发不再攒批。proxy_cache off关闭缓存避免 Nginx 缓存 SSE 响应。proxy_read_timeout默认是 60 秒AI 生成时间可能超过这个值会导致连接被断开。设大一点比如 3600 秒。proxy_http_version 1.1和proxy_set_header Connection SSE 需要长连接HTTP/1.1 支持 keep-alive这两个配置确保连接不被提前关闭。chunked_transfer_encoding on启用分块传输编码让数据可以边生成边传。还有一个坑如果你的 Nginx 前面还有一层负载均衡或者 CDN它们也可能有缓冲机制。比如某些云厂商的负载均衡默认会缓冲响应需要在控制台里关闭。这个排查起来比较麻烦因为你看不到那层配置只能通过现象判断。另外HTTP/2 对 SSE 的影响也值得注意。HTTP/2 支持多路复用理论上对 SSE 更友好但某些 Nginx 版本在 HTTP/2 下对 SSE 的处理有 bug可能导致消息粘连。如果遇到奇怪的问题可以试试强制用 HTTP/1.1。4. 实操过程与核心环节实现从零搭一套可用的流式对话4.1 后端 SSE 接口实现以 Node.js 为例先看后端。我用 Node.js Express 写一个最简单的 SSE 接口模拟 AI 逐字返回const express require(express); const app express(); app.use(express.json()); app.post(/api/chat, async (req, res) { // 设置 SSE 响应头 res.setHeader(Content-Type, text/event-stream); res.setHeader(Cache-Control, no-cache); res.setHeader(Connection, keep-alive); res.setHeader(X-Accel-Buffering, no); // 关键告诉 Nginx 不缓冲 // 模拟 AI 生成的内容 const fullText 你好我是 AI 助手。这是一段流式返回的测试内容用来演示打字机效果。; const chars fullText.split(); let index 0; const timer setInterval(() { if (index chars.length) { clearInterval(timer); res.write(data: [DONE]\n\n); // 发送结束标记 res.end(); return; } const char chars[index]; // 注意SSE 格式两个换行结尾 res.write(data: ${JSON.stringify({ content: char })}\n\n); index; }, 50); // 每 50ms 发一个字符 // 客户端断开时清理 req.on(close, () { clearInterval(timer); }); }); app.listen(3000, () console.log(Server running on port 3000));几个关键点X-Accel-Buffering: no这个响应头是给 Nginx 看的告诉它这个响应不要缓冲。有了这个头即使 Nginx 全局开了proxy_buffering这个接口也会关闭缓冲。这是个很实用的技巧比改 Nginx 配置更灵活。res.write()后要确保数据被 flush。Node.js 的res.write()通常会立即发送但如果用了压缩中间件比如compression可能会被缓冲。SSE 接口要禁用压缩。发送结束标记。我习惯在结束时发一个data: [DONE]前端收到这个标记就知道生成结束了可以做一些收尾操作。处理close事件。客户端断开时要清理定时器避免资源泄漏。如果后端是 Java用 Spring 的SseEmitter也类似PostMapping(/api/chat) public SseEmitter chat(RequestBody ChatRequest request) { SseEmitter emitter new SseEmitter(3600_000L); // 超时时间设长 executor.execute(() - { try { for (String chunk : generateChunks(request)) { emitter.send(SseEmitter.event().data(chunk)); Thread.sleep(50); } emitter.send(SseEmitter.event().data([DONE])); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }Java 这边要注意的是SseEmitter的默认超时时间比较短要手动设长。另外如果用了 Spring Security 或者过滤器要确保它们不会缓冲响应。4.2 前端接收与渲染完整实现前端我用 Vue 3 举例React 思路一样。核心是一个消息列表每条消息有一个content字段流式过程中不断追加。import { ref } from vue; import { fetchEventSource } from microsoft/fetch-event-source; import { marked } from marked; const messages ref([]); const isGenerating ref(false); let controller null; async function sendMessage(userInput) { // 添加用户消息 messages.value.push({ role: user, content: userInput }); // 添加 AI 消息占位 const aiMessage { role: assistant, content: }; messages.value.push(aiMessage); isGenerating.value true; controller new AbortController(); let buffer ; let lastRenderTime 0; try { await fetchEventSource(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages: messages.value }), signal: controller.signal, onmessage(event) { if (event.data [DONE]) { isGenerating.value false; return; } const data JSON.parse(event.data); buffer data.content; // 节流渲染50ms 更新一次 const now Date.now(); if (now - lastRenderTime 50) { aiMessage.content buffer; lastRenderTime now; } }, onerror(err) { isGenerating.value false; throw err; }, onclose() { aiMessage.content buffer; // 最终完整内容 isGenerating.value false; }, }); } catch (err) { isGenerating.value false; console.error(请求失败, err); } } function stopGenerating() { if (controller) { controller.abort(); isGenerating.value false; } }渲染部分用marked把 Markdown 转成 HTMLfunction renderMarkdown(text) { const normalized normalizeMarkdown(text); return marked.parse(normalized); }然后在模板里用v-html渲染。注意v-html有 XSS 风险如果内容来自不可信来源要用DOMPurify做净化。AI 返回的内容一般可信度较高但保险起见还是加上。4.3 自动滚动与“粘底”处理流式输出时消息不断变长用户希望看到最新内容所以要自动滚动到底部。但有个细节如果用户手动往上滚了说明他在看历史内容这时候就不应该强制滚动否则会打断用户。我的做法是监听滚动事件判断用户是否在底部const scrollContainer ref(null); const isAtBottom ref(true); function onScroll() { const el scrollContainer.value; if (!el) return; const threshold 50; // 距离底部 50px 内算在底部 isAtBottom.value el.scrollHeight - el.scrollTop - el.clientHeight threshold; } function scrollToBottom() { if (isAtBottom.value scrollContainer.value) { scrollContainer.value.scrollTop scrollContainer.value.scrollHeight; } }每次消息更新后调用scrollToBottom()。这样用户往上滚的时候不会被强制拉回底部体验更好。4.4 Nginx 完整配置示例把前面提到的配置整合一下这是一个可以直接用的 Nginx 配置server { listen 80; server_name your-domain.com; location / { root /var/www/html; try_files $uri $uri/ /index.html; } location /api/chat { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # SSE 关键配置 proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; chunked_transfer_encoding on; # 禁用压缩避免缓冲 gzip off; } }如果你的 Nginx 前面还有负载均衡记得在负载均衡那层也关闭缓冲。另外如果用了 HTTPSSSE 走 HTTPS 也没问题但要注意证书配置和超时设置。5. 常见问题与排查技巧实录5.1 流式变“攒批”消息一次性返回这是最高频的问题。现象是本地开发正常部署到服务器后流式效果消失所有内容等生成完才一起返回。排查思路按顺序来检查 Nginx 的proxy_buffering。这是最常见的原因。在location里加proxy_buffering off;或者在后端响应头里加X-Accel-Buffering: no。检查是否有压缩中间件。后端的compression中间件、Nginx 的gzip都会缓冲数据。SSE 接口要禁用压缩。检查是否有其他代理层。云负载均衡、CDN、API 网关都可能有缓冲机制需要在对应控制台关闭。检查后端是否 flush。有些框架的响应流有缓冲区要手动 flush 才能发出去。5.2 连接中断idle timeout waiting for SSE这个报错很常见意思是连接空闲超时被断开了。原因通常是某个环节的超时时间设得太短。Nginx 的proxy_read_timeout默认 60 秒AI 生成超过 60 秒就会断。设成 3600 秒。负载均衡的空闲超时云厂商的负载均衡通常有 60 秒空闲超时需要在控制台调大。后端的超时Spring 的SseEmitter默认超时 30 秒要手动设长。浏览器的超时一般不会但如果用了某些 HTTP 客户端库可能有默认超时。排查时可以从报错信息入手看是哪个环节断的。如果是stream disconnected before completion通常是中间代理断的。5.3 Markdown 渲染闪烁与错乱现象是流式过程中代码块、表格、列表渲染不正常内容跳动。代码块未闭合用前面说的normalizeMarkdown补全。表格未完成表格需要表头和分隔行都收到才能渲染半截表格会显示成纯文本。可以在渲染前检查表格是否完整不完整就先用纯文本显示。列表缩进错乱流式过程中列表项可能只收到一半导致缩进判断错误。这个比较难完美解决一般等列表项完整了再渲染。性能问题内容长了每次全量渲染会卡。用节流 增量渲染优化。5.4 消息粘连多条消息合并成一条现象是前端收到的消息不是一条一条的而是几条粘在一起。SSE 格式错误检查每条消息是否以\n\n结尾。少一个换行就会粘连。Nginx 缓冲缓冲会导致多条消息合并成一批发送关闭缓冲即可。HTTP/2 的 bug某些 Nginx 版本在 HTTP/2 下处理 SSE 有问题试试强制 HTTP/1.1。5.5 常见问题速查表问题现象可能原因解决方法流式变一次性返回Nginx 缓冲proxy_buffering off或X-Accel-Buffering: no连接 60 秒后断开超时设置太短调大proxy_read_timeout和负载均衡超时消息粘连SSE 格式错误或缓冲检查\n\n关闭缓冲Markdown 闪烁半截内容渲染补全未闭合标签节流渲染代码块不渲染未闭合渲染前补表格不渲染分隔行未收到等表格完整再渲染用户滚动被打断强制滚底判断用户是否在底部再滚动停止生成无效未中断请求用AbortController中断5.6 几个我踩过的坑坑一res.write()之后忘了 flush。有些框架的响应流有内部缓冲write之后数据没立刻发出去。解决办法是检查框架文档看是否需要手动 flush或者禁用压缩中间件。坑二Nginx 配置改了没生效。Nginx 配置修改后要nginx -s reload但有时候 reload 不生效需要nginx -s stop再启动。另外如果配置有语法错误reload 会失败但不会报错要用nginx -t检查。坑三fetch-event-source的onerror返回值搞错。返回undefined是不重连抛出异常是重连。我一开始搞反了导致断线后疯狂重连把服务端打挂了。坑四Markdown 渲染的 XSS 风险。v-html直接渲染 HTML 有 XSS 风险虽然 AI 返回的内容一般安全但如果用户能控制部分内容比如引用用户输入就可能被注入。用DOMPurify净化一下更稳妥。坑五移动端浏览器的 SSE 兼容性。某些移动端浏览器对 SSE 的支持不完整或者会在后台挂起连接。如果产品有移动端要测试一下必要时做降级处理。6. 性能优化与进阶技巧6.1 减少渲染次数节流与批量更新流式输出时如果每收到一个字符就更新一次 DOM内容长了会非常卡。我的做法是设置一个最小更新间隔比如 50ms把这段时间内收到的内容攒一起再更新。这样既保证了“打字机”的流畅感人眼对 50ms 的延迟基本无感又大幅减少了渲染次数。具体实现可以用一个缓冲区 定时器let buffer ; let timer null; function appendContent(chunk) { buffer chunk; if (!timer) { timer setTimeout(() { aiMessage.content buffer; timer null; }, 50); } }这样无论多快收到数据最多 50ms 更新一次。6.2 增量 Markdown 渲染全量重新渲染 Markdown 在内容长了之后性能很差。一个优化思路是把消息按“块”切分已经完整的块缓存渲染结果只渲染最后一个不完整的块。这样每次只需要渲染一小部分。实现起来稍微复杂但效果很明显。可以用marked的 lexer 把 Markdown 解析成 token 数组然后只渲染最后一个 token。不过这个方案对嵌套结构处理起来比较麻烦我一般只在内容特别长的时候才用。6.3 断线重连与状态恢复SSE 连接可能因为网络波动断开。如果断开时 AI 已经生成了一半重连后怎么恢复一个方案是服务端记录生成状态重连时带上lastEventId服务端从上次中断的地方继续推。这需要服务端维护会话状态实现成本较高。另一个方案是前端记录已收到的内容重连时把已收到的内容发给服务端服务端跳过这部分继续生成。这个方案对服务端要求低一些但需要模型支持“从中间继续”。实际项目里我一般选择不自动重连而是给用户一个“重新生成”按钮。这样实现简单用户也能接受。6.4 多模型切换与参数传递如果产品支持多个模型前端需要在请求里带上模型标识。后端根据模型标识调用不同的接口。这里要注意的是不同模型的流式格式可能不一样后端要做适配统一成 SSE 格式再推给前端。参数传递方面温度、最大长度、系统提示词这些都可以放在请求体里。但要注意请求体太大会影响首字节时间尽量精简。7. 写在最后一些个人体会这套东西我从零搭到跑通前后花了大概一周时间其中大部分时间都花在排查 Nginx 缓冲和 Markdown 渲染这两个问题上。回头看其实核心逻辑并不复杂难的是那些“看不见”的中间层——Nginx 的缓冲、负载均衡的超时、浏览器的兼容性这些不在代码里的东西往往才是决定体验的关键。如果你正在做类似的东西我的建议是先把最简单的链路跑通再逐层加东西。先本地直连后端确认 SSE 格式没问题再加上 Nginx确认缓冲关闭最后再优化 Markdown 渲染和滚动体验。不要一上来就把所有东西堆一起出了问题很难定位。另外多准备几套排查工具。浏览器的 Network 面板可以看 SSE 的原始数据流curl可以直接测试接口Nginx 的日志可以看到请求和响应的时间。这些工具在排查问题时比猜有用得多。最后分享一个小技巧如果你不确定 Nginx 是否在缓冲可以在后端每次write之后加一个时间戳前端收到时打印出来。如果时间戳是连续的说明没缓冲如果时间戳挤在一起说明被缓冲了。这个方法很直观一眼就能看出问题。

相关新闻

Word公式在UEditor中乱码的根治方案:OMML提取与MathJax渲染

Word公式在UEditor中乱码的根治方案:OMML提取与MathJax渲染

军工项目里的ueditor,说来都是泪。内网系统还在用1.4.3的老版本,从Word复制一篇带公式的技术文档到编辑框里,公式要么变成一串天书,要么变成小方框,要么直接消失。这个问题我前后折腾了快两周,客户现场催得…

2026/9/21 18:37:32 阅读更多 →
星火视频教程网源码拆解:5步掌握最佳实践

星火视频教程网源码拆解:5步掌握最佳实践

星火视频教程网源码拆解:5步掌握最佳实践 官方文档往往冗长枯燥,读两页就忘,核心痛点在于抓不住重点。 想搞懂星火视频教程网的底层逻辑,看源码是最高效的路径。 本文不聊虚的,直接上代码,带你用最佳实践视角拆解其核心实现。…

2026/9/21 18:37:32 阅读更多 →
Svelte 表格列分面(Column Faceting)完整指南:用 TanStack Svelte Table 构建高性能过滤界面

Svelte 表格列分面(Column Faceting)完整指南:用 TanStack Svelte Table 构建高性能过滤界面

Svelte 表格列分面(Column Faceting)完整指南:用 TanStack Svelte Table 构建高性能过滤界面 【免费下载链接】table 🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-T…

2026/9/21 18:36:32 阅读更多 →

最新新闻

3个方案对比:卡点视频生成技术图解原理

3个方案对比:卡点视频生成技术图解原理

3个方案对比:卡点视频生成技术图解原理 别再去翻那几百页的官方文档了,真的,没人有那个耐心。想搞懂 卡点视频 怎么在代码里实现,盯着 FFmpeg 或者 MoviePy 的英文 API 看,眼睛都花了还是抓不住重点。这时候,你需要的是…

2026/9/21 19:12:51 阅读更多 →
Handsontable 服务端数据实战:用 Django REST Framework 实现分页、排序、过滤与批量 CRUD 数据网格

Handsontable 服务端数据实战:用 Django REST Framework 实现分页、排序、过滤与批量 CRUD 数据网格

前端UI组件 【免费下载链接】handsontable JavaScript Data Grid / Data Table with a Spreadsheet Look & Feel. Works with React, Angular, and Vue. Supported by the Handsontable team ⚡ 项目地址: https://gitcode.com/gh_mirrors/ha/handsontable 点击…

2026/9/21 19:12:51 阅读更多 →
罗技鼠标宏源码解析:避开官方文档的5个隐形坑

罗技鼠标宏源码解析:避开官方文档的5个隐形坑

罗技鼠标宏源码解析:避开官方文档的5个隐形坑 Logitech G Hub 的官方文档像天书,翻半天只看到“支持按键映射”,却没人告诉你底层怎么跑。想搞懂罗技鼠标宏的 源码解析 ,别死磕 PDF,直接看执行逻辑。…

2026/9/21 19:12:51 阅读更多 →
FreshRSS WebSub 订阅数据目录全解析:`data/PubSubHubbub/feeds` 目录结构与推送机制

FreshRSS WebSub 订阅数据目录全解析:`data/PubSubHubbub/feeds` 目录结构与推送机制

FreshRSS WebSub 订阅数据目录全解析:data/PubSubHubbub/feeds 目录结构与推送机制 【免费下载链接】FreshRSS A free, self-hostable news aggregator… 项目地址: https://gitcode.com/gh_mirrors/fr/FreshRSS FreshRSS 原生支持 WebSub(原名 P…

2026/9/21 19:12:51 阅读更多 →
Vitess v23.0.6 发布详解:VReplication、VTGate 表达式引擎与复制链路的关键修复

Vitess v23.0.6 发布详解:VReplication、VTGate 表达式引擎与复制链路的关键修复

Vitess v23.0.6 发布详解:VReplication、VTGate 表达式引擎与复制链路的关键修复 【免费下载链接】vitess Vitess is a database clustering system for horizontal scaling of MySQL. 项目地址: https://gitcode.com/gh_mirrors/vi/vitess 本篇文章基于 Vit…

2026/9/21 19:12:51 阅读更多 →
gbrain 工作区模板仓库(template-repo)完全指南:从 Use this template 到持久化个人 Agent

gbrain 工作区模板仓库(template-repo)完全指南:从 Use this template 到持久化个人 Agent

gbrain 工作区模板仓库(template-repo)完全指南:从 Use this template 到持久化个人 Agent 【免费下载链接】gbrain Garrys Opinionated OpenClaw/Hermes Agent Brain 项目地址: https://gitcode.com/gh_mirrors/gb/gbrain 本指南以 g…

2026/9/21 19:11:51 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →