
1. 流式輸出為什么大模型應用都在搶這“幾百毫秒”我先從一個真實場景說起。當時團隊接入某個大模型問答服務測試時發現接口響應要等4到5秒才出第一個字最離譜的一次整個請求直接報了個idle timeout waiting for sse頁面白屏用戶把瀏覽器刷新了三遍。排查到最后發現問題不只在網關超時配置更深層的原因是我們把一次大模型請求當成了普通HTTP接口來處理——等模型推理完整個回復才一次性返回給前端。這就是不懂流式輸出要付出的代價。LLMLarge Language Model大語言模型的推理邏輯是逐token生成的。你在對話框里問一句“今天上海天氣怎么樣”模型不是一次性把整句話算出來而是先算出“今”再算出“天”再往后推“上”“?!薄疤臁薄皻狻泵恳徊蕉家柚懊嬉呀浬傻膖oken做條件概率計算。這個過程在模型端天生就是“邊推理邊產出”的。但如果你用傳統HTTP請求模式服務端會一直等模型算完最后一個token才把完整文本拼成一個字符串返回。問題立刻暴露出來了模型回復越長用戶等待時間越長。一次生成800字的回答模型端耗時可能就要8到10秒用戶盯著空白頁面心里已經開始罵產品經理了。流式輸出要解決的核心問題就是把這8到10秒的“靜默等待”拆分成一個又一個token的“即時到達”。用戶看到的是類似打字機效果第一個字不到1秒就出現在屏幕上后續內容持續滾動刷新。這不是什么炫技而是大模型應用的基本體驗門檻。現在市面上的主流方案里SSEServer-Sent Events服務端推送事件是出鏡率最高的一種。很多大模型API的流式接口、LangChain的流式回調、Dify里的流式響應配置底層都是走SSE。它不像WebSocket那樣需要建立額外連接、做單獨的協議協商而是建立在HTTP協議之上服務端把數據以特定格式“一段一段”推給客戶端前端用幾行代碼就能接住。但SSE看起來簡單實際踩坑的地方相當多。比如網關超時、緩沖被吞、EventSource不支持自定義Header、流式上下文里的SSE鑒權怎么做、Agent場景下多輪工具調用怎么保持流式輸出不斷……這些問題我在項目里一個不落全遇到了。這篇文章就把流式輸出和SSE從原理到實戰完整梳理一遍給正在做LLM應用接入的朋友一份可以直接抄作業的參考。2. 大模型推理鏈路先看懂token是怎么“蹦”出來的2.1 模型端到應用端一次完整的流式旅程要理解流式輸出得先知道一次請求從頭到尾經過了哪些環節。以目前最常見的架構為例大模型應用一般分三層模型端推理端負責真正跑模型推理把用戶輸入丟給模型模型逐token吐結果。這里說的推理服務可能是一臺GPU服務器上的vLLM、TGI、SGLang也可能是云端托管的模型API。應用后端你的業務代碼所在的地方負責接收前端請求、調用模型API、處理業務邏輯。LLM應用框架如LangChain、LlamaIndex以及Dify、FastGPT這類平臺都跑在這一層。前端瀏覽器、小程序或者App界面負責把模型輸出渲染給用戶看。流式輸出要做的事情就是把模型端已經“逐token產出”的特性貫穿到前端去。理想狀態下模型端每生成一個token就立刻把數據傳給應用后端后端再轉發給前端前端立即渲染出來。但現實往往沒這么美好。如果你的應用后端直接把完整響應攢到最后才返回那模型端的流式能力就被白白浪費了。這也是很多人調了SSE接口卻發現前端還是一下子收到全部內容的原因——數據可能被某個中間環節緩存住了或者后端代碼壓根沒做流式轉發。2.2 token、緩沖與采樣推理端的“打字機”速度有人可能會問為什么模型不能幾毫秒內一次性輸出全部內容答案是做不到也不應該這樣做。大模型的生成過程本質上是自回歸autoregressive。模型根據已有的token序列預測下一個token的概率分布采樣得到一個token然后把它拼接到序列末尾再預測下一個。每一步都要做一次神經網絡前向計算。雖然現代推理引擎做了很多優化比如KV Cache、連續批處理但生成一個token仍然需要時間一般在幾十毫秒到幾百毫秒之間。以GPT-4o這類模型為例輸出速度大約在每秒100多個token。一次生成500字的回復中文字符約等于500到800個token在模型端就需要5到8秒。如果按傳統HTTP請求用戶要白等這么久。但如果做流式輸出按下回車后大約1秒內就能看到第一個token后面每秒鐘都有新內容陸續出現用戶的感知完全不同。這中間還有個容易被忽略的點采樣策略。模型生成token時存在隨機性temperature參數越高生成越隨機同時還有top_p、top_k等采樣參數。這些參數影響的是token的多樣性不影響流式輸出的機制本身。但在工程上要留意有些推理服務和流式輸出組合在一起時會出現一個現象如果采樣參數設置不當模型可能一直在“思考”或者說生成空token流式接口半天不吐內容。遇到這種情況先排查采樣參數再看推理服務的空閑超時配置。2.3 從輪詢到長連接流式技術選型的來龍去脈流式輸出在大模型時代才火起來但“服務端持續向客戶端推送數據”這個需求其實早就存在。早期網頁聊天室、股票行情、實時通知都在用各種手段解決這個問題。技術選型大體經歷了幾個階段輪詢Polling前端每隔幾秒發一次普通HTTP請求問服務端“有新數據了嗎”。實現簡單但浪費帶寬實時性差。大模型場景下如果每秒輪詢一次對服務端壓力不小而且每次請求都有網絡開銷token的實時到達效果也不好。長輪詢Long Polling服務端收到請求后先掛起有數據了再響應。比普通輪詢實時性好一些但每次響應后連接都要重建仍有較大的協議開銷。WebSocket全雙工長連接客戶端和服務端可以互相推送消息。實時性最好但它是一個獨立的協議需要額外握手服務端部署要考慮連接狀態管理、心跳?;?、斷線重連等復雜問題。對于“主要讓服務端單向推送”的大模型輸出場景來說屬于殺雞用了牛刀。SSEServer-Sent Events基于HTTP的單向推送協議服務端可以持續向客戶端發送事件。它不需要額外握手天然復用HTTP基礎設施實現成本低。在大模型流式輸出這個場景里SSE幾乎是先天契合的——模型輸出的方向是單向的服務端到客戶端而SSE正好就是專為這種單向推送設計的輕量級方案。這也是今天絕大多數LLM API選擇SSE作為流式協議的根本原因。3. SSE協議深度拆解一個被低估的HTTP“半成品”3.1 一次握手持續推送SSE的工作模型SSE全稱是Server-Sent Events它的核心思想很樸素客戶端發一個普通的HTTP請求但服務端不立刻結束這個響應而是在同一個HTTP連接上持續不斷地把數據以特定格式推送給客戶端。打個比方普通HTTP響應就像點外賣——你下單商家做好一次性全部送過來SSE則像在餐廳點了一份現做現上的菜——你下單后廚房做好一道菜服務員就先端一道上來后廚繼續做做好再端直到上完為止。整個過程中你客戶端和餐廳服務端只建立了“一次”聯系但東西是分多次送過來的。這種模式下有幾個關鍵特點連接復用一次HTTP請求對應一個持久連接不需要像WebSocket那樣額外握手升級協議。文本協議SSE傳輸的是字符串格式的數據有特定的事件格式規范后面會詳細講。自動重連瀏覽器的EventSource接口內置了斷線重連機制連接異常斷開后能自動恢復。單向推送只能服務端向客戶端推客戶端不能通過這個連接往回發數據。如果需要雙向通信得另想辦法比如每個流式請求用獨立的SSE連接或者混用WebSocket。我在實際項目里最喜歡的SSE用法是把它當成“流式HTTP響應”來看待——后端接口對外還是普通HTTP服務前端請求時它立即返回200然后分塊把數據推下來。這樣對網關、負載均衡、日志系統都是完全透明的運維成本非常低。3.2 事件流格式data、id、event、retrySSE的傳輸格式有明確的規范定義在HTML5標準里。最簡單的SSE流示例長這樣HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: 這是第一行數據 data: 這是第二行數據 data: 這是第二行數據的續行每一段事件數據用空行分隔每行以字段名加冒號開頭。最常用的幾個字段data: 數據內容。如果同一事件有多行data客戶端會按換行符拼接瀏覽器EventSource會自動處理。event: 事件類型。默認是message可以自定義前端用addEventListener監聽對應類型。id: 事件ID??蛻舳酥剡B時會把Last-Event-ID頭帶回去服務端可以借此實現斷點續傳。retry: 重連時間毫秒告訴瀏覽器斷線后多久重試一次。還有一類特殊行以冒號開頭這是注釋行。服務端定期發一條: keep-alive的注釋可以防止某些代理服務器因為長時間無數據而中斷連接。很多大模型API的流式響應格式本質上就是SSE。以OpenAI兼容格式為例每個事件的數據部分是一個JSON字符串大致長這樣data: {id:chatcmpl-xxx,object:chat.completion.chunk,choices:[{delta:{content:你好},index:0}]} data: {id:chatcmpl-xxx,object:chat.completion.chunk,choices:[{delta:{content:},index:0}]} data: [DONE]注意這里是每行data帶一個JSON最后用data: [DONE]標記流結束??蛻舳艘龅木褪侵鹦凶x取data解析JSON把delta里的content字段拼起來展示。3.3 SSE vs WebSocket vs 普通HTTP一張表看懂區別很多人在做技術選型時會糾結大模型流式輸出到底該用SSE還是WebSocket。我把它們放到一張表里對比維度SSEWebSocket普通HTTP一次性響應協議基礎HTTPWS/WSS獨立協議HTTP通信方向單向服務端→客戶端全雙工一次性請求-響應連接建立普通HTTP請求握手成本低101切換協議額外握手普通HTTP請求瀏覽器兼容現代瀏覽器原生支持原生支持原生支持自動重連內置需自己實現無消息格式文本標準化格式文本/二進制皆可文本/二進制皆可服務端實現簡單復用HTTP棧需處理連接態、幀處理最簡單適合場景服務端單向推送大模型輸出雙向實時交互聊天室、游戲、協同編輯普通API請求大模型流式輸出這個場景本質是“模型一直說用戶偶爾打斷”的單向推送SSE的優勢非常明顯實現成本低不需要額外維護連接狀態也不存在跨域握手那種麻煩雖然SSE也有跨域限制但比WebSocket的跨域處理簡單一些。不過SSE在Agent場景下有個天然短板如果用戶想中途打斷模型生成比如點“停止生成”按鈕SSE本身沒定義客戶端主動發送控制消息的機制。實際項目中我們一般會另外提供一個普通的POST接口給前端調用來觸發中斷后端收到中斷請求后主動掐斷SSE流。這個設計后面細說。4. 實戰把LLM流式輸出接入你的應用4.1 環境約定與整體架構在動手寫代碼之前先梳理一下我們接下來的實驗環境。這里我會用一套當前主流的開源技術棧來做示例后端框架Python FastAPI現代LLM應用里非常常見異步支持好天然適合流式輸出。模型接入OpenAI兼容格式的API市面上大部分模型APIAzure OpenAI、DeepSeek、通義千問、Moonshot等都兼容這個格式。本地部署也可以用vLLM、Ollama等自建推理服務。前端用原生JavaScript演示方便你理解EventSource的本質不受框架約束。整體架構可以用一句話概括前端通過HTTP請求后端后端保持連接并流式轉發模型API的SSE數據前端按事件逐行解析渲染。這種模式下后端充當了一個“流式代理”的角色。前端不直接對接模型API好處很多可在中間做鑒權、記日志、做業務加工還避免把模型API Key泄露給前端。4.2 后端實現FastAPI接入模型流式接口后端最核心的一件事就是把模型API返回的SSE流“透傳”給前端。FastAPI里可以用StreamingResponse來實現代碼非常簡潔。先看一個最簡版from fastapi import FastAPI from fastapi.responses import StreamingResponse import httpx app FastAPI() MODEL_API_URL https://api.example.com/v1/chat/completions MODEL_API_KEY your-api-key app.post(/v1/chat/stream) async def chat_stream(prompt: str): # 向模型API發起流式請求 headers { Authorization: fBearer {MODEL_API_KEY}, Content-Type: application/json } payload { model: gpt-4o-mini, messages: [{role: user, content: prompt}], stream: True # 關鍵開啟流式 } async def event_generator(): async with httpx.AsyncClient(timeout60) as client: async with client.stream( POST, MODEL_API_URL, jsonpayload, headersheaders ) as response: async for line in response.aiter_lines(): if line.startswith(data: ): yield line \n\n return StreamingResponse( event_generator(), media_typetext/event-stream, headers{ Cache-Control: no-cache, X-Accel-Buffering: no, # 關閉Nginx緩沖很重要 } )這段代碼做了四件事接收前端的POST請求組織給模型API的請求參數特別注意stream: True必須打開。用httpx.AsyncClient.stream向模型API發起流式請求這樣后端不會等模型全部生成完才開始處理而是邊接收邊轉發。逐行讀取SSE響應判斷數據行是否以data:開頭如果是就原樣轉發給前端。用StreamingResponse把生成器包裝成HTTP流式響應指定media_typetext/event-stream。這里有個極其重要的細節我必須用單獨一段來強調有些模型API流式返回的數據行可能不會以data:開頭。比如有些推理服務會在正式數據前發一個retry: 10000或者冒號注釋行OpenAI兼容格式在流結束時還會發送data: [DONE]。你的后端轉發邏輯不要死板地只認data:否則可能會漏掉結束標記。一般我建議直接把所有收到的行整體組織成SSE格式轉發只在需要加工時針對性地解析data行。4.3 前端實現EventSource與fetch的取舍前端接SSE最省事的方案是瀏覽器原生的EventSourceconst eventSource new EventSource(/v1/chat/stream?prompt你好); eventSource.onmessage (event) { // event.data 就是服務端推過來的數據 const data JSON.parse(event.data); if (data.choices data.choices[0].delta data.choices[0].delta.content) { appendToChat(data.choices[0].delta.content); } }; eventSource.onerror (e) { // EventSource 會自動重連 console.error(SSE連接出錯, e); };但這里有一個非常常見的坑EventSource只能發GET請求不支持自定義Header。這在鑒權場景下很麻煩——如果前端需要帶一個Authorization頭去請求后端接口用原生EventSource是做不到的。我當時的解決方案是換成fetchReadableStream手動解析async function streamChat(prompt) { const response await fetch(/v1/chat/stream, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${getUserToken()}, }, body: JSON.stringify({ prompt }), }); if (!response.ok || !response.body) { throw new Error(請求失敗); } const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // 按空行拆分SSE事件 const events buffer.split(\n\n); buffer events.pop(); // 最后一段可能不完整保留 for (const event of events) { const dataLines event .split(\n) .filter((line) line.startsWith(data:)) .map((line) line.slice(5).trim()); if (dataLines.length 0) continue; const data dataLines.join(\n); if (data [DONE]) return; const json JSON.parse(data); const delta json.choices?.[0]?.delta?.content; if (delta) appendToChat(delta); } } }這段手動解析代碼有幾點值得注意用TextDecoder(utf-8, { stream: true })解碼應對中文字符可能被拆散在多個chunk里導致亂碼的問題。SSE事件以空行分隔但網絡傳輸時數據可能被任意切分所以要用buffer把不完整的事件暫存起來等下一批數據到達后再拼接。data行的解析要處理“一個事件多行data”的情況按協議規范用換行符拼接。[DONE]標記是很多模型API發送結束信號的約定方式收到后就應該結束循環。4.4 LangChain與Agent場景流式輸出怎么配合工具調用如果你的應用不是直接調用模型API而是用LangChain或Agent框架流式輸出的處理會稍微復雜一些。因為Agent在推理過程中會穿插工具調用比如先查數據庫、再調外部API、再總結回答整個鏈路是“模型生成 → 工具調用 → 模型再生成”的循環。LangChain 2.0以后提供了基于stream_events的流式回調機制可以在不同環節產生不同事件。實際項目里我是這樣處理的from langchain_openai import ChatOpenAI from langchain.agents import create_openai_tools_agent async for event in agent.astream_events(inputs, versionv2): kind event[event] if kind on_chat_model_stream: chunk event[data][chunk] if chunk.content: # 轉成SSE格式推給前端 yield fdata: {json.dumps({type: token, content: chunk.content})}\n\n elif kind on_tool_start: tool_name event[name] yield fdata: {json.dumps({type: tool_start, tool: tool_name})}\n\n elif kind on_tool_end: output event[data][output] yield fdata: {json.dumps({type: tool_end, output: str(output)[:200]})}\n\n這種做法的核心思路是把Agent的各個階段事件都封裝成SSE的自定義事件類型前端可以據此展示不同的UI反饋——比如模型生成時顯示打字機畫面工具調用時顯示“正在查詢數據庫”的Loading提示切換不同的卡片讓用戶看到Agent每一步在干什么而不是只看到一段干巴巴的最終回答。Agent場景下還要特別小心一個問題工具調用的中間結果往往很長比如數據庫查詢返回了幾百行記錄直接塞進prompt會讓上下文變得很大拖慢后續生成速度也可能導致超時。這種情況下要么對工具輸出做截斷要么在進入下一個模型調用前設置一個較長的空閑超時時間并且用注釋行: keep-alive保持SSE連接不斷開。4.5 Dify等低代碼平臺的流式配置如果不想從頭寫后端用Dify這類LLM應用平臺也能配置流式輸出。在Dify里模型配置界面有一個“流式響應”的開關開啟后Dify的應用API會自動把模型輸出以SSE格式推送給前端。它的前端SDKdify/webapp-sdk內置了SSE解析邏輯接入時基本不需要關心底層協議。但配置文件里真正需要留意的是推理超時和節點超時參數。做過一次排查Dify工作流里加了多個模型節點整體執行時間超過30秒SSE連接在中間就被服務端掐斷了。這種情況要把對應節點的超時時間調大并且確認網關層Nginx、Cloudflare等不會因為“空閑時間過長”斷開連接。低代碼平臺的流式輸出適合快速驗證產品原型但如果要做深度定制比如自定義鑒權、Agent多工具流式展示、精細的token計數還是建議自己寫后端可控性高得多。5. 關鍵避坑點SSE鑒權、超時與中間層緩沖5.1 SSE鑒權怎么做才安全搜索引擎熱詞里專門有一條“sse鑒權”確實SSE鑒權是大家最容易踩坑的地方。核心難點在于EventSource不支持自定義Header傳統的Authorization頭解決方案在原生SSE里行不通。實踐中我有三套方案按推薦程度排序方案一一次性Token放在URL Query參數里推薦const token await fetch(/api/sse-token).then(r r.json()); const eventSource new EventSource(/v1/chat/stream?token${token.value});服務端在返回SSE流之前校驗URL里的token校驗通過才開始推送。關鍵點是這個token必須是短期有效的一次性憑證比如有效期30秒或只能使用一次防止URL泄露后被惡意重放。生成token的接口本身走正常的Authorization鑒權。方案二用fetch替代EventSource自定義Header就是我前面展示過的用fetchReadableStream手動解析請求頭里正常帶Authorization。這種方式不依賴EventSource所以不受Header限制是我在生產環境用得最多的方案。方案三先鑒權拿Cookie再用SSE如果后端和前端同域在SSE連接建立前先去鑒權接口登錄服務端下發HttpOnly Cookie然后EventSource請求的時候瀏覽器會自動帶上Cookie服務端校驗Cookie完成鑒權。但注意SSE要處理跨域的話withCredentials必須設為true而且服務端要正確配置CORS跨域允許的Cookie憑據。另外無論哪種方案我強烈建議給SSE流式接口的對外暴露設置一層“API網關鑒權”比如在Nginx層做IP白名單限制或者在API網關配置訪問憑證防止接口被未授權方以流式方式抓取數據。5.2 before completion: idle timeout waiting for sse的真相這個錯誤在搜索熱詞里直接出現了很有代表性。它看起來像是模型那邊報的錯實際上是網關或代理層的空閑超時導致的。大模型推理時間較長在一次流式請求中模型端可能持續幾秒到幾十秒沒有生成新的token比如在思考、在等待工具調用結果此時連接上沒有任何數據流過去。網關如果配置了“空閑超時”idle timeout以為連接死了就會主動切斷于是報出idle timeout waiting for sse。從根本上解決這個問題需要從三個層面排查服務端持續發送心跳注釋行。SSE規范里的冒號注釋行就是為了應對這個場景。后端在轉發生成器的循環里可以加一個定時器每隔15秒發一個: keep-alive\n\n告訴網關“連接還活著”。import asyncio async def event_generator(): last_activity asyncio.get_event_loop().time() while True: try: # 從模型流中取數據偽代碼 chunk await model_stream.__anext__() yield fdata: {chunk}\n\n last_activity asyncio.get_event_loop().time() except StopAsyncIteration: break except asyncio.TimeoutError: current_time asyncio.get_event_loop().time() if current_time - last_activity 15: yield : keep-alive\n\n last_activity current_time調整網關超時配置。Nginx層面主要看proxy_read_timeout默認60秒對大模型場景不夠建議調到300秒以上。如果是Kubernetes Ingress對應的是nginx.ingress.kubernetes.io/proxy-read-timeout。location /v1/chat/stream { proxy_pass http://backend; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_buffering off; proxy_cache off; proxy_set_header Connection ; http_version 1.1; chunked_transfer_encoding off; }關閉中間層緩沖。這個問題極其隱蔽。Nginx默認會開啟proxy_buffering它會幫你把后端返回的數據攢到一定量再一次性發給客戶端。流式輸出在這種配置下會變成“半天蹦一句”體驗極差。解決辦法是在Nginx關閉緩沖或者在后端響應頭里加X-Accel-Buffering: noNginx認這個頭看到后會自動關閉緩沖。我在前面FastAPI代碼里已經加上了字段來源就在這里。Cloudflare這類CDN也會有類似的緩沖行為需要在緩存規則里對SSE接口做例外處理或者使用Content-Type: text/event-stream來觸發CDN的流式直傳邏輯。有些CDN對SSE有特殊優化但默認配置往往還是走緩沖這一點很容易被忽略。5.3 字符編碼與亂碼中文流式輸出為什么出現豆腐塊做中文大模型應用時一個高頻問題是流式輸出過程中出現亂碼或者某個字顯示成“”。根本原因在于網絡傳輸是按字節流處理的而一個中文字符在UTF-8編碼下占3個字節。SSE的數據在傳輸時TCP層會按任意大小切分一個中文字符的3個字節可能被拆到兩個TCP包甚至是兩個chunk里。前端如果直接對每個chunk做字節到字符串的解碼就會把一個完好的中文字符截斷導致亂碼。解決方案就是在4.3節那段代碼里寫到的用TextDecoder的{ stream: true }模式進行增量解碼。TextDecoder會內部緩存未完成的字節序列等后續字節到達后自動補全輸出正確的字符串。這個參數必須設置否則默認的非流式模式會直接丟棄無法解碼的字節照樣亂碼。用Python寫后端轉發時也會碰到類似問題。httpx的aiter_bytes()和aiter_lines()行為不太一樣。aiter_lines()內部已經做了流式解碼所以基于行的轉發不容易出現中文截斷問題。但如果你用aiter_bytes()自己做分片邏輯就要格外小心編碼處理最好還是直接基于行來操作。5.4 SSE事件多行data的拼接陷阱按照SSE規范一個事件可以包含多行data字段這多行內容最終會被連接成一個字符串行與行之間用換行符拼接。很多大模型API在返回超長內容時會在內部把數據拆成多行data發出來。后端如果簡單粗暴地“遇到一行data就轉發一次”前端就會出現莫名其妙的斷行。前端手動解析SSE時必須遵守規范先按空行切分事件再把每個事件內的所有data行取出來用\n拼接成一個完整的數據體最后再解析JSON或直接作為文本輸出。我見過不止一個團隊因為沒按這個規范做導致流式輸出在長回復情況下頻繁斷句、JSON解析報錯。5.5 常見問題速查表現象根本原因解決方案頁面等很久才出第一句話中間層緩沖或后端沒有做流式轉發關閉Nginx/CDN緩沖檢查后端是否真正流式轉發報idle timeout waiting for sse網關空閑超時模型端長時間無輸出調大超時、加心跳注釋行、關閉緩沖中文亂碼/豆腐塊UTF-8多字節字符被chunk截斷TextDecoder流式解碼后端按行轉發EventSource帶不了Authorization頭原生EventSource不支持自定義Header用fetchReadableStream手動解析或一次性Token方案流式輸出到一半突然中斷服務端異常、請求超時或連接被掐檢查服務端日志看是否異常退出調大超時增加重連機制收到[DONE]后頁面還在轉圈前端沒有正確處理結束標記收到[DONE]后立即結束解析并關閉加載狀態Agent工具調用環節卡頓工具執行耗時超過SSE連接的空閑時間工具調用期間發送心跳注釋調大超時或把工具調用狀態單獨推給前端長上下文下響應越來越慢上下文過大模型端每次預計算耗時增長做上下文壓縮/截斷或用支持長上下文的模型版本6. 生產環境落地的三點心得整個接入過程中我自己踩坑最多也最有收獲的幾個點最后集中提一下第一個是關于“流式輸出是否真的有必要”的判斷。如果你的應用是異步批量處理場景比如生成日報后發郵件通知用戶不關心中間過程那完全沒必要用SSE普通接口反而更合適。流式輸出適合交互式場景——聊天機器人、Copilot、人工審核輔助、代碼自動補全等用戶需要實時看到模型“正在做什么”這不僅僅是體驗問題更是信任問題看到內容在滾動用戶會認為系統沒有卡死。第二個是關于“可測試性”的重要性。SSE的調試比普通HTTP接口麻煩很多我建議給后端流式接口預留一個“非流式兼容模式”加個?streamfalse參數返回一次性JSON。這樣在本地開發、寫接口文檔、做自動化測試時直接用普通HTTP工具就能驗證邏輯不用非逼著每個開發都去SSE調試器里干活。生產環境默認開流式測試接口用非流式兩套跑通互不干擾。第三個是關于“降級”方案。雖然SSE是大模型應用的主流方案但它在復雜網絡環境下確實存在連接不穩定、被企業防火墻截斷等風險。我在生產環境做了一個自動降級機制前端如果發現SSE連接5秒內沒有收到任何數據自動切換為輪詢模式——每隔3秒向后端查一次“有新的輸出沒”后端把最近的流式輸出緩存在內存里支持按偏移量增量拉取。這樣在最壞情況下用戶體驗會退化但不會完全不可用。這套降級邏輯代碼量不大但對可用性的提升非常明顯。最后說一句流式輸出這件事原理不難難的是把鏈路里所有的“隱性風險點”都排查干凈。網關超時、緩沖關閉、字符編碼、鑒權方案、連接保活每一個環節都可能讓流式輸出變成“假流式”或“壞流式”。希望這篇文章能把你在LLM流式接入這條路上可能踩的坑提前標記出來。