跳至主要內容
OpenAI

2026年5月4日

工程

OpenAI 如何大規模提供低延遲語音 AI

作者:技術團隊成員 Yi Zhang 與 William McDonald

只有當對話能以接近說話的速度推進時,語音 AI 才會讓人感覺自然。當網路成為阻礙時,人們會立刻從尷尬的停頓、被截斷的插話,或延遲的打斷插入中察覺。這對 ChatGPT 語音、使用 Realtime API 建置產品的開發者、在互動式工作流程中運作的智慧體,以及需要在使用者仍在說話時處理音訊的模型都很重要。

以 OpenAI 的規模來看,這轉化為三項具體需求:

  • 能服務每週超過 9 億活躍使用者的全球涵蓋範圍
  • 快速建立連線,讓使用者在工作階段一開始就能立即開口
  • 低且穩定的媒體往返時間,以及低抖動與低封包遺失率,讓輪流對話感覺俐落

OpenAI 負責即時 AI 互動的團隊,最近重新設計了我們的 WebRTC 堆疊,以解決三項在擴大規模後開始彼此衝突的限制:每個工作階段一個連接埠的媒體終止方式不適合 OpenAI 的基礎架構;有狀態的 ICE(Interactive Connectivity Establishment)與 DTLS(Datagram Transport Layer Security)工作階段需要穩定的歸屬;而全球路由則必須讓第一跳延遲維持在低水位。在這篇文章中,我們將介紹我們建構的分離式 relay 加 transceiver 架構,如何在保留用戶端標準 WebRTC 行為的同時,改變封包在 OpenAI 基礎架構內部的路由方式。

WebRTC 讓我們能打造即時 AI 產品

WebRTC 是一項開放標準,用於在瀏覽器、行動應用程式與伺服器之間傳送低延遲音訊、視訊與資料。它常被與點對點通話聯想在一起,但對於用戶端到伺服器的即時系統而言,它同樣是務實的基礎,因為它將互動式媒體中最困難的部分標準化了:用於建立連線與 NAT(Network Address Translation)穿越的 ICE、用於加密傳輸的 DTLS 與 SRTP(Secure Real-time Transport Protocol)、用於壓縮與解碼音訊的編解碼協商、用於品質控制的 RTCP(Real-time Transport Control Protocol),以及如回音消除與抖動緩衝等用戶端功能。

這種標準化對 AI 產品很重要。若沒有 WebRTC,每個用戶端都需要各自處理如何跨越 NAT 建立連線、加密媒體、協商編解碼器(傳輸與解壓縮所選用的 coder-decoder),以及適應不斷變動的網路條件。有了 WebRTC,我們就能建立在瀏覽器與行動平台已廣泛實作的協定堆疊之上,把自己的工作重點放在連接即時媒體與模型的基礎架構上。

我們也建立在 WebRTC 生態系本身之上,包括成熟的開源實作,以及讓瀏覽器、行動應用程式與伺服器維持互通性的標準制定工作。Justin Uberti(WebRTC 最初的架構師之一)與 Sean DuBois(Pion 的創建者與維護者)的基礎性工作,使得像我們這樣的團隊得以建立在歷經考驗的媒體基礎架構上,而不必重新發明底層傳輸、加密與壅塞控制行為。我們很幸運,Justin 與 Sean 如今都成為 OpenAI 的同事,協助指引我們如何讓 WebRTC 與即時 AI 更加緊密結合。

對 AI 而言,最重要的特性是音訊會以連續串流的形式抵達。語音智慧體可以在使用者仍在說話時,就開始轉錄、推理、呼叫工具或產生語音,而不必等到完整上傳後才開始。這正是讓系統感覺像在對話,而不是像按鍵通話的差別。

選擇媒體架構

當我們選定 WebRTC 之後,下一個問題就是要在哪裡終止它(也就是接受並持有 WebRTC 連線,例如在邊緣),以及如何將這些工作階段連接到推論後端。終止位置很重要,因為它決定了我們如何處理即時工作階段狀態、媒體傳輸、路由、延遲與故障隔離。

方案 1:SFU 方法將 AI 納入為 WebRTC 參與者

SFU(Selective Forwarding Unit,選擇性轉送單元)是一種媒體伺服器,會從每位參與者接收一條 WebRTC 串流,並選擇性地轉送給其他人。在這個模型中,SFU 會為每位參與者終止一條獨立的 WebRTC 連線,而 AI 則作為工作階段中的另一位參與者加入。對於本質上就是多方互動的產品,例如群組通話、課堂或協作會議,這可能是很適合的模式。它能把音訊編解碼器、RTCP 訊息、資料通道、錄製,以及逐串流政策集中在同一處。1

即使是在用戶端對 AI 的產品中,SFU 通常也是預設的起點,因為它讓團隊可以重用一套已被驗證的系統來處理訊號、媒體路由、錄製、可觀測性,以及未來像人工接手或增加更多參與者之類的延伸功能。

方案 2:transceiver 方法會在邊緣終止 WebRTC,並轉換為後端協定

我們的工作負載不同。大多數工作階段都是 1:1——一位使用者對一個模型,或一個應用程式對一個即時智慧體——而且每一次輪替都對延遲十分敏感。對於這種流量型態,我們選擇了 transceiver 模式:由一個 WebRTC 邊緣服務終止用戶端連線,然後把媒體與事件轉換為較簡單的內部協定,用於模型推論、轉錄、語音生成、工具使用與流程協調。

在這個設計中,transceiver 是唯一持有 WebRTC 工作階段狀態的服務,其中包括 ICE 連線檢查、DTLS 握手、SRTP 加密金鑰,以及工作階段生命週期。這裡所說的「終止」,意指 transceiver 是完成這些握手並加密或解密媒體的端點。把這些狀態集中在同一處,讓工作階段歸屬更容易推理,也讓後端服務能像一般服務一樣擴展,而不必自己充當 WebRTC 對等端。

核心部署問題:WebRTC 遇上 Kubernetes

在選定 transceiver 模式後,我們的第一版實作是以 Pion 為基礎的單一 Go 服務,同時處理訊號與媒體終止。它支撐了 ChatGPT 語音、Realtime API 的 WebRTC 端點,以及多項研究專案。

在營運層面上,transceiver 服務做兩件事:

  • 訊號:SDP 協商、編解碼器選擇、ICE 憑證與工作階段設定
  • 媒體:終止下游 WebRTC 連線,並維持與後端服務的上游連線,以供推論與流程協調

我們希望這項服務能像我們其餘的基礎架構一樣運作:在 Kubernetes 上執行,讓工作負載能隨需求擴縮,並在主機間移動。但傳統每個工作階段一個連接埠的 WebRTC 模型,與這種環境的契合度很差,因為它依賴大量公開的 UDP 連接埠範圍,而當 Pod 被新增、移除或重新排程時,這些範圍很難暴露、保護與保留。2

連接埠耗盡

第一個問題就是每個工作階段一個連接埠的模型本身。在高併發下,這意味著要暴露並管理非常大的 UDP 連接埠範圍。

  • 雲端負載平衡器與 Kubernetes 服務的設計,並不是為了每項服務支援成千上萬個公開 UDP 連接埠。每多一段範圍,就會在負載平衡器設定、健康檢查、防火牆政策與部署安全性上增加營運複雜度。3
  • 大型 UDP 連接埠範圍很難保護,因為它們擴大了外部可達的攻擊面,也讓網路政策更難稽核。
  • 它們也不適合自動擴縮。在 Kubernetes 中,Pod 會持續被新增、移除與重新排程。若要求每個 Pod 都保留並宣告一大段穩定的連接埠範圍,就會讓這種彈性變得脆弱。4

這也是為什麼許多 WebRTC 系統會朝向每台伺服器單一 UDP 連接埠的方向發展,並在該連接埠之後以應用層進行多工分流。5

狀態黏著性

每台伺服器單一連接埠的設計解決了連接埠數量問題,但也帶來第二個問題:如何在整個叢集之間保留每個工作階段的歸屬。

ICE 與 DTLS 都是有狀態的協定。建立工作階段的那個程序,必須持續收到該工作階段的封包,才能驗證連線檢查、完成 DTLS 握手、解密 SRTP,並處理後續的工作階段變更,例如 ICE 重新啟動。若同一個工作階段的封包落到不同程序上,設定可能失敗,或媒體傳輸可能中斷。

這讓我們有了一個明確目標:對公開網際網路只暴露小而固定的 UDP 介面,同時仍能把每個封包路由到持有對應 WebRTC 工作階段的 transceiver。

WebRTC 媒體架構比較

為了達成這個目標,我們評估了幾種方法,包括 TURN(Traversal Using Relays around NAT),也就是由邊緣 relay 終止用戶端配置並代表它們轉送流量。2

方法

優點

缺點

每個工作階段使用唯一 IP:port(亦稱原生直接 UDP)

用戶端到伺服器的直接媒體路徑

資料路徑中沒有轉送層

每個工作階段都需要一個公開 UDP 連接埠

大型連接埠範圍難以暴露與保護

不適合 Kubernetes 與雲端負載平衡器

每台伺服器使用唯一 IP:port

比起每工作階段暴露,公開 UDP 介面小得多

每台伺服器一個共用 socket 可對多個工作階段進行多工分流

在單一主機上可順暢運作,但單靠它無法跨越共用且經負載平衡的叢集

單一主機上的工作階段多工分流,只有在封包已到達該主機後才有幫助;在經負載平衡的叢集中,第一個封包仍可能落到錯誤的執行個體,因此仍需要可決定性的方式,將每個工作階段導向持有它的程序


TURN relay(終止協定)

用戶端只需要連到 TURN relay 的位址與連接埠

可在邊緣集中管理政策

TURN 配置會增加設定往返次數

跨 TURN 伺服器移動或恢復配置仍然困難

無狀態轉送器 + 有狀態終止器(OpenAI 的 relay + transceiver)

公開 UDP 介面小

transceiver 仍持有完整的 WebRTC 工作階段

在媒體抵達持有工作階段的 transceiver 前,會多一次轉送跳點

需要 relay 與 transceiver 之間的自訂協調機制

架構概覽:relay + transceiver

我們推出的架構把封包路由與協定終止分開。訊號仍會到達 transceiver 以完成工作階段設定,而媒體則先經過 relay 進入。relay 是一層輕量級的 UDP 轉送層,對外公開的介面很小;而 transceiver 則是其後方具有狀態的 WebRTC 端點。

Relay 以無狀態方式將封包轉送至 transceiver

relay 不會解密媒體、不會執行 ICE 狀態機,也不參與編解碼器協商。它只讀取足夠的封包中繼資料來選擇目的地,然後將封包轉送到持有該工作階段的 transceiver。transceiver 看到的仍是正常的 WebRTC 流程,並且仍持有所有協定狀態。從用戶端的角度來看,WebRTC 工作階段沒有任何改變。

根據 ICE 憑證進行路由

在這套架構中,第一個封包的路由是關鍵步驟。relay 必須在封包路徑本身上,於尚未有任何工作階段存在之前,就把來自用戶端的第一個封包路由出去,而不是停下來查詢外部查找服務。

每個 WebRTC 工作階段其實都已帶有一個原生於協定的路由鉤子:ICE username fragment,也就是 ufrag,這是一個在工作階段設定期間交換,並會在 STUN 連線檢查中回傳的短識別碼。我們會產生伺服器端 ufrag,讓其中包含恰到好處的路由中繼資料,使 relay 能推斷目的叢集與工作階段所屬的 transceiver。

此序列圖顯示連線是如何建立的

在訊號階段,transceiver 會分配工作階段狀態,並在 SDP 回應中回傳共用的 relay VIP 與 UDP 連接埠。VIP 是位於 relay 叢集前方的虛擬 IP 位址;搭配連接埠後,它可為用戶端提供單一穩定的目的地,例如 `203.0.113.10:3478`,即使其後方其實有許多 relay 執行個體。用戶端在媒體路徑上的第一個封包通常是 STUN(Session Traversal Utilities for NAT)繫結請求,ICE 會用它來驗證封包是否能抵達所宣告的位址。

relay 只會解析這第一個 STUN 封包中足夠的資訊,以讀取伺服器 ufrag、解碼路由提示,並將封包轉送到持有該工作階段的 transceiver。每個 transceiver 都監聽在共用的 UDP socket 上,也就是綁定到內部 IP:port 的單一作業系統端點,而不是每個工作階段各有一個 socket。當 relay 依據用戶端來源 IP:port 與該 transceiver 目的地建立一個工作階段之後,後續的 DTLS、RTP 與 RTCP 封包就會在該工作階段內流動,而不需要再次解碼 ufrag。

relay 的工作階段刻意維持極簡,只包含用於指示封包轉送的記憶體內工作階段,以及監控所需的計數器與用於工作階段到期和清理的計時器。這樣的設計選擇,讓封包路由維持在封包路徑本身上。如果某個 relay 重新啟動並遺失工作階段,下一個 STUN 封包就能根據 ufrag 路由提示重建工作階段。為了讓它更可靠,我們也使用 Redis 快取,在路由建立後保存 <用戶端 IP + 連接埠,transceiver IP + 連接埠> 的對應關係,讓系統能更早恢復,而不必等到下一個 STUN 封包抵達。

Global Relay 與地理導向訊號

當我們把對外公開的 UDP 介面縮減為少量穩定的位址與連接埠後,就能在全球部署同樣的 relay 模式。Global Relay 是我們在全球各地分布的 relay 入口點叢集,全部都實作相同的封包轉送行為。

廣泛的地理入口可縮短用戶端到 OpenAI 的第一跳距離,因為封包可以先進入距離使用者更近的 relay,不論是在地理位置還是網路拓撲上,而不必先穿越公共網際網路前往遙遠區域。實際上,這表示在流量進入我們的骨幹網路前,延遲更低、抖動更少,也較少出現可避免的突發封包遺失。6

Global Relay 層會接收來自用戶端的封包,並轉送到收發器叢集

我們為訊號使用 Cloudflare 的地理與鄰近性導向,讓最初的 HTTP 或 WebSocket 請求能抵達附近的 transceiver 叢集。請求內容會決定工作階段的位置,以及要向用戶端宣告哪個 Global Relay 入口點。SDP 回應會提供 Global Relay 位址,而 ufrag 則包含足夠資訊,讓 Global Relay 能把媒體路由到指定叢集,並由 relay 路由到目標 transceiver。

地理導向訊號與 Global Relay 搭配後,能讓設定與媒體都走上鄰近的進入路徑,同時又把工作階段固定在單一 transceiver 上。這能降低訊號往返時間,以及第一次 ICE 連線檢查的往返時間,直接縮短使用者在語音開始前所需等待的時間。

Relay 的實作與效能

我們以 Go 撰寫 relay 服務,並刻意讓實作維持精簡。在 Linux 上,核心的網路堆疊會從機器的網路介面接收 UDP 封包,並把它們交付給 socket,也就是程序在綁定 IP:Port 後讀取的作業系統端點。Relay 在使用者空間中執行,因此一般 Go 程序會從該 socket 讀取封包標頭、更新少量流狀態,並在不終止 WebRTC 的情況下轉送封包。我們不需要任何核心旁路框架;那類框架雖然可讓使用者空間程序直接輪詢網路佇列以支援更高的封包速率,但也會增加營運複雜度。

關鍵設計選擇:

  • 不終止協定:Relay 只解析 STUN 標頭/ufrag;後續的 DTLS、RTP 與 RTCP 則使用快取狀態,讓封包保持不透明。
  • 短暫狀態:它會維護一個小型、短逾時、位於記憶體中的映射,將用戶端位址對應到 transceiver 目的地,用於流狀態與可觀測性。
  • 水平擴展性:多個 relay 執行個體可在負載平衡器後方並行運作。這些狀態並非難以恢復的 WebRTC 狀態,因此重新啟動只會造成極少量流量中斷,且流量也能快速恢復。

效率措施:

  • SO_REUSEPORT 是 Linux 的一項 socket 選項,可讓同一台機器上的多個 relay worker 綁定同一個 UDP 連接埠。之後核心會把傳入封包分配給這些 worker,避免單一讀取迴圈成為瓶頸。
  • runtime.LockOSThread 會把每個讀取 UDP 的 goroutine 固定到特定的作業系統執行緒上。搭配 SO_REUSEPORT 使用時,通常能讓同一流中的封包(來源與目的 IP:Port 加上協定)維持在同一個 CPU 核心上,改善快取區域性並減少內容切換。
  • 預先配置的緩衝區與極少的複製,能讓解析與配置的額外負擔維持在低水位,以避免 Go 的垃圾回收。

這套實作以相對精簡的 relay 佈署規模,就支撐了我們全球的即時媒體流量,因此我們選擇保留這個較簡單的設計,而不是承擔核心旁路方案的複雜性。

成果與經驗

這套架構讓我們能在 Kubernetes 中執行 WebRTC 媒體,而不必暴露成千上萬個 UDP 連接埠。這很重要,因為較小且固定的 UDP 介面更容易保護與負載平衡,也讓基礎架構在擴展時不必保留大範圍的公開連接埠。在 Kubernetes 有更佳基礎架構支援,以及因攻擊面縮小而更安全的情況下,這個設計也保留了用戶端所期待的標準 WebRTC 行為,並證明對我們的工作負載而言,不使用 SFU 的設計是正確的預設。大多數工作階段都是點對點、對延遲敏感,而且當推論服務不必表現得像 WebRTC 對等端時,也更容易擴展。

更廣泛的教訓是,最適合加入複雜性的地方,是薄型的路由層,而不是每個後端服務,也不是自訂的用戶端行為。把路由中繼資料編碼到協定原生欄位中,讓我們得到可決定性的第一封包路由、小型的公開 UDP 介面,以及足夠的彈性,能把入口點部署到世界各地、更接近使用者的位置。

有幾項選擇特別重要:

  • 在邊緣保留協定語意。用戶端仍然使用標準 WebRTC,從而維持瀏覽器與行動裝置的互通性。
  • 把困難的工作階段狀態集中在一處。transceiver 持有 ICE、DTLS、SRTP 與工作階段生命週期;relay 只負責轉送封包。
  • 利用設定流程中已存在的資訊進行路由。ICE ufrag 讓我們在不增加熱路徑查找依賴的情況下,獲得第一封包的路由鉤子。
  • 在訴諸核心旁路之前,先為常見情境最佳化。透過仔細使用 SO_REUSEPORT、執行緒固定與低配置解析的精簡 Go 實作,對我們的工作負載已經足夠。

只有當基礎架構讓延遲幾乎感覺不到時,即時語音 AI 才能真正運作。對我們而言,這意味著改變 WebRTC 部署的形態,同時不改變用戶端對 WebRTC 本身的期待。