Quay lại hướng dẫn
Khắc phục sự cố·17 tháng 7, 2026·Đọc 6 phút

Lỗi streaming LLM — SSE bị ngắt, stream bị treo và thiếu usage

Lỗi streaming hiếm khi do model — chúng thường nằm ở phần kết nối giữa bạn và model. Proxy có idle timeout sẽ ngắt các kết nối im lặng, nginx nuốt các event do buffering, còn stream chỉ được tiêu thụ một phần trông giống như "API đã ngừng phản hồi". Hãy kiểm tra phần kết nối trước.

Được xem xét lần cuối vào .

Lỗi streaming hiếm khi do model — chúng thường nằm ở phần kết nối giữa bạn và model. Proxy có idle timeout sẽ ngắt các kết nối im lặng, nginx nuốt các event do buffering, còn stream chỉ được tiêu thụ một phần trông giống như "API đã ngừng phản hồi". Hãy kiểm tra phần kết nối trước.

Lỗi

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

Tổng quan nguyên nhân và cách khắc phục

Nguyên nhânCách khắc phục
Thời gian chờ khi không hoạt động của proxy/bộ cân bằng tải (mặc định 60 giây trong nhiều cấu hình)Tăng read timeout cho đường dẫn API; các khoảng dừng suy nghĩ dài trông giống như kết nối không hoạt động đối với proxy.
Buffering phía trước SSE (nginx proxy_buffering, một số CDN)Tắt buffering cho route streaming (X-Accel-Buffering: no / proxy_buffering off).
Client ngừng tiêu thụ (thiếu await, iterator bị loại bỏ)Tiêu thụ đến cuối hoặc chủ động đóng — iterator bị GC giữa stream không thể phân biệt với một stream bị ngắt.
Mong đợi usage mà không yêu cầuOpenAI-wire: truyền stream_options: {"include_usage": true} — usage xuất hiện trong chunk cuối cùng.

Tái hiện bằng curl -N đi thẳng đến API

Bỏ qua mọi proxy. Nếu SSE thô chạy tốt trong toàn bộ quá trình tạo, vấn đề nằm ở đường dẫn của ứng dụng — thêm lại từng hop một:

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."}]}'

Sửa hop làm hỏng stream

nginx: proxy_buffering off + proxy_read_timeout 300s cho route. Serverless: kiểm tra giới hạn response streaming của nền tảng. Proxy doanh nghiệp: một số proxy hoàn toàn không hỗ trợ SSE — trong trường hợp đó, chuyển về non-streaming.

Xử lý đúng phần cuối

Dữ liệu thanh toán dạng stream đến sau cùng: chunk cuối mang usage (khi được yêu cầu) trước [DONE]. Cộng dồn các delta, đọc usage từ chunk cuối và xem việc đóng sớm (không có finish_reason) là có thể thử lại.

“SSE stream ended without [DONE]” — câu trả lời đã đầy đủ chưa?

Thông báo đó là bước kiểm tra của chính client, không phải lỗi API gửi đến: kết nối đã đóng trước dòng data: [DONE] mà một stream tương thích với OpenAI dùng để kết thúc. Có ba nguyên nhân: một hop ở giữa đã đóng kết nối (do proxy timeout và buffering như trên); server gặp lỗi giữa câu trả lời và đóng mà không có frame kết thúc; hoặc câu trả lời đã hoàn tất nhưng chỉ sentinel bị mất. Chunk cuối cùng bạn nhận được quyết định trường hợp nào: có finish_reason nghĩa là văn bản đầy đủ, còn không có finish_reason nghĩa là văn bản bị ngắt và nên được thử lại. Đừng giao việc kiểm tra đó cho SDK — OpenAI Python SDK lặng lẽ kết thúc vòng lặp khi kết nối đóng mà không có [DONE], và chỉ ném lỗi khi một chunk chứa đối tượng error. Hãy đặt bước kiểm tra trong code của bạn:

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))

Stream rỗng: 200, sau đó không có gì

Đôi khi stream mở với HTTP 200 rồi kết thúc mà không có một content chunk nào — không có văn bản, không có tool call, đôi khi thậm chí không có role chunk. Gần như luôn có nghĩa là upstream đã lỗi sau khi header được gửi: provider quá tải, gateway có upstream riêng từ chối yêu cầu, hoặc proxy làm rơi body. Hãy xử lý như stream bị ngắt và thử lại với backoff, đồng thời ghi log raw body của một response lỗi, vì nguyên nhân thường là một error event bên trong stream mà SDK đã bỏ qua. Claude Code xử lý cùng điều kiện này bằng cách thử lại request mà không streaming.

Nếu bạn gọi qua Kunavo

Kunavo stream SSE theo chuẩn OpenAI-wire (hỗ trợ stream_options.include_usage) và các event theo chuẩn Anthropic trên /v1/messages, vì vậy việc tái hiện bằng curl ở trên cũng là bài kiểm tra tương thích. Đối với các model Claude và GPT trên /v1/chat/completions, stream của Kunavo kết thúc bằng data: [DONE] bất kể câu trả lời đã hoàn tất hay chưa: sau chunk có finish_reason nếu đã hoàn tất, và sau một error chunk — type upstream_error, code upstream_disconnect hoặc upstream_timeout — nếu upstream bị ngắt giữa câu trả lời, để các OpenAI SDK ném APIError thay vì đưa cho bạn phần mảnh như toàn bộ câu trả lời. Stream upstream kết thúc trước khi có content hoặc lỗi trước token đầu tiên sẽ không đến với bạn dưới dạng 200 rỗng: lần thử sẽ được chuyển sang kênh khác nếu model có kênh đó, nếu không thì trả về dưới dạng HTTP error. Request dạng stream lỗi trước khi có output không bị tính phí; request bị lỗi giữa câu trả lời thì không được đảm bảo như vậy — hãy gửi lại thay vì cho rằng nó miễn phí.

Câu hỏi thường gặp

Tại sao usage trong các response dạng stream của tôi lại là null?

Trên các API OpenAI-wire, usage không được đưa vào stream trừ khi bạn truyền stream_options: {"include_usage": true}; khi đó usage xuất hiện trong chunk cuối. Native stream của Anthropic báo usage trong các event message_start/message_delta.

Làm cách nào để phát hiện stream bị ngắt thay vì đã hoàn tất?

Stream hoàn tất sẽ kết thúc bằng finish_reason (hoặc message_stop của Anthropic), sau đó là [DONE]. Kết nối đóng mà không có các dấu hiệu đó nghĩa là stream đã bị ngắt — hãy xem đó là lỗi có thể thử lại, không phải câu trả lời ngắn.

“SSE stream ended without [DONE]” có nghĩa là gì?

Client của bạn đã đọc stream đến cuối kết nối nhưng không thấy dòng data: [DONE] đóng một stream tương thích với OpenAI. API không gửi thông báo đó; client hoặc agent của bạn đã viết nó. Nếu chunk cuối có finish_reason, câu trả lời đã hoàn tất và chỉ phần đóng kết nối bị mất. Nếu không có, câu trả lời đã bị proxy, timeout hoặc lỗi phía server ngắt giữa chừng, và request nên được thử lại.

“stream ended without finish_reason” có nghĩa là gì?

Client đã đọc stream đến cuối nhưng không chunk nào có finish_reason — trường mà stream tương thích với OpenAI dùng để cho biết câu trả lời đã hoàn tất (stop, length, tool_calls). Không có trường này, phần văn bản bạn có chỉ là một mảnh, dù câu cuối trông tự nhiên đến đâu. Hãy thử lại request; nếu lặp lại, hãy tìm proxy timeout hoặc buffering giữa bạn và API.

Tại sao API LLM lại trả về stream rỗng?

Vì lỗi xảy ra sau khi HTTP header đã được gửi: provider quá tải, upstream của gateway từ chối request hoặc proxy làm rơi body. Dòng trạng thái vẫn là 200 nên việc kiểm tra status sẽ không phát hiện. Hãy xem stream không có content chunk là request thất bại, thử lại với backoff và ghi log một response thô để tìm error event bên trong.

Có an toàn không nếu bỏ qua [DONE] bị thiếu khi tôi đã có văn bản?

Chỉ an toàn khi chunk cuối có finish_reason. Nếu không có, văn bản bạn có là một mảnh có thể kết thúc giữa câu hoặc giữa tool-call, với các đối số JSON chưa hoàn chỉnh. Hãy thử lại thay vì lưu nó làm câu trả lời.

Hướng dẫn liên quan

Thông tin chi tiết hơn về ý nghĩa lỗi có trong tài liệu tham chiếu lỗi; bạn chỉ mất một phút để lấy khóa qua đăng ký và hướng dẫn xác thực.