实时语音 Agent 最危险的时刻,可能正是监控看起来最空闲的时候。
假设一个后端已经接入 40 通电话。多数用户在听、在思考,CPU 只有 35%,发布看板于是判断“还有余量”。几秒后,产品演示结束,用户差不多同时开口;几项后台工具调用也在此时返回;部署流程又开始排空其中一个实例。系统并没有突然多出 40 位客户,它只是终于要兑现此前已经接下的工作。
而这份“已经接下的承诺”,恰恰不在许多负载测试的统计单位里。
Google 最新的会话感知负载均衡工程文章把区别说得很清楚:QPS 反映新请求的到达速度,CPU 与内存反映眼前压力,活跃会话数则反映系统已经承诺要继续服务的并发。OpenAI 关于 GPT-Live 连续交互架构的说明,又解释了为什么这份承诺越来越难估算:同一会话可以边听边说、持续积累上下文、在模型实例之间切换,还能一边保持对话,一边等待后台推理或工具结果。
本文不是 GPT-Live 的 API benchmark。OpenAI 当前说法是 GPT-Live API 即将推出;已经正式可用的 GPT-Realtime 模型则支持 WebRTC、WebSocket 与 SIP。本文没有运行生产流量,也没有做供应商横评。下面给出的是一套建议故障演练:小团队可以先在自己的实时技术栈上运行,并在模型、传输方式、区域或路由策略变化后重复执行。
今天最值得改的不是模型,而是容量验收方式:不要再用请求量和平均延迟单独批准上线。先建立会话账本,把建连与媒体、对话与后台任务拆开测;主动制造清理失败;练习排空、重连与恢复;最后确认系统关闭了每一份曾经打开的承诺。
新变化:一次对话里同时跑着五只时钟
传统轮次式语音产品,往往像一串边界明确的任务:录音、转写、生成、合成、播放。连续交互系统不是这个形状。OpenAI 对 GPT-Live 的描述是:全双工模型每秒多次判断要说话、继续听、暂停、打断还是调用工具;更复杂的工作可以交给后台模型,而实时对话不必停下来等。
于是,一个用户眼中的“通话”,内部至少有五只时钟:
- 连接时钟:从用户准备加入,到信令、媒体就绪、断开,以及最终清理完成;
- 媒体时钟:数据包到达、抖动、解码、播放、插话中断与静默;
- 对话时钟:临时转写、最终轮次、上下文增长与压缩;
- 委派时钟:工具或推理任务的发起、返回、取消与结果对账;
- 基础设施时钟:路由、扩缩容、Pod 终止、部署与跨区域故障。
其中一只坏掉,整体平均值仍可能很好看。模型输出很快,修复不了媒体建连太慢;声音连续,也不能证明被取消的工具真的停了;浏览器显示通话结束,不代表服务器已经把会话计数减掉;静默时 CPU 很低,更不表示这个实例能承受所有用户同时开口。
OpenAI 的工程文章还提到,长会话可能在模型实例扩缩容期间继续增长,最终越过上下文窗口。它的生产设计会预热替代实例、预填当前上下文,让新旧路径短暂并行,再在新实例就绪后切换。这里应该把它理解为某一家厂商公开的架构记录,而不是每个 API 或供应商都会提供的保证。真正可复用的结论只有一条:一通眼下很安静的电话,也可能已经预占了未来的计算责任。
重新定义容量:它是一份承诺,不是一张瞬时截图
对小团队来说,实时容量至少有三层:
- 观测负载:CPU、内存、网络、模型利用率、队列长度,以及眼下正在执行的工具任务。
- 承诺负载:系统已经接入的会话;只要用户重新开口、打断,或后台结果返回,系统就必须立即响应。
- 恢复负载:重连、故障切换、会话交接、上下文预填、重试与连接排空造成的临时重叠。
QPS 对第一层仍然有用,却几乎看不见第二层的持续时间与潜在工作。只按第一层扩容,还会在恢复真正发生前把第三层所需的安全余量用光。
但也不能简单地用会话数替换 CPU。Google 的文章明确建议使用混合信号,因为每个会话的成本不同:一个安静的翻译会话、一通不断插话的客服通话、一通频繁调用工具的排障通话,虽然都只贡献“1 个连接”,实际负担完全不同。
可以先建立一个可校准的压力分数:
committed_pressure =
active_sessions
+ speaking_weight * speaking_sessions
+ delegation_weight * sessions_with_background_work
+ recovery_weight * sessions_handing_off_or_reconnecting
这些权重不是行业常数。先使用偏保守的初值,在自己的流量组合上重放,再比较它与资源饱和、长尾延迟之间的关系,并为每次调整记录版本。如果一个复杂分数没有绑定到真实 trace,它只是一种更精致的猜测。
先设计一个像产品的场景
不要从几千条一模一样的合成连接开始。第一组测试应该让失败看起来像用户真正会遇到的问题。
设想一个三人团队准备发布语音引导助手。每通电话通常持续 4 到 8 分钟。Agent 会解释配置步骤,在用户操作时安静等待,允许用户插话,并把账户查询委派给后台服务。如果查询较慢,Agent 应该诚实说明还在等待,不能捏造结果;如果网络断开,用户可以重连一次;如果服务正在部署,旧会话必须按照明示策略完成排空或切换。
为同一测试建立六类会话角色:
| 角色 | 行为 | 暴露的承诺 |
|---|---|---|
| 倾听者 | 长时间静默,偶尔回应 | 空闲连接与未来开口时的保留容量 |
| 连续说话者 | 持续讲话,只留短暂停顿 | 连续媒体与推理压力 |
| 插话者 | 在助手播放期间开口 | 打断插话、取消旧输出与转写顺序 |
| 工具等待者 | 发起一项有时限的后台查询 | 委派归属与超时处理 |
| 重连者 | 断网后返回一次 | 重复会话、状态恢复与计费边界 |
| 排空中会话 | 部署期间保持通话 | 停止新接入与优雅终止 |
测试组合比绝对数量更重要。第一轮先跑 12 到 30 个会话。这个规模不能证明生产容量,但可以让团队逐条复核每份 trace。先把状态机跑对,再逐步增加并发,直到找到真实的容量拐点。
先做会话账本,再写压测脚本
每一通被系统接受的会话,都应创建一条权威记录。看板计数是派生状态;会话账本才是用来解释计数的证据。
realtime_session_receipt:
drill_id: "voice-onboarding-v1"
session_id: "sess-018"
fixture_role: "tool-waiter"
client_region: "ap-northeast"
transport: "webrtc"
provider_model: "pin-exact-model-or-service-version"
admitted_at: null
media_ready_at: null
first_playable_audio_at: null
last_media_at: null
disconnect_observed_at: null
cleanup_completed_at: null
owner_instance: ""
active_counter_delta:
increment: 0
decrement: 0
media:
jitter_ms_p95: null
packets_lost_delta: null
discarded_packets_delta: null
playback_underruns: null
conversation:
interruptions: null
interruption_cancelled_output: null
provisional_turns_finalized: null
delegation:
started: 0
completed: 0
cancelled: 0
orphaned: 0
recovery:
reconnect_attempts: 0
duplicate_live_sessions: 0
handoff_overlap_ms: null
end_reason: ""
cleanup_status: "pending"
如果条件允许,生命周期时间戳应来自同一套单调时钟。原始事件和聚合指标都要保留。p95 曲线无法告诉你,那一通掉线电话是否同时留下了仍在运行的工具任务和没有归零的计数。
浏览器端可以通过 W3C WebRTC Statistics API记录接收数据包、丢包、抖动、被丢弃的迟到数据包,以及可用时的往返延迟。但这些字段不能不加解释地使用:例如 packetsLost 是估算值,可能出现反直觉变化;不同实现提供的字段也不完全相同;网络指标还有可能泄露位置或说话行为。只收集决策真正需要的数据,明确保留期限,不要让可观测性本身变成新的隐私漏洞。
用五组 SLO,替代一个延迟数字
实时产品的发布包,至少要把五组目标分开记录。
1. 接入。 测量用户准备加入到会话被接受、会话被接受到媒体就绪的时间;容量关闭时是否正确拒绝;以及已经超预算的后端是否仍被分配新会话。
2. 媒体。 测量首个可播放音频、抖动、丢包、迟到包丢弃、播放断流,以及交接期间的空白。服务器首字节时间不能代替用户真正听到声音的时间。
3. 交互。 测量系统确认插话的时间、旧输出完全停止的时间、转写最终确认延迟、错误轮次切分,以及重叠语音能否形成连贯记录。OpenAI 的当前说明区分了“推测中的对话视图”和“最终权威转写”;这提醒我们,partial transcript 不能直接当成审计事实。
4. 后台委派。 测量任务确认、工具完成、取消完成、孤儿任务、取消后迟到的结果,以及后台工作未完成时实时路径能否继续服务。
5. 生命周期。 测量活跃会话计数误差、清理延迟、重连后的重复活跃会话、排空成功率、强制终止、状态丢失与恢复重叠。
阈值必须来自自己的产品基线和风险,而不是照抄本文。语言陪练可以容忍的重连,在实时付款确认中可能不可接受;客服助手可以在诚实说明的前提下等待较久的工具结果,同声翻译对媒体中断的容忍度则小得多。
不过,凡是会产生失控状态的事件,都应设置零容忍不变量:会话计数不能为负;同一不可复制的会话不能出现两个活跃归属方;断开已经完成时,不能留下仍有权限的孤儿任务;后端宣布排空后,不能再接入新会话。
运行一套带“故意静默”的 30 会话阶梯
第一套建议演练分三步,每步 10 个会话。模型、区域、客户端版本、传输方式、工具 fixture 与路由版本都要固定。
A 步:安静的承诺。 接入 10 个会话,其中 8 个保持静默,1 个持续说话,1 个等待工具。维持三分钟,确认 CPU 即使不高,路由系统也能看到 10 份已经接下的承诺。
B 步:同步激活。 再加入 10 个会话,然后让此前静默的会话在五秒内全部开口。这不是自然流量预测,而是一种安全办法,用来检验“静默所代表的承诺”。记录 CPU 与内存响应、媒体长尾延迟、拒绝接入、过载分配,以及恢复所需时间。
C 步:生命周期压力。 加入最后 10 个混合角色。强制四个客户端掉线,让其中两个重连;取消两项工具任务;再把一个后端标记为排空。确认新会话不再落到该实例,而旧会话按照约定的排空策略结束或迁移。
至少重复三轮,才能开始判断趋势。每轮随机选择接受故障的后端。所有失败与废弃的尝试都必须留在分母里。
Google 的文章建议同时改变并发会话数、会话时长、到达节奏、静默与说话比例、取消与掉线率、后端数量,以及单后端最大会话数。还应跟踪会话在后端间的分布、过载分配、启动 p95/p99、首个媒体流、掉线会话,以及强制断开后的计数行为。这些维度比“一次发完就结束”的请求压测更符合实时 Agent 的真实负担。
主动注入那些最容易被看板抹掉的故障
先一次只加一种故障,等单项行为稳定后再组合:
| 故障 | 必须观察到的行为 | 阻断上线的失败 |
|---|---|---|
| 客户端没有正常 close 就消失 | 检测传输/同意失效、超时,只清理一次 | 会话仍被计数,或高权限任务继续运行 |
| 超时与断开同时发生 | 只有一次幂等终态迁移 | 重复减计数或出现负数 |
| 工具结果在取消后到达 | 标为过期并阻止播报,或进入明示对账 | 把过期结果当成当前事实说给用户 |
| 后端开始排空 | 停止新接入,旧会话执行约定策略 | 新会话仍落在排空实例 |
| 会话归属进程停止 | 明示重连、切换,或有边界的失败 | 状态静默丢失,用户无感知 |
| 计数 exporter 卡住 | 指标新鲜度告警,路由采取保守值 | 过期的低计数继续吸入流量 |
| 网络质量下降 | 抖动/丢包可见,触发媒体降级或明确失败 | 服务器延迟仍为绿色,用户音频已损坏 |
| 用户说话时开始上下文交接 | 记录新旧归属与重叠时间 | 重复输出,或漏掉用户语音 |
WebRTC 本身已经定义了持续的传输同意机制。RFC 7675要求端点定期续期;一旦某个候选连接上的同意失效,就要创建新会话或执行 ICE restart。但这并不等于应用已经清理完成。它只是给了应用一个信号,后者仍需把信号收敛成一次幂等的生命周期迁移。
部署排空同样如此。Kubernetes 会暴露正在终止的 endpoint,也支持优雅关闭;但它的 Pod 生命周期文档明确说,某些应用还需要额外的会话排空与完成逻辑。Google Cloud 的连接排空文档也提醒:另一个后端并不了解既有 TCP 连接,可能直接发送 RST。基础设施可以停止新路由、争取收尾时间,却无法凭空补出丢失的应用状态。
把计数准确性设成发布不变量
Google 的示例在流开始时增加活跃会话计数,并在 finally 中减去。重点不是它使用哪种语言,而是所有退出路径最终只能收敛到同一个终态迁移。
演练时要同时观察三个独立数量:
ledger_open_sessions
runtime_active_sessions
routing_reported_sessions
它们不可能在每一个微秒完全相同,但必须在规定的观察窗口内收敛。告警不仅要看方向,还要看数据年龄:
- runtime 高于 ledger,通常意味着清理泄漏或延迟;
- runtime 低于 ledger,可能表示虚假容量或会话归属丢失;
- routing 低于 runtime,可能让后端过载;
- routing 高于 runtime,会把可用容量闲置;
- 任何没有新鲜时间戳的数值都应视为 unknown,而不是 0。
不要靠定时覆盖数值来“修好”漂移,又不留下差异记录。每次对账都应生成凭证,写明权威来源、受影响会话、修正动作与推测原因。否则数字会重新整齐,真正的 bug 却从审计链中消失。
高并发下,还要测试 tracker 本身的开销与竞争。一个逻辑正确的全局原子计数器,可能成为热点内存位置;分片或聚合设计可以缓解竞争,却会增加上报延迟。该怎么取舍,取决于路由读取信号的频率,以及流量突增的速度。
只有签署会话容量凭证,才能升级发布范围
不要因为“30 通都跑完了”就放行。真正批准的是一组具体配置及其证据边界。
session_capacity_decision:
fixture_version: "voice-onboarding-v1"
client_revision: "pin"
server_revision: "pin"
routing_revision: "pin"
provider_model: "pin"
region: "pin"
completed_runs: 0
discarded_runs: 0
max_tested_concurrency: null
thresholds:
media_ready_p95_ms: "team-defined"
first_playable_audio_p95_ms: "team-defined"
cleanup_convergence_ms: "team-defined"
overload_assignment_rate: "team-defined"
invariants:
negative_counter: "must-be-zero"
duplicate_owner: "must-be-zero"
orphaned_privileged_work: "must-be-zero"
admission_during_drain: "must-be-zero"
untested:
- "multi-region failover unless actually run"
- "provider outage unless actually run"
- "GPT-Live API behavior before public availability"
decision: "hold-until-filled"
owner: ""
review_at: ""
决策只使用四种状态:hold(暂停)、limited launch(有限发布)、promote(扩大范围)、rollback(回滚)。有限发布必须写明允许的区域、并发、会话时长、功能范围、降级方案与值班响应。“Beta”只有在真实缩小暴露面时才算控制措施。
模型 alias、媒体路径、工具策略、上下文处理、自动扩缩容、负载均衡器、最长会话时长或部署流程一旦变化,都要重新测试。浏览器仍能连上,不代表容量合同没有变。
常见失败模式
把静默会话当免费容量。 它此刻成本低,却仍保留随时恢复说话的权利。忽略这份承诺,就会在高峰来临前先花光恢复余量。
所有会话都使用同一权重。 这样会掩盖说话、后台委派与恢复状态的差异。只有当这些角色已经进入 trace,角色权重才有意义。
计数被减了两次。 超时、取消、掉线和进程清理可能同时争夺同一会话。终态迁移必须幂等,也必须专门测试竞态。
重连制造第二份真相。 新连接可能完全合法,但旧服务实例仍认为自己是权威归属方。会话身份、替换规则、计费与迟到结果处理都要提前定义。
把排空理解成“负载均衡器会处理”。 网络层可以停止新分配;应用状态、待完成工具与用户提示仍然需要明确归属。
只看服务器延迟。 用户真正经历的是抖动、迟到包、播放断流,以及插话后仍停不下来的旧输出。客户端证据必须进入同一份凭证。
用平均值批准上线。 实时故障集中在长尾与竞态。至少保留建连、首个音频、清理与恢复最差样本的逐会话 trace。
把厂商架构当成自己的保证。 OpenAI 的 relay/transceiver 与有状态交接,是某套生产系统的重要一手材料;它不能证明你的供应商、账户等级、区域或应用天然拥有相同行为。
这套演练适用于哪里,又不适用于哪里
当产品维持双向、有状态的媒体流,且用户会经历建连、插话、后台任务、重连或交接时,这套协议值得使用:语音助手、实时翻译、实时辅导、互动客服、多人游戏中的 AI 角色,以及 world model 交互界面。
对于离线转写、批量语音合成或一次性音频文件,它就太重了。那些任务仍然需要延迟、质量、权利与重试测试,但引入活跃会话控制面未必降低风险。生成式音频与非实时语音,更适合从 YBlog 之前的音频生产 fixture开始。
本文也不替团队选择基础设施。OpenAI 公开说,它的点对点负载采用无状态 relay 加有状态 transceiver,由后者集中持有 ICE、DTLS、SRTP 与会话生命周期(工程细节)。多人产品可能更适合 SFU,服务器之间的实时系统也可能选择其他传输。测试只要求状态归属清楚、迁移可观察,并不要求照抄架构图。
隐私与安全仍是独立门禁。语音 trace 可能包含敏感讲话、临时转写、网络元数据、工具结果和推断出来的行为。减少采集、控制访问、定义保留与删除,并测试同意和升级处理流程。容量测试通过,不等于可以录音、拿去训练、克隆声音,或授权高风险自动操作。
证据边界与仍未解决的问题
现有证据足以支持架构判断与测试维度,却不足以证明它们可以无条件迁移到每个系统。
可确认的一手事实包括:OpenAI 对 GPT-Live 全双工、后台委派、有状态交接和 relay/transceiver 的公开描述;Google 的活跃会话跟踪与混合负载均衡建议;WebRTC 标准化的客户端指标和持续同意机制;以及 Kubernetes、Google Cloud 对终止与排空的公开行为。
厂商主张则包括产品偏好、规模与内部延迟改善,不能直接变成你的目标 SLO。OpenAI 报告称,把媒体/推理前端从 Python asyncio 改写成 Go 后,新系统 p95 的帧交付平滑度达到旧系统 p50;这只是 OpenAI 特定系统的结果,不是通用的语言性能 benchmark。
对具体团队来说,仍未知的内容包括供应商内部排队、会话迁移细节、区域路由、账户限制、模型 alias 变化、音频保留、停机恢复,以及最终 GPT-Live API 的实际接口。把这些写进凭证的 unknown 区域,不要用 ChatGPT 产品体验或旧 Realtime API 假设去补空白。
本文没有真正跑过所建议的 30 会话阶梯。这里的数字只是 fixture 规模,不是观测到的容量上限。技术栈或预算较小的团队可以降低数量;只有在 trace 与清理仍可复核时,才应继续增加并发。
一套 48 小时发布计划
第 0–4 小时:定义合同。 画出会话状态机,分别标明媒体、对话状态、后台任务与清理的归属方。选择一个产品场景和六类角色,确定 trace 的访问、保留与删除规则。
第 4–12 小时:接入账本。 加入单调时间戳、计数变化、归属实例、客户端 WebRTC stats、后台任务状态、重连身份,以及唯一终止原因。并排比较 ledger、runtime 与 routing 三种计数。
第 12–20 小时:运行静默步骤。 接入 10 个混合会话,让多数保持安静,确认路由能够看到承诺负载。逐条审阅凭证,再增加并发。
第 20–30 小时:激活并破坏。 触发同步讲话、强制掉线、取消竞态、一次重连、指标过期和一个排空中的后端。失败与废弃运行都要保留。
第 30–38 小时:重复。 至少再跑两轮,检查最差的逐会话 trace,而不只看总览。根据真实拐点与用户影响,校准权重和阈值。
第 38–44 小时:对账。 证明 ledger、runtime 与 routing 最终收敛。搜索孤儿工具任务、重复归属、负计数、过期结果与缺失的终止原因。
第 44–48 小时:签署或暂停。 完成容量凭证。任何不变量失败,都应 hold,并写明下一项最小实验。边界内 fixture 通过后,也只能在已测区域、并发、时长与功能范围内发布,同时设置回滚触发器。
实时 Agent 的健康,不取决于它能否很快回答一次请求。真正的健康,是每一通已接入会话从静默、讲话、插话、后台工作,到掉线、排空和清理都能被观察、解释并最终关闭。发布时该问的不是“我们能启动多少请求”,而是“我们能兑现多少承诺,又能否证明已经结束的承诺真的关干净了”。
参考资料
- OpenAI:如何在六个月内构建实时响应的 GPT-Live 系统
- OpenAI:Introducing GPT-Live
- OpenAI:如何大规模交付低延迟语音 AI
- OpenAI API:GPT-Realtime 模型
- Google Developers Blog:用会话感知负载均衡扩展实时 AI Agent
- W3C:WebRTC Statistics API 指标定义
- IETF RFC 7675:STUN Consent Freshness
- IETF RFC 8827:WebRTC Security Architecture
- Kubernetes:Pod 生命周期与终止流程
- Kubernetes:Pod 与 endpoint 的终止行为
- Google Cloud:Connection draining
- Google Cloud:Backend service、session affinity 与 timeout