Cách OpenAI cung cấp giải pháp giọng nói AI độ trễ thấp ở quy mô lớn
Bởi Yi Zhang và William McDonald, Thành viên Ban kỹ thuật
Giải pháp giọng nói AI chỉ thực sự tự nhiên khi cuộc trò chuyện diễn ra với tốc độ của lời nói. Khi mạng bị cản trở, người dùng sẽ nhận ra ngay qua những khoảng dừng gượng gạo, các lượt ngắt lời bị cắt cụt hoặc việc chen lời bị trễ. Điều đó quan trọng với ChatGPT Voice, với các nhà phát triển xây dựng bằng API Realtime, với các tác nhân hoạt động trong quy trình tương tác và với các mô hình cần xử lý âm thanh khi người dùng vẫn đang nói.
Ở quy mô của OpenAI, điều đó quy thành ba yêu cầu cụ thể:
- Phạm vi tiếp cận toàn cầu cho hơn 900 triệu người dùng hoạt động hằng tuần
- Thiết lập kết nối nhanh để người dùng có thể bắt đầu nói ngay khi phiên bắt đầu
- Thời gian truyền nhận âm thanh thấp và ổn định, ít bị giật và ít mất dữ liệu, để việc luân phiên lượt nói diễn ra mượt mà
Đội ngũ tại OpenAI chịu trách nhiệm về tương tác AI thời gian thực gần đây đã tái cấu trúc stack WebRTC để giải quyết ba ràng buộc đang bắt đầu xung đột khi mở rộng quy mô: chấm dứt truyền âm thanh theo mô hình một cổng cho mỗi phiên không phù hợp với hạ tầng OpenAI, các phiên ICE (Interactive Connectivity Establishment) và DTLS (Datagram Transport Layer Security) có trạng thái cần được sở hữu ổn định, và định tuyến toàn cầu phải giữ độ trễ chặng đầu tiên ở mức thấp. Trong bài viết này, chúng tôi trình bày kiến trúc tách lớp relay kèm transceiver mà chúng tôi xây dựng để giữ nguyên hành vi WebRTC tiêu chuẩn cho client trong khi thay đổi cách các gói tin được định tuyến bên trong hạ tầng của OpenAI.
WebRTC là một tiêu chuẩn mở để truyền âm thanh, video và dữ liệu có độ trễ thấp giữa trình duyệt, ứng dụng di động và server. Nó thường gắn với gọi ngang hàng, nhưng cũng là nền tảng thực tế cho các hệ thống thời gian thực giữa client và server vì nó chuẩn hóa các phần khó của tương tác âm thanh: ICE để thiết lập kết nối và vượt qua NAT (Network Address Translation), DTLS và SRTP (Secure Real-time Transport Protocol) để truyền tải được mã hóa, thương lượng codec để nén và giải mã âm thanh, RTCP (Real-time Transport Control Protocol) để kiểm soát chất lượng, và các tính năng phía client như khử tiếng vọng và bộ đệm chống giật.
Sự chuẩn hóa đó rất quan trọng đối với các sản phẩm AI. Nếu không có WebRTC, mỗi client sẽ cần một cách riêng để thiết lập kết nối qua NAT, mã hóa truyền âm thanh, thương lượng codec (bộ mã hóa-giải mã được chọn cho truyền dẫn và giải nén) và thích ứng với các điều kiện mạng thay đổi. Với WebRTC, chúng tôi có thể xây dựng trên một stack giao thức đã được triển khai sẵn trên các trình duyệt và nền tảng di động, từ đó tập trung công việc của mình vào hạ tầng kết nối truyền thông thời gian thực với các mô hình.
Chúng tôi cũng xây dựng trên chính hệ sinh thái WebRTC, bao gồm các triển khai mã nguồn mở đã ổn định và công việc tiêu chuẩn giúp trình duyệt, ứng dụng di động và server có thể tương tác với nhau. Công trình nền tảng của Justin Uberti (một trong những kiến trúc sư ban đầu của WebRTC) và Sean DuBois (người tạo ra và duy trì Pion) đã giúp những nhóm như chúng tôi có thể xây dựng trên hạ tầng truyền thông đã được kiểm chứng thực tế thay vì phải tái tạo hành vi vận chuyển, mã hóa và kiểm soát tắc nghẽn ở tầng thấp. Chúng tôi rất may mắn khi cả Justin và Sean hiện đều là đồng nghiệp tại OpenAI, giúp định hướng cách chúng tôi đưa WebRTC và AI thời gian thực đến gần nhau hơn.
Đối với AI, thuộc tính quan trọng nhất là âm thanh đến dưới dạng một luồng liên tục. Một tác nhân thoại có thể bắt đầu chép lời, suy luận, gọi công cụ hoặc tạo lời nói khi người dùng vẫn đang nói, thay vì chờ tải lên đầy đủ. Đó là khác biệt giữa một hệ thống mang lại cảm giác hội thoại và một hệ thống giống như nhấn để nói.
Sau khi chọn WebRTC, câu hỏi tiếp theo là sẽ chấm dứt nó ở đâu (nơi chúng tôi chấp nhận và nắm quyền sở hữu kết nối WebRTC—ví dụ ở biên) và sẽ kết nối các phiên đó với backend suy luận như thế nào. Điểm chấm dứt rất quan trọng vì nó quyết định cách chúng tôi xử lý trạng thái phiên thời gian thực, truyền tải âm thanh, định tuyến, độ trễ và cô lập lỗi.
Một SFU, hay selective forwarding unit, là server âm thanh nhận một luồng WebRTC từ mỗi người tham gia và chọn lọc chuyển tiếp các luồng đó cho những người khác. Trong mô hình này, SFU chấm dứt một kết nối WebRTC riêng cho mỗi người tham gia, và AI tham gia như một người tham gia khác trong phiên. Đây có thể là lựa chọn phù hợp cho các sản phẩm vốn dĩ có nhiều bên tham gia, như cuộc gọi nhóm, lớp học hoặc cuộc họp cộng tác. Nó giữ codec âm thanh, thông điệp RTCP, kênh dữ liệu, ghi âm và chính sách theo từng luồng ở cùng một nơi.1
Ngay cả trong các sản phẩm giữa client và AI, SFU thường là điểm khởi đầu mặc định vì nó cho phép các đội nhóm tái sử dụng một hệ thống đã được kiểm chứng cho báo hiệu, định tuyến âm thanh, ghi âm, khả năng quan sát và các phần mở rộng trong tương lai như chuyển tiếp sang con người hoặc thêm người tham gia.
Công việc của chúng tôi khác trường hợp thông thường. Phần lớn các phiên là 1:1— nghĩa là một người dùng nói chuyện với một mô hình, hoặc một ứng dụng nói chuyện với một tác nhân thời gian thực—mỗi lượt trao đổi đều cần phản hồi nhanh Với dạng lưu lượng đó, chúng tôi chọn mô hình transceiver: một dịch vụ WebRTC ở biên chấm dứt kết nối client rồi chuyển đổi âm thanh và dữ liệu sự kiện sang các giao thức nội bộ đơn giản hơn để suy luận mô hình, phiên âm, tạo giọng nói, sử dụng công cụ và điều phối.
Trong thiết kế này, transceiver là dịch vụ duy nhất quản lý trạng thái phiên WebRTC, bao gồm các lần kiểm tra kết nối ICE, bắt tay DTLS, khóa mã hóa SRTP và vòng đời phiên. “Chấm dứt” ở đây nghĩa là transceiver đóng vai trò là điểm cuối hoàn tất các bước bắt tay đó và mã hóa hoặc giải mã âm thanh. Việc giữ trạng thái ở một nơi dễ dàng giúp việc quản lý phiên hợp lý hơn, đồng thời cho phép các dịch vụ backend mở rộng như các dịch vụ thông thường thay vì tự hoạt động như peer của WebRTC.
Sau khi chọn mô hình transceiver, việc đầu tiên chúng tôi làm là triển khai một dịch vụ Go đơn lẻ xây dựng trên Pion, xử lý cả báo hiệu lẫn chấm dứt âm thanh. Nó đang vận hành ChatGPT Voice, endpoint WebRTC của API Realtime và một số dự án nghiên cứu.
Về mặt vận hành, dịch vụ transceiver thực hiện hai nhiệm vụ:
- Báo hiệu: thương lượng SDP, chọn codec, thông tin xác thực ICE và thiết lập phiên
- Âm thanh: chấm dứt các kết nối WebRTC từ phía người dùng và duy trì các kết nối với các dịch vụ backend để suy luận và điều phối
Chúng tôi muốn dịch vụ này chạy như phần còn lại của hạ tầng: hoạt động trên Kubernetes, nơi khối lượng công việc có thể tăng giảm quy mô và di chuyển giữa các host khi nhu cầu thay đổi. Nhưng mô hình WebRTC truyền thống (một cổng mỗi phiên) lại không phù hợp với môi trường đó, vì nó phụ thuộc vào các dải cổng UDP công cộng lớn, khó mở, khó bảo mật, và khó duy trì khi các pod được thêm vào, loại bỏ hoặc chuyển sang máy khác.2
Vấn đề đầu tiên gặp phải là do chính mô hình một cổng cho mỗi phiên. Khi có nhiều kết nối cùng lúc, chúng tôi phải mở và quản lý các dải cổng UDP rất lớn.
- Load balancer đám mây và các dịch vụ Kubernetes không được thiết kế để xứ lý hàng chục nghìn cổng UDP công cộng cho mỗi dịch vụ. Mỗi lần thêm dải cổng mới là khiến công việc vận hành trở nên phức tạp hơn trong cấu hình load balancer, kiểm tra tình trạng hoạt động, chính sách tường lửa và triển khai an toàn.3
- Các dải cổng UDP lớn khó bảo mật. Chúng mở rộng khu vực mà bên ngoài có thể tiếp cận và khiến chính sách mạng khó kiểm tra hơn.
- Chúng cũng không phù hợp với tự động mở rộng. Các pod liên tục được thêm, xoá đi, và chuyển sang máy khác trong Kubernetes. Việc yêu cầu mỗi pod phải dành riêng và công khai một dải cổng lớn ổn định sẽ làm cho tính linh hoạt mở rộng trở nên mong manh.4
Đây là lý do nhiều hệ thống WebRTC chuyển sang mô hình một cổng UDP cho mỗi server, với việc phân luồng ở cấp ứng dụng diễn ra phía sau cổng đó.5
Thiết kế một cổng cho mỗi server giải quyết số lượng cổng, nhưng lại đưa đến vấn đề thứ hai: duy trì quyền quản lý từng phiên trên toàn bộ cụm.
ICE và DTLS là các giao thức có trạng thái. Tiến trình đã tạo ra một phiên cần tiếp tục nhận các gói tin của phiên đó để có thể xác thực các lần kiểm tra kết nối, hoàn tất bắt tay DTLS, giải mã SRTP và xử lý các thay đổi về sau của phiên như khởi động lại ICE. Nếu các gói tin của cùng một phiên rơi vào một tiến trình khác, quá trình thiết lập có thể thất bại hoặc âm thanh có thể bị gián đoạn.
Điều đó dẫn đến một mục tiêu cụ thể: công khai một bề mặt UDP nhỏ, cố định trên mạng, trong khi vẫn định tuyến mọi gói tin đến transceiver quản lý phiên WebRTC tương ứng.
Chúng tôi đã đánh giá nhiều cách để đạt được điều đó, bao gồm TURN (Traversal Using Relays around NAT), nơi một relay ở biên chấm dứt các kết nối từ client và thay chúng chuyển tiếp lưu lượng thay cho client.2
Approach | Pros | Cons |
Unique IP:port per session (also known as native direct UDP) | Direct client-to-server media path No forwarding layer in the data path | Requires one public UDP port per session Large port ranges are difficult to expose and secure Poor fit for Kubernetes and cloud load balancers |
Unique IP:port per server | Much smaller public UDP footprint than per-session exposure One shared socket per server can demultiplex many sessions | Works cleanly on a single host, but not across a shared load-balanced fleet by itself Session demultiplexing on a single host only helps after a packet reaches that host; across a load-balanced fleet, the first packet can still land on the wrong instance, so you still need a deterministic way to steer each session to the process that owns it |
TURN relay (protocol-terminating) | Clients only need to reach the TURN relay address and port Can centralize policy at the edge | TURN allocations add setup round trips Moving or recovering allocations across TURN servers is still difficult |
Stateless forwarder + stateful terminator (OpenAI’s relay + transceiver) | Small public UDP footprint Transceiver still owns the full WebRTC session | Adds one forwarding hop before media reaches the owning transceiver Requires custom coordination between relay and transceiver |
Kiến trúc mà chúng tôi triển khai tách biệt việc định tuyến gói tin khỏi việc chấm dứt giao thức. Báo hiệu vẫn đến transceiver để thiết lập phiên, trong khi âm thanh đi vào qua relay trước. Relay là một lớp chuyển tiếp UDP nhẹ ít để lại dấu vết, còn nằm sau đó là transceiver là điểm cuối WebRTC có trạng thái.
Relay không giải mã âm thanh, không chạy máy trạng thái ICE và không tham gia thương lượng codec. Nó đọc vừa đủ siêu dữ liệu gói tin để chọn đích, rồi chuyển tiếp gói tin tới transceiver quản lý phiên. Transceiver vẫn nhìn thấy một luồng WebRTC bình thường và vẫn quản lý toàn bộ trạng thái giao thức. Từ phía client thì phiên WebRTC không có gì thay đổi.
Định tuyến gói đầu tiên là bước then chốt trong thiết lập này. Relay phải định tuyến gói tin đầu tiên từ client trước khi có bất kỳ phiên nào tồn tại trên chính đường truyền gói tin, thay vì tạm dừng để tra cứu một dịch vụ bên ngoài.
Mọi phiên WebRTC đều đã mang theo một cơ chế định tuyến tự nhiên của giao thức: ICE username fragment, hay ufrag, một định danh ngắn được trao đổi trong quá trình thiết lập phiên và được lặp lại trong các lần kiểm tra kết nối STUN. Chúng tôi tạo ufrag phía server sao cho chứa vừa đủ siêu dữ liệu định tuyến để relay có thể suy ra cụm đích và transceiver sở hữu phiên.
Trong quá trình báo hiệu, transceiver cấp phát trạng thái phiên và trả về một địa chỉ relay VIP relay dùng chung cùng cổng UDP trong câu trả lời SDP. VIP là địa chỉ IP ảo đứng trước fleet relay. Khi kết hợp với cổng, nó cung cấp cho client một đích đến ổn định duy nhất, chẳng hạn `203.0.113.10:3478`, dù thực tế có nhiều instance của relay đứng sau VIP này. Gói âm thanh đầu tiên của client thường là yêu cầu liên kết STUN (Session Traversal Utilities for NAT), thứ mà ICE dùng để xác minh rằng các gói tin có thể tới địa chỉ đã công bố.
Relay chỉ phân tích vừa đủ gói STUN đầu tiên để đọc ufrag của server, giải mã gợi ý định tuyến và chuyển tiếp gói tin đến transceiver sở hữu. Mỗi transceiver lắng nghe trên một ổ cắm UDP dùng chung, tức một endpoint hệ điều hành được gắn với một IP:cổng nội bộ, chứ không phải một ổ cắm cho mỗi phiên. Sau khi relay tạo một phiên từ IP:cổng nguồn của client tới đích transceiver đó, các gói DTLS, RTP và RTCP tiếp theo sẽ chuyển tiếp trong phiên mà không cần giải mã lại ufrag.
Phiên của relay được thiết kế tối giản một cách có chủ đích, chỉ gồm một phiên trong bộ nhớ để phục vụ chuyển tiếp gói tin, cùng các bộ đếm cần thiết cho mục đích giám sát và bộ hẹn giờ cho việc hết hạn và dọn dẹp phiên. Lựa chọn thiết kế này giữ việc định tuyến gói tin trực tiếp trên đường đi của gói. Nếu một relay khởi động lại và mất phiên, gói STUN tiếp theo sẽ dựng lại phiên từ gợi ý định tuyến trong ufrag. Để tăng độ tin cậy hơn nữa, hệ thống sử dụng bộ nhớ đệm Redis để lưu trữ mapping của <IP client + Cổng, IP transceiver + Cổng> ngay khi tuyến được thiết lập, nhờ đó có thể khôi phục sớm hơn nhiều, trước khi gói STUN tiếp theo đến.
Sau khi thu hẹp bề mặt UDP công khai xuống chỉ còn một số ít địa chỉ và cổng ổn định, chúng tôi có thể triển khai mô hình relay thống nhất trên toàn cầu. Global Relay là cụm các ingress point của relay phân tán theo địa lý, tất cả đều thực hiện cùng một hành vi chuyển tiếp gói tin.
Phân bổ ingress rộng khắp về mặt địa lý giúp rút ngắn chặng đầu tiên từ client vào OpenAI, vì một gói tin có thể nhập vào mạng của chúng tôi tại relay gần người dùng nhất về cả khoảng cách địa lý lẫn cấu trúc liên kết (topology) mạng, thay vì phải đi qua mạng internet công cộng tới một khu vực xa trước. Kết quả thực tế là độ trễ thấp hơn, ít giật hơn và ít các đợt mất gói tin không cần thiết hơn trước khi lưu lượng đến được backbone.6
Chúng tôi sử dụng điều hướng theo địa lý và khoảng cách gần (geo and proximity steering) của Cloudflare cho báo hiệu để yêu cầu HTTP hoặc WebSocket ban đầu đến được một cụm transceiver gần đó. Ngữ cảnh của yêu cầu quyết định vị trí của phiên và ingress point của Global Relay nào sẽ được quảng bá cho client. Câu trả lời SDP cung cấp địa chỉ Global Relay, trong khi ufrag chứa đủ thông tin để Global Relay định tuyến truyền thông tới cụm được chỉ định và để relay định tuyến tới transceiver đích.
Khi kết hợp lại, báo hiệu điều hướng theo địa lý và Global Relay đặt cả quá trình thiết lập lẫn luồng âm thanh vào đường truyền gần đó, đồng thời vẫn neo phiên vào một transceiver duy nhất. Nhờ đó, thời gian báo hiệu truyền tới và truyền lại và cho lần kiểm tra kết nối ICE đầu tiên, từ đó trực tiếp rút ngắn thời gian người dùng phải chờ trước khi có thể bắt đầu nói.
Chúng tôi viết dịch vụ relay bằng Go và cố ý giữ phạm vi triển khai hẹp. Trên Linux, stack mạng của kernel nhận các gói UDP từ giao diện mạng của máy và chuyển chúng tới một ổ cắm, là endpoint hệ điều hành mà một tiến trình đọc sau khi gắn với một IP:Cổng. Relay chạy trong không gian người dùng, nên một tiến trình Go thông thường sẽ đọc header gói tin từ ổ cắm đó, cập nhật một lượng nhỏ trạng thái luồng và chuyển tiếp gói tin mà không chấm dứt WebRTC. Chúng tôi không cần tới bất kỳ công nghệ kernel bypass nào, vốn cho phép chương trình đọc trực tiếp hàng đợi mạng để xử lý được nhiều gói tin hơn nhưng cũng khiến vận hành phức tạp hơn.
Các lựa chọn thiết kế chính:
- Không chấm dứt giao thức: Relay chỉ phân tích header STUN và ufrag; các gói DTLS, RTP và RTCP sau đó được chuyển tiếp dựa trên trạng thái bộ nhớ đệm (cached state), giữ các gói tin ở dạng opaque (không đọc hoặc thay đổi được).
- Trạng thái tạm thời: Relay duy trì một bảng ánh xạ nhỏ, tồn tại ngắn hạn trong bộ nhớ, ánh xạ từ địa chỉ client đến đích transceiver để quản lý trạng thái luồng và hỗ trợ khả năng quan sát.
- Mở rộng quy mô theo chiều ngang: Nhiều instance của relay chạy song song phía sau load balancer. Trạng thái không phải là trạng thái WebRTC cố định, nên việc khởi động lại chỉ gây gián đoạn lưu lượng tối thiểu và khôi phục luồng nhanh chóng.
Các biện pháp tối ưu hiệu quả:
SO_REUSEPORTlà một tùy chọn ổ cắm Linux cho phép nhiều worker relay trên cùng một máy gắn vào cùng một cổng UDP. Sau đó kernel phân phối các gói đến qua các worker đó, giúp tránh nút thắt ở một vòng lặp đọc duy nhất.runtime.LockOSThreadghim mỗi goroutine đọc UDP vào một luồng hệ điều hành cụ thể. Khi kết hợp vớiSO_REUSEPORT, điều này có xu hướng giữ các gói từ cùng một luồng (IP:Cổng nguồn và đích cộng với giao thức) trên cùng một lõi CPU, cải thiện tính cục bộ của bộ nhớ đệm và giảm chuyển đổi ngữ cảnh.- Bộ đệm phân bổ sẵn và sao chép tối thiểu giúp giữ chi phí phân tích và phân bổ ở mức thấp để tránh thu gom rác trong Go.
Triển khai này đã xử lý lưu lượng truyền thông thời gian thực toàn cầu của chúng tôi mà ít để lại dấu tích relay, nên chúng tôi giữ thiết kế đơn giản hơn thay vì chuyển sang dùng cách bypass.
Kiến trúc này cho phép chúng tôi chạy luồng âm thanh WebRTC trong Kubernetes mà không phải mở hàng nghìn cổng UDP ra bên ngoài. Điều này rất quan trọng vì diện tích tiếp xúc UDP nhỏ hơn và cố định sẽ dễ bảo mật hơn, dễ cân bằng tải hơn, đồng thời giúp hạ tầng mở rộng quy mô mà không phải dành sẵn một khoảng cổng công cộng lớn. Với hỗ trợ hạ tầng tốt hơn từ Kubernetes và độ bảo mật cao hơn nhờ bề mặt nhỏ hơn, thiết kế này cũng giữ nguyên hành vi WebRTC tiêu chuẩn cho client và khẳng định rằng thiết kế không SFU là lựa chọn mặc định phù hợp cho khối lượng công việc của chúng tôi. Phần lớn các phiên của chúng tôi là kiểu điểm-điểm (point-to-point), nhạy với độ trễ và dễ mở rộng quy mô hơn khi các dịch vụ suy luận không cần hành xử như peer của WebRTC.
Bài học lớn hơn ở đây là: nơi tốt nhất để thêm độ phức tạp là ở lớp định tuyến mỏng, chứ không phải đưa vào từng dịch vụ backend, cũng không nên để ở phía hành vi tùy chỉnh của client. Việc nhúng siêu dữ liệu định tuyến vào một trường sẵn có của giao thức đã cho chúng tôi khả năng định tuyến gói đầu tiên một cách chắc chắn, ít để lại dấu tích UDP, và đủ linh hoạt để đặt các ingree point gần người dùng trên toàn thế giới.
Một vài lựa chọn đặc biệt quan trọng:
- Giữ nguyên ngữ nghĩa giao thức ở biên. Clietn vẫn dùng WebRTC tiêu chuẩn, nhờ đó khả năng tương tác giữa trình duyệt và thiết bị di động vẫn được bảo toàn.
- Giữ các trạng thái phiên cố định ở một nơi. Transceiver chịu trách nhiệm quản lý ICE, DTLS, SRTP và vòng đời phiên; relay chỉ chuyển tiếp gói tin.
- Định tuyến dựa trên thông tin đã có sẵn trong quá trình thiết lập. Ufrag ICE cho chúng tôi một móc định tuyến gói đầu tiên mà không cần thêm bất kỳ tra cứu phức tạp nào trên đường truyền chính (hot-path).
- Hãy tối ưu cho trường hợp phổ biến trước khi nghĩ đến kernel bypass. Một triển khai Go gọn nhẹ, kết hợp sử dụng khéo léo
SO_REUSEPORT, ghim luồng và phân tích ít lượt phân bổ là đủ sức đáp ứng cho khối lượng công việc của chúng tôi.
Giải pháp giọng nói AI thời gian thực chỉ hoạt động khi hạ tầng khiến độ trễ trở nên gần như không thể nhận thấy. Với chúng tôi, điều đó đồng nghĩa với việc thay đổi hình thức triển khai WebRTC mà không thay đổi những gì client kỳ vọng từ chính WebRTC.
Tác giả
Tài liệu tham khảo
2. GitHub - l7mp/stunner: Cổng truyền âm thanh Kubernetes cho WebRTC(mở trong cửa sổ mới)
3. Vài điểm chính về cổng WebRTC [Ví dụ] - BlogGeek.me(mở trong cửa sổ mới)
4. Triển khai lên Kubernetes - tài liệu LiveKit(mở trong cửa sổ mới)
6. Cloudflare Calls: hàng triệu cây phân tầng nối tiếp nhau đến tận cùng(mở trong cửa sổ mới)


