跳至主要内容
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(选择性转发单元)是一种媒体服务器,它从每个参与者接收一路 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 接入面小得多

每台服务器一个共享套接字即可解复用多个会话

在单机上工作良好,但单靠它无法跨共享负载均衡集群顺畅工作

单机上的会话解复用只有在数据包到达该主机后才有帮助;在负载均衡集群中,首个数据包仍可能落到错误实例,因此仍需要一种确定性方法,将每个会话引导到拥有它的进程


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 用户名片段,即 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 套接字,也就是绑定到某个内部 IP:port 的单个操作系统端点,而不是每个会话一个套接字。当 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 数据包,并将其交付给套接字——即进程在绑定 IP:Port 后读取的操作系统端点。relay 运行在用户态,因此一个普通的 Go 进程从该套接字读取数据包头,更新少量流状态,并在不终止 WebRTC 的情况下转发数据包。我们并不需要任何内核绕过框架;这类框架虽然能让用户态进程直接轮询网络队列、以获得更高包速率,但也会增加运维复杂性。

关键设计选择:

  • 不终止协议:relay 只解析 STUN 头部/ufrag;对后续 DTLS、RTP 和 RTCP 使用缓存状态,让数据包保持不透明。
  • 临时状态:它维护一个小型、短超时的内存映射,将客户端地址映射到 transceiver 目标,用于流状态和可观测性。
  • 水平扩展性:多个 relay 实例可在负载均衡器后并行运行。其状态不属于硬性的 WebRTC 状态,因此重启只会造成极少流量丢失,并能快速恢复流。

效率措施:

  • SO_REUSEPORT 是一个 Linux 套接字选项,允许同一台机器上的多个 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 在不增加热路径查找依赖的情况下,为我们提供了首包路由钩子。
  • 在考虑内核绕过之前,先针对常见情况进行优化。一个精简的 Go 实现,加上谨慎使用 SO_REUSEPORT、线程固定和低分配解析,已经足以满足我们的工作负载。

只有当基础设施让延迟变得“无感”时,实时语音 AI 才能真正发挥作用。对我们来说,这意味着改变 WebRTC 部署的形态,而不改变客户端对 WebRTC 本身的预期。