Jiayun's Blog

探索与分享

📌 好文共赏 | Editor’s Pick

原文:OpenAI’s WebRTC Problem 作者:Luke Curley (kixelated) | 发布:2026-05-06 | 阅读时长:约 12 分钟 来源:moq.dev(Media over QUIC 项目官方博客)

多模评分:Opus 9.2 / Sonnet 9.0 / Gemini 8.8(综合 9.0 / 10

一句话推荐理由:这是一封写给"准备照搬 OpenAI 语音 AI 架构"的所有工程团队的劝退信——作者是少数同时在 Twitch 和 Discord 把 WebRTC SFU 从零写过两遍的人,他用一种近乎刻薄的直白告诉你:WebRTC 不是为你的语音 AI 产品设计的,OpenAI 用的那一套"看起来很标准"的方案,每一层都是"被现实逼出来的 hack"。

为什么值得读

2026 年初到现在,几乎所有 SaaS 公司都在为产品塞进"语音 AI"功能。OpenAI 的工程博客 Delivering Low-Latency Voice AI at Scale 在 4 月底发出来后,迅速被当成"权威参考架构"在各种小群里转发:Pion 作 SFU、用 STUN-only 的中继做无状态负载均衡、把 source IP/port 路由换成 ufrag 路由——看着像教科书。

kixelated 这篇反驳文章的价值,在于它打破了"WebRTC = 实时音视频"这个 15 年没变过的肌肉记忆

文章的核心论点只有一句:WebRTC 是为 Google Meet 在 2011 年设计的产物,它的每一条默认行为,都和"语音 AI"这个新产品形态严重错位。

但作者把这句话拆开成了四块独立但严丝合缝的证据:

  1. 协议层错位:WebRTC 把"低延迟、可丢包"刻进了实现底层,但语音 AI 的用户更愿意多等 200ms 也别把"car wash"识别成"car was"。
  2. 流量模型错位:TTS 是"比实时还快"在生成的,理想做法是边生成边播缓存;WebRTC 没有缓冲、按到达时间渲染,OpenAI 只能反过来在发送端 sleep,再被网络抖动反咬一口。
  3. 基础设施错位:WebRTC 协议本身要求每连接一个临时端口,OpenAI 不得不为此把 5 个子协议(STUN/SRTP/DTLS/TURN/SCTP)muxed 到一个端口、靠 ufrag 解多路复用,再用一个 Redis 集群存连接表——所有这些都是为了把 WebRTC 塞进 Kubernetes。
  4. 握手层错位:建立一个 WebRTC 连接要 8 个 RTT,是为了支持 P2P 场景下"两个无公网 IP 的浏览器互打洞"——可你的语音 AI 后端是固定 IP 的数据中心服务器。

更难得的是,作者并没有止步于"骂"。他把后半篇用来讲QUIC 的几个隐藏超能力:CONNECTION_ID 取代源 IP/port 路由、QUIC-LB 把负载均衡变成无状态、preferred_address 让 anycast + unicast 自动健康检查——这些技术细节单独拿出来都是论文级别的内容,被压缩到 5 段话里讲完。

这是一篇罕见的同时具备"亲历者怨气"和"协议理论密度"的工程文章。

核心观点深度解读

1. "WebRTC 是 45 个 RFC 的麻袋"——肌肉记忆的真正成本

原文:“WebRTC consists of ~45 RFCs dating back to the early 2000s. And some de-facto standards that are technically drafts (ex. TWCC, REMB). Not a fun fact when you have to implement them all.”

绝大多数语音 AI 团队的工程师在评估"用什么传 audio"的时候,会下意识地跳到 WebRTC——因为它显得像 "实时音视频的标准答案"。但作者用一句话点破了这个错觉:WebRTC 不是一个协议,而是一摞协议的合集,从 2000 年代初一直堆到今天。

把它展开就是 ICE / STUN / TURN / DTLS / SRTP / SRTCP / SCTP / SDP / mDNS 再加上 RTP 系的扩展(TWCC、REMB、NACK、PLI、FIR、AV1 RTP 封装等)——他自己在 Twitch 时用 Pion 跑过这套,又因为性能问题把每一层都 fork 重写过一遍;在 Discord 又用 Rust 把整个 SFU 重写了一次。这两段经历让他成了少数有资格说"我太懂 WebRTC,所以我再也不想用它"的人

这条暗线和我之前介绍过的《curl 之父亲测 Mythos》里 Daniel Stenberg 对"AI 安全工具汇报"的反应一模一样:当一个"标准做法"被反复推荐时,最值得问的问题是"那些真正实现过它的人,怎么看?"——而那些人通常都在 Twitch、Discord、Cloudflare、Zoom 这种规模上撞过墙。

2. "WebRTC 太'aggressive'"——抖动缓冲被钉死,是 LLM 时代的产品级灾难

WebRTC 默认会把抖动缓冲(jitter buffer)限制在 20–200ms 之间,并且优先丢包以维持低延迟。这个设计来自一个非常合理的产品假设:会议是双向的、连续的、节奏紧凑的——如果一个人说话停一下等下一个包到,整个对话节奏就崩了。

但语音 AI 的用户场景完全不同:

  • 用户对模型输入的准确性远比对延迟敏感。把 “should I walk or drive to the car wash” 错听成 “should I walk or drive to the car was”,再生成一段牛头不对马嘴的回复,比多等 200ms 难受得多。
  • LLM 推理本身的 TTFT 就已经在数百 ms 量级,多等 100–200ms 在用户感知上几乎不可见。

原文:“But I’m not allowed to wait. It’s impossible to even retransmit a WebRTC audio packet within a browser; we tried at Discord. The implementation is hard-coded for real-time latency or else.”

这就是 kixelated 反复强调的"product fit"问题:浏览器里的 WebRTC 实现把"宁可丢包也要低延迟"刻进了实现深处,连他们 Discord 团队都没找到打开 audio NACK 的可靠方法。这是一个标准给错产品的经典案例——不是协议写错了,是产品形态变了,标准没跟上。

3. "TTS 比实时还快"——OpenAI 不得不在发送端 sleep

第三个反直觉的点是文章里我最喜欢的小节。语音 AI 的 TTS 生成速度通常显著快于实时播放速度:2 秒 GPU 算力就能生成 8 秒音频。理想情况下,你应该把这 8 秒在 2 秒内全部传到客户端,让客户端自己慢慢播——这样网络抖动几乎对用户不可见。

但 WebRTC 是"按时间戳即时渲染"的设计:到达即播,没到达就丢。OpenAI 的工程师为了在 WebRTC 框架内"避免 buffer bloat",只能反过来在服务端给每个 audio packet 加一个 sleep,让它"在该被渲染的瞬间"才到达。

原文:“OpenAI is literally introducing artificial latency, and then aggressively dropping packets to ‘keep latency low’. It’s the equivalent of screen sharing a YouTube video instead of buffering it.”

这是非常典型的抽象层级错位的代价:你用一个"为会议设计的协议"来传"已经预先生成好的连续音频",被迫把"快"压成"慢",再被网络抖动反咬。从信息论的角度,这就是把更多熵硬塞回了一个已经收窄的通道里。

4. "Ports Ports Ports"——OpenAI 那篇博客真正的技术货色,以及它为什么是个 hack

OpenAI 那篇原始博客的核心创新(也是 HN 上被讨论最多的部分)是:用 STUN-only 的中继做无状态负载均衡,把 ufrag 当作连接路由 key。听起来挺漂亮,但 kixelated 给出了一个非常"内幕"的解读:

WebRTC 规范本来要求每个连接一个临时端口——这样源 IP/port 变化(手机切 WiFi/4G、NAT 重写)的时候,目标端口不变就能继续路由。但这个设计在大规模生产环境里完全用不了

  • 每台服务器的端口数有限,连接量稍微大点就用光。
  • 企业防火墙喜欢把临时端口(49152–65535)一刀切封掉。
  • Kubernetes 的网络模型对随机端口极不友好。

所以所有大规模 WebRTC 服务都在做同一件违反规范的事:把多个连接 mux 到固定端口。Twitch 当年直接占了 UDP:443 装成 HTTPS/QUIC;Discord 用 50000–50032 这 33 个端口(每核一个)。

但 mux 到一个端口之后,你就得自己想办法识别每个包属于哪条连接:

  • STUN:可以用 ufrag 路由。
  • SRTP/SRTCP:用浏览器随机生成的 ssrc(32-bit),可能撞车——Discord 真撞过,他们的应对方案居然是用所有可能的密钥都试一遍解密,看哪个 key work
  • DTLS:祈祷 RFC9146 普及,否则没办法。
  • TURN:作者也没实现过。

原文:“We really hope the user’s source IP/port never changes, because we broke that functionality.” (作者翻译 OpenAI 那段 “Relay parses only STUN headers/ufrag; it uses cached state for subsequent DTLS, RTP, and RTCP, keeping packets opaque”)

换言之,OpenAI 的"无状态中继"是一个用 ufrag 做路由 key + Redis 兜底 + 牺牲连接迁移能力的复合 hack。能 work,但不优雅,而且每个新进语音 AI 赛道的团队都得重新趟一遍这个坑。

5. "8 个 RTT 才能握上手"——P2P 遗留税

原文:“It takes a minimum of 8* round trips (RTT) to establish a WebRTC connection.”

作者把 8 个 RTT 的来源逐条拆开:

  • 信令服务器(如 WHIP):TCP 1 + TLS 1.3 1 + HTTP 1 = 3 RTT。
  • 媒体服务器:ICE 1 + DTLS 1.2 2 + SCTP 2 = 5 RTT。

总共 8 个 RTT。即使信令和媒体跑在同一台主机上,这两边的握手仍然是冗余地各走一遍

这一切冗余的根源是 WebRTC 必须支持 P2P 场景——两个没有公网 IP 的浏览器之间打洞,需要 STUN 探测公网地址、可能要 TURN 中继兜底、还要做对称加密的端到端密钥交换。在 2011 年想做 Google Meet 时,这些都是合理的设计决定。

但在 2026 年的语音 AI 场景里:你的对手端是一个有固定 IP 的数据中心服务器,根本没有 P2P 的事情。8 个 RTT 全是为不会发生的场景买的税。对比之下,QUIC + TLS 1.3 一个 RTT 就完成连接 + 加密握手。

6. "分叉协议是 WebRTC 文化"——Discord 的极端解法

文章里另一处冷峻的事实:

原文:“Discord has forked WebRTC so hard that native clients only implement a tiny fraction of the protocol. No more SDP/ICE/STUN/TURN/DTLS/SCTP/SRTP/etc. But we still have to implement everything for web clients.”

Discord 的桌面 / iOS / Android 原生客户端根本不跑完整的 WebRTC 栈,他们只实现了关键的少数几层,把剩下的全部砍掉。但只要还得支持 web 客户端,就还得维护一整套 libwebrtc 兼容代码——这是为什么所有非 Google Meet 的会议软件都极力把你推向"下载我们的 App"。

这条暗线和我之前介绍过的《Quack:DuckDB 从零设计数据库 wire 协议》里的故事互为镜像:当一个老协议变成包袱时,要么咬牙做兼容(DuckDB 选了 PostgreSQL wire),要么彻底丢弃(DuckDB 自己又造了 Quack)。WebRTC 的情况是"想丢丢不掉"——浏览器 API 锁死了你的选项。

7. "QUIC FIXES THIS"——CONNECTION_ID、QUIC-LB、anycast 三件套

文章后半段是真正的硬核技术。kixelated 用一个工程师的视角讲了 QUIC 的三个被严重低估的特性:

(1) CONNECTION_ID 取代源 IP/port 路由

QUIC 在每个包里塞了一个 0–20 字节的 CONNECTION_ID,由接收方选。这意味着客户端切网络(WiFi → 4G)、NAT 重写源端口、负载均衡器把流量重路由——所有这些情况下,只要 CONNECTION_ID 不变,QUIC 就能识别为同一条连接。WebRTC 拼命用 ufrag/ssrc 做的事情,QUIC 在协议第一字段就给你了。

(2) QUIC-LB:完全无状态的负载均衡

OpenAI 那篇博客提到"Relay 服务器查 Redis 找 backend"——QUIC-LB 的方案是:让 backend 把自己的 ID 编码进 CONNECTION_ID。这样之后每个包都"自带路由信息",load balancer 解前几个字节就知道往哪发,不需要任何共享状态、不需要 Redis、不需要 sticky session。Cloudflare 全球 anycast 已经在大规模用这个机制。

这一条单独拿出来都和我之前写过的《Cloudflare QUIC 死亡螺旋》能对上:那篇文章讲的是 QUIC 在 cwnd 极端情况下的 bug,而这篇讲的是 QUIC 在正常路由下的工程优雅。它们一起说明了一件事:QUIC 在 2024–2026 这两年才真正开始把它"对工程师友好"的那一面展示出来——以前它一直被当成"另一个 TCP+TLS 替代品",现在它是"分布式系统的传输层原语"。

(3) anycast + unicast 双层架构

最漂亮的设计:

  • 所有边缘节点 advertise 同一个 anycast 地址(如 1.2.3.4)。
  • 客户端用 anycast 地址做握手——BGP 自动选最近的节点。
  • 握手时服务器通过 QUIC 的 preferred_address 把后续连接迁移到自己专属的 unicast 地址(如 5.6.7.8)。
  • 服务器满载时"撤回" anycast 通告,新连接自动飘到别处,但旧连接还在 unicast 上稳定保留。

原文:“Just like that, no load balancers needed! The anycast address is basically a health check!”

这是一个把网络层路由当作分布式系统原语用的极简范式,本质和 SRV record + DNS round robin 时代的玩法完全不同——它把"健康检查"内化成"我还要不要 advertise 这个 anycast 前缀"这一个动作。

8. "那应该用什么?"——一个非常务实的迁移路径

文章最后给出了一个非常具体的建议,而且和它的批评强度相当克制:

  1. 第一步:先用 WebSocket 流式传 audio。 “It makes for a boring blog post, but it’s simple, works with Kubernetes, and SCALES.” 这是绝大多数语音 AI 团队在 <100 万 DAU 阶段最划算的选择——尤其是当 TTS 已经"比实时还快"的时候,head-of-line blocking 是特性不是 bug。
  2. 第二步:等真的需要选择性丢包/优先级时,再上 WebTransport + MoQ。 WebTransport 走 HTTP/3 over QUIC,在浏览器里原生支持;MoQ 提供更高层的缓存/扇出语义(虽然作者诚实地说"MoQ 对 1:1 audio 不是完美匹配")。

原文:“I just don’t think the obvious solution is a good fit for Voice AI. And the obvious solution is very difficult to scale. WebRTC is Jared Leto. There I said it.”

最后这句把"显而易见的方案不一定是好方案"用一种很 self-aware 的方式总结了——他自承自己是"辞职做爱好项目"的人,OpenAI 工程师面临的是真实的 scale 压力。批评的姿态是开放的。

延伸阅读图谱

Luke Curley (kixelated) 的相关博文(出自同一个博客)

  1. Replacing WebRTC (2023):这篇是 WebRTC is the Problem 的"前传"。3 年前作者已经把 WebRTC 拆成 Media / Data / P2P / SFU 四块,逐块分析"哪些可以用 WebTransport 替代、哪些不能"。读完它再看这次的 OpenAI 反驳,会理解作者为什么三句话就能拍出来 8 RTT 的拆解——他已经走过这条路 3 年。
  2. QUIC’s (hidden) Super Powers:本篇里 CONNECTION_ID / QUIC-LB / anycast 三件套的完整版讲解,技术密度更高。
  3. Distribution @ Twitch:作者在 Twitch 八年的分发协议进化史,从 HLS、LL-HLS 一路打到 WebRTC、再到 MoQ。了解他"为什么这么烦 WebRTC"的真正源头。
  4. Never* use Datagrams:QUIC datagram 看似是 WebRTC 不可靠媒体传输的天然替代品,但作者解释为什么 99% 场景应该用 QUIC streams 而不是 datagrams。
  5. Forward? Error? Correction?:丢包恢复(FEC)远比想象的难做对。WebRTC 在这一层的"标准做法"为什么常常没用。
  6. The MoQ Onion:MoQ 协议栈逐层解剖,“Media over Transfork over WebTransport over QUIC over UDP over IP over Ethernet over Fiber over Light over Space over Time”。

对照阅读:协议设计/演化的相关文章

  1. OpenAI: Delivering low-latency Voice AI at scale(被批评的原文):必读,了解争议起点。OpenAI 真实交代了 STUN-only relay、Redis 路由表、Pion 选型背后的工程权衡。
  2. HTTP/3 explained(Daniel Stenberg):curl 之父写的 QUIC + HTTP/3 入门手册,免费在线书。了解 QUIC 协议本身的最佳参考。
  3. Cloudflare: HTTP/3 vs HTTP/2 vs HTTP/1.1:Cloudflare 的 QUIC 部署经验,包括 CDN 边缘的真实数据。
  4. Tailscale: How NAT Traversal Works:解释为什么 P2P 通信里 STUN/TURN/ICE 这套必须存在——同时也解释了为什么客户端-服务器场景下 99% 不需要它们。
  5. Pion Library Author on WebRTC SFU Architecture:OpenAI 用的 Go 实现,作者是 Sean DuBois。读一下他的 SFU 教程可以理解作者"用 Pion 但被迫 fork 每一层"的具体痛点。
  6. RFC 9114 (HTTP/3)RFC 9000 (QUIC)RFC 9001 (TLS over QUIC):QUIC 三件套官方规范。

反方观点 / 对 WebRTC 的辩护

  1. WebRTC for the Curious(Sean DuBois 写的 WebRTC 教科书):作者在 WebRTC 阵营里属于最聪明的实践者之一,他不会同意 kixelated 的全部结论。读这本书可以看到"WebRTC 在正确使用下能做什么"的另一面。
  2. HN 评论区的 WebRTC 老兵反驳:原贴 HN #48051951 下有几位前 Zoom / 前 Twilio 工程师指出,作者部分关于"audio NACK 配不出来"的吐槽其实是 SDP munging 的技能问题,并不是 WebRTC 本身的限制。
  3. LiveKit Blog:LiveKit 是 2024–2026 这一波最主流的 WebRTC 商用 SFU 服务,他们公开的工程博客代表了"WebRTC 路线"目前能做到的工程上限。

编辑延伸思考:为什么"显然的方案"在每一次范式迁移时都会错?

读完这篇文章,我一直在想一个更大的问题:为什么"标准方案"在新范式出现时几乎总是错的?

WebRTC 在 2011 年是一个几乎完美的设计选择:浏览器原生支持、解决了 P2P 打洞、自带媒体编解码、向后兼容 SIP/RTP 生态。十年下来,所有需要"实时音视频"的场景(会议、直播互动、游戏语音、远程协作)几乎都被它统治。在这个时间点,把"语音 AI 的实时音频传输"也归到"WebRTC 适合"的范畴里,是最低认知成本的判断

但语音 AI 在协议层面其实是和 Google Meet 完全不一样的形态

维度Google Meet(2011 设计目标)语音 AI(2026 实际形态)
拓扑多对多 P2P / SFU 转发1:1 客户端-服务器
节奏紧凑双向对话偏单向、半双工
容忍度低延迟 > 准确性准确性 > 延迟(200ms内不敏感)
流量实时生成、实时消费TTS 比实时快,可缓冲
客户端浏览器,IP 不固定浏览器 + App 都有,IP 可变但服务端固定
加密端到端(DTLS-SRTP)TLS 1.3 就够

这个表里没有一行是 WebRTC 设计时优化的方向。但因为 WebRTC 在浏览器里是唯一原生的实时音频 API,所以语音 AI 团队的默认选项就是它——这个默认选项的引力大到 OpenAI 都没逃出去。

这个模式在工程史上反复出现:

  • HTTP/1.1 → HTTP/2 → HTTP/3:HTTP/1.1 head-of-line blocking 的问题,明明 Google 在 2012 年用 SPDY 已经解决,但花了十年才在 HTTP/3 里通过 QUIC 真正普及。期间所有 "network performance" 文章都还在讲 HTTP/2 的 server push(一个最终被废弃的设计)。
  • REST → GraphQL → RPC:REST 在 2010 年代是"显而易见的 API 标准",直到大家发现移动端的过度获取问题。GraphQL 解了一半,gRPC/tRPC 解了另一半。
  • Redux → Hooks → Signals:React 状态管理的范式每 4–5 年大改一次,每次都伴随"我们以为这就是终局"的错觉。

这条暗线和我之前推荐过的《Redis 的野心代价》其实是同一个故事的另一面:当一个工具足够好以至于变成默认选项时,它会被强行塞进它原本没想过要服务的场景,并因此扭曲——要么扭曲工具自身(Redis 长出 Stream/Pub-Sub/JSON),要么扭曲使用者(OpenAI 自己写一个 STUN-only relay 来跨越 WebRTC 的端口模型)。

kixelated 这篇文章的价值,远不止于"语音 AI 应该用 WebSocket"这一条工程建议。它真正的价值在于演示了"协议级抽象错位"的具体形态

  • 抽象层级太低 → 你在 ufrag/ssrc 上做路由本不该出现在 SFU 层级的逻辑。
  • 抽象层级太高 → 你被 jitter buffer 和 retransmit policy 这种"会议假设"绑死,没办法暴露给应用层做决策。
  • 时代假设变了 → 8 RTT 的握手是为 P2P 设计的,对你的固定 IP 数据中心是纯负担。

学会识别"协议级抽象错位",是 2026 年这一波 AI 基础设施工程师最值得练的一项肌肉——因为接下来的几年,会有大量"这个旧协议要不要硬塞进新形态"的判断要做:

  • MCP/AGNTCY 这些 agent 间通信协议,要不要复用 HTTP/JSON-RPC?
  • 模型推理流式输出,要不要用 SSE 还是 WebSocket 还是 gRPC streaming 还是 WebTransport?
  • 浏览器里跑本地小模型,是用 WebGPU + ONNX Runtime Web 还是上 WASI-NN?

每一次这种选择,都是"显然方案"和"形态适配方案"的对抗。读 kixelated 这种"亲历者怨气 + 协议理论密度"双高的文章,是给自己装一个抗肌肉记忆的工具

配套资料导览

为这篇文章我准备了几份延伸材料:

  • mindmap.svg:把作者的论证结构画成一张思维导图——从"WebRTC 是什么"出发,分出"协议错位 / 流量错位 / 基础设施错位 / 握手错位"四条主干,再汇合到"WebSocket → WebTransport+QUIC"的迁移路径。
  • concept-cards.md:12 张关键概念卡片,包括 SFU、jitter buffer、SDP、ufrag/ssrc、QUIC-LB、CONNECTION_ID、anycast、preferred_address 等的简明定义。
  • glossary.md:英中对照术语表,覆盖 30+ 个 WebRTC / QUIC / Voice AI 相关的关键术语。
  • cover.svg:封面图(深色 + WebRTC vs QUIC 的对比意象)。

谁应该读

  • 正在为产品塞语音 AI 功能的工程团队:尤其是处于"刚做完原型,准备 scale"那一阶段的团队。这篇文章能帮你省掉至少一次 OpenAI 那样的"实际跑起来才发现端口模型不对"的代价。
  • WebRTC / SFU / SIP 老兵:你会感同身受作者每一个吐槽,并且可能在某些细节上不同意——这种"半同意 + 半反对"的阅读体验本身就有价值。
  • 协议设计者 / 网络工程师:作者展示了 QUIC 的几个工程超能力(CONNECTION_ID 路由、QUIC-LB、anycast preferred_address),它们在标准文档里很难一眼看出来。
  • 关心"AI 基础设施新范式"的产品/架构师:理解"为什么 OpenAI 没有再发明一个传输协议"和"为什么他们将来可能会",比单纯跟踪新模型发布更有长期价值。
  • 任何在工作里被"显然的方案"困住过的工程师:把它当作一个反肌肉记忆的训练样本来读。

一句话总结:这是一篇用六年 WebRTC SFU 实战经验写就的、对"语音 AI 默认架构"的解构。它的可贵之处不在结论(用 WebSocket 起步,将来上 QUIC/WebTransport),而在它把"协议级抽象错位"这件抽象的事,用 8 RTT、45 RFC、ufrag 解多路复用、Redis 路由表这种具体得不能再具体的细节铺平了。