Response streaming — vì sao chatbot hiện chữ từng chút một
Một câu trả lời dài 8 giây. Bạn thấy chữ đầu tiên sau 280ms. Đó không phải hiệu ứng — đó là SSE và TTFT, hai khái niệm quyết định chatbot cảm giác sống hay chết.
Mở ChatGPT, Claude hay Gemini, nhập một câu hỏi rồi nhấn Enter. Thường bạn sẽ không phải đợi đến khi toàn bộ câu trả lời được tạo xong. Thay vào đó, chữ xuất hiện dần từng phần, như thể có ai đó đang trả lời ở phía bên kia.
Hiện tượng đó được gọi là response streaming. Thay vì chờ model tạo xong toàn bộ nội dung rồi mới gửi một phản hồi hoàn chỉnh, server chuyển từng phần đầu ra của model về trình duyệt ngay khi chúng sẵn sàng. Nhờ vậy, người dùng nhìn thấy chữ đầu tiên rất sớm, dù toàn bộ câu trả lời vẫn cần thêm vài giây để hoàn tất.
Gửi một lần hay gửi dần
Có hai cách phổ biến để trả kết quả từ server về trình duyệt.
Non-streaminglà cách quen thuộc của mô hình request/response truyền thống. Trình duyệt gửi yêu cầu, server xử lý, chờ model tạo xong toàn bộ nội dung, rồi trả về một phản hồi hoàn chỉnh trong một lần. Trong suốt thời gian đó, người dùng chỉ thấy trạng thái “đang tạo” hoặc một spinner quay.
Streaming thì khác. Khi model tạo ra phần đầu tiên của câu trả lời, server gửi phần đó về ngay. Sau đó, mỗi phần mới tiếp tục được gửi đi qua cùng kết nối đang mở. Nội dung cuối cùng có thể giống hệt non-streaming, nhưng trải nghiệm người dùng khác hẳn — họ không phải chờ đến cuối mới thấy kết quả.
Trình duyệt gửi yêu cầu; server chờ model sinh xong toàn bộ đầu ra, rồi trả về một cục JSON hoàn chỉnh. Trong suốt thời gian đó, người dùng thấy spinner quay.
Mỗi khi model sinh ra một token mới, server gửi ngay về trình duyệt qua cùng kết nối đang mở. Người dùng thấy chữ đầu sau vài trăm ms, nội dung lớn dần theo thời gian.
Vì sao TTFT quan trọng hơn tổng thời gian
Điểm khác biệt quan trọng nhất giữa hai cách nằm ở một con số gọi là TTFT — time to first token, tức thời gian từ lúc người dùng gửi yêu cầu đến lúc nhìn thấy token đầu tiên. Trong giao diện chat, TTFT ảnh hưởng đến cảm giác “nhanh” mạnh hơn cả tổng thời gian tạo xong toàn bộ câu trả lời.
Lấy ví dụ một câu trả lời dài khoảng 400 token — model cần khoảng 8 giây để sinh xong.
Với non-streaming, người dùng không thấy gì trong suốt 8 giây đó. Phần lớn sẽ chuyển tab, kiểm tra điện thoại, hoặc cho rằng công cụ bị đứng. Với streaming, token đầu tiên thường xuất hiện sau vài trăm mili-giây. Tổng thời gian sinh vẫn là 8 giây, nhưng trong lúc chờ, người dùng đã đọc được phần lớn câu trả lời. Đến lúc token cuối về tới, họ gần như đã đọc xong.
Nói cách khác, người dùng muốn thấy AI trả lời như một con người đang typing — đó là thứ tạo nên cảm giác sống động, không phải tổng số giây tính toán.
SSE hoạt động ra sao
Một cách phổ biến để triển khai streaming trên web là server-sent events (SSE). Với SSE, server giữ kết nối HTTP mở và gửi dữ liệu xuống trình duyệt từng đợt dưới dạng luồng văn bản, thay vì đóng phản hồi ngay sau một lần trả kết quả.
Phản hồi SSE thường dùng header Content-Type: text/event-stream. Dữ liệu được gửi theo từng block văn bản, mỗi block có dạng như bên dưới.
Mỗi block kết thúc bằng một dòng trống, và trình duyệt có thể xử lý ngay khi block đó đến nơi. Trên trình duyệt, API quen thuộc để nhận luồng SSE là EventSource.
Về phía model, inference server (vLLM, SGLang hay TensorRT-LLM) sinh đầu ra theo từng token kế tiếp. Điều đó cho phép server chuyển tiếp token mới gần như ngay lập tức, thay vì đợi đến khi cả đoạn văn hoàn thành. Quá trình sinh kết thúc khi gặp điều kiện dừng — ví dụ một token đặc biệt tên là <eos> (end of stream), hoặc giới hạn số token đã đặt trước.
Streaming không miễn phí
Streaming giúp người dùng thấy chữ sớm, nhưng bù lại có ba thứ khó hơn so với non-streaming.
Thứ nhất, bạn không biết toàn bộ câu trả lời trước khi nó ra xong. Nghĩa là không thể chạy validation trên cả output — ví dụ kiểm tra JSON có hợp lệ — trước khi hiển thị. Nếu ở token thứ 180, model bắt đầu hallucinate, phần 179 token trước đó đã in ra màn hình và không rút lại được.
Thứ hai, xử lý lỗi giữa dòng phức tạp hơn. Nếu connection rớt ở giây thứ 3, trình duyệt đã nhận được 50 token. Bạn cần quyết định: hiển thị tiếp như cũ, hay báo lỗi rồi cho người dùng retry? Mỗi hướng đều có đánh đổi về UX và độ chính xác.
Thứ ba, hạ tầng trung gian thường buffer mặc định. Nhiều proxy, load balancer và CDN của doanh nghiệp gom nhiều dòng output lại thành một cục trước khi gửi tiếp. Để SSE hoạt động đúng, chúng cần được cấu hình riêng — nếu không, người dùng vẫn thấy spinner như cũ, dù server đã làm hết mọi thứ.
- TTFT thấp → cảm giác nhanh.
- Có thể cancel giữa chừng, tiết kiệm compute.
- Keep-alive giữ connection, tránh timeout.
- Không validate được toàn bộ output trước khi user thấy.
- Error giữa dòng khó retry sạch sẽ.
- Proxy / CDN thường buffer mặc định, phải cấu hình riêng.
Streaming là mặc định cho chat, không phải cho mọi thứ
Streaming hợp nhất với các giao diện nơi người dùng đọc đầu ra ngay lập tức — chat, code suggestion trong IDE, tóm tắt tài liệu, copilot viết email. Ở những chỗ này, TTFT là metric quan trọng hơn cả: người dùng không quan tâm server tiêu bao nhiêu GPU, họ quan tâm chữ có xuất hiện nhanh hay không.
Ngược lại, khi đầu ra là dữ liệu có cấu trúc được dùng nguyên khối — JSON gửi vào một API khác, function call đi vào một tool, hoặc embedding vector — streaming thường không giúp được gì. Cho JSON.parse chạy trên một chuỗi chưa hoàn chỉnh chỉ thêm bug. Các batch job chạy qua đêm cũng không cần streaming vì không có người ngồi nhìn.
Còn một lợi ích ít được nhắc tới: streaming cho phép dừng giữa chừng. Người dùng đọc hai câu đầu, thấy model đi sai hướng, bấm “Stop” — phần tính toán còn lại được tiết kiệm. Với non-streaming, không có lối thoát đó: đã gọi là đã tính xong.