Cách phát trực tuyến phản hồi AI (streaming) cho giao diện chat mà không đứt luồng

Streaming cho chữ AI hiện dần, nhưng kèm lỗi đệm, hủy và đứt luồng. Bài dựa trên tài liệu Anthropic, OpenAI, WHATWG, Next.js, Vercel.
Giao diện chat AI để người dùng nhìn màn hình trống hàng chục giây rồi mới hiện cả đoạn trả lời thường bị coi là chậm, dù mô hình không chậm hơn. Phát trực tuyến (streaming) cho chữ hiện dần, nhưng đưa vào những kiểu lỗi mà cách gọi thường không có.
Bài đối chiếu đến ngày 23/10/2026, dựa trên tài liệu của Anthropic, OpenAI, WHATWG, MDN, Next.js, Vercel, AI SDK, nginx, Cloudflare và Nielsen Norman Group. Hầu hết trang của hãng AI không ghi ngày cập nhật, nên Click Star ghi ngày đọc.
Bài trả lời: streaming chạy thế nào, giao diện xử lý gì, đường truyền làm hỏng nó ra sao. Click Star đã có bài riêng về lỗi 429 và thử lại, nên bài này đi sâu vào giao diện và truyền tải.
Điểm chính
- Streaming không làm mô hình viết nhanh hơn. Nó đưa chữ đầu tiên đến mắt người dùng sớm hơn, nên cảm giác phản hồi tốt hơn.
- Anthropic và OpenAI đều trả luồng bằng server-sent events (SSE) nhưng tên sự kiện khác nhau; nên đổi cả hai về một định dạng nội bộ.
- Chat gửi POST kèm lịch sử nên thường đọc luồng bằng fetch, vì EventSource chỉ nhận URL và withCredentials.
- Nút Dừng ở trình duyệt chỉ ngắt HTTP; máy chủ phải đổi việc đó thành tín hiệu hủy cho lệnh gọi mô hình.
- Proxy, CDN, bước nén và giới hạn thời gian của hàm serverless đều có thể đệm hoặc cắt luồng.
- Lỗi có thể đến sau mã 200. Chỉ coi câu trả lời là xong khi nhận sự kiện kết thúc: message_stop (Anthropic) hoặc response.completed (OpenAI).

Streaming đổi gì trong cảm giác chờ
Theo Nielsen Norman Group (bài trích từ sách năm 1993, có cập nhật cho web), 0,1 giây là ngưỡng để hệ thống có vẻ phản ứng tức thì, 1,0 giây là ngưỡng để mạch nghĩ không đứt và 10 giây là ngưỡng giữ được sự chú ý. Câu trả lời dài của mô hình thường vượt các ngưỡng này nếu chờ trọn vẹn.
Tài liệu Anthropic về giảm độ trễ (đọc ngày 23/10/2026) tách thời gian đến token đầu tiên (TTFT) khỏi tổng thời gian và nói streaming cải thiện cảm giác phản hồi. Nhận định của Click Star: tổng thời gian có thể y nguyên, thứ đổi là lúc người dùng có chữ để đọc, miễn là chữ đến đều.
Cơ chế: máy chủ gửi từng sự kiện
Theo tài liệu Anthropic (đọc ngày 23/10/2026), đặt "stream": true thì Messages API trả server-sent events. Luồng đi theo thứ tự: message_start, các khối nội dung (mỗi khối có content_block_start, 1 hoặc nhiều content_block_delta, content_block_stop), 1 hoặc nhiều message_delta, rồi message_stop. Sự kiện ping có thể xen vào.
Chữ nằm trong text_delta và có thể bị cắt giữa từ:
event: content_block_delta
data: {"type": "content_block_delta","index": 0,"delta": {"type": "text_delta", "text": "ello frien"}}
Tham số công cụ đến dưới dạng JSON từng phần (input_json_delta), chỉ nên phân tích khi nhận content_block_stop. Lý do dừng chỉ nằm ở message_delta. Mã nên bỏ qua kiểu sự kiện lạ vì Anthropic có thể thêm kiểu mới.
OpenAI (đọc ngày 23/10/2026) đặt stream=true ở endpoint Responses; mỗi sự kiện có thuộc tính type. Với văn bản, các sự kiện hay dùng là response.created, response.output_text.delta, response.completed và error; trang tham chiếu còn có response.failed và response.incomplete.
Khuyến nghị của Click Star: viết 1 lớp nối đổi sự kiện của từng hãng về định dạng nội bộ gồm 4 loại (bắt đầu, mảnh chữ, kết thúc kèm lý do, lỗi).
EventSource hay fetch: trình duyệt đọc luồng thế nào
Theo đặc tả HTML của WHATWG (cập nhật 22/10/2026), mỗi sự kiện SSE kết thúc bằng 1 dòng trống; dữ liệu thiếu dòng trống cuối bị bỏ, dòng bắt đầu bằng dấu hai chấm là chú thích, nội dung luôn là UTF-8. EventSource tự nối lại khi đứt và gửi Last-Event-ID; phản hồi 204 báo ngừng nối lại.
Theo MDN (sửa ngày 3/9/2026), HTTP/1.1 giới hạn 6 kết nối SSE cho mỗi trình duyệt và tên miền, HTTP/2 mặc định 100 luồng. EventSource trong đặc tả chỉ nhận URL và withCredentials. Suy luận của Click Star: không gửi được thân yêu cầu hay tiêu đề riêng, nên chat dùng POST thường đọc bằng fetch.
Với fetch, MDN (sửa ngày 1/9/2026) mô tả response.body.getReader() và read() trả về done và value. Mảnh byte không khớp ranh giới sự kiện hay ký tự: chữ Việt có dấu chiếm 2 đến 3 byte UTF-8 ("ế" là 3 byte, Click Star tự kiểm bằng Python).
Cờ stream của TextDecoder.decode báo còn dữ liệu ở lần gọi sau (MDN). Click Star suy ra: bật cờ này để ký tự bị cắt giữa 2 mảnh không thành ký tự lỗi, rồi ghép theo dòng trống trước khi phân tích JSON, hoặc dùng SDK của hãng.
Hiển thị từng phần trên giao diện chat
Tài liệu AI SDK (đọc ngày 23/10/2026) đặt 4 trạng thái cho useChat: submitted, streaming, ready và error, dùng để hiện nút Dừng, khóa nút Gửi và biểu tượng chờ. Tùy chọn throttle gộp cập nhật giao diện (ví dụ trong tài liệu: 50 mili giây).
Về an toàn, OWASP (đọc ngày 23/10/2026) khuyên gán văn bản không tin cậy qua textContent, tránh innerHTML, và dùng DOMPurify khi buộc hiện HTML. Đầu ra mô hình là dữ liệu không tin cậy; Click Star đã có bài riêng về prompt injection.
Về truy cập, MDN (sửa ngày 23/9/2025) nói điểm số trực tiếp hay bảng giá thường không nên báo mỗi lần, và aria-busy ngăn thông báo lúc đang cập nhật. Click Star suy ra: đừng để trình đọc màn hình đọc từng mảnh, chỉ báo ngắn khi xong, và cần thử thực tế. Click Star khuyến nghị tắt hiệu ứng gõ chữ khi người dùng bật prefers-reduced-motion.
Nút Dừng và việc hủy ở máy chủ
Theo MDN, AbortController hủy được yêu cầu fetch lẫn việc đọc thân phản hồi. Nhưng AI SDK (Stopping Streams) nhấn mạnh việc đó chỉ ngắt HTTP: máy chủ phải đổi việc ngắt thành AbortSignal rồi chuyển cho lệnh gọi mô hình, nếu không lệnh gọi có thể chạy tiếp.
Trên Vercel, hủy là tùy chọn bật. Theo Functions API Reference (cập nhật 11/8/2026), thêm "supportsCancellation": true cho từng đường dẫn trong vercel.json; chỉ có ở runtime Node.js. Khi máy khách ngắt, request.signal phát tín hiệu và hàm bị chấm dứt, nên việc bắt buộc phải xong (ghi cơ sở dữ liệu) cần bọc bằng waitUntil hoặc after.
Đường truyền: nơi chữ bị giữ lại

Proxy ngược. Theo tài liệu nginx (đọc ngày 23/10/2026), proxy_buffering mặc định bật; tắt bằng tiêu đề phản hồi X-Accel-Buffering: no. proxy_read_timeout mặc định 60 giây, tính giữa 2 lần đọc liên tiếp: máy chủ gốc im quá mức đó thì kết nối đóng.
CDN, nền tảng và nén. Next.js (cập nhật 15/10/2026) nói CDN có thể đệm cả phản hồi, gzip và Brotli có thể đệm mảnh trước khi đẩy đi, và AWS Lambda phải bật chế độ response streaming (không phải mặc định). Trang lỗi 524 của Cloudflare (cập nhật 23/7/2026) ghi mốc 125 giây mặc định không có phản hồi HTTP; gói Enterprise tăng được.
Hàm serverless. Theo trang Maximum Duration của Vercel (cập nhật 24/8/2026), với fluid compute thời lượng mặc định là 300 giây; tối đa 300 giây ở Hobby, 800 giây ở Pro và Enterprise. Hàm quá hạn bị chấm dứt nên luồng dài bị cắt. Next.js cần Route Handler của App Router, vì API route kiểu Pages không hỗ trợ streaming.
Kết nối rảnh. Với request chạy dài trên HTTP/2, Vercel gửi khung PING lúc rảnh (cùng trang); HTTP/1.1 không có và nên gửi nhịp tim (heartbeat). Đặc tả SSE gợi ý dòng chú thích khoảng 15 giây một lần.
Khi luồng dừng giữa chừng

Ngoài kết thúc bình thường, một luồng còn ít nhất 5 kết cục khác. Quy tắc Click Star suy ra từ trình tự sự kiện: định nghĩa "xong" bằng sự kiện kết thúc, không bằng việc kết nối đóng. Lỗi có thể đến sau mã 200, như bài về lỗi 429 của Click Star đã nêu.
Anthropic gửi lỗi giữa luồng dưới dạng sự kiện error (ví dụ overloaded_error). Lý do dừng max_tokens và model_context_window_exceeded đều nghĩa là câu trả lời bị cắt: hãy hiện "chưa hoàn tất".
Theo Refusals and fallback của Anthropic (đọc ngày 23/10/2026), từ chối (stop_reason là refusal) có thể đến giữa luồng; hãy coi phần đã có là chưa hoàn tất và bỏ đi (phần đã stream vẫn tính phí).
OpenAI cảnh báo streaming khiến kiểm duyệt khó hơn vì đầu ra từng phần khó đánh giá. Nếu phải lọc chữ trước khi khách thấy, Click Star cho rằng cần đệm theo câu.
Nối lại: với Claude 4.5 trở xuống, gửi phần đã nhận làm phần mở đầu tin nhắn trợ lý; với Claude 4.6 trở lên, đưa phần đó vào tin nhắn người dùng kèm lời nhắc viết tiếp. OpenAI cho chạy nền kèm stream, theo dõi sequence_number và nối lại bằng starting_after, nhưng thời gian đến token đầu cao hơn.
Tài liệu Page Lifecycle API của Chrome (cập nhật 16/10/2026) nói trang đóng băng thì bộ đếm giờ và hàm xử lý fetch không chạy. Click Star suy ra: khi người dùng quay lại, hãy kiểm luồng còn sống không và hiện nút "Tiếp tục".
Ghi log và cách đo
Khung tham khảo của Click Star (không phải chuẩn ngành): mỗi lượt ghi TTFT (Anthropic định nghĩa là thời gian từ lúc gửi lời nhắc đến token đầu tiên), tổng thời gian, số token và kết cục. Kết cục thuộc 6 nhóm: hoàn tất, người dùng dừng, lỗi giữa luồng, bị cắt, bị từ chối, đứt kết nối.
AI SDK có onAbort và onEnd để ghi kết cục ở máy chủ. Chỉ ghi kết cục và số đo, không ghi nguyên văn hội thoại vì có thể chứa dữ liệu cá nhân.
Chọn phương án nào (nhận định của Click Star)
- Không streaming: hợp câu trả lời ngắn, tác vụ nền, hoặc khi phải duyệt trọn nội dung; request dài thì Anthropic khuyên streaming vì mạng có thể cắt kết nối rảnh.
- Streaming bằng fetch (POST): hợp chat thông thường, kèm xử lý hủy và lỗi.
- Chạy nền kèm nối lại: hợp tác vụ vài phút, đổi lại token đầu đến chậm hơn.
Ý nghĩa với doanh nghiệp Việt Nam
Với chatbot website hay công cụ nội bộ, rủi ro nằm ở đường truyền và giao diện nhiều hơn ở mô hình. Người dùng hay chuyển qua lại giữa Zalo, trình duyệt và ứng dụng, nên tab ẩn và đứt luồng không hiếm (suy luận, chưa có số đo cho Việt Nam).
Đánh giá của Click Star. Streaming là cải thiện giao diện rẻ, nhưng phần khó là xử lý khi luồng không kết thúc đúng. Nên coi 5 kết cục không hoàn tất là một phần của thiết kế từ đầu, và nối nút Dừng thông suốt đến lệnh gọi mô hình. Công khai lợi ích: Click Star làm dịch vụ tích hợp AI và website, nên có quan tâm thương mại tới chủ đề này.
Việc nên làm: lộ trình 4 tuần (khung tham khảo của Click Star)

- Tuần 1: đổi sự kiện về định dạng nội bộ, đọc bằng fetch với TextDecoder bật cờ stream, hiển thị bằng textContent.
- Tuần 2: thêm 4 trạng thái giao diện, nút Dừng nối đến máy chủ, lưu phần dở vào lịch sử.
- Tuần 3: tắt đệm ở proxy, đặt thời lượng tối đa và nhịp tim, kiểm toàn chuỗi bằng script và điện thoại thật.
- Tuần 4: bật ghi TTFT và kết cục, chạy thử nhóm nhỏ, rồi mở cho tất cả khách.
Những sai lầm thường gặp
- Coi kết nối đóng là câu trả lời hoàn tất.
- Chỉ thử ở máy local, không qua proxy, CDN và hàm serverless thật.
- Nút Dừng chỉ ngắt giao diện, trong khi lệnh gọi mô hình vẫn có thể chạy tiếp.
- Đưa văn bản mô hình vào innerHTML.
- Quên thời lượng tối đa và nhịp tim.
- Để phần dở của câu bị cắt hoặc bị từ chối chảy vào bước tự động như CRM hay tin nhắn.
- Ghi nguyên văn hội thoại vào log khi chỉ cần kết cục.
Điều chưa biết
Click Star không có số đo ở Việt Nam về tỷ lệ luồng đứt hay mức đệm của nhà mạng, và không thấy trang Cloudflare nào nói luồng đã chảy có bị tính mốc 125 giây hay không (trang lỗi 524 không nêu). Tài liệu Rules của Cloudflare có mục Response Body Buffering, và Tunnel cần Content-Type: text/event-stream để không đệm.
Cách tính phí khi hủy giữa chừng chưa thấy nêu. Chưa có hướng dẫn chính thức đọc được về chat streaming với trình đọc màn hình.
Bài liên quan
Cần tư vấn cụ thể cho doanh nghiệp?
Click Star giúp anh/chị xây dựng giải pháp phù hợp ngành nghề và quy mô. Trả lời mọi câu hỏi trong giờ làm việc.



