Wie OpenAI latenzarme Sprach-KI im großen Maßstab bereitstellt
Von Yi Zhang und William McDonald, Mitglieder der technischen Belegschaft
Sprach-KI fühlt sich nur dann natürlich an, wenn ein Gespräch im Sprechtempo abläuft. Wenn das Netzwerk im Weg steht, hören Menschen das sofort: als unangenehme Pausen, abgeschnittene Unterbrechungen oder verzögertes Hereinsprechen. Das ist wichtig für den ChatGPT Sprachchat, für Entwickler:innen, die mit der Realtime API arbeiten, für Agenten in interaktiven Workflows und für Modelle, die Audio verarbeiten müssen, während eine Person noch spricht.
Im Maßstab von OpenAI ergeben sich daraus drei konkrete Anforderungen:
- Globale Reichweite für mehr als 900 Millionen wöchentlich aktive Nutzende
- Schneller Verbindungsaufbau, damit eine Person sprechen kann, sobald eine Sitzung beginnt
- Niedrige und stabile Medien-Round-Trip-Zeit bei geringem Jitter und Paketverlust, damit der Sprecherwechsel direkt wirkt
Das Team bei OpenAI, das für KI-Interaktionen in Echtzeit verantwortlich ist, hat vor Kurzem unseren WebRTC-Stack neu aufgebaut, um drei Einschränkungen zu begegnen, die im großen Maßstab miteinander kollidierten: Medienabschluss mit einem Port pro Sitzung passt nicht gut zur OpenAI-Infrastruktur, zustandsbehaftete ICE- (Interactive Connectivity Establishment) und DTLS-Sitzungen (Datagram Transport Layer Security) brauchen stabile Zuständigkeiten, und globales Routing muss die Latenz des ersten Hops niedrig halten. In diesem Beitrag erläutern wir die aufgeteilte Architektur aus Relay plus Transceiver, die wir entwickelt haben, um für Clients das standardmäßige WebRTC-Verhalten zu erhalten und zugleich zu ändern, wie Pakete innerhalb der OpenAI-Infrastruktur geroutet werden.
WebRTC ist ein offener Standard zum Senden von Audio, Video und Daten mit geringer Latenz zwischen Browsern, mobilen Apps und Servern. Er wird oft mit Peer-to-Peer-Telefonie verbunden, ist aber auch eine praktische Grundlage für Client-zu-Server-Echtzeitsysteme, weil er die schwierigen Teile interaktiver Medien standardisiert: ICE für den Verbindungsaufbau und NAT- (Network Address Translation) Traversal, DTLS und SRTP (Secure Real-time Transport Protocol) für verschlüsselten Transport, Codec-Aushandlung zum Komprimieren und Dekodieren von Audio, RTCP (Real-time Transport Control Protocol) für Qualitätskontrolle sowie clientseitige Funktionen wie Echo-Unterdrückung und Jitter-Pufferung.
Diese Standardisierung ist für KI-Produkte wichtig. Ohne WebRTC bräuchte jeder Client eine andere Antwort auf die Frage, wie sich Verbindungen über NATs aufbauen, Medien verschlüsseln, Codecs aushandeln (die zur Übertragung und Dekomprimierung ausgewählten Coder-Decoder) und an veränderte Netzwerkbedingungen anpassen lassen. Mit WebRTC können wir auf einem Protokoll-Stack aufbauen, der bereits in Browsern und auf mobilen Plattformen implementiert ist, und unsere eigene Arbeit auf die Infrastruktur konzentrieren, die Echtzeitmedien mit Modellen verbindet.
Wir bauen außerdem auf dem WebRTC-Ökosystem selbst auf, einschließlich ausgereifter Open-Source-Implementierungen und der Standardisierungsarbeit, die Browser, mobile Apps und Server interoperabel hält. Grundlegende Arbeit von Justin Uberti (einem der ursprünglichen Architekten von WebRTC) und Sean DuBois (Schöpfer und Maintainer von Pion) machte es Teams wie unserem möglich, auf praxiserprobter Medieninfrastruktur aufzubauen, statt Transport, Verschlüsselung und Staukontrollverhalten auf niedriger Ebene neu zu erfinden. Wir haben das Glück, dass Justin und Sean inzwischen unsere Kollegen hier bei OpenAI sind und mithelfen, WebRTC und Echtzeit-KI enger zusammenzubringen.
Für KI ist die wichtigste Eigenschaft, dass Audio als kontinuierlicher Stream ankommt. Ein gesprochener Agent kann mit der Transkription beginnen, schlussfolgern, Tools aufrufen oder Sprache erzeugen, während die nutzende Person noch spricht, statt auf einen vollständigen Upload zu warten. Das ist der Unterschied zwischen einem System, das im Gespräch natürlich interaktiv wirkt, und einem, das wie Push-to-Talk wirkt.
Nachdem wir uns für WebRTC entschieden hatten, war die nächste Frage, wo wir es terminieren würden (wo wir also die WebRTC-Verbindung annehmen und besitzen würden, zum Beispiel am Edge) und wie diese Sitzungen mit dem Inferenz-Backend verbunden werden sollten. Die Terminierung ist wichtig, weil sie bestimmt, wie wir Echtzeit-Sitzungszustand, Medientransport, Routing, Latenz und Fehlerisolierung handhaben.
Eine SFU oder Selective Forwarding Unit ist ein Medienserver, der von jeder teilnehmenden Person einen WebRTC-Stream empfängt und Streams selektiv an die anderen weiterleitet. In diesem Modell terminiert die SFU für jede teilnehmende Person eine separate WebRTC-Verbindung, und die KI nimmt als weitere teilnehmende Partei an der Sitzung teil. Das kann gut zu Produkten passen, die von Natur aus mehrere Parteien umfassen, etwa Gruppenanrufe, Unterrichtsszenarien oder auf Zusammenarbeit ausgerichtete Meetings. So bleiben Audio-Codecs, RTCP-Nachrichten, Datenkanäle, Aufzeichnung und Richtlinien pro Stream an einem Ort.1
Selbst bei Client-zu-KI-Produkten ist eine SFU oft der Standard-Ausgangspunkt, weil Teams damit ein erprobtes System für Signalisierung, Medienrouting, Aufzeichnung, Observability und spätere Erweiterungen wie die Übergabe an Menschen oder das Hinzufügen weiterer Teilnehmender wiederverwenden können.
Unsere Arbeitslast ist anders. Die meisten Sitzungen sind 1:1 – eine Person spricht mit einem Modell oder eine Anwendung spricht mit einem Echtzeit-Agenten – und jede Gesprächsrunde ist latenzempfindlich. Für diese Form des Datenverkehrs haben wir uns für ein Transceiver-Modell entschieden: Ein WebRTC-Edge-Service terminiert die Client-Verbindung und wandelt Medien und Ereignisse anschließend in einfachere interne Protokolle für Modellinferenz, Transkription, Spracherzeugung, Tool-Nutzung und Orchestrierung um.
In diesem Design ist der Transceiver der einzige Service, dem der WebRTC-Sitzungszustand gehört, einschließlich ICE-Konnektivitätsprüfungen, des DTLS-Handshakes, der SRTP-Verschlüsselungsschlüssel und des Sitzungslebenszyklus. „Terminierung“ bedeutet hier, dass der Transceiver der Endpunkt ist, der diese Handshakes abschließt und die Medien ver- oder entschlüsselt. Diesen Zustand an einem Ort zu halten, machte die Zuständigkeit für Sitzungen leichter nachvollziehbar, und Backend-Services konnten so wie gewöhnliche Services skalieren, statt selbst als WebRTC-Peers zu agieren.
Nachdem wir uns für das Transceiver-Modell entschieden hatten, bestand unsere erste Implementierung aus einem einzelnen Go-Service auf Basis von Pion, der sowohl Signalisierung als auch Medienabschluss handhabte. Er betreibt den ChatGPT Sprachchat, den WebRTC-Endpunkt der Realtime API und eine Reihe von Forschungsprojekten.
Betrieblich übernimmt der Transceiver-Service zwei Aufgaben:
- Signalisierung: SDP-Aushandlung, Codec-Auswahl, ICE-Anmeldedaten und Sitzungsaufbau
- Medien: Terminierung nachgelagerter WebRTC-Verbindungen und Aufrechterhaltung vorgelagerter Verbindungen zu Backend-Services für Inferenz und Orchestrierung
Wir wollten, dass der Service wie der Rest unserer Infrastruktur läuft: auf Kubernetes, wo Workloads hoch- und herunterskaliert und je nach Nachfrage zwischen Hosts verschoben werden können. Doch das konventionelle WebRTC-Modell mit einem Port pro Sitzung passt schlecht in diese Umgebung, weil es von großen öffentlichen UDP-Portbereichen abhängt, die sich schwer exponieren, absichern und beibehalten lassen, wenn Pods hinzugefügt, entfernt oder neu geplant werden.2
Das erste Problem war das Modell mit einem Port pro Sitzung selbst. Bei hoher Parallelität bedeutet das, sehr große UDP-Portbereiche offenzulegen und zu verwalten.
- Cloud-Load-Balancer und Kubernetes-Services sind nicht auf Zehntausende öffentliche UDP-Ports pro Service ausgelegt. Jeder zusätzliche Bereich erhöht die betriebliche Komplexität bei Load-Balancer-Konfiguration, Statusprüfungen, Firewall-Richtlinien und sicherem Rollout.3
- Große UDP-Portbereiche sind schwer abzusichern, weil sie die von außen erreichbare Angriffsfläche vergrößern und Netzwerk-Richtlinien schwerer prüfbar machen.
- Außerdem passen sie schlecht zu Autoscaling. Pods werden in Kubernetes laufend hinzugefügt, entfernt und neu geplant. Wenn jeder Pod einen großen stabilen Portbereich reservieren und ankündigen muss, wird diese Elastizität fragil.4
Darum entwickeln sich viele WebRTC-Systeme hin zu einem einzelnen UDP-Port pro Server, mit Demultiplexing auf Anwendungsebene hinter diesem Port.5
Designs mit einem einzelnen Port pro Server lösen die Portanzahl, führen aber ein zweites Problem ein: die Wahrung der Zuständigkeit für jede Sitzung über eine gesamte Flotte hinweg.
ICE und DTLS sind zustandsbehaftete Protokolle. Der Prozess, der eine Sitzung erstellt hat, muss weiterhin die Pakete dieser Sitzung empfangen, damit er Konnektivitätsprüfungen validieren, den DTLS-Handshake abschließen, SRTP entschlüsseln und spätere Sitzungsänderungen wie ICE-Neustarts verarbeiten kann. Wenn Pakete derselben Sitzung bei einem anderen Prozess landen, kann der Aufbau fehlschlagen oder die Medienverbindung abbrechen.
Daraus ergab sich für uns ein konkretes Ziel: eine kleine, feste UDP-Oberfläche zum öffentlichen Internet bereitzustellen und dennoch jedes Paket an den Transceiver zu routen, dem die entsprechende WebRTC-Sitzung gehört.
Wir haben mehrere Wege dafür evaluiert, darunter TURN (Traversal Using Relays around NAT), bei dem ein Edge-Relay Client-Allokationen terminiert und Datenverkehr in ihrem Namen weiterleitet.2
Ansatz | Vorteile | Nachteile |
Eindeutige IP:Port-Kombination pro Sitzung (auch als natives direktes UDP bekannt) | Direkter Medienpfad vom Client zum Server Keine Weiterleitungsschicht im Datenpfad | Erfordert einen öffentlichen UDP-Port pro Sitzung Große Portbereiche sind schwer offenzulegen und abzusichern Passt schlecht zu Kubernetes und Cloud-Load-Balancern |
Eindeutige IP:Port-Kombination pro Server | Deutlich kleinere öffentliche UDP-Oberfläche als bei Freigabe pro Sitzung Ein gemeinsamer Socket pro Server kann viele Sitzungen demultiplexen | Funktioniert auf einem einzelnen Host gut, aber nicht ohne Weiteres über eine gemeinsam per Load Balancer verteilte Flotte hinweg Die Sitzungsdemultiplexierung auf einem einzelnen Host hilft erst, nachdem ein Paket diesen Host erreicht hat; in einer per Load Balancer verteilten Flotte kann das erste Paket weiterhin auf der falschen Instanz landen, daher braucht es trotzdem einen deterministischen Weg, jede Sitzung zu dem Prozess zu lenken, dem sie gehört |
TURN-Relay (mit Protokoll-Terminierung) | Clients müssen nur die TURN-Relay-Adresse und den Port erreichen Richtlinien können am Edge zentralisiert werden | TURN-Allokationen fügen zusätzliche Round-Trips beim Setup hinzu Das Verschieben oder Wiederherstellen von Allokationen über TURN-Server hinweg bleibt schwierig |
Zustandsloser Forwarder + zustandsbehafteter Terminator (OpenAIs Relay + Transceiver) | Kleine öffentliche UDP-Oberfläche Der Transceiver besitzt weiterhin die vollständige WebRTC-Sitzung | Fügt einen Weiterleitungs-Hop hinzu, bevor Medien den zuständigen Transceiver erreichen Erfordert benutzerdefinierte Koordination zwischen Relay und Transceiver |
Die von uns ausgelieferte Architektur trennt Paket-Routing von der Protokoll-Terminierung. Die Signalisierung erreicht für den Sitzungsaufbau weiterhin den Transceiver, während Medien zuerst über das Relay eingehen. Das Relay ist eine leichtgewichtige UDP-Weiterleitungsschicht mit kleiner öffentlicher Oberfläche, und der Transceiver ist der zustandsbehaftete WebRTC-Endpunkt dahinter.
Das Relay entschlüsselt keine Medien, führt keine ICE-Zustandsmaschinen aus und beteiligt sich nicht an der Codec-Aushandlung. Es liest gerade genug Paketmetadaten, um ein Ziel auszuwählen, und leitet das Paket dann an den Transceiver weiter, dem die Sitzung gehört. Der Transceiver sieht weiterhin einen normalen WebRTC-Flow und behält weiterhin den gesamten Protokollzustand. Aus Sicht des Clients ändert sich an der WebRTC-Sitzung nichts.
Das Routing des ersten Pakets ist in diesem Setup der Schlüsselschritt. Ein Relay muss das erste Paket eines Clients routen, bevor auf dem Paketpfad selbst irgendeine Sitzung existiert, statt bei einem externen Lookup-Service anzuhalten.
Jede WebRTC-Sitzung trägt bereits einen protokolleigenen Routing-Hook: das ICE-Username-Fragment oder ufrag, einen kurzen Bezeichner, der beim Sitzungsaufbau ausgetauscht und in STUN-Konnektivitätsprüfungen gespiegelt wird. Wir erzeugen den serverseitigen ufrag so, dass er gerade genug Routing-Metadaten enthält, damit das Relay den Ziel-Cluster und den zuständigen Transceiver ableiten kann.
Während der Signalisierung reserviert der Transceiver Sitzungszustand und gibt in der SDP-Antwort eine gemeinsame Relay-VIP und einen UDP-Port zurück. Eine VIP ist eine virtuelle IP-Adresse vor der Relay-Flotte; zusammen mit dem Port gibt sie dem Client ein einzelnes stabiles Ziel, etwa `203.0.113.10:3478`, auch wenn sich dahinter viele Relay-Instanzen befinden. Das erste Paket des Medienpfads vom Client ist üblicherweise eine STUN-Bindungsanfrage (Session Traversal Utilities for NAT), mit der ICE prüft, ob Pakete die angekündigte Adresse erreichen können.
Das Relay parst gerade genug von diesem ersten STUN-Paket, um den serverseitigen ufrag zu lesen, den Routing-Hinweis zu decodieren und das Paket an den zuständigen Transceiver weiterzuleiten. Jeder Transceiver lauscht auf einem gemeinsamen UDP-Socket, also einem Betriebssystem-Endpunkt, der an eine interne IP:Port-Kombination gebunden ist, nicht einem Socket pro Sitzung. Nachdem das Relay aus der Quell-IP:Port-Kombination des Clients zu diesem Transceiver-Ziel eine Sitzung erstellt hat, fließen nachfolgende DTLS-, RTP- und RTCP-Pakete innerhalb der Sitzung weiter, ohne den ufrag erneut zu decodieren.
Die Sitzung des Relays ist bewusst minimal und besteht nur aus einer In-Memory-Sitzung zur Paketweiterleitung sowie den nötigen Zählern für Monitoring und Timern für Sitzungsablauf und Bereinigung. Diese Designentscheidung hält das Paket-Routing direkt auf dem Paketpfad. Wenn ein Relay neu startet und die Sitzung verliert, baut das nächste STUN-Paket die Sitzung anhand des ufrag-Routing-Hinweises wieder auf. Um das noch zuverlässiger zu machen, wird zusätzlich ein Redis-Cache verwendet, um die Zuordnung von <Client-IP + Port, Transceiver-IP + Port> zu speichern, sobald die Route feststeht, damit sie deutlich früher wiederhergestellt werden kann, noch bevor das nächste STUN-Paket eintrifft.
Sobald wir die öffentliche UDP-Oberfläche auf eine kleine Zahl stabiler Adressen und Ports reduziert hatten, konnten wir dasselbe Relay-Muster weltweit bereitstellen. Global Relay ist unsere Flotte geografisch verteilter Relay-Eingangspunkte, die alle dasselbe Paket-Weiterleitungsverhalten implementieren.
Breit verteilte geografische Ingress-Punkte verkürzen den ersten Hop vom Client zu OpenAI, weil ein Paket in unser Netzwerk an einem Relay nahe bei der nutzenden Person eintreten kann – sowohl geografisch als auch in der Netzwerktopologie –, statt zuerst das öffentliche Internet zu einer weit entfernten Region zu durchqueren. Praktisch bedeutet das geringere Latenz, weniger Jitter und weniger vermeidbare Verlustspitzen, bevor der Datenverkehr unser Backbone erreicht.6
Für die Signalisierung nutzen wir Geo- und Proximity-Steering von Cloudflare, damit die anfängliche HTTP- oder WebSocket-Anfrage einen nahegelegenen Transceiver-Cluster erreicht. Der Anfragekontext bestimmt den Ort der Sitzung und welchen Global-Relay-Eingangspunkt wir dem Client ankündigen. Die SDP-Antwort liefert die Global-Relay-Adresse, während der ufrag genügend Informationen enthält, damit Global Relay Medien zum vorgesehenen Cluster und das Relay sie zum Ziel-Transceiver routen kann.
Gemeinsam sorgen geogesteuerte Signalisierung und Global Relay dafür, dass sowohl Setup als auch Medien einen nahegelegenen Eintrittspfad nutzen, während die Sitzung an einem einzigen Transceiver verankert bleibt. Das verringert die Round-Trip-Zeit für die Signalisierung und für die erste ICE-Konnektivitätsprüfung, wodurch sich direkt verkürzt, wie lange eine Person warten muss, bevor Sprache beginnen kann.
Wir haben den Relay-Service in Go geschrieben und die Implementierung bewusst eng gehalten. Unter Linux empfängt der Netzwerk-Stack des Kernels UDP-Pakete von der Netzwerkschnittstelle des Rechners und liefert sie an einen Socket, also den Betriebssystem-Endpunkt, den ein Prozess nach dem Binden an eine IP:Port-Kombination ausliest. Relay läuft im Userspace, daher liest ein normaler Go-Prozess Paket-Header von diesem Socket, aktualisiert einen kleinen Teil des Flow-Zustands und leitet Pakete weiter, ohne WebRTC zu terminieren. Wir brauchten kein Framework zur Kernel-Umgehung, das einem Userspace-Prozess erlauben würde, Netzwerk-Warteschlangen für höhere Paketraten direkt abzufragen, aber auch betriebliche Komplexität hinzufügen würde.
Wichtige Designentscheidungen:
- Keine Protokoll-Terminierung: Relay parst nur STUN-Header und ufrag; für nachfolgende DTLS-, RTP- und RTCP-Pakete verwendet es zwischengespeicherten Zustand und hält die Pakete damit opak.
- Ephemerer Zustand: Es hält eine kleine In-Memory-Zuordnung mit kurzem Timeout von Client-Adresse zu Transceiver-Ziel für Flow-Zustand und Observability.
- Horizontale Skalierbarkeit: Mehrere Relay-Instanzen laufen parallel hinter einem Load Balancer. Der Zustand ist kein harter WebRTC-Zustand, daher verursachen Neustarts nur minimale Verkehrseinbrüche und eine schnelle Wiederherstellung der Flows.
Effizienzmaßnahmen:
SO_REUSEPORTist eine Linux-Socket-Option, die es mehreren Relay-Workern auf derselben Maschine erlaubt, denselben UDP-Port zu binden. Der Kernel verteilt eingehende Pakete dann auf diese Worker, wodurch ein Engpass in einer einzelnen Read-Loop vermieden wird.runtime.LockOSThreadbindet jede UDP-lesende Goroutine an einen bestimmten OS-Thread. In Kombination mitSO_REUSEPORTführt das häufig dazu, dass Pakete desselben Flows (Quell- und Ziel-IP:Port plus Protokoll) auf demselben CPU-Kern bleiben, was Cache-Lokalität verbessert und Kontextwechsel reduziert.- Vorab zugewiesene Puffer und minimales Kopieren halten Parsing- und Allokationsaufwand niedrig, um Garbage Collection in Go zu vermeiden.
Diese Implementierung bewältigte unseren globalen Echtzeit-Medienverkehr mit einer relativ kleinen Relay-Fläche, daher blieben wir bei dem einfacheren Design, statt zusätzlich den Weg über Kernel Bypass zu gehen.
Mit dieser Architektur können wir WebRTC-Medien in Kubernetes betreiben, ohne Tausende von UDP-Ports offenzulegen. Das ist wichtig, weil eine kleinere und feste UDP-Oberfläche leichter abzusichern und per Load Balancer zu verteilen ist und weil sich die Infrastruktur so skalieren lässt, ohne große öffentliche Portbereiche zu reservieren. Mit besserer Infrastruktur-Unterstützung durch Kubernetes und mehr Sicherheit durch die kleinere Angriffsfläche erhält dieses Design außerdem das standardmäßige WebRTC-Verhalten für Clients und bestätigt, dass ein Design ohne SFU für unsere Arbeitslast die richtige Standardeinstellung war. Die meisten unserer Sitzungen sind Punkt-zu-Punkt, latenzempfindlich und leichter zu skalieren, wenn Inferenz-Services sich nicht wie WebRTC-Peers verhalten müssen.
Allgemein lässt sich daraus lernen, dass der beste Ort für zusätzliche Komplexität eine schlanke Routing-Schicht ist, nicht jeder Backend-Service und auch nicht ein benutzerdefiniertes Client-Verhalten. Routing-Metadaten in ein protokolleigenes Feld zu kodieren gab uns deterministisches Routing des ersten Pakets, eine kleine öffentliche UDP-Oberfläche und genug Flexibilität, den Eintritt in der Nähe von Nutzenden auf der ganzen Welt zu handhaben.
Einige Entscheidungen waren besonders wichtig:
- Protokollsemantik am Edge erhalten. Clients sprechen weiterhin Standard-WebRTC, wodurch die Interoperabilität mit Browsern und mobilen Geräten erhalten bleibt.
- Den festen Sitzungszustand an einem Ort halten. Der Transceiver besitzt ICE, DTLS, SRTP und den Sitzungslebenszyklus; das Relay leitet nur Pakete weiter.
- Anhand von Informationen routen, die bereits beim Setup vorhanden sind. Der ICE-ufrag gab uns einen Routing-Hook für das erste Paket, ohne eine Lookup-Abhängigkeit auf dem Hot Path hinzuzufügen.
- Erst den häufigsten Fall optimieren, bevor Kernel Bypass in Betracht gezogen wird. Eine schlanke Go-Implementierung mit sorgfältigem Einsatz von
SO_REUSEPORT, Thread-Pinning und Parsing mit geringer Allokation reichte für unsere Arbeitslast aus.
Echtzeit-Sprach-KI funktioniert nur, wenn die Infrastruktur Latenz unsichtbar wirken lässt. Für uns bedeutete das, die Form unserer WebRTC-Bereitstellung zu ändern, ohne zu ändern, was Clients von WebRTC selbst erwarten.


