打断不是听到声音就闭嘴:语音 Agent 的话权状态怎样真正收敛
元信息
- 文章类型:语音交互状态机与事件协议
- 目标读者:实时语音 Agent、智能硬件、客服语音和多模态系统的工程师与技术负责人
- 读者问题:怎样区分打断、附和、旁人语音、环境声和停顿,并让取消、播放与历史最终一致?
- 核心结论:打断不是一次 VAD 命中,而是一条从候选检测到物理停止、再到历史修复的状态收敛链。
- 可带走产物:六阶段状态机、播放确认字段、四类重叠输入测试表。
备用标题
- 用户说“等等”之后,语音 Agent 到底要停掉哪几层
- VAD 检测到声音,为什么还不能直接判定用户打断
- 模型已经取消,扬声器为什么还在说:一次打断的完整闭环
开头:最危险的不是慢半秒,而是停错
语音 Agent 正在解释一个步骤,用户轻声说了句“嗯”。系统立刻停止播放,把“嗯”当成一个新问题,随后又问“你想了解什么?”
另一次,用户明确说“等等,不是杭州,是青岛”。模型端很快返回取消成功,但播放器缓冲里还有一秒多语音,旧答案继续说完;下一轮上下文里还保留了用户没有听见的后半段。系统表面上支持打断,实际同时犯了三类错误:错误让出话权、停止不彻底、历史不一致。
这类问题无法靠把 VAD 阈值再调灵敏一点解决。因为 VAD 回答的是“这里像不像有人声”,而产品真正要回答的是:这段声音是否面向系统、是否意图接管话权、当前输出是否应撤销、已经播出的内容保留到哪里。
一句话结论是:自然打断需要两次判断和两次确认。先把重叠声音判成一个交互动作,再让模型、TTS、网络队列、播放器和会话历史收敛到同一事实。
一、静音边界只是候选事件,不是对话句号
OpenAI Realtime API 的 VAD 文档把公开能力分得很清楚:server_vad主要依据静音切分音频,semantic_vad则结合用户说出的词判断话语是否完成。系统会收到input_audio_buffer.speech_started和input_audio_buffer.speech_stopped等事件,也可以配置threshold、prefix_padding_ms、silence_duration_ms、create_response与interrupt_response。
这些配置很有用,但它们仍然不是完整话权策略。
较短的silence_duration_ms能更快产生结束边界,却更容易把思考停顿当成说完;较高的激活阈值可能更抗噪,也可能漏掉轻声纠正。semantic_vad增加了语义完成度判断,却不能自动知道一句“嗯”是听者附和、不同意、催促,还是准备接管。即使interrupt_response被设为自动中断,应用仍需要决定怎样处理已经进入播放器的音频和已经写入会话的 assistant 内容。
因此第一条工程原则是:
VAD_EVENT != FLOOR_DECISIONVAD 事件应该进入话权控制器,成为判定依据之一,而不是直接等同YIELD_TO_USER。至少还要结合声学持续时间、AEC 残留、ASR 稳定前缀、当前会话状态、是否唤醒了设备、说话人方向或身份,以及最近一次用户动作。
这里没有一个适合所有场景的固定阈值。车内、客厅、耳机、客服热线和会议终端的噪声、回声与说话距离完全不同。正确做法是保留可观察字段和场景化策略,而不是在文章里发明一个“300ms 最佳阈值”。
二、重叠声音至少要分成四类动作
Full-Duplex-Bench v1.5 把重叠处理拆成四类场景:用户打断、听者附和、旁人对话和环境语音。这个分类对工程非常重要,因为四类输入虽然都可能触发 VAD,却要求完全不同的系统动作。
1. 竞争性打断
例如“等等”“不是杭州”“先别执行”。用户的目标是接管话权并修改当前目标。系统应尽快降低或停止输出,同时保留一个很短的语义确认窗口,避免由回声或误识别触发不可逆取消。
2. 听者附和
例如“嗯”“对”“我在听”。它通常表示继续,而不是接管。系统可以不做任何响应,也可以播放极短 backchannel,但不能把附和自动提交成新的业务轮次,更不能因此丢弃当前 assistant 的主回答。
3. 旁人语音
例如设备附近有人说“把门关一下”,但并非面向 Agent。没有说话人、方向、唤醒状态或上下文证据时,系统应倾向IGNORE_AMBIENT或请求澄清,而不是把旁人的话变成用户指令。
4. 环境语音与非语音声
电视、人声播客、咳嗽、碰撞和回声都可能形成活动片段。仅凭幅度或持续时间无法稳定判断其交互意义。端侧 AEC、声源特征和服务端语义应共同降低误触发,但任何一层都不应被写成绝对正确。
这四类不是为了给模型贴漂亮标签,而是为了定义不同副作用:继续、短附和、让出话权、忽略或澄清。只有动作不同,分类才有工程价值。
三、打断要经过六个收敛阶段
把“用户开口”直接连到“取消回答”,会把一个时间序列问题压成一个布尔值。更稳妥的状态机至少包含六个阶段。
阶段 1:SPEAKING
系统正在生成或播放主要回答。此时仍持续接收输入,但新的语音活动只产生候选事件,不立即改写会话主状态。
阶段 2:INTERRUPT_CANDIDATE
端侧检测到疑似用户语音。为了降低用户感知停止时间,可以先做可逆动作:快速 ducking、冻结新音频入队、保留当前播放位置。这里不宜立刻删除上下文或撤销有副作用的工具。
阶段 3:CLASSIFYING
话权控制器合并多类证据,输出明确动作:
CONTINUE_SPEAKING BACKCHANNEL YIELD_TO_USER IGNORE_AMBIENT ASK_CLARIFICATION动作必须携带event_id、response_id、置信度、原因和所用证据。没有身份的“stop=true”无法在重连、迟到事件或并发输出中正确归属。
阶段 4:YIELDING
只有判定为YIELD_TO_USER后,系统才进入撤销链:停止继续生成、停止 TTS、丢弃尚未发送的音频块、清空客户端播放队列。每一层都应返回独立确认,而不是共用一个“取消成功”。
阶段 5:PLAYBACK_STOPPED
客户端确认扬声器已经停止产生可听输出,并上报实际播放位置。到这里,用户体验层的停止才算完成。模型取消成功或 WebSocket 不再收到新 chunk,都不能替代这个确认。
阶段 6:HISTORY_REPAIRED
服务端按客户端确认已播放的范围截断 assistant 内容,随后把新的用户输入接到正确历史上。只有历史修复完成,系统才重新进入LISTENING或开始下一次响应。这个确认是软件播放链路的代理信号,不证明用户在声学环境中一定听见。
六个阶段的关键不在状态名称,而在每一步都有进入条件、退出确认和超时策略。超时也不能静默:如果模型已取消但播放器未确认,应标记STOP_UNKNOWN,阻止旧输出被当成已经安静。
四、取消请求不等于用户已经听不到
一次实时输出常同时存在四个进度:
generated_until_ms:模型或语音解码已经生成到哪里;sent_until_ms:服务端已经发送到客户端哪里;buffered_until_ms:客户端播放器已经缓冲到哪里;played_until_ms:客户端播放器确认推进到哪里,是比生成/发送进度更接近用户侧的代理信号。
假设回答共生成 10 秒,服务端已经发送 8 秒,客户端缓冲 4 秒,用户在实际播放 2.3 秒时打断。若服务端只停止生成,仍有 1.7 秒缓冲可能继续播放;若历史保留 8 秒或 10 秒,下一轮模型会误以为用户已经听过后面的前提。
因此客户端需要回报与具体response_id绑定的播放确认,例如:
{"type":"playback.ack","event_id":"evt_demo_ack_01","response_id":"resp_demo_07","received_until_ms":8040,"buffered_until_ms":3980,"played_until_ms":2310,"device_output_delay_ms":36}这些字段是本文给出的工程设计示例,不是 OpenAI 公布的内部协议。实际实现还要考虑播放器时钟、解码延迟、音频设备重采样和 ACK 频率。静音、蓝牙切换或物理扬声器故障也说明播放 ACK 不能证明人耳确实听见;它只是应用通常能获得的最近代理。关键语义是:历史优先依据用户侧确认的播放进度,而不是依据生成端进度。
五、历史修复必须和播放停止绑定同一个 response
如果只截断文本而不绑定音频时间,系统很难知道 2.3 秒对应哪一段语义。一个可实现的方法是让每个音频 chunk 记录response_id、chunk_seq、时间区间和semantic_span:
{"type":"response.audio.chunk","response_id":"resp_demo_07","chunk_seq":18,"start_pts_ms":2160,"duration_ms":120,"semantic_span":[42,51]}发生打断时,服务端用最后确认的played_until_ms映射到可保留语义前缀。为了避免切在半个词、半个数字或否定词之前,还应按稳定语义边界向前收缩,并明确记录history.truncated事件。
这里还要防两个竞态:
- 旧 response 的迟到音频在队列清理后再次到达;
- 新 response 已开始,旧 response 的取消确认才返回。
处理方法不是“谁最后到就信谁”,而是所有输出、取消与 ACK 都绑定response_id和单调递增的状态版本。旧版本事件可以留在审计日志中,但不能再次推动当前播放器和会话状态。
六、Backchannel 应是动作,不是普通消息
很多系统把“嗯”“好”“对”交给普通 ASR 和对话轮次处理。结果是一次不想接管话权的附和,触发了四件不该发生的事:提交用户消息、结束 assistant 输出、生成新回答、把短词写进长期上下文。
更合理的做法是让 backchannel 成为交互动作:
{"type":"turn.action","action":"BACKCHANNEL","target_response_id":"resp_demo_07","reason":"listener_acknowledgement","commit_as_user_turn":false,"interrupt_output":false}这个示例仍是工程设计,不是公共 API 字段。它表达的边界是:附和可以被观测、计数和评测,但默认不改变任务目标,不创建完整用户轮次,也不要求主回答停止。
当然,短词不能只靠词表硬编码。“不对”可能是明确纠正,“对”也可能是在回答系统提问。分类必须结合系统是否正在说、当前问题类型、语调和后续语音。不能确认时,ASK_CLARIFICATION比错误执行更安全。
七、一个能落地的事件信封
话权事件至少需要以下公共字段:
{"event_id":"evt_demo_01","schema_version":1,"session_id":"sess_demo_42","response_id":"resp_demo_07","source":"client_audio","monotonic_ns":938472993822,"seq":1201,"type":"turn.action","payload":{}}排序不能只依赖跨机器 wall clock。客户端、服务端和音频设备的时钟会漂移,重连还会改变连接身份。更稳妥的是在每个来源内使用单调时钟和序号,同时记录时钟偏移估计;跨来源因果通过event_id、response_id和父事件关系连接。
事件信封还让以下问题可诊断:是谁先判定了打断?取消发给了哪个 response?播放 ACK 是否来自旧连接?历史截断依据哪个播放位置?没有这些字段,现场只会留下“用户说停了,但系统还说了一会儿”的主观描述。
八、四类场景必须分别验收
不能用十条干净的“用户说停止”音频证明打断系统可用。至少需要以下四组对抗场景:
| 场景 | 期望动作 | 关键失败 | 核心指标 |
|---|---|---|---|
| 明确纠正或取消 | YIELD_TO_USER | 漏打断、停止过慢、旧结果继续播放 | 有效打断率、物理停止延迟、历史修复成功率 |
| 听者附和 | BACKCHANNEL或继续 | 错误让出话权、主回答被切断 | 附和误中断率、回答连续性 |
| 旁人语音 | 忽略或澄清 | 错误执行旁人指令 | 非目标说话误接管率 |
| 环境声与回声 | 忽略 | 误停止、循环触发 | 环境误打断率、恢复时间 |
还要覆盖不同播放音量、设备距离、AEC 状态、网络抖动和语言现象。中文里的“嗯”“啊”“那个”“不是”承担的功能不同,不能直接照搬英文数据集阈值。
指标也必须成对看。降低物理停止延迟可能提高误打断;提高语义确认门槛可能减少误停,却增加真正纠正的等待。Full-Duplex-Bench v1.5 报告的“快速让出并修复”与“优先保持连续”两类策略,正说明不存在脱离场景的单一最优点。
九、什么时候更简单的方案反而更好
不是所有产品都需要复杂话权状态机。
对于短命令、强确认、不可逆动作或合规播报,清晰回合边界通常更容易审计。用户说完、系统复述、用户确认、再执行,虽然不够像自然闲聊,却可能更可靠。此时可以只保留硬停止按钮和明确取消协议,而不追求附和、重叠生成和动态话权。
即使需要自然打断,也可以分阶段实现:先让播放队列可撤销并有PLAYBACK_STOPPED确认;再绑定played_until_ms修复历史;最后才增加 backchannel 和旁人语音分类。把所有能力一次性堆进去,会让误判来源难以定位。
十、发布前评审清单
在声称系统“支持打断”前,至少回答以下问题:
- VAD 事件和最终话权动作是否分开记录?
- 每次动作是否绑定正确的 session、response 和 event?
- 模型取消、TTS 取消、网络丢弃和播放器停止是否分别确认?
PLAYBACK_STOPPED超时后系统怎样降级,是否会继续接受旧 chunk?- 历史是否按
played_until_ms而不是生成进度截断? - Backchannel 是否默认不提交完整用户轮次?
- 旁人语音和环境声是否有独立对抗测试?
- 指标是否同时覆盖漏打断与误打断,而不是只追求最快停止?
如果其中任何一项仍只能回答“应该没问题”,系统支持的更可能只是停止按钮,而不是可靠打断。
事实、推导与未知项
已确认事实
- OpenAI Realtime API 公开
server_vad与semantic_vad两种模式,并提供 speech started/stopped 事件以及interrupt_response等配置。 - Full-Duplex-Bench v1.5 分别评测用户打断、听者附和、旁人对话和环境语音,并使用停止/响应延迟等指标分析重叠处理。
- GPT-Live 官方描述产品能在输出期间持续处理输入并多次选择说、继续听、暂停、打断或调用工具。
本文工程设计
- 六阶段收敛状态机、统一事件信封、
played_until_ms、semantic_span、Backchannel 动作和历史修复协议。 - 模型、TTS、网络和播放器分层确认,以及旧 response 迟到事件的版本隔离。
未知项
- GPT-Live 内部怎样识别 backchannel、旁人语音与有效打断。
- OpenAI 产品内部是否使用播放 ACK、何种时间轴或历史截断协议。
- 任一固定阈值在具体设备、语言与声学环境中的最优性。
参考资料
- OpenAI Developers, Voice activity detection (VAD),访问于 2026-08-08。
- OpenAI, Introducing GPT-Live,2026-07-08。
- Lin et al., Full-Duplex-Bench v1.5: Evaluating Overlap Handling for Full-Duplex Speech Models,2025。
- Full-Duplex-Bench, official code repository,访问于 2026-08-08。