WebRTC 已经双向,语音 Agent 为什么还会抢话?

一个语音 Agent 的 WebRTC 指标可以全部正常,仍然像一个不会听人说话的客服。
麦克风一直上传,扬声器也一直下载,连接的确是双向的。可用户只是说了声"嗯",系统立刻把回答掐断;用户说"不是杭州,是青岛",模型已经停了,播放器却又多播半句;用户没有打断,只是旁边的人说话,后台却创建了一个新回合;更隐蔽的情况是声音停了,但对话历史仍记录了那段未被用户听见的回答,下一轮模型会基于一段不存在的共同上下文继续推理。
这些问题说明,"双方能同时传音频"与"双方能自然地同时交流"不是同一个能力。
一句话结论
全双工语音 Agent 至少有三层:传输层负责同时收发媒体,推理层负责让新输入改变正在生成的输出,交互认知层负责判断停顿、附和、背景声和有效打断;真正的生产门槛,是收音、模型响应、播放队列和对话历史能否在同一个事件协议里收敛,而不是界面上有没有打开 WebRTC。
一、先把"全双工"拆成三个不同问题
工程讨论里,"全双工"经常同时指三件事。
第一件是传输:客户端能否一边发送麦克风音频,一边接收远端音频。第二件是推理:模型已经开始生成时,新输入能否进入当前状态并改变后续输出。第三件是交互认知:系统能否判断一段重叠声音是在附和、抢话、修正、呼叫旁人,还是只是噪声。
三层可以分别成功,也可以分别失败。
传输层成功、推理层失败时,麦克风数据持续到达服务器,模型却仍要等当前 TTS 播完才处理。推理层成功、交互认知层失败时,系统具备取消接口,却把每一个"嗯"和咳嗽都当作用户接管。交互判断正确、播放状态失败时,模型决定让出话权,但客户端缓冲仍继续发声。播放确实停了、历史状态失败时,系统又会把用户从未听到的文字当成已经说过。
所以"全双工"不能作为一个布尔字段。更实用的做法是把它写成三层能力声明,并为每层记录不同的证据和失败模式。
二、传输层:sendrecv 只说明 RTP 可以双向流动

W3C WebRTC 规范把 RTCRtpTransceiverDirection 定义为 sendrecv、sendonly、recvonly、inactive 和 stopped。其中 sendrecv 的含义很具体:发送端可以发送 RTP,接收端可以接收 RTP,前提是远端接受协商。
这个定义没有任何一项在回答:用户是否说完、系统是否该继续说、一次重叠是不是打断、刚收到的声音属于谁。
WebRTC 擅长解决的是媒体连接问题:轨道、编码、协商、网络路径、带宽估计、抖动与时钟。它能让两个方向的数据同时存在,也能提供丢包、往返时延、抖动和音频能量等观测。传输层验收应关注连接建立、上行连续性、下行连续性、网络退化和设备切换。
但即使这些指标全部通过,系统仍可能是一条披着双向通道的回合式流水线。最典型的伪全双工是:用户音频不断上传,服务端把它缓存起来,直到当前回答完成后才提交给模型。网络层是 sendrecv,交互控制仍是"你说完我再说"。
因此第一条边界可以写得非常明确:WebRTC 是全双工语音的必要传输条件之一,不是自然话权决策的充分条件。
三、推理层:新输入必须能改变正在发生的输出

假设 Agent 正在说:"杭州明天会下雨......"用户插入:"不是杭州,是青岛。"
推理层全双工不是"服务器也收到了这段音频",而是这段修正能否改变正在运行的响应。系统至少要完成五个动作。
- 为新输入创建可追踪的事件身份,并记录它对应的会话时刻。
- 找到当前仍在生成或播放的
response_id。 - 决定继续、暂停、取消还是重规划,而不是统一执行停止。
- 让取消同时作用于模型输出和客户端播放队列。
- 用用户实际听到的边界修正对话历史,再从新城市继续回答。
这里最容易偷换的概念是"可取消"。有一个 cancel() 接口,只能证明响应可以被终止,不能证明系统知道何时应该终止,也不能证明终止后状态已经修复。
OpenAI Realtime VAD 文档提供了一个很好的边界样本。API 会产生 input_audio_buffer.speech_started 和 speech_stopped 事件;server_vad 主要按静音切分,semantic_vad 会根据用户话语判断是否完成;在语音对话中还可以设置 interrupt_response。这些能力使应用能够建立打断链路,但事件和开关本身不替应用回答"这次重叠到底是哪一种交互意图"。
推理层的验收也不应只测"取消请求是否返回成功"。至少要测:新输入到达模型的时间、停止继续生成的时间、客户端真正停止播放的时间、重规划开始的时间,以及历史修正是否完成。最后一项往往比前几项更容易漏。
四、交互认知层:检测到声音,不等于理解了话权
模型说话时出现一段新声音,可能对应完全不同的动作。
- "嗯""对""我在听"可能是附和,通常不要求 Agent 立即放弃话权。
- "等等""不对""先停一下"通常是明确接管,应快速停止并等待新指令。
- 用户在思考时的吸气、填充词或半句重复,可能意味着仍在组织表达。
- 旁人对话、电视、车辆声和扬声器回声不应自动创建新用户回合。
- 用户修正一个实体时,需要停止旧答案、保留任务目标并替换局部条件。
传统 VAD 主要回答 speech / no-speech。即使加入语义结束检测,它也主要帮助判断一段话是否完整,并不天然覆盖附和、第三方说话、背景噪声和部分修正。这些场景需要把声学证据、说话人线索、当前任务状态、关键词、重叠时长和历史交互放在一起判断。
这里不应过早宣称某个模型"理解了打断"。更稳妥的工程表达是:系统对重叠输入作出一项带原因、置信度和后果的动作决策,并能从错误决策中恢复。
例如,系统可以输出:
text
decision = BACKCHANNEL_ACCEPT
reason = short_ack_without_task_change
action = keep_speaking_and_keep_listening
confidence = 0.82
也可以输出:
text
decision = USER_TAKEOVER
reason = explicit_stop_intent
action = cancel_response_clear_playback_reopen_floor
confidence = 0.97
这并不要求一定存在一个独立"话权控制模型"。它只是要求系统把决策对象显式化,而不是把所有逻辑藏进一个 VAD 阈值和若干不可观测的回调。
五、真正棘手的是四份状态同时存在

三层能力最后会汇聚成四份容易分裂的运行状态。
第一份是收音状态:哪些音频已经采集、上传、识别,属于主用户还是环境。第二份是模型响应状态:哪个 response_id 正在生成,是否已经取消,取消发生在什么输出位置。第三份是播放状态:哪些音频已经进入设备缓冲,用户实际听到了哪一毫秒。第四份是对话历史:系统认为自己已经说过什么,下一轮模型会看到什么。
生产事故通常不是单个组件完全失效,而是四份状态对同一事件给出不同答案。
分裂一:模型停了,扬声器没停
服务端取消成功,但已经下发到客户端的音频仍在队列里。日志显示响应已取消,用户却听到半句尾巴。修复点不只是更快调用取消,而是为播放队列增加 playback_generation 或等价版本号,使旧响应的后续音频无法继续入队,并允许本地立即清空。
分裂二:扬声器停了,历史没截断
客户端及时停止播放,但服务端把整段助手文本留在历史中。下一轮模型会假设用户听过未播放部分,产生跳步、指代错位或重复确认。系统必须记录 heard_until_ms 或等价的已播放边界,并把历史修正到真实共享上下文。
分裂三:检测到新声音,但没有稳定事件身份
同一段语音在 VAD、ASR、网关和业务层被重复解释,可能既触发取消,又创建新回合,还在回声消除后被第二次提交。没有 input_event_id、来源和因果链,团队只能看到多个看似正确的日志,无法证明哪一次决策导致了用户体验。
这也是为什么全双工系统需要事件协议,而不是更多零散回调。
六、一份最小的全双工事件账本

为了让三层和四份状态可追踪,可以从以下字段开始,不必一次设计成庞大框架。
yaml
input_event_id: in_042
source: primary_user
captured_at_ms: 18420
speech_kind: explicit_interruption
floor_owner_before: assistant
active_response_id: resp_017
decision: user_takeover
decision_reason: explicit_stop_and_task_correction
decision_confidence: 0.97
model_cancel_at_ms: 18492
playback_stop_at_ms: 18541
heard_until_ms: 1270
history_truncated_to: assistant_audio_1270ms
next_action: await_corrected_city
这些字段承担四项职责。
input_event_id把声学事件、模型动作、播放器动作和业务结果连接起来。active_response_id与playback_generation防止旧响应在取消后继续注入。heard_until_ms把真实播放边界带回历史,而不是假设生成内容等于已听内容。decision_reason与decision_confidence让误打断和漏打断可以分类复盘。
事件账本不需要绑定某一家模型接口。WebRTC、WebSocket、SIP、端到端语音模型或级联 ASR / LLM / TTS 都可以使用同样的因果字段。不同系统只是在各字段由谁产生、何时提交上有所不同。
七、四个对抗探针,比"能打断"演示更有价值

要判断系统是不是真的形成了三层全双工,至少运行四个对抗探针。
探针 A:附和不能自动夺走话权
Agent 讲一个需要十秒的步骤,用户在中间说"嗯""对"。通过条件不是"系统听到了",而是系统记录到附和、继续当前回答,并且没有重复创建新回合。如果所有短声音都触发停止,说明推理层可取消,但交互认知层没有通过。
探针 B:实体修正必须停止、清队列并重规划
Agent 回答杭州天气时,用户说"不是杭州,是青岛"。记录新输入到模型停止、播放器停止、历史截断和新响应开始的四段时间。只测其中一个延迟会把状态分裂藏起来。
探针 C:旁人语音不能污染主会话
在 Agent 播放时加入旁人对话或电视声音。系统应该能把它标成第三方或低置信度环境语音,至少不立即中断并写入主用户历史。若系统无法识别说话人,也应采用保守策略,例如短暂抑制、请求确认或等待更明确的接管信号。
探针 D:用户没听到的内容不能留在历史里
在回答中段强制打断,然后下一轮追问"你刚才最后说了什么"。如果模型引用了已取消且未播放的尾句,说明播放状态与历史状态没有收敛。
这四个探针分别覆盖附和、有效打断、噪声边界和历史一致性。它们比一次顺利的"随时打断演示"更能暴露生产问题。
八、三层应该分别看什么指标

传输层可以看 RTT、抖动、丢包、上行音频断裂、下行缓冲和设备切换恢复。推理层可以看输入接受延迟、模型取消延迟、播放停止延迟、重规划延迟和历史修正成功率。交互认知层则要看错误抢话、漏打断、误打断、附和保持率、旁人语音污染率和停顿等待质量。
这些指标之间存在张力。
把 silence_duration_ms 调短,可能更快响应,也可能增加思考停顿被误判为结束。把打断门槛调低,可能缩短有效打断时间,也可能提高背景声和附和导致的误停止。把本地扬声器门控做得很激进,可能让用户更快接管,也可能把正常双人重叠变成生硬切断。
所以验收不能只用一个"全双工延迟"总分。至少要把速度、分类正确性、状态一致性和任务完成放在同一张结果表中。
九、不是所有场景都应该追求全双工

连续交互会增加价值,也会增加状态空间。
在开放对话、语言陪练、复杂客服和长任务 Agent 中,用户频繁停顿、改口、附和或插入新条件,三层全双工很可能值得建设。对于"开灯""停止""回充"这类短命令,明确回合、强确认和确定性执行可能更可靠。合规播报、金融确认或高风险动作也可能有意禁止重叠,要求先完整播报关键条件,再接受明确确认。
更简单的替代方案包括:保持半双工,但优化端点检测;只支持显式关键词打断;播放时本地门控扬声器,但不让任意背景声进入主历史;对少数高价值场景开放连续交互,而不是全产品一次切换。
选择这些方案不代表系统落后。全双工是一项场景能力,不是成熟度徽章。
结语
WebRTC 让双方可以同时传音频,但它不会替系统决定谁该在此刻说话。
自然全双工需要三层同时成立:媒体能双向流动,新输入能改变正在发生的输出,系统能解释重叠声音的交互含义。进入生产后,还要再加一条:收音、模型、播放和历史必须对同一事件给出一致答案。
所以评审一个语音 Agent 时,不要只问"能不能打断"。继续追问四件事:为什么打断、停止了哪一个响应、用户实际听到哪里、下一轮历史留下了什么。
能回答这四个问题,才算从双向音频走到了可观测、可恢复、可验收的全双工交互。
参考资料
- OpenAI:Introducing GPT-Live,连续交互与动作决策的第一方说明,访问于 2026-08-08。
- OpenAI Developers:Voice activity detection,VAD 事件、server / semantic VAD 与 interrupt_response 的第一方说明,访问于 2026-08-08。
- W3C:WebRTC: Real-Time Communication in Browsers,
RTCRtpTransceiverDirection与sendrecv的标准定义,访问于 2026-08-08。 - Full-Duplex-Bench,停顿、附和、轮流发言与打断评测研究。
- Full-Duplex-Bench v1.5,用户打断、听者附和、旁人对话、环境语音与停止延迟评测研究。
事实、推导与未知项
- 已确认事实:W3C
sendrecv定义媒体发送与接收;OpenAI Realtime API 提供 VAD 事件与可配置打断行为;GPT-Live 官方描述输出期间持续处理输入并频繁选择交互动作。 - 作者工程推导:三层全双工模型、四份运行状态、事件账本字段和四个对抗探针,是根据公开能力与生产故障模式形成的系统分析框架。
- 未知项:GPT-Live 内部是否使用独立话权控制器、何种双流结构、怎样维护播放边界与历史修复,公开资料没有披露,本文不作确定描述。