Mở đầu — vì sao bài này quan trọng
Bạn có bao giờ để ý rằng khi gõ một câu hỏi cho ChatGPT, câu trả lời không hiện ra cùng một lúc, mà "chảy" ra từng chữ một, như thể có ai đó đang gõ phím ở đầu bên kia? Đó không phải là hiệu ứng trang trí. Đó là streaming — một trong những quyết định thiết kế quan trọng nhất khi bạn xây dựng bất kỳ sản phẩm nào có AI bên trong.
Trong suốt khóa học này, chúng ta đã học cách viết prompt cho tốt: zero-shot, few-shot, chain-of-thought, structured output. Nhưng có một thực tế mà nhiều người mới làm sản phẩm AI bỏ quên: một prompt hoàn hảo vẫn có thể tạo ra trải nghiệm tệ nếu cách bạn trả kết quả về cho người dùng bị sai. Một mô hình lớn có thể mất 15–40 giây để sinh xong một câu trả lời dài. Nếu bạn bắt người dùng ngồi nhìn màn hình trắng suốt 40 giây đó, họ sẽ nghĩ sản phẩm của bạn bị treo và bỏ đi — dù prompt bạn viết xuất sắc đến đâu.
Bài này không nói về việc viết prompt hay hơn. Bài này nói về lớp trải nghiệm (UX) bao quanh prompt của bạn khi nó chạy trong thời gian thực — cụ thể là kỹ thuật streaming và những cân nhắc thiết kế đi kèm. Đây là kiến thức mà một người làm business AI thực thụ bắt buộc phải nắm, vì nó là ranh giới giữa một demo chạy được và một sản phẩm người dùng thật sự yêu thích.
Khái niệm cốt lõi
Streaming là gì
Bình thường, khi bạn gọi một API LLM theo kiểu thông thường (gọi là blocking hay non-streaming), quy trình diễn ra thế này: bạn gửi prompt đi, server chờ mô hình sinh toàn bộ câu trả lời, rồi mới trả về cho bạn một cục dữ liệu hoàn chỉnh. Người dùng chờ từ đầu đến cuối mà không thấy gì.
Streaming thì khác. Mô hình ngôn ngữ sinh câu trả lời theo từng token (một token xấp xỉ 3–4 ký tự tiếng Anh, hoặc một phần của từ). Thay vì đợi sinh xong tất cả token rồi mới gửi về, streaming gửi từng token (hoặc từng nhóm token nhỏ) ngay khi vừa sinh ra, qua một kết nối mở liên tục. Nhờ vậy, giao diện của bạn có thể hiển thị chữ dần dần, ngay khi chúng xuất hiện.
Về mặt kỹ thuật, cơ chế phổ biến nhất để làm việc này là SSE (Server-Sent Events) — một chuẩn cho phép server đẩy dữ liệu về trình duyệt qua một kết nối HTTP giữ mở. Các API như OpenAI, Anthropic Claude, Google Gemini đều hỗ trợ tham số stream: true để bật chế độ này.
Vì sao phải stream — lợi ích thực sự
Điểm cốt lõi bạn cần hiểu là: streaming không làm mô hình chạy nhanh hơn. Tổng thời gian để sinh xong câu trả lời gần như không đổi. Cái nó thay đổi là cảm nhận về tốc độ (perceived latency) của người dùng. Đây là các lợi ích cụ thể:
- Giảm thời gian chờ chữ đầu tiên (Time To First Token — TTFT). Với blocking, người dùng chờ toàn bộ, ví dụ 20 giây. Với streaming, chữ đầu tiên có thể hiện sau 0.5–1.5 giây. Người dùng thấy phản hồi gần như tức thì, dù nội dung vẫn đang được viết tiếp.
- Giữ chân sự chú ý. Con người có xu hướng đọc theo tốc độ chữ hiện ra. Khi chữ chảy ra đều đặn, người dùng bắt đầu đọc ngay và không cảm thấy đang "chờ".
- Cho phép dừng sớm (early exit). Người dùng đọc được vài dòng đầu, nhận ra AI hiểu sai ý, có thể bấm "Dừng" ngay mà không phải đợi hết — tiết kiệm cả thời gian của họ lẫn chi phí token của bạn.
- Tạo cảm giác "sống". Giao diện chữ chảy tạo ấn tượng có một trí tuệ đang "suy nghĩ và viết", tăng độ tin cậy và mức độ gắn kết cảm xúc với sản phẩm.
Đánh đổi và giới hạn của streaming
Streaming không phải lúc nào cũng là lựa chọn đúng. Bạn cần hiểu các đánh đổi:
- Khó xử lý structured output. Nếu prompt của bạn yêu cầu trả về JSON (nhớ lại Bài 9), thì trong lúc stream, chuỗi JSON còn dang dở — chưa đóng ngoặc, chưa đủ trường. Bạn không thể parse nó cho tới khi nhận đủ. Việc hiển thị JSON đang chảy dở cho người dùng cũng vô nghĩa.
- Xử lý lỗi phức tạp hơn. Với blocking, lỗi xảy ra trước khi bạn hiển thị gì. Với streaming, lỗi có thể xảy ra giữa chừng — khi màn hình đã hiện nửa câu trả lời rồi. Bạn phải thiết kế cách xử lý tình huống "đứt gánh giữa đường" này.
- Không phù hợp cho xử lý nền (background jobs). Nếu AI đang chạy để phân loại 10.000 email trong nền, không có người ngồi nhìn, thì streaming chẳng có ý nghĩa gì — chỉ làm code phức tạp thêm.
- Kiểm duyệt nội dung khó hơn. Nếu bạn muốn lọc nội dung không phù hợp trước khi cho người dùng thấy, streaming làm việc này khó, vì chữ đã hiện ra trước khi bạn kịp kiểm duyệt toàn bộ.
Tình huống thực tế
Ví dụ 1 — Tiki và trợ lý mô tả sản phẩm cho người bán
Giả sử đội sản phẩm tại một sàn thương mại điện tử lớn như Tiki xây một tính năng: người bán nhập tên và vài đặc điểm sản phẩm, AI tự viết mô tả bán hàng 200–300 từ. Ở phiên bản đầu, họ dùng blocking. Prompt tốt, kết quả chất lượng, nhưng người bán phải chờ trung bình 18 giây nhìn một nút loading xoay tròn. Dữ liệu cho thấy 34% người bán thoát ra trước khi kết quả hiện — họ tưởng hệ thống lỗi.
Đội chuyển sang streaming. TTFT giảm còn khoảng 1.2 giây, mô tả chảy ra dần như đang được viết trực tiếp. Tỷ lệ thoát giữa chừng giảm xuống dưới 8%. Điều thú vị: tổng thời gian sinh không đổi, vẫn 18 giây, nhưng vì người bán bắt đầu đọc ngay từ giây thứ hai, họ không còn cảm giác chờ đợi.
Bài học: vấn đề của phiên bản đầu không nằm ở prompt hay mô hình, mà ở perceived latency. Streaming không sửa tốc độ thật, nó sửa cảm nhận — và trong sản phẩm, cảm nhận của người dùng chính là thực tế.
Ví dụ 2 — Startup fintech ở Singapore và bài học về structured output
Một startup fintech giả định ở Singapore xây trợ lý tư vấn chi tiêu. Kiến trúc của họ: AI vừa trả lời bằng văn bản (giải thích cho người dùng), vừa trả về một khối JSON chứa số liệu để vẽ biểu đồ ngân sách. Kỹ sư mới bật stream: true cho toàn bộ, rồi cố parse JSON theo từng token. Kết quả: ứng dụng crash liên tục, vì JSON đang chảy dở luôn ở trạng thái không hợp lệ — {"budget": 500, "cat — chưa đóng.
Họ sửa bằng cách tách luồng: dùng hai lời gọi. Một lời gọi stream phần văn bản giải thích cho người dùng đọc mượt mà. Một lời gọi blocking riêng chỉ để lấy JSON số liệu, xử lý xong mới vẽ biểu đồ một lần. Phần chữ hiện ngay tức thì, biểu đồ hiện sau 2 giây khi dữ liệu đã đầy đủ và hợp lệ.
Bài học: đừng stream cái không nên stream. Văn bản cho người đọc thì stream; dữ liệu có cấu trúc cho máy vẽ thì để blocking rồi xử lý trọn gói. Nhận diện đúng loại đầu ra quan trọng hơn là bật streaming ở mọi nơi.
Ví dụ 3 — Tổng đài chatbot của một ngân hàng Việt
Một ngân hàng tại Việt Nam triển khai chatbot chăm sóc khách hàng (liên hệ tới chủ đề Bài 26). Ban đầu họ stream mọi câu trả lời. Nhưng bộ phận tuân thủ (compliance) phát hiện rủi ro: đôi khi AI bắt đầu chảy ra một câu trả lời sai về lãi suất, và vì chữ đã hiện, khách hàng đọc được thông tin sai trước khi hệ thống kịp phát hiện.
Giải pháp của họ là một mô hình lai (hybrid). Với câu hỏi thông thường (giờ mở cửa, hướng dẫn thao tác), họ stream để phản hồi nhanh và tự nhiên. Với câu hỏi nhạy cảm liên quan số liệu tài chính, hệ thống chuyển sang blocking: sinh xong toàn bộ, chạy qua một lớp kiểm tra guardrail (liên hệ Bài 39), rồi mới hiển thị. Trong lúc đó họ hiện một chỉ báo trạng thái "Đang kiểm tra thông tin..." để người dùng không sốt ruột.
Bài học: streaming và kiểm soát nội dung là hai mục tiêu đôi khi mâu thuẫn. Trong lĩnh vực có rủi ro cao, bạn phải chọn — và một chiến lược lai theo từng loại câu hỏi thường là câu trả lời khôn ngoan nhất.
Hướng dẫn từng bước
Đây là quy trình để đưa streaming vào một tính năng AI một cách bài bản:
Bước 1 — Quyết định có nên stream không. Tự hỏi ba câu: (a) Có con người đang chờ đọc kết quả không? (b) Đầu ra có phải văn bản dài để đọc, hay dữ liệu có cấu trúc cho máy? (c) Có cần kiểm duyệt toàn bộ trước khi hiển thị không? Nếu câu trả lời là "có người chờ, văn bản dài, không cần kiểm duyệt nặng" — hãy stream. Ngược lại, cân nhắc blocking.
Bước 2 — Bật streaming ở tầng API. Trong lời gọi API, đặt tham số stream: true. API sẽ trả về một luồng các sự kiện (event) thay vì một object hoàn chỉnh. Mỗi event chứa một mẩu nội dung nhỏ (thường gọi là delta hoặc chunk).
Bước 3 — Đẩy luồng từ server ra client. Server của bạn nhận các chunk từ LLM và chuyển tiếp chúng tới trình duyệt, thường qua SSE. Đừng gom hết chunk lại ở server rồi mới gửi — như thế bạn đã vô tình biến nó thành blocking.
Bước 4 — Hiển thị dần trên giao diện. Ở phía client, mỗi khi nhận một chunk, nối nó vào phần văn bản đang hiển thị. Thêm hiệu ứng con trỏ nhấp nháy ở cuối để báo hiệu "đang viết tiếp".
Bước 5 — Cung cấp nút Dừng (Stop). Cho người dùng khả năng ngắt giữa chừng. Khi họ bấm dừng, hãy đóng kết nối streaming để ngừng tính token — đây vừa là tôn trọng người dùng vừa là tiết kiệm chi phí (liên hệ Bài 38).
Bước 6 — Xử lý khi luồng kết thúc và khi lỗi. Khi luồng đóng bình thường, tắt con trỏ nhấp nháy, bật lại các nút (copy, gửi lại). Nếu luồng đứt giữa chừng do lỗi mạng hoặc lỗi server, hiển thị thông báo rõ ràng và giữ lại phần văn bản đã nhận được, kèm nút "Thử lại".
Bước 7 — Đo lường. Theo dõi hai chỉ số then chốt: TTFT (chữ đầu tiên hiện sau bao lâu) và tỷ lệ thoát giữa chừng. Đây là hai con số cho bạn biết trải nghiệm streaming của bạn có thực sự tốt hay không.
Lỗi thường gặp & mẹo
Lỗi 1 — Cố parse JSON đang stream. Đây là lỗi phổ biến nhất. Nếu prompt yêu cầu structured output, đừng stream rồi parse từng phần. Hoặc dùng blocking cho phần JSON, hoặc chờ nhận đủ toàn bộ rồi mới parse một lần.
Lỗi 2 — Gom chunk ở server. Nhiều người bật stream: true ở API nhưng lại đợi nhận hết ở server rồi mới trả về client. Kết quả là công sức bằng không — người dùng vẫn chờ như blocking. Phải chuyển tiếp từng chunk ngay lập tức.
Lỗi 3 — Quên xử lý lỗi giữa chừng. Với streaming, lỗi có thể xảy ra sau khi đã hiện nửa câu. Nếu không xử lý, người dùng thấy một câu bị cụt lửng lơ, không biết chuyện gì đang xảy ra. Luôn có phương án cho tình huống đứt gánh.
Lỗi 4 — Không có nút Dừng. Nếu AI đi lạc đề và viết một đoạn dài vô ích, người dùng chỉ biết ngồi nhìn. Nút Dừng là bắt buộc, không phải tùy chọn.
Lỗi 5 — Stream mọi thứ một cách máy móc. Streaming đẹp cho văn bản dài, nhưng với câu trả lời một dòng ("Có", "Đơn hàng #123 đã giao") thì blocking đơn giản và đủ nhanh. Đừng phức tạp hóa những thứ vốn nhanh.
Mẹo: Nếu bạn dùng chain-of-thought (Bài 7) và không muốn người dùng thấy phần "suy nghĩ" lộn xộn, hãy stream nhưng ẩn phần reasoning, chỉ hiện câu trả lời cuối. Nhiều sản phẩm hiện phần suy nghĩ trong một khung thu gọn được với nhãn "Đang suy nghĩ...", tạo cảm giác minh bạch mà không làm rối màn hình.
Mẹo: Thêm một chỉ báo trạng thái có ý nghĩa trong khoảng thời gian trước khi chữ đầu tiên xuất hiện (ví dụ "Đang tìm thông tin...", "Đang soạn câu trả lời..."). Khoảng lặng 1 giây trước token đầu tiên vẫn cần được "lấp đầy" về mặt tâm lý.
Bài tập thực hành
- Phân loại quyết định. Với mỗi tính năng sau, quyết định nên stream hay blocking và giải thích: (a) trợ lý viết email marketing 300 từ; (b) API phân loại 5.000 review sản phẩm chạy ban đêm; (c) chatbot trả lời "còn hàng hay hết hàng"; (d) trợ lý phân tích trả về JSON số liệu để vẽ dashboard.
- Thiết kế xử lý lỗi. Vẽ (bằng lời hoặc sơ đồ) luồng xử lý cho tình huống: người dùng đang xem một câu trả lời stream dở thì mất mạng. Giao diện nên hiển thị gì? Nút nào cần xuất hiện? Phần văn bản đã nhận được xử lý ra sao?
- Ước lượng TTFT. Chọn một sản phẩm AI bạn hay dùng (ChatGPT, Gemini, hay một chatbot tiếng Việt). Bấm giờ thủ công 5 lần xem chữ đầu tiên hiện sau bao lâu. Ghi lại con số trung bình và tự đánh giá trải nghiệm đó nhanh hay chậm.
- Kịch bản lai. Bạn xây chatbot cho một công ty bảo hiểm Việt Nam. Hãy liệt kê 3 loại câu hỏi nên stream và 3 loại nên blocking-kèm-kiểm-duyệt, kèm lý do cho từng loại.
Tóm tắt
Streaming là kỹ thuật gửi câu trả lời của LLM về người dùng theo từng token ngay khi vừa sinh ra, thay vì đợi sinh xong toàn bộ. Điều then chốt cần nhớ: streaming không làm mô hình chạy nhanh hơn, nó chỉ cải thiện cảm nhận về tốc độ (perceived latency) bằng cách giảm thời gian chờ chữ đầu tiên (TTFT), giữ chân sự chú ý, và cho phép người dùng dừng sớm.
Nhưng streaming không phải "bật là xong". Nó đi kèm đánh đổi thật: khó xử lý structured output như JSON, xử lý lỗi giữa chừng phức tạp hơn, kiểm duyệt nội dung khó hơn, và vô nghĩa với các tác vụ chạy nền. Người làm sản phẩm giỏi biết khi nào nên stream và khi nào nên blocking — thường là một chiến lược lai theo từng loại đầu ra và mức độ rủi ro, như ba tình huống Tiki, fintech Singapore và ngân hàng Việt đã minh họa.
Cuối cùng, hãy nhớ: một prompt xuất sắc chỉ phát huy hết giá trị khi được bọc trong một trải nghiệm real-time tử tế. Streaming, nút Dừng, chỉ báo trạng thái, và xử lý lỗi mượt mà — đó là lớp UX biến một prompt tốt thành một sản phẩm mà người dùng thực sự muốn quay lại.