Chico Notes
研究与调研

实时语音中的打断、VAD 和 Turn Detection

区分语音活动、端点、轮次结束与真实打断,建立支持回声抑制、误打断恢复、播放截断和全链路评测的生产级 Voice Agent 状态机。

持续修订的工程笔记

实时语音中的打断、VAD 和 Turn Detection

证据快照:本文核对时间为 2026-08-06。OpenAI Realtime、Gemini Live、Deepgram Flux、LiveKit Agents 与 Pipecat Smart Turn 都在持续迭代。文中涉及参数和事件名称时以当前官方文档为准;架构判断、目标阈值和状态机属于生产工程建议,不代表厂商统一标准。

摘要

VAD 只判断有没有声音,Turn Detection 判断用户是否要交出或夺回话权。生产系统必须把端点、打断、回声、播放截断和状态一致性放进同一状态机。

读者将学到什么

读完本文,你应该能回答:

  1. VAD、Endpointing、End-of-Utterance、Turn Detection 和 Barge-in 有什么区别。
  2. 为什么听到用户发声后立刻停播,会把“嗯嗯”、咳嗽、电视声和回声都误判成打断。
  3. 如何用声学、ASR、语义、会话状态和说话对象信息做两阶段打断判断。
  4. 用户打断后,怎样取消模型、清理播放队列并把会话历史截断到实际听到的位置。
  5. AEC、降噪、播放器时钟和 WebRTC Stats 为什么是对话状态机的一部分。
  6. 如何建立覆盖真实打断、Backchannel、旁人说话、环境声和自我回声的评测集。

1. 低延迟之外,Voice Agent 还需要正确地“让出话权”

上一篇《Voice Agent 如何把端到端延迟控制在 500ms》讨论了如何缩短用户说完到 Agent 首段有效语音播放之间的关键路径。但只追求更快,会立刻遇到另一个问题:

系统什么时候应该开始说,什么时候应该继续说,什么时候必须立即停下?

一个只靠固定静音阈值的系统通常会在两个极端之间摇摆:

  • 阈值过短:用户稍微停顿,Agent 就抢话;
  • 阈值过长:用户已经说完,Agent 仍然迟迟不响应。

一个只靠 VAD 做打断的系统也会遇到类似矛盾:

  • 检测到任何声音就停:响应快,但误打断很多;
  • 等 ASR 输出完整词句再停:更准确,但 Agent 可能继续说几百毫秒甚至更久。

人类对话不是严格的“你说完,我再说”。真实交流里经常存在:

  • 用户在 Agent 说话时说“嗯”“对”“我知道了”;
  • 用户纠正 Agent:“不是周五,是周六”;
  • 用户直接说“停”“等一下”;
  • 用户在思考时停顿一两秒;
  • 旁边的人、电视或车载导航发出语音;
  • Agent 的扬声器声音被麦克风重新采集;
  • 双方同时开始说话,然后其中一方主动让出话权。

Stivers 等人对十种语言的研究显示,人类对话普遍倾向于减少长时间沉默和重叠,但具体响应节奏存在差异。[1] 这意味着自然语音交互的目标不是追求“任何时候都最快”,而是:

该接话时快速接话
该等待时不要抢话
该让出时立即让出
不该被打断时稳定继续

这四个目标彼此冲突,需要一个明确的 Turn Control 系统,而不是几个散落在 ASR 和播放器里的布尔变量。


2. 先把容易混淆的概念分开

2.1 VAD:现在是否存在语音活动

VAD(Voice Activity Detection,语音活动检测)通常输出:

speech_started
speech_continued
speech_stopped

或者每帧的语音概率:

{
  "speech_probability": 0.93,
  "frame_start_ms": 1840,
  "frame_end_ms": 1870
}

VAD 的主要任务是把音频分成“语音”和“非语音”区域。它并不知道:

  • 这句话是否完整;
  • 用户是不是在思考;
  • 用户是否在对 Agent 说话;
  • 这个声音是不是 Agent 自己的回声;
  • “嗯嗯”是在抢话,还是只表示自己在听。

Silero VAD 是常见的本地实现。项目文档称其模型约 2MB,可在 CPU 上以低于 1ms 的时间处理 30ms 以上的音频块,并支持 8kHz 与 16kHz 音频。[14] 这些数字说明 VAD 可以很轻,但“轻”不等于它可以独立解决轮次问题。

2.2 Endpointing:一段语音是否可以切片提交

Endpointing 关注的是:

当前音频片段是否应该结束,并作为一个可处理的输入提交?

它常结合:

  • 静音持续时间;
  • VAD speech stop;
  • ASR 文本稳定性;
  • 标点和句法;
  • 最大语音时长;
  • 业务槽位是否完整。

Endpointing 可以服务于转录切片,但不一定意味着 Agent 应立即回答。例如会议转录可以把语音切成段,却不需要抢占说话权。

2.3 End-of-Utterance:用户这一段表达是否结束

End-of-Utterance(EOU)比纯 Endpointing 更强调表达完整性:

“我想查一下……”               → 大概率未结束
“我想查一下昨天的订单。”       → 大概率结束
“先比较延迟、成本,还有……”     → 大概率未结束
“先比较延迟、成本和准确率。”     → 大概率结束

OpenAI Realtime 当前同时提供 server_vadsemantic_vad:前者按静音切分;后者使用语义分类器判断用户是否完成表达,并通过 eagerness 控制等待倾向。[4]

2.4 Turn Detection:谁应该拥有下一段话权

Turn Detection 解决的是更大的问题:

用户是在继续当前轮次、交出话权、发出 Backchannel,还是试图夺回话权?

它的输出不应该只有 end=true,而应更接近:

{
  "action": "SHIFT_TO_AGENT",
  "confidence": 0.87,
  "signals": [
    "speech_stopped_180ms",
    "semantic_complete",
    "required_slots_present"
  ]
}

或者:

{
  "action": "HOLD_USER_FLOOR",
  "confidence": 0.82,
  "signals": [
    "trailing_conjunction",
    "rising_hesitation",
    "recent_long_pause_pattern"
  ]
}

TurnGPT 的研究说明,句法、语用完整性和对话上下文可以帮助预测话轮转换,而不仅是观察静音。[2] 后续 Response-conditioned Turn-taking 又进一步指出,系统准备说什么,也可以反过来影响当前时机是否适合接话。[3]

2.5 Barge-in / Interruption:用户在 Agent 发言时尝试改变话权

Barge-in 通常指用户在 Agent 正在输出语音时发声。但“出现重叠语音”并不自动等于“用户想打断”。

真实打断需要同时回答:

检测到了人声吗?
这是用户本人吗?
用户是在对 Agent 说吗?
这段声音包含夺取话权的意图吗?
系统应该暂停、停止、恢复还是继续?

2.6 Backchannel:用户发声,但不想接管完整话轮

常见 Backchannel 包括:


嗯嗯

是的
我懂
right
uh-huh

它们常表示“我在听,你继续”。如果系统把所有 Backchannel 当作真实打断,Agent 会不断停顿,谈话变得支离破碎。

2.7 Full Duplex:能同时听和说,不等于会处理重叠

Full Duplex(全双工)表示系统可以在输出语音时继续接收和处理输入音频。它只是基础能力。

一个系统即使能同时收发音频,也可能:

  • 无法区分 Backchannel 和真实打断;
  • 停得太慢;
  • 停下后不知道是否恢复;
  • 历史中保留了用户没有听到的回答;
  • 在旁人说话时错误改变任务。

因此全双工的难点不是“两个流同时打开”,而是重叠发生后怎样做正确的会话决策。

2.8 概念对照表

概念核心问题典型输入典型输出
VAD有没有语音音频帧speech / non-speech
Endpointing音频片段是否提交VAD、静音、ASRsegment commit
EOU表达是否完整音频、文本、语义complete / incomplete
Turn Detection下一步谁说EOU、上下文、角色hold / shift / wait
Interruption是否夺回话权重叠音频、文本、状态pause / stop / ignore
Backchannel是否只在反馈短语、韵律、时长continue / acknowledge
Full Duplex能否同时听说双向媒体流concurrent input/output

3. “检测到用户声音”只是打断判断的第一步

生产系统至少要区分以下重叠场景。

3.1 明确停止命令


等一下
别说了
stop
hold on

这类指令要求极快让出话权。即使 ASR 还没有输出完整句子,系统也可以利用:

  • 极短高置信命令词;
  • 语音 onset;
  • 历史用户词汇;
  • 本地关键词检测;
  • 当前 Agent 正在长回答的状态。

对于“停”这一类控制词,等待 500ms 的完整语义确认通常是不合理的。

3.2 内容纠正

不是北京,是上海
等下,我说的是第二个
不对,金额应该是 199

用户不只是希望 Agent 停下,还希望立即修改当前任务状态。系统需要:

  1. 停止当前播放;
  2. 取消或冻结旧生成;
  3. 保留已听到的上下文;
  4. 将纠正内容绑定到被纠正的对象;
  5. 对已经执行的写操作做补偿或重新确认。

3.3 新问题或新意图

用户在 Agent 解释 A 时突然问 B:

先别讲这个,退款多久到账?

这通常是完整的话权转移。系统应结束旧回答,而不是稍后继续旧段落。

3.4 Backchannel

嗯嗯

我知道

它可能表示:

  • 继续说;
  • 我同意;
  • 我想接话;
  • 我已经听够了。

单看文本也不够。相同的“好”在不同持续时间、音调、音量和位置上,意图可能不同。

3.5 用户继续上一轮

Agent 因过早 Endpointing 开始响应,用户其实只是停顿后继续原句:

我想订明天……
(Agent 开始说)
……下午三点去上海的票。

这类事件应被视为 TurnResumed,而不是独立新问题。

3.6 旁人说话与背景媒体

场景包括:

  • 用户对同事说话;
  • 电视或播客播放人声;
  • 车内其他乘客说话;
  • 会议中非目标发言人讲话。

VAD 会正确地检测出“有语音”,但系统不应该一定响应。

3.7 回声和 Agent 自触发

扬声器播放的 TTS 被麦克风采集后,可能触发:

VAD speech_started
→ Agent 误以为用户打断
→ 停止自己
→ 播放停止后 VAD 又安静
→ Agent 恢复
→ 再次自触发

这不是模型语义问题,而是音频链路问题。

3.8 非语言声音

咳嗽、笑声、清嗓、敲击、键盘和道路噪声都可能跨过声学阈值。真实系统必须允许“先暂停观察,确认无有效文本后恢复”,而不是任何声音都永久终止当前回答。

Full-Duplex-Bench v1.5 将重叠处理拆成四类:真实用户打断、Backchannel、用户对其他人说话和环境人声,并分别衡量行为、停止延迟与恢复延迟。[18] 这个分类非常适合作为生产测试集的基础。


4. 打断判断需要哪些信号

一个稳健的 Interruption Classifier 不应只依赖单一模型,而应融合多层信号。

4.1 声学信号

VAD probability
speech duration
energy / SNR
pitch contour
speaking rate
prosody
onset sharpness
overlap duration
speaker embedding
far-end echo correlation

声学信号能最快得到,适合第一阶段“先暂停”。但声学无法完整理解内容。

4.2 ASR 信号

partial transcript
stable word count
word timestamps
language
ASR confidence
command keyword
sentence boundary

例如:

  • 只有 80ms 的声音且没有稳定词:更可能是噪声;
  • 已稳定识别出“停”:应立即终止;
  • 识别出“嗯嗯”:需要结合韵律判断 Backchannel;
  • 识别出“不是……”:可能是纠正。

4.3 语义信号

语义层可以判断:

  • 当前表达是否完整;
  • 是否包含改变当前任务的意图;
  • 是否是确认、否定或纠正;
  • 是否与 Agent 当前内容相关;
  • 用户是不是在继续上一句。

4.4 会话状态

同一段声音在不同状态下应产生不同动作。

Agent 正在播放长解释
Agent 正在询问确认
Agent 正在读验证码
Agent 正在等待工具
Agent 正在执行不可逆写操作
当前语音是否允许被打断

例如,在读出一次性验证码时,“嗯”通常不应中止;在执行转账前确认金额时,任何“不是”“等等”都应阻止提交。

4.5 业务槽位与预期输入

如果系统正在收集:

手机号
身份证号
地址
信用卡后四位
长段口述

用户中间停顿的概率更高。Turn Detector 应知道当前正在完成什么槽位,而不是使用全局固定静音阈值。

4.6 Addressee 和说话人信号

在多方环境中可以加入:

  • 说话人识别或 Speaker Diarization;
  • 麦克风阵列方向;
  • 唤醒词;
  • 设备按键;
  • 摄像头视线和面向方向;
  • 最近一轮说话人身份。

2025 年的一项研究表明,语言、声学与视觉信号联合可以改善 Turn-taking 和 Backchannel 预测。[20] 对纯语音产品而言,这不意味着必须开启摄像头,而是说明“是否在对我说话”本身是独立信号。

4.7 推荐输出不是布尔值,而是动作分布

{
  "event_id": "int_01K...",
  "generation_id": 47,
  "probabilities": {
    "TAKE_FLOOR": 0.71,
    "BACKCHANNEL": 0.18,
    "SIDE_SPEECH": 0.05,
    "NOISE": 0.04,
    "ECHO": 0.02
  },
  "recommended_action": "PAUSE_AND_CONFIRM",
  "signals": {
    "speech_ms": 210,
    "stable_words": 1,
    "first_word": "等等",
    "echo_correlation": 0.08
  }
}

动作可以包括:

IGNORE
DUCK_AUDIO
PAUSE_AND_CONFIRM
STOP_AND_COMMIT_USER_TURN
RESUME_AGENT_AUDIO
ASK_CLARIFICATION

5. 一套生产级 Turn Control 架构

这套架构有三个关键点。

5.1 播放器必须理解 generation_id

播放器不能只是一个不断追加 PCM 的 FIFO。它至少需要支持:

write(generation_id, audio_chunk)
pause(generation_id)
resume(generation_id)
clear(generation_id)
played_position_ms(generation_id)

旧 Generation 的迟到音频不能进入新回答。

5.2 Turn Orchestrator 是唯一状态决策者

VAD、ASR、播放器和模型都可以发事件,但不应分别修改会话状态。否则会出现:

  • VAD 认为用户开始说话,播放器停了;
  • ASR 没识别出文本,又让播放器恢复;
  • 模型服务同时已经启动新回答;
  • 旧 TTS 音频继续迟到。

所有状态转换应通过单一 Orchestrator,按 turn_idgeneration_id 和事件序号处理。

5.3 历史提交必须依赖实际播放进度

模型生成了多少,不等于用户听到了多少。历史提交器要从客户端播放器获得实际 Playout Cursor,而不是根据服务端发送字节数猜测。


6. 推荐的状态机

状态字段建议包括:

{
  "session_id": "ses_42",
  "user_turn_id": 18,
  "agent_generation_id": 47,
  "state": "POSSIBLE_INTERRUPT",
  "agent_audio_started_at": 1722870092.221,
  "agent_played_ms": 1840,
  "user_speech_started_at": 1722870094.076,
  "interruption_candidate_id": "int_883",
  "transcript_version": 31,
  "pending_tool_operations": ["op_901"]
}

6.1 POSSIBLE_INTERRUPT 是最重要的中间状态

如果从 AGENT_SPEAKING 直接跳到 INTERRUPTED,任何 VAD 误报都会永久结束当前回答。

更稳妥的设计是:

检测到用户活动
→ 立即降低或暂停 Agent 音频
→ 在短窗口内等待 ASR / 语义确认
→ 真打断:清队列、截历史、转入用户轮次
→ 假打断:从暂停点继续播放

LiveKit 当前文档也支持 False Interruption 识别和在没有有效转录时恢复 Agent 发言,并提供 false_interruption_timeoutresume_false_interruptionmin_durationmin_words 等控制。[9]

6.2 暂停、停止和取消不是同一个动作

  • Duck:把音量快速降到较低水平,仍保留播放位置;
  • Pause:停止输出,但保留当前队列,可恢复;
  • Clear:丢弃当前 Generation 的未播放音频;
  • Cancel:要求 LLM/TTS/工具停止继续产生结果;
  • Truncate:把会话历史裁剪到用户实际听到的位置。

生产实现应分别记录这些动作,不能都叫 interrupt()


7. 两阶段打断:先快速让出,再确认意图

7.1 第一阶段:Acoustic Fast Path

目标是在用户明确开始说话后,尽快避免 Agent 继续压过用户。

触发信号可以是:

VAD speech_started
speech duration > 80–150ms
energy above adaptive threshold
not strongly correlated with far-end audio

动作:

DUCK 或 PAUSE
记录 playout cursor
启动短确认计时器

这里的目标是“礼貌地让出声道”,不是立即修改长期会话状态。

7.2 第二阶段:Semantic Confirmation

在接下来的短窗口内综合:

  • 稳定 ASR 词数;
  • 是否命中停止或纠正词;
  • 语音持续时间;
  • 是否对 Agent 说话;
  • 是否只是 Backchannel;
  • 是否是 Agent 回声;
  • 当前任务是否允许被打断。

输出:

TRUE_INTERRUPTION
FALSE_INTERRUPTION
BACKCHANNEL
UNCERTAIN

7.3 显式停止词走超快速路径

停 / stop / 等一下 / 别说了

这些词一旦达到高置信度,应绕过普通最小时长和词数规则:

立即 Clear
取消当前生成
保留已播放位置
进入用户轮次

7.4 Backchannel 可以不打断,也可以轻量确认

策略示例:

Backchannel推荐动作
很短的“嗯嗯”,低音量,不带新内容Agent 继续
“对,但是……”立即让出
“我知道了”且 Agent 正在重复解释可能结束当前回答
多次连续“嗯”且用户明显想接话暂停并确认

不要只做全局词表。Backchannel 的意义依赖音调、持续时间和 Agent 当前语义位置。

7.5 不确定时优先可恢复动作

不确定时,Pause 通常比 Clear 安全:

Pause 可恢复
Clear 不可恢复

但播放器的暂停时间也不能过长,否则用户只咳了一声,Agent 却沉默两秒。需要根据场景设定短的 False Interruption Timeout。


8. Turn Detection:从固定静音到语义与预测

8.1 固定静音阈值适合作为保底

最简单规则:

silence_duration_ms >= 500
→ end_of_utterance

优点:

  • 可解释;
  • 容易部署;
  • 不依赖文本;
  • 对多语言友好。

缺点:

  • 用户自然停顿会被切断;
  • 长阈值增加延迟;
  • 不理解句法和语义;
  • 对口述号码、地址和代码很差。

8.2 Semantic EOU

Semantic EOU 根据用户说了什么决定等待多久。

OpenAI 的 semantic_vad 会根据用户表达完成概率决定是否继续等待,eagerness=low 更愿意等待,high 更早切分。[4]

这种方式适合:

  • 开放式问答;
  • 用户有自然停顿;
  • 需要减少抢话;
  • ASR 文本质量较高。

风险包括:

  • 语义模型本身有延迟;
  • ASR 错误会影响判断;
  • 不同语言和口音表现不同;
  • 业务槽位可能比自然语言完整性更重要。

8.3 Eager End-of-Turn 与可撤销推测

Deepgram Flux 当前使用结构化事件:

StartOfTurn
EagerEndOfTurn
TurnResumed
EndOfTurn

EagerEndOfTurn 表示中等置信度,可以提前启动 LLM;如果用户继续说话,收到 TurnResumed 后取消;高置信 EndOfTurn 再正式提交。[6][7]

官方文档报告默认配置下 End-of-Turn 检测 p50 约 260ms,并说明启用 Eager EOT 可能因为推测执行增加约 50%–70% 的 LLM 调用。[8] 这些数字是厂商当前口径,实际系统仍需按语言、网络和场景重新测量。

8.4 本地 Turn Model

Pipecat Smart Turn v3.2 是开源原生音频 Turn Detection 模型,当前项目文档称其支持 23 种语言,在部分 CPU 上可在 10–100ms 内推理,并建议和轻量 VAD 配合:VAD 检测到停顿后,再对当前轮次音频运行 Turn Model。[13]

这种架构的价值是:

VAD 负责便宜地发现候选停顿
Turn Model 负责判断停顿是否真的是轮次结束

需要注意:项目自报性能不等于你的生产环境性能;必须单独验证中文口语、噪声、长口述和特定设备。

8.5 联合声学与语言信号

TurnGPT 强调对话上下文和语用完整性;Smart Turn 使用原生音频;2026 年 JAL-Turn 又提出联合声学与语言特征;DualTurn 则尝试从双通道对话音频中直接学习话轮动作。[2][13][21][22]

工程上不必立刻训练大模型,但可以借鉴共同结论:

只看静音、只看文本或只看音频都不完整。最稳妥的系统通常是轻量声学触发,加语义和业务状态确认。

8.6 动态 Endpointing

推荐按场景调整:

短命令:更低静音阈值
长口述:更高阈值
号码/地址:等待槽位完整
用户近期多次被抢话:提高等待
用户连续短问:降低等待
高噪声:提高 speech start 阈值
弱网:避免频繁小片段提交

不要把 silence_duration_ms 当作部署时写死、之后永不调整的常量。


9. 厂商 API 体现的是不同控制边界

9.1 OpenAI Realtime

当前 Realtime API 支持:

  • server_vad:按静音切分;
  • semantic_vad:按语义完成度切分;
  • thresholdprefix_padding_mssilence_duration_ms
  • eagerness
  • create_responseinterrupt_response。[4]

在中断一致性方面:

  • WebRTC 和 SIP 由服务端管理输出音频缓冲,用户中断时会自动截断未播放音频;
  • WebSocket 的播放缓冲由客户端管理,客户端必须停止播放、记录已播放毫秒数,并发送 conversation.item.truncate。[5]

这一区别说明:

传输协议不只是网络选择,它会改变谁负责维护“用户实际听到了哪里”的会话事实。

9.2 Gemini Live

Gemini Live 当前自动活动检测支持:

startOfSpeechSensitivity
prefixPaddingMs
endOfSpeechSensitivity
silenceDurationMs

并提供:

START_OF_ACTIVITY_INTERRUPTS
NO_INTERRUPTION

当 VAD 判定用户打断时,进行中的生成会被取消和丢弃,客户端应停止播放并清空队列;待处理 Function Call 也可能被取消。[11][12]

它也支持关闭自动活动检测,改由客户端发送 activityStartactivityEnd,适合 Push-to-talk、长口述或应用自有 VAD。[12]

9.3 LiveKit Agents

LiveKit 当前 Turn Handling 包含:

  • VAD、STT Endpointing、Realtime LLM 与 Audio Turn Detector 等检测模式;
  • Adaptive Interruption Handling,用于区分真实抢话和 Backchannel;
  • min_durationmin_words
  • False Interruption 恢复;
  • 用户打断后按实际听到部分截断历史;
  • Preemptive Generation。[9][10]

当前文档推荐大多数级联式 Agent 使用 Turn Detector,并在可用时使用 Adaptive Interruption。需要注意的是:当 Realtime Model 自己启用服务端 Turn Detection 时,框架会直接响应模型的中断事件,绝大多数框架级 Interruption 参数不再生效;此时应该在 Realtime Model 侧调 VAD、Semantic VAD 和中断策略,而不是同时维护两套相互冲突的判断器。[9]

它展示了一种框架层思路:把不同 ASR、LLM、TTS 和 Realtime Model 的差异收敛到统一 Turn State,同时明确每一种部署模式中谁拥有最终决策权。

9.4 Deepgram Flux

Flux 把 Turn Detection 融入流式 ASR,直接输出轮次事件。优势是:

  • 文本与轮次状态由同一流提供;
  • TurnResumed 明确表达用户继续说话;
  • 可以在 EagerEndOfTurn 做推测生成;
  • EndOfTurn 再提交。

代价是业务更依赖该事件模型,切换 STT 时需要重新适配。

9.5 自建级联

自建常见组合:

WebRTC AEC
+ Silero VAD
+ Streaming ASR
+ Smart Turn / Semantic Classifier
+ 自有 Turn Orchestrator
+ LLM
+ Streaming TTS

优势:

  • 可替换各组件;
  • Trace 最完整;
  • 容易和业务状态、权限、工具结合;
  • 可以本地化或私有部署。

代价:

  • 状态机复杂;
  • 需要自己处理取消和播放一致性;
  • 调参和评测成本高;
  • 不同服务的时间戳和 ID 需要统一。

10. 打断后的核心问题:生成、发送、播放和历史不一致

一段 Agent 回答至少存在五个进度:

GENERATED
SENT
RECEIVED
BUFFERED
PLAYED

它们不是同一个位置。

假设模型生成了 20 秒回答:

服务端已生成:20s
服务端已发送:8s
客户端已收到:6s
播放器已缓存:4s
用户实际听到:2.3s

用户此时打断,下一轮上下文最多只能假设用户听到了 2.3 秒。

10.1 必须维护 Audio-to-Text Alignment

推荐每个 TTS Segment 保存:

{
  "generation_id": 47,
  "segment_id": "seg_9",
  "text_start": 84,
  "text_end": 113,
  "audio_start_ms": 1680,
  "audio_end_ms": 2410,
  "server_sent_at": 1722870093.12,
  "client_received_at": 1722870093.18,
  "played": true
}

打断时根据 played_position_ms 找到:

  • 已完整播放的文本;
  • 部分播放的 Segment;
  • 完全未播放的文本。

历史可以保存:

完整已播放内容
+ 部分 Segment 的安全摘要或截断标记

不要假装获得了精确到字的同步,除非 TTS 确实提供 Word/Phoneme Alignment。

10.2 WebSocket 客户端必须主动截断

OpenAI 当前文档明确要求,在 WebSocket 模式下客户端检测到 speech_started 后:

  1. 立即停止播放;
  2. 记录已经播放的时长;
  3. 发送 conversation.item.truncate,删除未播放部分。[5]

其他自建链路也应实现等价语义,即使事件名称不同。

10.3 取消必须传播到所有层

Turn Orchestrator
→ LLM cancel
→ TTS cancel
→ Audio transport cancel
→ Player clear
→ Tool cancel / detach
→ History truncate

只取消 LLM 不够,因为:

  • TTS 可能已经收到大量文本;
  • 网络层可能仍有音频在途;
  • 播放器可能缓存几秒;
  • 工具结果可能稍后回到旧 Generation。

每个事件都要带 generation_id,迟到事件必须被丢弃。

10.4 语音打断不一定意味着取消业务工具

假设 Agent 正在调用:

查询物流:可取消或忽略旧结果
生成报表:可以转后台继续
创建日程:可能已经提交,不能假装取消
转账:必须遵守事务和审批状态

因此要把:

speech_response_cancel
business_operation_cancel

分开。

工具操作建议声明:

{
  "operation_id": "op_901",
  "cancelable": false,
  "idempotency_key": "...",
  "side_effect": "write",
  "commit_state": "COMMITTED"
}

用户说“等一下”时,系统可以停止说话,但不能声称一个已经提交的写操作被取消。必要时应查询最终状态并解释。


11. AEC 不是音质优化,而是打断系统的前提

11.1 为什么回声会破坏状态机

Agent 的扬声器输出通过空气或系统混音重新进入麦克风。VAD 看到的是清晰人声,ASR 甚至可能正确识别 Agent 自己刚说过的话。

如果没有 AEC:

Agent 播放 TTS
→ 麦克风采集 TTS
→ VAD 判定用户发声
→ Agent 自我打断

11.2 AEC 需要准确的 Far-end Reference

AEC 需要知道扬声器正在播放的参考信号。以下情况会降低效果:

  • TTS 不经过 WebRTC 音频路径;
  • 系统把音频先混入其他播放器;
  • 播放队列和参考信号不同步;
  • Bluetooth 切换导致延迟突变;
  • 用户调节系统音量;
  • 设备同时播放通知或音乐;
  • 多麦克风和扬声器路由变化。

因此媒体层要记录实际输出路径,而不只是配置了 echoCancellation: true 就认为问题解决。

11.3 浏览器侧建议

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
    channelCount: 1,
  },
});


const track = stream.getAudioTracks()[0];
console.log("requested", track.getConstraints());
console.log("actual", track.getSettings());
console.log("capabilities", track.getCapabilities());

W3C Media Capture 规范定义了 echoCancellationnoiseSuppressionautoGainControl 和音频延迟等约束,但实际能力取决于浏览器、设备和平台。[15]

重点是检查 getSettings(),而不是只看请求参数。

11.4 监控 Echo 指标

WebRTC Stats 定义了:

echoReturnLoss
echoReturnLossEnhancement
audioLevel
totalAudioEnergy

同时还应记录:

far_end_audio_level
near_end_audio_level
double_talk_ratio
echo_suspected_interruptions
playback_device
input_device
headset / speakerphone

W3C 也定义了 Jitter Buffer、丢包和播放相关指标,可帮助区分网络播放延迟与 Turn Detection 延迟。[16]

11.5 AEC 不能替代说话对象判断

AEC 只能尝试移除系统播放的声音,不能解决:

  • 电视声;
  • 旁人说话;
  • 用户对同事说话;
  • 同房间另一台设备播放 Agent 声音。

这些仍需要 Addressee、Speaker 或语义判断。


12. 最小实现:可恢复的 Interruption Controller

下面的 Python 示例展示:

  • 两阶段打断;
  • Generation 隔离;
  • False Interruption 恢复;
  • 播放位置截断;
  • 语音取消与工具取消分离。

它是框架无关的控制逻辑,不包含具体 WebRTC、ASR 或 TTS SDK。

from __future__ import annotations


import asyncio
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Protocol




class TurnState(str, Enum):
    LISTENING = "listening"
    USER_SPEAKING = "user_speaking"
    POSSIBLE_END = "possible_end"
    THINKING = "thinking"
    AGENT_SPEAKING = "agent_speaking"
    POSSIBLE_INTERRUPT = "possible_interrupt"
    INTERRUPTED = "interrupted"




class Player(Protocol):
    async def duck(self, generation_id: int) -> None: ...
    async def pause(self, generation_id: int) -> None: ...
    async def resume(self, generation_id: int) -> None: ...
    async def clear(self, generation_id: int) -> None: ...
    async def played_ms(self, generation_id: int) -> int: ...




class Generator(Protocol):
    async def cancel(self, generation_id: int) -> None: ...




class History(Protocol):
    async def truncate_agent_turn(
        self,
        *,
        generation_id: int,
        played_ms: int,
    ) -> None: ...




@dataclass
class InterruptionCandidate:
    candidate_id: str
    generation_id: int
    speech_started_at_ms: int
    speech_duration_ms: int = 0
    stable_words: list[str] = field(default_factory=list)
    echo_probability: float = 0.0
    take_floor_probability: float = 0.0
    classified: bool = False




@dataclass
class SessionState:
    state: TurnState = TurnState.LISTENING
    generation_id: int = 0
    user_turn_id: int = 0
    transcript_version: int = 0
    candidate: InterruptionCandidate | None = None




class TurnController:
    def __init__(
        self,
        *,
        player: Player,
        generator: Generator,
        history: History,
        false_interruption_timeout_ms: int = 450,
    ) -> None:
        self.player = player
        self.generator = generator
        self.history = history
        self.false_interruption_timeout_ms = (
            false_interruption_timeout_ms
        )
        self.session = SessionState()
        self._lock = asyncio.Lock()
        self._confirm_task: asyncio.Task[None] | None = None


    async def on_agent_playout_started(
        self,
        generation_id: int,
    ) -> None:
        async with self._lock:
            self.session.generation_id = generation_id
            self.session.state = TurnState.AGENT_SPEAKING


    async def on_user_activity_started(
        self,
        *,
        candidate_id: str,
        at_ms: int,
        echo_probability: float,
    ) -> None:
        async with self._lock:
            if self.session.state != TurnState.AGENT_SPEAKING:
                self.session.state = TurnState.USER_SPEAKING
                return


            generation_id = self.session.generation_id
            candidate = InterruptionCandidate(
                candidate_id=candidate_id,
                generation_id=generation_id,
                speech_started_at_ms=at_ms,
                echo_probability=echo_probability,
            )
            self.session.candidate = candidate
            self.session.state = TurnState.POSSIBLE_INTERRUPT


            # 第一阶段:先礼貌地让出声道,但保留恢复能力。
            if echo_probability < 0.80:
                await self.player.duck(generation_id)


            self._cancel_confirmation_task()
            self._confirm_task = asyncio.create_task(
                self._confirm_after_timeout(candidate_id)
            )


    async def on_interruption_features(
        self,
        *,
        candidate_id: str,
        speech_duration_ms: int,
        stable_words: list[str],
        take_floor_probability: float,
        echo_probability: float,
    ) -> None:
        async with self._lock:
            candidate = self.session.candidate
            if (
                candidate is None
                or candidate.candidate_id != candidate_id
            ):
                return


            candidate.speech_duration_ms = speech_duration_ms
            candidate.stable_words = stable_words
            candidate.take_floor_probability = (
                take_floor_probability
            )
            candidate.echo_probability = echo_probability


            normalized = "".join(stable_words).lower()
            explicit_stop = any(
                word in normalized
                for word in ("停", "等一下", "别说", "stop", "hold on")
            )


            if explicit_stop:
                await self._commit_true_interruption(candidate)
                return


            if (
                speech_duration_ms >= 220
                and take_floor_probability >= 0.72
                and echo_probability < 0.65
            ):
                await self._commit_true_interruption(candidate)


    async def on_user_activity_stopped(
        self,
        *,
        candidate_id: str,
    ) -> None:
        async with self._lock:
            candidate = self.session.candidate
            if (
                candidate is None
                or candidate.candidate_id != candidate_id
                or candidate.classified
            ):
                return


            has_words = len(candidate.stable_words) > 0
            likely_false = (
                not has_words
                or candidate.echo_probability >= 0.80
                or candidate.take_floor_probability < 0.35
            )


            if likely_false:
                await self._resume_false_interruption(candidate)


    async def _confirm_after_timeout(
        self,
        candidate_id: str,
    ) -> None:
        await asyncio.sleep(
            self.false_interruption_timeout_ms / 1000
        )


        async with self._lock:
            candidate = self.session.candidate
            if (
                candidate is None
                or candidate.candidate_id != candidate_id
                or candidate.classified
            ):
                return


            if (
                candidate.take_floor_probability >= 0.60
                and candidate.echo_probability < 0.65
            ):
                await self._commit_true_interruption(candidate)
            else:
                await self._resume_false_interruption(candidate)


    async def _commit_true_interruption(
        self,
        candidate: InterruptionCandidate,
    ) -> None:
        candidate.classified = True
        generation_id = candidate.generation_id


        # 先清播放器,避免已经排队的音频继续出声。
        played_ms = await self.player.played_ms(generation_id)
        await self.player.clear(generation_id)


        # 再取消尚未完成的生成。工具事务需要独立判断。
        await self.generator.cancel(generation_id)


        # 历史只保留用户实际听到的部分。
        await self.history.truncate_agent_turn(
            generation_id=generation_id,
            played_ms=played_ms,
        )


        self.session.state = TurnState.INTERRUPTED
        self.session.user_turn_id += 1
        self.session.candidate = None
        self._cancel_confirmation_task()


    async def _resume_false_interruption(
        self,
        candidate: InterruptionCandidate,
    ) -> None:
        candidate.classified = True
        await self.player.resume(candidate.generation_id)
        self.session.state = TurnState.AGENT_SPEAKING
        self.session.candidate = None
        self._cancel_confirmation_task()


    def _cancel_confirmation_task(self) -> None:
        if self._confirm_task is not None:
            self._confirm_task.cancel()
            self._confirm_task = None

12.1 这段代码还需要补什么

生产版本还需要:

  • 单调递增事件序号,防止乱序;
  • turn_idgeneration_idtranscript_version 联合校验;
  • ASR Partial 的撤销和修订;
  • Player Duck 与 Pause 的不同策略;
  • TTS Segment 与音频时间映射;
  • WebRTC/SIP/WebSocket 不同的 Truncation 实现;
  • 多语言停止词;
  • 旁人说话和 Speaker ID;
  • 工具事务的可取消性与幂等;
  • Trace 和采样音频脱敏;
  • 超时、断网与客户端重连。

12.2 不要在锁内做长时间网络调用

示例为了展示逻辑简化了并发处理。真实实现应:

  1. 在锁内验证状态并生成 Action Plan;
  2. 释放锁;
  3. 并行执行播放器、模型和历史操作;
  4. 再用版本号提交状态。

否则一个慢播放器调用可能阻塞所有音频事件。


13. Open Mic、Push-to-talk 和混合模式怎么选

模式优点缺点适合场景
Open Mic + 自动 Turn Detection最自然,可随时打断回声、噪声和误判最复杂客服、助手、陪伴、车载
Push-to-talk边界明确,几乎不依赖 VAD用户操作成本高,不像自然对话专业工具、嘈杂环境、高风险任务
Manual Activity Start/End应用掌控切分,可接自有 VAD需要可靠客户端状态长口述、会议、定制设备
Hybrid默认自动,必要时按键接管产品逻辑更复杂通用桌面端、移动端、企业应用

OpenAI 当前文档指出,关闭 VAD 后使用 Push-to-talk 可以避免 VAD 失败,并且因为不等待 VAD Timeout,实际体验可能很快。[5] Gemini Live 也允许关闭自动活动检测,改用 activityStart / activityEnd。[12]

13.1 建议保留用户可见的手动接管

即使默认 Open Mic,也可以提供:

  • 按住说话;
  • 点击停止;
  • 键盘空格打断;
  • “长口述模式”;
  • “不要自动打断我”。

这不是承认技术失败,而是给高噪声和特殊表达方式提供稳定逃生路径。

13.2 高风险操作不要只依赖自然轮次

对于:

支付
删除
发送邮件
提交工单
修改权限

在最终写操作前仍应使用显式确认状态。Turn Detection 决定谁说话,不应该替代业务审批。


14. 如何评测打断和 Turn Detection

14.1 只测 Endpoint Latency 不够

一个激进系统可以很快,但会频繁抢话。至少要同时测:

endpoint_latency_ms
false_end_rate
missed_end_rate
user_cutoff_rate
turn_resumed_rate

14.2 打断指标

speech_start_detection_ms
barge_in_duck_ms
barge_in_stop_ms
true_interruption_recall
false_interruption_rate
missed_interruption_rate
resume_false_interruption_rate
resume_success_rate

定义示例:

barge_in_stop_ms =
client_agent_audio_stopped_at
- user_interruption_audio_started_at

终点必须是客户端真正停止播放,而不是服务端收到 Cancel。

14.3 行为指标

backchannel_hold_rate
side_speech_ignore_rate
background_speech_ignore_rate
explicit_stop_success_rate
correction_capture_rate
self_echo_interruption_rate

14.4 一致性指标

history_playout_mismatch_rate
stale_generation_audio_rate
late_event_apply_rate
uncancelled_tts_audio_ms
wrong_tool_cancel_rate

14.5 成本指标

speculative_llm_cancel_rate
wasted_llm_tokens
wasted_tts_audio_ms
turn_model_inference_ms
interruption_classifier_cost
sampled_audio_storage_bytes

14.6 推荐时间戳

user_audio_onset_at
vad_speech_started_at
asr_first_partial_at
interruption_candidate_at
player_ducked_at
stable_word_first_at
interruption_confirmed_at
player_cleared_at
model_cancelled_at
history_truncated_at
false_interruption_declared_at
player_resumed_at
user_audio_offset_at
eou_candidate_at
eou_confirmed_at

14.7 测试场景矩阵

场景期望行为
用户说“停”快速停止并进入用户轮次
用户说“不是 A,是 B”停止、保留纠正、更新对象
用户短促“嗯嗯”Agent 继续或轻量 Duck 后恢复
用户停顿 800ms 后继续不抢话,或 Eager 后正确取消
用户对旁人说话不改变当前任务
背景电视出现人声不打断
咳嗽、笑声、清嗓短暂暂停后恢复或直接忽略
扬声器回声不自我打断
用户与 Agent 同时起句快速协商话权
Agent 正在调用只读工具可以停止说话,工具可继续
Agent 已提交写操作停止说话但读取最终业务状态
网络抖动导致播放队列变长仍按真实 Playout Cursor 截断

Full-Duplex-Bench 将 Pause Handling、Backchannel、Turn-taking 与 Interruption Management 纳入统一测试;v1.5 又细化重叠场景和 Stop/Response Latency。[17][18] 2026 年的 v3 则进一步加入真实人类口语中的自我修正、犹豫等 Disfluency 和多步工具任务。[19]

同年发布的 τ-Voice 把全双工语音交互、领域策略和可验证工具任务放进同一评测环境。论文报告中,受测 Voice Agent 在真实噪声和多样口音条件下只保留了文本 Agent 约 30%–45% 的任务能力,且多数失败被归因为 Agent 行为而非单纯音频质量。[24] 这个结果不应被外推为所有系统的统一比例,但它提醒我们:

Turn-taking 看起来自然
≠ 业务任务可靠完成

工具调用完成
≠ 用户听到了正确确认

单轮低延迟
≠ 多轮状态没有被打断破坏

因此,Voice Agent 的评测需要把对话动作、业务状态、工具副作用和声音时序放在同一条 Trace 中。

14.8 音频测试不能只用合成 TTS

评测集应覆盖:

真实用户录音
不同麦克风
手机免提
耳机
车载 Bluetooth
电话 8kHz
办公室噪声
街道噪声
中文口头语
中英混说
方言和口音

合成音频适合可重复回归,但不能替代真实设备的 AEC、噪声和韵律分布。


15. 最常见的生产失败模式

失败模式表现根因修复方向
VAD 即打断“嗯嗯”也让 Agent 永久停下没有确认状态两阶段 Pause/Confirm
停得太慢用户说了半句,Agent 仍在播等完整 ASR;队列太长声学 Fast Path;缩小缓冲
Agent 自我打断TTS 一播放就触发 VADAEC/路由失败Far-end reference;Echo 指标
用户被频繁抢话思考停顿被当成结束固定短静音Semantic EOU;动态阈值
Agent 总是迟钝每轮都等很长静音固定长阈值Eager EOT;场景参数
假打断后不恢复咳嗽一次,Agent 永久沉默只有 Clear,没有 PauseFalse Interruption Resume
恢复位置错误Agent 重复或跳过内容播放器无 CursorGeneration-aware Player
历史包含未听内容后续引用用户没听到的话按生成文本提交历史按 Playout Truncate
旧音频突然出现新轮开始后旧 TTS 进到没校验 generation_id端到端 Generation 隔离
旁人改变了任务用户对同事说话被当命令无 Addressee 判断Speaker / 方向 / 语义
工具被错误取消用户打断语音导致写事务中断把语音和业务取消合并独立 Operation State
指标看起来很好服务端停了,客户端仍播终点取 response.cancel测 Client Playout Stop
只在安静办公室测试上线后车载和免提崩溃测试分布单一真实设备与噪声回归

16. 推荐的调参与上线流程

16.1 先做可解释基线

第一版建议:

VAD
+ 固定保守 EOU
+ 明确停止词
+ Pause/Confirm 打断
+ False Interruption Resume
+ 播放 Cursor 与 Truncation

先保证:

  • 不自我打断;
  • 不丢历史;
  • 能正确取消;
  • Trace 完整。

不要一开始就同时上多个语义模型和自适应策略,否则失败难以归因。

16.2 按失败簇优化

收集并标注:

false_end
missed_end
false_interruption
missed_interruption
backchannel_error
side_speech_error
echo_error
history_error

每个失败保留:

  • 脱敏音频;
  • VAD 概率;
  • ASR Partial;
  • Turn Model 输出;
  • 播放状态;
  • 设备和环境;
  • 最终人工标签。

16.3 参数按 Slice 管理

profile: zh_open_mic_desktop
vad:
  start_threshold: 0.62
  min_speech_ms: 120
endpoint:
  min_silence_ms: 260
  max_silence_ms: 950
turn:
  semantic_threshold: 0.74
interruption:
  duck_immediately: true
  min_words: 1
  false_timeout_ms: 450
  explicit_stop_fast_path: true

这只是配置形式示例,不是通用推荐值。

至少按以下 Slice 分开评估:

语言
电话 / WebRTC
耳机 / 免提
安静 / 噪声
短命令 / 长口述
工具轮次 / 非工具轮次
新用户 / 高频用户

16.4 用 Shadow 比较,不直接全量切换

新 Turn Model 上线时,可以:

  1. 旧策略继续控制生产行为;
  2. 新模型在 Shadow 中输出决策;
  3. 比较两者与人工标签;
  4. 只在低风险流量 Canary;
  5. 观察抢话、误打断和成本;
  6. 支持按策略版本回滚。

16.5 安全阻断项不能靠平均分抵消

以下指标应是硬门槛:

错误执行写操作 == 0
跨用户/跨租户指令响应 == 0
明确“停”却继续长时间播放 == 0
历史提交未播放敏感内容 == 0

17. 级联 Turn Control 与原生全双工模型怎么选

维度级联 VAD + ASR + Turn Model原生全双工 Speech Model
可解释性高,可看到每层事件内部行为更黑盒
工具与业务状态容易集成依赖模型与 API 能力
延迟下限有多阶段开销理论上更低
组件替换容易模型耦合强
打断自然度需要大量工程模型可能原生学习重叠
历史与事务治理控制面清晰仍需外部状态机
私有部署有更多选择取决于开放模型
评测难度每层可测需要黑盒实时评测

原生全双工模型可以同时听和说,并直接学习何时暂停、回应或继续。Full-Duplex-Bench、DualTurn、BayLing-Duplex 等工作都在推动这一方向。[17][22][23]

但即使使用原生 Speech-to-Speech 模型,生产系统仍需要:

  • 播放队列控制;
  • 工具事务状态;
  • 用户权限;
  • 真实 Playout Cursor;
  • 历史截断;
  • Trace 和安全门槛。

原生模型可以替代部分 Turn Detection,但不能替代业务控制面。


18. 实践检查清单

概念与状态

  • VAD、Endpointing、EOU、Turn Detection 和 Interruption 已分别定义。
  • 系统存在 POSSIBLE_INTERRUPT,而不是声音一来就永久终止。
  • Pause、Duck、Clear、Cancel 和 Truncate 是不同动作。
  • 所有事件带 turn_idgeneration_id 和序号。
  • 迟到的旧 Generation 事件会被丢弃。

打断

  • 显式停止词走快速路径。
  • Backchannel 不会默认抢占话权。
  • 咳嗽、笑声和短噪声可以恢复。
  • 旁人说话和背景媒体有独立测试。
  • 用户继续上一句时能触发 Turn Resumed 或等价取消。

Endpointing

  • 固定静音只是保底,不是唯一依据。
  • 长口述、号码、地址等场景有单独策略。
  • 评估 false-end 和 missed-end,而不只看延迟。
  • 推测执行有取消和成本预算。
  • 参数按语言、设备和场景分 Slice 管理。

播放与历史

  • Player 按 Generation 管理队列。
  • 客户端能上报实际 played_ms
  • 打断时先停止本地播放,再取消服务端。
  • 历史只保留用户实际听到的部分。
  • TTS 文本与音频 Segment 有可追踪映射。

音频链路

  • AEC 使用真实 Far-end Reference。
  • 检查 getSettings(),而不只看请求约束。
  • 监控 Echo、设备切换和 Double Talk。
  • 耳机、免提、Bluetooth 和电话分别测试。
  • 音频输入和播放器使用可比较的时钟。

工具与业务

  • 语音取消和工具事务取消分开。
  • 每个 Tool Operation 声明是否可取消、是否有副作用。
  • 写操作使用幂等键和最终状态回验。
  • 高风险写操作仍需显式确认。
  • 用户打断后不会让旧工具结果污染新轮次。

评测与发布

  • 测试集包含真实打断、Backchannel、旁人、背景声和回声。
  • barge_in_stop_ms 以客户端停止播放为终点。
  • 同时报告误打断率、漏打断率和恢复成功率。
  • 生产失败音频经过脱敏后回流评测集。
  • 新策略先 Shadow、再 Canary,并支持回滚。

总结

实时语音中的 Turn Control 不是一个 VAD 参数,而是一套跨音频、模型、播放器、工具和会话历史的实时状态机。

生产系统需要做到:

  1. 用 VAD 发现语音活动,但不让 VAD 独自决定话权;
  2. 用声学、ASR、语义、Addressee 和业务状态共同判断轮次;
  3. 对打断采用“快速暂停、随后确认、误判恢复”的两阶段策略;
  4. 将 Backchannel、旁人说话、环境声和回声与真实打断分开;
  5. 使用 Generation-aware Player,记录实际播放位置;
  6. 在打断后同时处理模型取消、音频清理和历史截断;
  7. 将语音响应取消与业务工具事务分开;
  8. 用真实重叠场景评估停止延迟、误打断、恢复和一致性。

最终,真正自然的 Voice Agent 并不是永远最快说话的系统,而是能够在不确定的重叠中做出正确社交动作的系统:知道什么时候接话,什么时候等待,什么时候继续,也知道什么时候必须立即闭嘴。


参考资料

  1. Stivers et al., Universals and cultural variation in turn-taking in conversation
    https://www.mpi.nl/publications/item66202/universals-and-cultural-variation-turn-taking-conversation

  2. Ekstedt and Skantze, TurnGPT: a Transformer-based Language Model for Predicting Turn-taking in Spoken Dialog
    https://aclanthology.org/2020.findings-emnlp.268/

  3. Jiang, Ekstedt and Skantze, Response-conditioned Turn-taking Prediction
    https://arxiv.org/abs/2305.02036

  4. OpenAI, Realtime API — Voice activity detection
    https://developers.openai.com/api/docs/guides/realtime-vad

  5. OpenAI, Realtime conversations — interruptions, truncation and push-to-talk
    https://developers.openai.com/api/docs/guides/realtime-conversations

  6. Deepgram, Understanding the Flux State Machine
    https://developers.deepgram.com/docs/flux/state

  7. Deepgram, Optimize Voice Agent Latency with Eager End of Turn
    https://developers.deepgram.com/docs/flux/voice-agent-eager-eot

  8. Deepgram, Migrating from Nova-3 to Flux / Flux Quickstart
    https://developers.deepgram.com/docs/flux/nova-3-migration
    https://developers.deepgram.com/docs/flux/quickstart

  9. LiveKit, Turns overview
    https://docs.livekit.io/agents/logic/turns/

  10. LiveKit, Adaptive interruption handling
    https://docs.livekit.io/agents/logic/turns/adaptive-interruption-handling/

  11. Google AI for Developers, Live API capabilities guide
    https://ai.google.dev/gemini-api/docs/live-api/capabilities

  12. Google AI for Developers, Live API WebSockets reference
    https://ai.google.dev/api/live

  13. Pipecat, Smart Turn v3.2
    https://github.com/pipecat-ai/smart-turn

  14. Silero, Silero VAD
    https://github.com/snakers4/silero-vad

  15. W3C, Media Capture and Streams
    https://www.w3.org/TR/mediacapture-streams/

  16. W3C, Identifiers for WebRTC’s Statistics API
    https://www.w3.org/TR/webrtc-stats/

  17. Lin et al., Full-Duplex-Bench: A Benchmark to Evaluate Full-duplex Spoken Dialogue Models on Turn-taking Capabilities
    https://arxiv.org/abs/2503.04721

  18. Lin et al., Full-Duplex-Bench v1.5: Evaluating Overlap Handling for Full-Duplex Speech Models
    https://arxiv.org/abs/2507.23159

  19. Lin et al., Full-Duplex-Bench-v3: Benchmarking Tool Use for Full-Duplex Voice Agents Under Real-World Disfluency
    https://arxiv.org/abs/2604.04847

  20. Lin et al., Predicting Turn-Taking and Backchannel in Human-Machine Conversations Using Linguistic, Acoustic, and Visual Signals
    https://arxiv.org/abs/2505.12654

  21. Yang et al., JAL-Turn: Joint Acoustic-Linguistic Modeling for Real-Time and Robust Turn-Taking Detection
    https://arxiv.org/abs/2603.26515

  22. Rajaa, DualTurn: Learning Turn-Taking from Dual-Channel Generative Speech Pretraining
    https://arxiv.org/abs/2603.08216

  23. BayLing-Duplex: Native Full-Duplex Speech Dialogue with a Single Autoregressive LLM
    https://arxiv.org/abs/2606.14528

  24. Ray et al., τ-Voice: Benchmarking Full-Duplex Voice Agents on Real-World Domains
    https://arxiv.org/abs/2603.13686


图片清单

1. 原创封面图

  • 文件名:voice-agent-interruption-turn-detection-cover.png
  • 比例:16
  • 用途:文章封面与社交分享图
  • Alt:用户与语音 Agent 的两条声波在重叠区域经过判断后分别形成继续、暂停和让出话权的路径
  • 生成提示词:
A refined editorial illustration for a technical article about interruption handling, voice activity detection and turn-taking in realtime voice agents. Two distinct human and synthetic voice waveforms overlap in the center, where a precise decision system separates genuine interruption, backchannel, background speech and echo into different elegant paths. Include subtle playback cursor, pause, resume and cancellation motifs without text or literal UI labels. Deep navy, warm white, restrained cyan and amber accents, modern technical magazine style, spacious 16:9 composition, no text, no letters, no numbers, no logo, no watermark.

2. Turn Control 总体架构图

  • 文件名:voice-turn-control-architecture.mmd
  • 用途:解释客户端音频处理、VAD/ASR/Turn Model、Orchestrator、Agent 和可观测性边界
  • Alt:支持打断恢复与播放历史一致性的实时语音 Turn Control 架构
  • 形式:正文第 5 节 Mermaid Flowchart

3. Turn 与打断状态机

  • 文件名:voice-turn-interruption-state-machine.mmd
  • 用途:解释 User Speaking、Possible End、Agent Speaking、Possible Interrupt 和 False Interruption Resume
  • Alt:实时语音 Agent 在轮次结束和用户打断之间切换的状态机
  • 形式:正文第 6 节 Mermaid State Diagram

4. 打断一致性时序图

  • 文件名:voice-interruption-truncation-sequence.mmd
  • 用途:后续转成博客图时展示用户发声、暂停播放器、确认打断、清队列、取消模型和截断历史
  • Alt:用户打断 Agent 后,从客户端停止播放到会话历史截断的完整时序
  • 建议 Mermaid:

讨论

继续讨论这篇笔记

有问题、补充案例或不同观点,可以通过 GitHub Discussions 继续交流。

On this page

实时语音中的打断、VAD 和 Turn Detection摘要读者将学到什么1. 低延迟之外,Voice Agent 还需要正确地“让出话权”2. 先把容易混淆的概念分开2.1 VAD:现在是否存在语音活动2.2 Endpointing:一段语音是否可以切片提交2.3 End-of-Utterance:用户这一段表达是否结束2.4 Turn Detection:谁应该拥有下一段话权2.5 Barge-in / Interruption:用户在 Agent 发言时尝试改变话权2.6 Backchannel:用户发声,但不想接管完整话轮2.7 Full Duplex:能同时听和说,不等于会处理重叠2.8 概念对照表3. “检测到用户声音”只是打断判断的第一步3.1 明确停止命令3.2 内容纠正3.3 新问题或新意图3.4 Backchannel3.5 用户继续上一轮3.6 旁人说话与背景媒体3.7 回声和 Agent 自触发3.8 非语言声音4. 打断判断需要哪些信号4.1 声学信号4.2 ASR 信号4.3 语义信号4.4 会话状态4.5 业务槽位与预期输入4.6 Addressee 和说话人信号4.7 推荐输出不是布尔值,而是动作分布5. 一套生产级 Turn Control 架构5.1 播放器必须理解 generation_id5.2 Turn Orchestrator 是唯一状态决策者5.3 历史提交必须依赖实际播放进度6. 推荐的状态机6.1 POSSIBLE_INTERRUPT 是最重要的中间状态6.2 暂停、停止和取消不是同一个动作7. 两阶段打断:先快速让出,再确认意图7.1 第一阶段:Acoustic Fast Path7.2 第二阶段:Semantic Confirmation7.3 显式停止词走超快速路径7.4 Backchannel 可以不打断,也可以轻量确认7.5 不确定时优先可恢复动作8. Turn Detection:从固定静音到语义与预测8.1 固定静音阈值适合作为保底8.2 Semantic EOU8.3 Eager End-of-Turn 与可撤销推测8.4 本地 Turn Model8.5 联合声学与语言信号8.6 动态 Endpointing9. 厂商 API 体现的是不同控制边界9.1 OpenAI Realtime9.2 Gemini Live9.3 LiveKit Agents9.4 Deepgram Flux9.5 自建级联10. 打断后的核心问题:生成、发送、播放和历史不一致10.1 必须维护 Audio-to-Text Alignment10.2 WebSocket 客户端必须主动截断10.3 取消必须传播到所有层10.4 语音打断不一定意味着取消业务工具11. AEC 不是音质优化,而是打断系统的前提11.1 为什么回声会破坏状态机11.2 AEC 需要准确的 Far-end Reference11.3 浏览器侧建议11.4 监控 Echo 指标11.5 AEC 不能替代说话对象判断12. 最小实现:可恢复的 Interruption Controller12.1 这段代码还需要补什么12.2 不要在锁内做长时间网络调用13. Open Mic、Push-to-talk 和混合模式怎么选13.1 建议保留用户可见的手动接管13.2 高风险操作不要只依赖自然轮次14. 如何评测打断和 Turn Detection14.1 只测 Endpoint Latency 不够14.2 打断指标14.3 行为指标14.4 一致性指标14.5 成本指标14.6 推荐时间戳14.7 测试场景矩阵14.8 音频测试不能只用合成 TTS15. 最常见的生产失败模式16. 推荐的调参与上线流程16.1 先做可解释基线16.2 按失败簇优化16.3 参数按 Slice 管理16.4 用 Shadow 比较,不直接全量切换16.5 安全阻断项不能靠平均分抵消17. 级联 Turn Control 与原生全双工模型怎么选18. 实践检查清单概念与状态打断Endpointing播放与历史音频链路工具与业务评测与发布总结参考资料图片清单1. 原创封面图2. Turn Control 总体架构图3. Turn 与打断状态机4. 打断一致性时序图