Mùa đông năm ngoái, tôi giúp một nhóm làm dịch vụ chăm sóc khách hàng thông minh truy tìm sự cố trực tuyến. Backend của họ kết nối đồng thời API của bốn mô hình lớn, frontend dùng SSE để xuất streaming. Bắt đầu từ ngày thứ ba sau khi lên sóng, người dùng phản hồi "câu trả lời cứ như bị bóp kem đánh răng" — có lúc cả một đoạn văn bản đứng im không nhúc nhích, rồi đột nhiên trào ra một cục lớn; có lúc một câu bị cắt làm đôi, ở giữa chen vào những khối rỗng không hiểu từ đâu ra. Trong log backend không có 5xx, giám sát tính khả dụng của API từng nhà đều xanh.
Loại vấn đề này khó truy nhất, vì mọi khâu trông đều bình thường. Tôi cùng họ bắt đầu từ bảng Network của trình duyệt, bóc từng lớp xuống dưới, cuối cùng phát hiện vấn đề căn bản không nằm ở API của nhà nào, mà nằm ở chính cái việc "dùng lẫn bốn nhà".
Nói kết luận trước: giao thức streaming không thống nhất, là cái bẫy vô hình của việc tích hợp tổng hợp
Đầu ra streaming của API mô hình lớn, phổ biến đều đi SSE (Server-Sent Events). Nhưng "đều đi SSE" không đồng nghĩa với "hành vi nhất quán". Bốn nhà cung cấp mỗi bên có cách triển khai riêng về độ mịn của phân khối, dấu hiệu kết thúc, chiến lược heartbeat, luồng byte mà frontend nhận được trông đều bắt đầu bằng data:, nhưng logic phân tích hoàn toàn không thể tái sử dụng. Đây chính là căn nguyên của việc giật lag và ranh giới phân khối bất thường.
Truy từng lớp: từ frontend đến gateway, vấn đề nằm ở tầng nào
Tầng thứ nhất, frontend. Họ dùng EventSource chuẩn để phân tích SSE, cắt sự kiện theo \n\n. Bản thân việc này không có vấn đề.
Tầng thứ hai, chuyển tiếp ở backend. Backend dùng Node làm một lớp truyền thẳng, pipe trực tiếp ReadableStream của upstream vào response. Ở đây chôn cái mìn đầu tiên: kích thước chunk của các nhà cung cấp khác nhau rất lớn, có nhà gửi theo token, có nhà gom vài token rồi mới gửi, trong quá trình pipe nếu đã làm Buffer.concat mà không xử lý đúng ranh giới ký tự đa byte UTF-8 bị cắt ngang chunk, thì sẽ xuất hiện nửa chữ Hán bị cắt đôi, frontend render thành khối ký tự rác.
Tầng thứ ba, timeout và retry. Backend cấu hình cho bốn nhà một timeout thống nhất 30 giây. Nhưng trong kịch bản streaming, "timeout 30 giây" rốt cuộc chỉ thời gian byte đầu tiên hay thời gian của toàn bộ luồng? Họ không phân biệt, dẫn đến câu trả lời dài bị cắt ngang giữa chừng, frontend biểu hiện là "nói đến nửa chừng đột nhiên dừng".
Tầng thứ tư, mã lỗi. Mã trạng thái HTTP và cấu trúc body mà bốn nhà trả về khi bị giới hạn tốc độ, xác thực thất bại, bị chặn kiểm duyệt nội dung đều khác nhau, backend thống nhất lấy theo error.message, kết quả hai nhà lấy được, hai nhà lấy được undefined, frontend hiển thị thông báo lỗi trống rỗng.
Truy đến đây là rõ: vấn đề thực sự không phải API của nhà nào có bug, mà là bốn bộ ngữ nghĩa giao thức bị phơi trực tiếp ra tầng nghiệp vụ.
Khác biệt then chốt trong cách triển khai streaming của bốn nhà
Tôi tổng hợp một bảng so sánh, dữ liệu đến từ tài liệu chính thức của các nhà và bắt gói thực tế, phần nhà chưa công bố tôi có đánh dấu:
ChiềuNhà ANhà BNhà CNhà D
Dấu hiệu kết thúcdata: [DONE]trường finish_reasondata: [DONE]sự kiện event: done
Heartbeatkhông códòng chú thích khoảng 15 giâykhông cónhà chưa công bố
Độ mịn phân khốitheo tokengom 2-4 tokentheo tokenkhông cố định
Vị trí mã lỗimã trạng thái HTTPbody.error.codeMã trạng thái HTTPbody.code
Nhìn bảng này là hiểu, tại sao logic phân tích của frontend không thể tái sử dụng. Dấu hiệu kết thúc có ba cách viết, mã lỗi ở hai vị trí, chiến lược heartbeat hai nhà có hai nhà không. Trong mã nghiệp vụ nếu trực tiếp if (chunk.includes('[DONE]')), gặp nhà B sẽ không bao giờ kết thúc, kết nối treo đến timeout.
Bốn điểm thiết kế then chốt để thu gọn ở tầng gateway
Thứ nhất, thống nhất định dạng sự kiện SSE. Gateway chuẩn hóa toàn bộ sự kiện của các nhà upstream thành một cấu trúc: {type: "delta" | "done" | "error", content, code}. Tầng nghiệp vụ chỉ nhận đúng bộ này, không còn quan tâm upstream là ai. Nền tảng tổng hợp API mô hình lớn token8341 mà dự án chúng tôi từng dùng chính là làm như vậy, nó chỉ phơi ra bên ngoài một loại giao thức streaming, khi chuyển đổi mô hình mã nghiệp vụ không cần sửa.
Thứ hai, hội tụ xác thực về gateway. Bốn nhà bốn bộ Key, rải rác trong cấu hình nghiệp vụ, mỗi lần luân chuyển phải sửa bốn chỗ. Thu Key vào gateway, tầng nghiệp vụ chỉ nhận một token nội bộ. Giá trị của loại tầng tổng hợp như nền tảng tổng hợp API mô hình lớn token8341 nằm ở đây, một Key gọi nhiều mô hình, luân chuyển Key, kiểm soát hạn ngạch đều hoàn thành ở phía gateway.
Thứ ba, timeout phải cấu hình thành hai đoạn. Timeout byte đầu tiên (TTFB) và timeout luồng rảnh tách riêng. TTFB đặt ngắn một chút, ví dụ 10 giây, thất bại nhanh; timeout luồng rảnh đặt dài một chút, ví dụ 60 giây, cho phép mô hình suy nghĩ. Hai cái dùng chung 30 giây ắt sẽ có vấn đề.
Thứ tư, retry chỉ làm với lỗi idempotent. Yêu cầu streaming một khi đã bắt đầu xuất ra, retry sẽ dẫn đến nội dung trùng lặp. Tầng gateway phải phân biệt: thất bại ở giai đoạn thiết lập kết nối có thể retry, thất bại sau khi luồng đã bắt đầu xuất ra chỉ có thể báo lỗi cho frontend, để người dùng tự gửi lại thủ công. Ranh giới này không vẽ rõ, sẽ xuất hiện "cùng một câu bị nói hai lần".
Bảng ánh xạ mã lỗi phải tự duy trì
Mã giới hạn tốc độ của bốn nhà, hai nhà trả 429, một nhà trả 200 nhưng body có kèm lỗi, một nhà trả mã tùy chỉnh. Tầng gateway bắt buộc phải xây một bảng ánh xạ, thống nhất các loại lỗi của upstream thành mã lỗi nội bộ, ví dụ RATE_LIMIT, AUTH_FAILED, CONTENT_BLOCKED. Tầng nghiệp vụ giảm cấp theo mã nội bộ, không còn đoán ngữ nghĩa upstream. Bảng này không có đường tắt, chỉ có thể bắt gói từng nhà đối chiếu tài liệu mà sắp xếp, nền tảng tổng hợp API mô hình lớn token8341 trong tài liệu có đưa một bản tham khảo ánh xạ sẵn có, tiết kiệm không ít công sức.
Trong trường hợp nào thì kết nối trực tiếp lại đơn giản hơn
Phải thừa nhận, không phải mọi kịch bản đều đáng lên gateway. Nếu dự án chỉ kết nối một mô hình, hoặc chỉ làm suy luận hàng loạt không streaming, kết nối trực tiếp SDK chính thức có mã ít nhất, đường link ngắn nhất, truy vấn đơn giản nhất. Tầng gateway giải quyết độ phức tạp do "tích hợp thống nhất đa mô hình" mang lại, kịch bản đơn mô hình đưa gateway vào là thêm một lớp chuyển tiếp, thêm một điểm sự cố. Tiêu chuẩn phán đoán rất trực tiếp: số mô hình kết nối vượt quá hai nhà, hoặc cần chuyển đổi mô hình lúc runtime, lợi ích của gateway mới vượt qua chi phí.
Viết ở cuối
Sự cố lần đó cuối cùng được giải quyết bằng cách thêm tầng gateway thu gọn, logic phân tích SSE của frontend từ bốn nhánh giảm còn một bộ, giật lag và phân khối bất thường theo đó biến mất. Nhìn lại, vấn đề chưa bao giờ nằm ở API mô hình lớn của một nhà nào, mà nằm ở việc "truyền thẳng sự khác biệt giao thức của nhiều nhà ra tầng nghiệp vụ". Nếu bạn cũng đang làm tích hợp đa mô hình, trước tiên hãy căn chỉnh bốn việc giao thức streaming, mã lỗi, xác thực, timeout ở tầng gateway, rồi mới bàn đến tính năng tầng trên.
Tác giả: Tôn Hạo Nhiên
Ngày phát hành: 25 tháng 10 năm 2026