跳到主要內容
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 建立連線、如何加密媒體、如何協商編碼器(傳輸及解壓時使用的編碼/解碼器),以及如何適應不斷變化的網絡狀況。有了 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 answer 中回傳共用的 relay VIP 與 UDP 連接埠。VIP 是位於 relay 叢集前方的虛擬 IP 位址;配合連接埠,它可為客戶端提供單一且穩定的目的地,例如 `203.0.113.10:3478`,即使其背後有多個 relay 實例。客戶端在媒體路徑上的第一個封包通常是 STUN(Session Traversal Utilities for NAT)binding request,ICE 會用它來驗證封包能否到達宣告的位址。

relay 只解析這個首個 STUN 封包中足夠的部分,以讀取伺服器 ufrag、解碼路由提示,並把封包轉發至擁有該工作階段的 transceiver。每個 transceiver 都會監聽共用 UDP socket,即綁定在內部 IP:port 上的一個作業系統端點,而不是每個工作階段一個 socket。當 relay 建立了從客戶端來源 IP:port 到該 transceiver 目的地的工作階段後,其後的 DTLS、RTP 及 RTCP 封包便會在該工作階段內流動,而無需再次解碼 ufrag。

relay 的工作階段被刻意設計得非常精簡,只包含一個用於指示封包轉發的記憶體內工作階段,以及監控所需的計數器,還有用於工作階段過期及清理的計時器。這種設計選擇讓封包路由能直接留在封包路徑上。若 relay 重新啟動並遺失了工作階段,下一個 STUN 封包就會根據 ufrag 路由提示重建該工作階段。為了令其更可靠,我們亦使用 Redis 快取來保存<client IP + Port, transceiver IP + Port>的對應關係,一旦路由建立便可提早恢復,而不用等到下一個 STUN 封包到達。

Global Relay 與地理導向訊號

當我們把公開 UDP 表面縮減為少量穩定的位址與連接埠後,就可以在全球部署相同的 relay 模式。Global Relay 是我們分布於不同地理位置的 relay 入口點叢集,全部都實作相同的封包轉發行為。

廣泛的地理分布入口可縮短客戶端到 OpenAI 的第一跳,因為封包可以先進入一個靠近用戶的 relay,再進入我們的網絡;這裏的「靠近」既包括地理位置,也包括網絡拓撲,而不必先跨越公眾互聯網前往遙遠區域。實際上,這代表更低延遲、更少抖動,以及在流量進入我們骨幹網絡前更少可避免的封包遺失突發。6

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

我們使用 Cloudflare 的地理與鄰近導向來處理訊號,讓最初的 HTTP 或 WebSocket 請求能到達附近的 transceiver 叢集。請求內容會決定工作階段的位置,以及向客戶端宣告哪一個 Global Relay 入口點。SDP answer 會提供 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 固定到特定的 OS thread。配合 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 部署的形態。