返回指南
疑難排解·2026年7月17日·閱讀約 6 分鐘

LLM 串流錯誤——SSE 截斷、串流掛起與遺失使用量

串流失敗通常不是模型的問題——而是你與模型之間的傳輸管線。閒置逾時代理會終止安靜的連線,nginx 緩衝會吞掉事件,而只消耗一半的串流看起來就像「API 停止回應」。先檢查傳輸管線。

最後審核於 。

串流失敗通常不是模型的問題——而是你與模型之間的傳輸管線。閒置逾時代理會終止安靜的連線,nginx 緩衝會吞掉事件,而只消耗一半的串流看起來就像「API 停止回應」。先檢查傳輸管線。

錯誤

symptoms
- Stream stops mid-sentence, connection closed (no error event)
- Client hangs after the last token, never sees [DONE]
- usage is null on streamed responses
- Works in curl, dies behind nginx / a corporate proxy

原因與解決方法一覽

原因解決方法
代理/負載平衡器閒置逾時(許多設定的預設值為 60s)提高 API 路徑的讀取逾時;長時間的思考停頓對代理而言看起來像閒置。
SSE 前方的緩衝(nginx proxy_buffering、部分 CDN)停用串流路由的緩衝(X-Accel-Buffering: no / proxy_buffering off)。
用戶端停止消耗(缺少 await、迭代器被捨棄)消耗至結尾或明確關閉——在串流中途被 GC 回收的迭代器,與串流截斷無法區分。
未提出使用量請求,卻期待收到使用量OpenAI-wire:傳入 stream_options: {"include_usage": true}——使用量會出現在最後一個區塊。

使用 curl -N 直接對 API 重現

繞過所有代理。如果原始 SSE 在完整生成期間都正常流動,問題就在你的應用程式路徑——一次重新加入一個中繼:

raw-stream.sh
curl -N https://api.kunavo.com/v1/chat/completions \
  -H "Authorization: Bearer $KUNAVO_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"claude-sonnet-5","stream":true,
       "stream_options":{"include_usage":true},
       "max_tokens":300,
       "messages":[{"role":"user","content":"Count slowly to 20 in words."}]}'

修正造成故障的中繼

nginx:對該路由設定 proxy_buffering off + proxy_read_timeout 300s。Serverless:檢查平台對回應串流的限制。企業代理:有些完全不支援 SSE——在那裡改用非串流模式。

正確處理尾端

串流計費資料最後才會到達:在有提出請求時,最後一個區塊會在 [DONE] 之前攜帶使用量。彙總增量,從最後一個區塊讀取使用量,並將提早關閉(沒有 finish_reason)視為可重試。

「SSE 串流在沒有 [DONE] 的情況下結束」——回答完整嗎?

這段訊息是用戶端自己的檢查結果,不是 API 傳送的錯誤:連線在收到 OpenAI 相容串流結尾的 data: [DONE] 行之前關閉了。造成這種情況有三個原因:中間的中繼關閉了連線(上述代理逾時和緩衝問題);伺服器在回答中途失敗,未傳送終止框架就關閉;或回答其實已完成,只是遺失了結束標記。你收到的最後一個區塊會決定是哪一種:若其中有 finish_reason,表示文字完整;若沒有 finish_reason,表示內容被截斷,應重新嘗試。不要把這項檢查交給 SDK——OpenAI Python SDK 在連線未收到 [DONE] 就關閉時,會安靜地結束迴圈,只有在區塊帶有錯誤物件時才會擲出例外。請在自己的程式碼中加入檢查:

stream_check.py
from openai import OpenAI

client = OpenAI(base_url="https://api.kunavo.com/v1", api_key="sk-kn-...")

finish_reason, parts = None, []
stream = client.chat.completions.create(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": "Count slowly to 20 in words."}],
    stream=True,
)
for chunk in stream:  # raises openai.APIError on a chunk that carries "error"
    for choice in chunk.choices:
        parts.append(choice.delta.content or "")
        finish_reason = choice.finish_reason or finish_reason

if finish_reason is None:
    raise RuntimeError("stream closed without a finish_reason: cut off, retry it")
print("".join(parts))

空串流:200,接著什麼都沒有

有時串流會以 HTTP 200 開啟,卻在沒有任何內容區塊的情況下結束——沒有文字、沒有工具呼叫,有時甚至沒有 role 區塊。這幾乎總是上游在標頭送出後發生故障:供應商過載、閘道自身的上游拒絕請求,或代理丟棄了內文。請將它視為截斷串流,以退避方式重試,並記錄一次失敗回應的原始內文,因為原因通常是串流內的錯誤事件,而你的 SDK 跳過了它。Claude Code 遇到相同情況時,會在不使用串流的情況下重新嘗試請求。

如果你透過 Kunavo 呼叫

Kunavo 在 /v1/messages 上提供標準 OpenAI-wire SSE(支援 stream_options.include_usage)和 Anthropic-wire 事件,因此上述 curl 重現也是相容性測試。對於 /v1/chat/completions 上的 Claude 和 GPT 模型,Kunavo 串流無論回答是否完成,都會以 data: [DONE] 結束:完成時位於 finish_reason 區塊之後;若上游在回答中途斷線,則位於錯誤區塊之後——錯誤區塊的 type 為 upstream_error,code 為 upstream_disconnect 或 upstream_timeout。因此 OpenAI SDK 會擲出 APIError,而不是將片段交給你作為完整回覆。若上游串流在任何內容之前結束,或在第一個 token 之前失敗,你不會收到空的 200:有可用模型時,該嘗試會在另一個通道上重試,否則會以 HTTP 錯誤回傳。在任何輸出到達你之前失敗的串流請求不會計費;在回答中途中斷的請求則不保證不計費——請重新送出,而不要假設它是免費的。

常見問題

為什麼我的串流回應中的 usage 是 null?

在 OpenAI-wire API 中,除非傳入 stream_options: {"include_usage": true},否則串流不會包含 usage;傳入後,它會出現在最後一個區塊。Anthropic 的原生串流會在 message_start/message_delta 事件中回報 usage。

如何判斷串流是被截斷,還是已完成?

已完成的串流會以 finish_reason(或 Anthropic 的 message_stop)結束,接著是 [DONE]。若連線在沒有這些標記的情況下關閉,就表示被截斷——將它視為可重試的失敗,而不是較短的回答。

「SSE 串流在沒有 [DONE] 的情況下結束」是什麼意思?

你的用戶端已讀取到連線結尾,卻沒有看到關閉 OpenAI 相容串流的 data: [DONE] 行。API 沒有傳送這段訊息;是你的用戶端或代理寫出的。如果最後一個區塊帶有 finish_reason,回答就完整,只是結束標記遺失。如果沒有,回答被代理、逾時或伺服器端故障截斷,應重新嘗試請求。

「串流在沒有 finish_reason 的情況下結束」是什麼意思?

用戶端已讀取到串流結尾,但沒有任何區塊帶有 finish_reason——這是 OpenAI 相容串流用來表示回答完成的欄位(stop、length、tool_calls)。沒有它,你手上的文字就是片段,即使最後一句看起來很自然也是如此。請重新嘗試請求;如果問題重複,請檢查你與 API 之間是否有代理逾時或緩衝。

LLM API 為什麼會回傳空串流?

因為故障發生在 HTTP 標頭送出之後:供應商過載、閘道的上游拒絕請求,或代理丟棄了內文。狀態列仍顯示 200,因此只檢查狀態會漏掉它。請將沒有內容區塊的串流視為失敗請求,以退避方式重試,並記錄一個原始回應,以找出其中的錯誤事件。

如果我已經有文字,可以安全地忽略缺少 [DONE] 嗎?

只有在最後一個區塊帶有 finish_reason 時才可以。沒有 finish_reason 時,你手上的文字就是片段,可能在句子中途或工具呼叫中途結束,JSON 引數也可能不完整。請重新嘗試,而不要將它儲存為回答。

相關指南

更多錯誤語意請參閱 錯誤參考;透過 註冊 和 身分驗證指南 取得金鑰只需一分鐘。