Przejdź do treści głównej
OpenAI

4 maja 2026

Inżynieria

Jak OpenAI zapewnia Voice AI o niskich opóźnieniach na dużą skalę

Yi Zhang i William McDonald, Technical Staff

Voice AI wydaje się naturalne tylko wtedy, gdy rozmowa toczy się z szybkością mowy. Kiedy sieć staje na przeszkodzie, ludzie od razu słyszą to jako niezręczne pauzy, urwane wejścia w słowo lub opóźnione przerwanie wypowiedzi. Ma to znaczenie dla ChatGPT Voice, dla deweloperów tworzących z użyciem Realtime API, dla agentów działających w interaktywnych przepływach pracy oraz dla modeli, które muszą przetwarzać dźwięk, gdy użytkownik nadal mówi.

W skali OpenAI przekłada się to na trzy konkretne wymagania:

  • Globalny zasięg dla ponad 900 milionów aktywnych użytkowników tygodniowo
  • Szybkie zestawianie połączenia, aby użytkownik mógł zacząć mówić zaraz po rozpoczęciu sesji
  • Niski i stabilny czas obiegu mediów w obie strony, przy niskim jitterze i utracie pakietów, tak aby wymiana tur wypowiedzi była płynna

Zespół w OpenAI odpowiedzialny za interakcje AI w czasie rzeczywistym niedawno przeprojektował nasz stos WebRTC, aby rozwiązać trzy ograniczenia, które zaczęły się ze sobą zderzać przy dużej skali: kończenie mediów w modelu jeden port na sesję nie pasuje dobrze do infrastruktury OpenAI, stanowe sesje ICE (Interactive Connectivity Establishment) i DTLS (Datagram Transport Layer Security) wymagają stabilnej odpowiedzialności, a globalny routing musi utrzymywać niskie opóźnienie pierwszego przeskoku. W tym artykule omawiamy rozdzieloną architekturę relay plus transceiver, którą zbudowaliśmy, aby zachować standardowe działanie WebRTC po stronie klientów, jednocześnie zmieniając sposób kierowania pakietów wewnątrz infrastruktury OpenAI.

WebRTC pozwala nam tworzyć produkty AI działające w czasie rzeczywistym

WebRTC to otwarty standard do przesyłania dźwięku, obrazu i danych o niskich opóźnieniach między przeglądarkami, aplikacjami mobilnymi i serwerami. Często kojarzy się go z połączeniami peer-to-peer, ale jest też praktyczną podstawą systemów czasu rzeczywistego klient-serwer, ponieważ standaryzuje najtrudniejsze elementy mediów interaktywnych: ICE do ustanawiania łączności i przechodzenia przez NAT (Network Address Translation), DTLS i SRTP (Secure Real-time Transport Protocol) do szyfrowanego transportu, negocjację kodeków do kompresji i dekodowania dźwięku, RTCP (Real-time Transport Control Protocol) do kontroli jakości oraz funkcje po stronie klienta, takie jak redukcja echa i buforowanie jittera.

Ta standaryzacja ma znaczenie dla produktów AI. Bez WebRTC każdy klient potrzebowałby innego rozwiązania dotyczącego ustanawiania łączności przez NAT-y, szyfrowania mediów, negocjacji kodeków (koderów-dekoderów wybranych do transmisji i dekompresji) oraz dostosowywania się do zmieniających się warunków sieciowych. Dzięki WebRTC możemy budować na stosie protokołów, który jest już zaimplementowany w przeglądarkach i na platformach mobilnych, koncentrując własne prace na infrastrukturze łączącej media czasu rzeczywistego z modelami.

Opieramy się także na samym ekosystemie WebRTC, w tym dojrzałych implementacjach open source i pracach standaryzacyjnych, które utrzymują interoperacyjność przeglądarek, aplikacji mobilnych i serwerów. Fundamentalna praca Justina Ubertiego (jednego z oryginalnych architektów WebRTC) i Seana DuBois (twórcy i opiekuna Pion) umożliwiła zespołom takim jak nasz budowanie na sprawdzonej infrastrukturze medialnej zamiast wymyślania od nowa zachowań niskopoziomowego transportu, szyfrowania i kontroli przeciążenia. Mamy szczęście, że zarówno Justin, jak i Sean są dziś naszymi współpracownikami w OpenAI i pomagają wyznaczać kierunek, w którym zbliżamy do siebie WebRTC i AI czasu rzeczywistego.

Dla AI najważniejszą właściwością jest to, że dźwięk dociera jako ciągły strumień. Mówiący agent może zacząć transkrypcję, rozumowanie, wywoływanie narzędzi lub generowanie mowy, gdy użytkownik nadal mówi, zamiast czekać na pełne przesłanie. To różnica między systemem, który wydaje się konwersacyjny, a takim, który przypomina naciśnij i mów.

Wybór architektury mediów

Gdy wybraliśmy WebRTC, kolejnym pytaniem było to, gdzie je zakończyć (gdzie zaakceptujemy i przejmiemy połączenie WebRTC — na przykład na brzegu sieci) i jak połączyć te sesje z backendem inferencyjnym. Miejsce zakończenia ma znaczenie, ponieważ decyduje o tym, jak obsługujemy stan sesji czasu rzeczywistego, transport mediów, routing, opóźnienia i izolację awarii.

Opcja 1: Podejście SFU uwzględnia AI jako uczestnika WebRTC

SFU, czyli selective forwarding unit, to serwer mediów, który odbiera po jednym strumieniu WebRTC od każdego uczestnika i selektywnie przekazuje strumienie pozostałym. W tym modelu SFU kończy oddzielne połączenie WebRTC dla każdego uczestnika, a AI dołącza jako kolejny uczestnik sesji. Może to dobrze pasować do produktów z natury wieloosobowych, takich jak połączenia grupowe, klasy czy spotkania współpracy. Pozwala utrzymać kodeki audio, komunikaty RTCP, kanały danych, nagrywanie i polityki dla poszczególnych strumieni w jednym miejscu.1

Nawet w produktach klient-AI SFU jest często domyślnym punktem wyjścia, ponieważ pozwala zespołom wykorzystać jeden sprawdzony system do sygnalizacji, routingu mediów, nagrywania, obserwowalności i przyszłych rozszerzeń, takich jak przekazanie do człowieka lub dodanie większej liczby uczestników.

Opcja 2: Podejście transceiver kończy WebRTC na brzegu i konwertuje je na protokół backendowy

Nasze obciążenie wygląda inaczej. Większość sesji to 1:1 — jeden użytkownik rozmawia z jednym modelem albo jedna aplikacja komunikuje się z jednym agentem czasu rzeczywistego — przy wrażliwości na opóźnienia w każdej turze. Dla takiego kształtu ruchu wybraliśmy model transceiver: brzegowa usługa WebRTC kończy połączenie klienta, a następnie przekształca media i zdarzenia w prostsze protokoły wewnętrzne dla inferencji modelu, transkrypcji, generowania mowy, użycia narzędzi i orkiestracji.

W tym projekcie transceiver jest jedyną usługą, która posiada stan sesji WebRTC, w tym kontrole łączności ICE, handshake DTLS, klucze szyfrowania SRTP i cykl życia sesji. „Termination” oznacza tu, że transceiver jest punktem końcowym, który realizuje te handshaki oraz szyfruje i odszyfrowuje media. Utrzymywanie tego stanu w jednym miejscu ułatwiło rozumienie odpowiedzialności za sesję i pozwoliło usługom backendowym skalować się jak zwykłe usługi zamiast działać jako równorzędne węzły WebRTC.

Podstawowy problem wdrożeniowy: WebRTC spotyka Kubernetes

Po wyborze modelu transceivera nasza pierwsza implementacja była pojedynczą usługą w Go zbudowaną na Pion, która obsługiwała zarówno sygnalizację, jak i kończenie mediów. Zasila ona ChatGPT Voice, punkt końcowy WebRTC w Realtime API oraz szereg projektów badawczych.

Operacyjnie usługa transceivera wykonuje dwa zadania:

  • Sygnalizacja: negocjacja SDP, wybór kodeków, poświadczenia ICE i konfiguracja sesji
  • Media: kończenie połączeń WebRTC po stronie downstream i utrzymywanie połączeń upstream z usługami backendowymi dla inferencji i orkiestracji

Chcieliśmy, aby usługa działała jak reszta naszej infrastruktury: na Kubernetesie, gdzie obciążenia mogą skalować się w górę i w dół oraz przenosić się między hostami wraz ze zmianą popytu. Jednak konwencjonalny model WebRTC jeden port na sesję słabo pasuje do tego środowiska, ponieważ opiera się na dużych zakresach publicznych portów UDP, które trudno wystawić, zabezpieczyć i zachować, gdy pody są dodawane, usuwane lub ponownie planowane.2

Wyczerpywanie portów

Pierwszym problemem był sam model jeden port na sesję. Przy dużej współbieżności oznacza to konieczność wystawiania i zarządzania bardzo dużymi zakresami portów UDP.

  • Load balancery chmurowe i usługi Kubernetes nie są projektowane z myślą o dziesiątkach tysięcy publicznych portów UDP na usługę. Każdy dodatkowy zakres zwiększa złożoność operacyjną konfiguracji load balancera, kontroli zdrowia, polityki zapory i bezpieczeństwa wdrożeń.3
  • Duże zakresy portów UDP trudno zabezpieczyć, ponieważ rozszerzają powierzchnię dostępną z zewnątrz i utrudniają audyt polityk sieciowych.
  • To także słabe dopasowanie do autoskalowania. Pody są stale dodawane, usuwane i ponownie planowane w Kubernetesie. Wymaganie, aby każdy pod rezerwował i ogłaszał duży stabilny zakres portów, czyni tę elastyczność kruchą.4

Dlatego wiele systemów WebRTC zmierza w stronę pojedynczego portu UDP na serwer, z demultipleksowaniem na poziomie aplikacji za tym portem.5

Lepkość stanu

Modele z jednym portem na serwer rozwiązują problem liczby portów, ale wprowadzają drugi problem: zachowanie odpowiedzialności za każdą sesję w całej flocie.

ICE i DTLS to protokoły stanowe. Proces, który utworzył sesję, musi nadal odbierać pakiety tej sesji, aby móc weryfikować kontrole łączności, dokończyć handshake DTLS, odszyfrować SRTP i przetwarzać późniejsze zmiany sesji, takie jak restarty ICE. Jeśli pakiety tej samej sesji trafią do innego procesu, konfiguracja może się nie powieść albo media mogą przestać działać.

To dało nam konkretny cel: wystawić do publicznego internetu niewielką, stałą powierzchnię UDP, a jednocześnie nadal kierować każdy pakiet do transceivera, który jest właścicielem odpowiadającej mu sesji WebRTC.

Porównanie architektur mediów WebRTC

Oceniliśmy kilka sposobów osiągnięcia tego celu, w tym TURN (Traversal Using Relays around NAT), gdzie brzegowy relay kończy alokacje klientów i przekazuje ruch w ich imieniu.2

Podejście

Zalety

Wady

Unikalne IP:port na sesję (znane też jako natywne bezpośrednie UDP)

Bezpośrednia ścieżka mediów klient-serwer

Brak warstwy przekazywania na ścieżce danych

Wymaga jednego publicznego portu UDP na sesję

Duże zakresy portów są trudne do wystawienia i zabezpieczenia

Słabe dopasowanie do Kubernetesa i chmurowych load balancerów

Unikalne IP:port na serwer

Znacznie mniejsza publiczna powierzchnia UDP niż przy ekspozycji per sesja

Jedno współdzielone gniazdo na serwer może demultipleksować wiele sesji

Działa dobrze na pojedynczym hoście, ale samo z siebie nie skaluje się czysto w ramach współdzielonej floty za load balancerem

Demultipleksowanie sesji na pojedynczym hoście pomaga dopiero wtedy, gdy pakiet dotrze do tego hosta; w ramach floty za load balancerem pierwszy pakiet nadal może trafić do niewłaściwej instancji, więc nadal potrzebny jest deterministyczny sposób kierowania każdej sesji do procesu, który jest jej właścicielem


Relay TURN (kończący protokół)

Klienci muszą docierać tylko do adresu i portu relay TURN

Umożliwia centralizację polityk na brzegu

Alokacje TURN dodają dodatkowe przebiegi przy zestawianiu połączenia

Przenoszenie lub odzyskiwanie alokacji między serwerami TURN nadal jest trudne

Bezstanowy forwarder + stanowy terminator (relay + transceiver OpenAI)

Mała publiczna powierzchnia UDP

Transceiver nadal posiada pełną sesję WebRTC

Dodaje jeden skok przekazywania, zanim media dotrą do będącego właścicielem transceivera

Wymaga niestandardowej koordynacji między relay a transceiverem

Przegląd architektury: relay + transceiver

Architektura, którą wdrożyliśmy, rozdziela routing pakietów od kończenia protokołu. Sygnalizacja nadal trafia do transceivera na potrzeby konfiguracji sesji, podczas gdy media najpierw wchodzą przez relay. Relay to lekka warstwa przekazywania UDP o niewielkiej publicznej powierzchni, a transceiver to stanowy punkt końcowy WebRTC znajdujący się za nią.

Relay bezstanowo przekazuje pakiety do transceivera

Relay nie odszyfrowuje mediów, nie uruchamia maszyn stanów ICE ani nie uczestniczy w negocjacji kodeków. Odczytuje tylko tyle metadanych pakietu, ile potrzeba do wyboru celu, a następnie przekazuje pakiet do transceivera, który jest właścicielem sesji. Transceiver nadal widzi normalny przepływ WebRTC i nadal posiada cały stan protokołu. Z perspektywy klienta nic w sesji WebRTC się nie zmienia.

Routing na podstawie poświadczeń ICE

Routing pierwszego pakietu to kluczowy krok w tej konfiguracji. Relay musi skierować pierwszy pakiet od klienta, zanim jakakolwiek sesja będzie istnieć na samej ścieżce pakietu, zamiast zatrzymywać się na zewnętrznej usłudze wyszukiwania.

Każda sesja WebRTC już zawiera natywny dla protokołu punkt zaczepienia do routingu: fragment nazwy użytkownika ICE, czyli ufrag, krótki identyfikator wymieniany podczas konfiguracji sesji i odtwarzany w kontrolach łączności STUN. Generujemy ufrag po stronie serwera tak, aby zawierał tylko tyle metadanych routingu, ile potrzeba, by relay mógł wywnioskować docelowy klaster i właściciela transceivera.

Diagram sekwencji pokazuje, jak ustanawiane jest połączenie

Podczas sygnalizacji transceiver alokuje stan sesji i zwraca współdzielony VIP relay oraz port UDP w odpowiedzi SDP. VIP to wirtualny adres IP znajdujący się przed flotą relayów; w połączeniu z portem daje klientowi pojedynczy stabilny cel, taki jak `203.0.113.10:3478`, mimo że za nim znajduje się wiele instancji relay. Pierwszym pakietem klienta na ścieżce mediów jest zwykle żądanie powiązania STUN (Session Traversal Utilities for NAT), którego ICE używa do sprawdzenia, czy pakiety mogą dotrzeć do ogłoszonego adresu.

Relay analizuje tylko tyle z tego pierwszego pakietu STUN, aby odczytać serwerowy ufrag, zdekodować wskazówkę routingu i przekazać pakiet do będącego właścicielem transceivera. Każdy transceiver nasłuchuje na współdzielonym gnieździe UDP, czyli jednym punkcie końcowym systemu operacyjnego powiązanym z wewnętrznym adresem IP:port, a nie na jednym gnieździe na sesję. Po utworzeniu przez relay sesji od źródłowego IP:port klienta do celu transceivera, kolejne pakiety DTLS, RTP i RTCP przepływają w ramach tej sesji bez ponownego dekodowania ufrag.

Sesja relay jest celowo minimalna i składa się tylko z sesji w pamięci służącej do przekazywania pakietów, wraz z niezbędnymi licznikami do monitorowania i timerami wygaśnięcia oraz czyszczenia sesji. Ten wybór projektowy utrzymuje routing pakietów bezpośrednio na ścieżce pakietu. Jeśli relay uruchomi się ponownie i utraci sesję, kolejny pakiet STUN odbuduje sesję na podstawie wskazówki routingu w ufrag. Aby zwiększyć niezawodność, używany jest także cache Redis do przechowywania mapowania <IP klienta + port, IP transceivera + port> po ustanowieniu trasy, tak aby można je było odzyskać znacznie wcześniej, zanim nadejdzie następny pakiet STUN.

Global Relay i sygnalizacja sterowana geograficznie

Gdy ograniczyliśmy publiczną powierzchnię UDP do niewielkiej liczby stabilnych adresów i portów, mogliśmy wdrożyć ten sam wzorzec relay globalnie. Global Relay to nasza flota geograficznie rozproszonych punktów wejścia relay, które wszystkie realizują to samo zachowanie przekazywania pakietów.

Szeroko rozproszony geograficznie ingress skraca pierwszy skok klient-OpenAI, ponieważ pakiet może wejść do naszej sieci przez relay blisko użytkownika — zarówno geograficznie, jak i w topologii sieci — zamiast najpierw przemierzać publiczny internet do odległego regionu. W praktyce oznacza to niższe opóźnienia, mniejszy jitter i mniej możliwych do uniknięcia skoków utraty pakietów, zanim ruch trafi do naszego szkieletu sieciowego.6

Warstwa Global Relay odbiera pakiety od klienta i przekazuje je do klastra transceiverów

Do sygnalizacji używamy geograficznego i bliskościowego sterowania Cloudflare, aby początkowe żądanie HTTP lub WebSocket trafiało do pobliskiego klastra transceiverów. Kontekst żądania określa lokalizację sesji i to, który punkt wejścia Global Relay jest ogłaszany klientowi. Odpowiedź SDP podaje adres Global Relay, a ufrag zawiera wystarczające informacje, aby Global Relay mógł skierować media do wyznaczonego klastra, a relay do docelowego transceivera.

Razem sygnalizacja sterowana geograficznie i Global Relay umieszczają zarówno konfigurację, jak i media na pobliskiej ścieżce wejścia, przy jednoczesnym zakotwiczeniu sesji do jednego transceivera. Zmniejsza to czas obiegu dla sygnalizacji i dla pierwszej kontroli łączności ICE, co bezpośrednio skraca czas oczekiwania użytkownika przed rozpoczęciem mowy.

Implementacja i wydajność relay

Napisaliśmy usługę relay w Go i celowo utrzymaliśmy wąski zakres implementacji. W Linuksie stos sieciowy jądra odbiera pakiety UDP z interfejsu sieciowego maszyny i dostarcza je do gniazda, czyli punktu końcowego systemu operacyjnego, który proces odczytuje po zbindowaniu adresu IP:Port. Relay działa w przestrzeni użytkownika, więc zwykły proces Go odczytuje nagłówki pakietów z tego gniazda, aktualizuje niewielką ilość stanu przepływu i przekazuje pakiety bez kończenia WebRTC. Nie potrzebowaliśmy żadnego frameworka kernel bypass, który pozwalałby procesowi w przestrzeni użytkownika odpytywać kolejki sieciowe bezpośrednio dla wyższych przepływności pakietów, ale też zwiększałby złożoność operacyjną.

Kluczowe wybory projektowe:

  • Brak kończenia protokołu: Relay analizuje tylko nagłówki STUN/ufrag; dla kolejnych pakietów DTLS, RTP i RTCP używa pamięci podręcznej stanu, zachowując pakiety jako nieprzezroczyste.
  • Stan efemeryczny: Utrzymuje małą, krótkotrwałą mapę w pamięci z adresu klienta do celu transceivera na potrzeby stanu przepływu i obserwowalności.
  • Skalowalność pozioma: Wiele instancji relay działa równolegle za load balancerem. Stan nie jest twardym stanem WebRTC, więc restarty powodują minimalne spadki ruchu i szybkie odzyskiwanie przepływów.

Środki zwiększające wydajność:

  • SO_REUSEPORT to opcja gniazda Linux, która pozwala wielu procesom roboczym relay na tej samej maszynie zbindować ten sam port UDP. Jądro rozdziela wtedy przychodzące pakiety między te procesy, co pozwala uniknąć wąskiego gardła pojedynczej pętli odczytu.
  • runtime.LockOSThread przypina każdą goroutine odczytującą UDP do konkretnego wątku systemu operacyjnego. W połączeniu z SO_REUSEPORT zwykle pomaga to utrzymać pakiety tego samego przepływu (źródłowy i docelowy IP:Port plus protokół) na tym samym rdzeniu CPU, poprawiając lokalność cache i zmniejszając przełączanie kontekstu.
  • Wstępnie alokowane bufory i minimalne kopiowanie utrzymują niski narzut parsowania i alokacji, aby uniknąć garbage collection w Go.

Ta implementacja obsłużyła nasz globalny ruch medialny czasu rzeczywistego przy stosunkowo niewielkim śladzie relay, dlatego pozostaliśmy przy prostszym projekcie zamiast wybierać ścieżkę kernel bypass.

Wyniki i wnioski

Ta architektura pozwala nam uruchamiać media WebRTC w Kubernetesie bez wystawiania tysięcy portów UDP. Ma to znaczenie, ponieważ mniejsza i stała powierzchnia UDP jest łatwiejsza do zabezpieczenia i równoważenia obciążenia, a także pozwala infrastrukturze skalować się bez rezerwowania dużych zakresów publicznych portów. Dzięki lepszemu wsparciu infrastrukturalnemu ze strony Kubernetesa i większemu bezpieczeństwu wynikającemu z mniejszej powierzchni projekt ten zachowuje również standardowe działanie WebRTC dla klientów i potwierdza, że projekt bez SFU był właściwym domyślnym wyborem dla naszego obciążenia. Większość naszych sesji to połączenia punkt-punkt, wrażliwe na opóźnienia i łatwiejsze do skalowania, gdy usługi inferencyjne nie muszą zachowywać się jak równorzędne węzły WebRTC.

Szerszy wniosek jest taki, że najlepszym miejscem na dodanie złożoności jest cienka warstwa routingu, a nie każda usługa backendowa i nie niestandardowe zachowanie klienta. Zakodowanie metadanych routingu w polu natywnym dla protokołu dało nam deterministyczny routing pierwszego pakietu, niewielką publiczną powierzchnię UDP i wystarczającą elastyczność, aby umieszczać ingress blisko użytkowników na całym świecie.

Kilka decyzji było szczególnie ważnych:

  • Zachowanie semantyki protokołu na brzegu. Klienci nadal używają standardowego WebRTC, co utrzymuje interoperacyjność przeglądarek i urządzeń mobilnych.
  • Utrzymanie trudnych stanów sesji w jednym miejscu. Transceiver posiada ICE, DTLS, SRTP i cykl życia sesji; relay tylko przekazuje pakiety.
  • Routing na podstawie informacji już obecnych podczas konfiguracji. ICE ufrag dał nam punkt zaczepienia do routingu pierwszego pakietu bez dodawania zależności od wyszukiwania na gorącej ścieżce.
  • Optymalizacja pod typowy przypadek przed sięgnięciem po kernel bypass. Wąska implementacja w Go ze starannym użyciem SO_REUSEPORT, przypinaniem wątków i parsowaniem o niskiej liczbie alokacji była wystarczająca dla naszego obciążenia.

Voice AI w czasie rzeczywistym działa tylko wtedy, gdy infrastruktura sprawia, że opóźnienie wydaje się niewidoczne. Dla nas oznaczało to zmianę kształtu naszego wdrożenia WebRTC bez zmiany tego, czego klienci oczekują od samego WebRTC.