文章

实时语音 Agent 生产实战:用 Turn Detection、Two-Pass EOU 与 Barge-in 压缩对话空白时间

实时语音 Agent 的卡顿往往不只是模型慢,真正的空白还来自端点检测、停顿确认、音频缓冲与打断处理。本文结合 LiveKit、NVIDIA Riva 与 Deepgram,讲解 Turn Detection、Two-Pass EOU 与 Barge-in 的生产治理方法,并给出分段延迟指标与落地调优顺序,帮助你压缩用户感知的对话空白。

实时语音 Agent 生产实战:用 Turn Detection、Two-Pass EOU 与 Barge-in 压缩对话空白时间

文本聊天里,100 毫秒级的差异通常不明显;语音对话不同。人类会把”对方说完以后多久开始回答”直接当成系统是否自然的一部分。

对一个典型的级联语音 Agent,可以把用户感知的首响应空白近似拆成:

T_gap ≈ T_endpoint + T_stt_finalize + T_llm_first_token + T_tts_first_audio + T_playback_buffer

很多团队只优化 T_llm_first_token,却留下了 500ms、800ms 甚至更长的静音确认窗口。结果是模型很快,产品听起来仍然迟钝。

真正的生产优化应该先回答三个问题:

  1. 什么时候可以认为用户这一轮真的说完了?
  2. 能不能在最终确认之前就提前启动下游计算?
  3. 用户在系统说话时重新开口,如何在几十到几百毫秒内让整个流水线一致地停下来?

这分别对应 Turn DetectionTwo-Pass EOUBarge-in

核心原理一:Turn Detection 不是简单的静音计时器

VAD、STT Endpointing 和语义 Turn Detector 的区别

最简单的做法是 Voice Activity Detection:检测到连续一段静音,就判定用户结束发言。它成本低、响应快,但有一个天然缺陷:停顿不等于说完

用户可能只是思考、换气,或者说出”我想查一下……嗯……上个月的账单”。如果系统在第一个停顿就抢答,体验反而比多等 200ms 更差。

生产系统通常存在三类方案:

方案特点适用场景
VAD-only成本低、响应快,但易误判停顿追求极低延迟、语言覆盖优先
STT Endpointing依赖 ASR 的 end-of-utterance 信号常规对话流
语义 / 音频 Turn Detector结合声学甚至语义信息判断是否完成多数语音 Agent 的推荐方案

LiveKit 当前文档明确区分了这些模式,并建议多数语音 Agent 使用 Turn Detector;其默认 endpointing 窗口在普通配置下为 0.5s ~ 3.0s,使用 audio turn detector 时默认可缩短到 0.3s ~ 2.5s。这些数字不是所有系统的最佳值,但说明了一个重要原则:结束判定需要下限,也需要兜底上限。

生产上不要只调一个 silence_ms

比固定静音阈值更可靠的配置方式,是把参数拆开:

turn_detection:
  mode: semantic_audio
  min_endpoint_delay_ms: 300
  max_endpoint_delay_ms: 2500
  vad_min_silence_ms: 300
  force_commit_timeout_ms: 3000

这里的关键不是具体数字,而是四个边界:

  • 太短,容易 false end-of-turn
  • 太长,增加首响应空白;
  • 必须允许模型或 STT 提前给出高置信度结束信号;
  • 必须有最大等待时间,不能让异常会话无限悬挂。

核心原理二:Two-Pass EOU 用”可取消的提前计算”换延迟

为什么需要两阶段结束判定

如果系统一定要等到”最终确认用户说完”以后才调用 LLM,就会天然损失整段确认窗口。

Two-Pass End of Utterance 的思路是:

  1. 第一阶段出现较短静音,产生 early EOU
  2. 立即把当前 transcript 发给 LLM,开始推理;
  3. ASR 状态继续保留;
  4. 如果用户继续讲话,取消旧 LLM/TTS 任务,用新的 transcript 重启;
  5. 到达最终 EOU 后,当前轮次才正式提交。

NVIDIA Riva 的当前文档已经提供 Two-Pass EOU。其示例中,第一阶段 stop_history_eou 可设置在较短窗口,官方文档提到基于其测试建议值约为 240ms;最终 EOU 仍按更长窗口确认。这个机制本质上和 CPU 的 speculative execution 很像:先做可能有用的计算,但必须接受计算被作废。

不能只看首响应时间,还要看推理重启成本

提前触发一定会引入额外成本。用户一句话中如果有多次自然停顿,可能出现:

240ms silence -> LLM request A
user resumes  -> cancel A
240ms silence -> LLM request B
user resumes  -> cancel B
final EOU     -> LLM request C

因此 Two-Pass EOU 的评估指标至少要包含:

  • early_eou_trigger_rate
  • llm_restart_rate
  • wasted_output_tokens
  • cancel_latency_ms
  • first_agent_audio_after_final_speech_ms

NVIDIA ACE 的示例文档也特别提醒,提前 EOU 可能造成额外 LLM 调用和计算成本。生产环境不能只展示”首响降低了多少”,而隐藏了 GPU 成本放大。

核心原理三:Barge-in 是全链路取消,不是播放器暂停

用户重新开口时,系统真正要取消什么

很多 Demo 的 Barge-in 只做一件事:停止扬声器播放。这还不够。用户一旦开始插话,至少需要同步处理:

Speech start detected
  |--> stop client playback buffer
  |--> cancel TTS streaming
  |--> cancel / suppress current LLM generation
  |--> mark assistant turn as interrupted
  |--> truncate conversation history to actually heard audio
  |--> open new user turn

如果只停止播放器,LLM 和 TTS 后台仍然会继续消耗资源;更严重的是,系统可能把”用户根本没有听到的后半段回答”写入对话历史,导致下一轮模型误以为这些内容已经沟通过。

LiveKit 的 interruption 机制会在用户打断后停止 Agent Speech,并自动把历史截断到用户实际听到的部分。Deepgram 的 Twilio 示例则强调,服务端停止生成后,客户端仍需要清空 Twilio 已缓冲但尚未播放的音频,否则会出现”用户已经打断,旧声音还继续播放”的尾巴。

Barge-in 要有 generation id

推荐给每一轮生成分配单调递增的 generation_id

interface VoiceTurn {
  turnId: string;
  generationId: number;
  state: "listening" | "thinking" | "speaking" | "interrupted" | "done";
}

function onAudioChunk(chunk: AudioChunk, generationId: number) {
  if (generationId !== session.activeGenerationId) return;
  playback.enqueue(chunk);
}

这样即使取消信号和网络包乱序,旧 TTS 音频也不会在新一轮会话里”复活”。

工程落地:把语音会话建模成状态机

最容易出问题的实现,是把 ASR、LLM、TTS 三个 WebSocket 当成三个独立回调系统。生产版本应该显式维护会话状态:

LISTENING
  | early EOU
  v
SPECULATIVE_THINKING
  | final EOU
  +--------------------+
  |                    |
  | user resumes       v
  +----> CANCELLED <-- THINKING
                          |
                          v
                       SPEAKING
                          | user barge-in
          +---------------+-------------+
          |                             |
          v                             v
     INTERRUPTED                     IDLE

状态机至少负责:

  • 哪个 transcript 是 provisional,哪个是 final;
  • 当前允许哪个 generation 输出音频;
  • 什么事件触发取消;
  • 取消以后哪些 buffer 必须清空;
  • 对话历史最终提交到哪个字符 / 时间戳;
  • 网络重连后是否允许旧事件继续生效。

建议的生产延迟指标

不要只记录一个 voice_latency。至少按阶段拆分:

指标含义
speech_end_to_turn_commit_ms用户最后有效语音到本轮提交
turn_commit_to_llm_first_token_msTurn commit 到 LLM 首 token
llm_first_token_to_tts_first_audio_ms首 token 到首个可播放音频
speech_end_to_agent_audio_ms用户结束说话到听见 Agent 的总空白
barge_in_detect_ms用户重新开口到系统识别打断
barge_in_silence_ms打断信号到旧音频真正停止
false_end_rate误判用户说完的比例
restart_rate提前推理后被取消重启的比例
wasted_tokens_per_turn因取消浪费的 LLM 输出 token

这里最值得盯的是 speech_end_to_agent_audio_ms 的 P95/P99。平均值很容易被大量短句掩盖,而真正让用户觉得”这个机器人不自然”的往往是少数长停顿。

一个可落地的调优顺序

第一阶段:先建立真实分段指标

先把 ASR、LLM、TTS、播放缓冲全部打时间戳,不要先改参数。否则只能靠主观听感优化。

第二阶段:压缩 Endpointing,但同步监控 false end

逐步缩短最小 endpoint delay,并按语言、口音、场景分桶观察:

  • 短指令;
  • 长句;
  • 犹豫和停顿;
  • 电话 8kHz 音频;
  • 嘈杂环境;
  • 中文夹英文、数字和专有名词。

任何”延迟下降”都必须同时看 false_end_rate

第三阶段:再上 Two-Pass EOU

只有在取消机制可靠后,才允许 early EOU 触发 LLM。否则一个错误的提前判定会产生重复回答、旧音频串流和会话历史错乱。

第四阶段:最后优化 Barge-in

Barge-in 的目标不是检测越敏感越好,而是:

  • 真插话能迅速停止;
  • 咳嗽、键盘声、电视声不频繁误触发;
  • 后台生成同步取消;
  • 用户下一轮上下文保持一致。

Deepgram 当前文档也提醒,纯客户端能量型 VAD 容易被环境噪音触发;其建议在适用场景下使用模型级 speech/turn detection,以降低误打断。

适用场景

这套方法尤其适合:

  • 电话客服 Agent:用户对长静音极其敏感,并且经常主动插话。
  • 销售与外呼 Agent:自然轮次和打断能力直接影响对话完成率。
  • 车载 / 设备语音助手:噪音高,固定静音阈值容易误判。
  • 实时翻译与会议助手:需要尽早开始处理,但又不能过早提交错误分句。
  • 游戏 NPC / 陪伴式语音应用:对节奏、抢话和回话时机要求更高。

对于离线录音转写、批量语音摘要等场景,则没必要承担复杂的 speculative turn control。

常见误区

误区一:把 VAD 参数调到极小就能得到最低延迟。 阈值过小会把正常思考停顿判成句号。最终结果往往是首响更快,但抢话更多、重启更多。

误区二:只测 LLM TTFT。 TTFT 很重要,但语音体验还包含 endpointing 和 TTS 首音频。一个 TTFT 150ms 的系统,也可能因为 800ms 静音确认而显得迟钝。

误区三:Two-Pass EOU 是免费优化。 提前计算可能被取消。必须把无效请求和浪费 token 计入容量规划。

误区四:Barge-in 等于 mute。 真正的 Barge-in 必须联动播放缓冲、TTS、LLM 和 conversation state。

误区五:所有语言共用一套 Endpointing。 语言节奏、停顿习惯、STT 能力不同。应至少按语言和音频通道做分桶配置与验证。

上线检查

  • 已记录 speech-end、turn-commit、LLM-first-token、TTS-first-audio、playback-start 时间戳。
  • 已建立 speech_end_to_agent_audio_ms 的 P50/P95/P99。
  • 已测试长停顿、犹豫、噪音、重口音和低采样率音频。
  • 已统计 false-end 和 premature-response 比例。
  • Two-Pass EOU 的提前推理可以在用户继续说话时可靠取消。
  • 取消后旧 generation 的 token 和音频都不会重新进入输出流。
  • Barge-in 后播放器缓冲会同步清空。
  • 对话历史只保留用户实际听到的 Agent 内容。
  • 已监控 speculative restart 带来的额外 token / GPU 成本。
  • Endpointing 参数按语言、音频通道或业务场景支持独立配置。
  • 已准备关闭 early EOU、退回 final-only 模式的快速开关。

参考资料

  1. LiveKit, Turns overview — https://docs.livekit.io/agents/logic/turns/
  2. LiveKit, Turn detector — https://docs.livekit.io/agents/logic/turns/turn-detector/
  3. LiveKit, Adaptive interruption handling — https://docs.livekit.io/agents/logic/turns/adaptive-interruption-handling/
  4. NVIDIA Riva, ASR Overview — https://docs.nvidia.com/deeplearning/riva/user-guide/docs/asr/asr-overview.html
  5. NVIDIA Riva, Building and Deploying ASR Pipelines — https://docs.nvidia.com/deeplearning/riva/user-guide/docs/public/asr/asr-pipeline-configuration.html
  6. Deepgram, Build a Flux-enabled Voice Agent — https://developers.deepgram.com/docs/flux/agent
  7. Deepgram, Audio Preprocessing & Barge-In — https://developers.deepgram.com/voice-agent/optimize/audio-preprocessing-barge-in
  8. Deepgram, Flux Quickstart — https://developers.deepgram.com/docs/flux/quickstart

常见问题

语音 Agent 延迟低,为什么用户仍然觉得卡?
因为用户感知的是上一轮说话结束到下一轮声音出现之间的整段空白,其中包含端点检测、ASR 最终确认、LLM 首 token、TTS 首音频和播放缓冲,而不是单一模型延迟。
Two-Pass EOU 会不会导致重复调用 LLM?
会。第一阶段可以提前触发下游推理,但如果用户继续说话,就需要取消旧推理并重新触发,因此必须同时监控重启率、浪费 token 和实际端到端收益。
Barge-in 只要停止 TTS 播放就够了吗?
不够。生产系统还要取消仍在生成的 LLM/TTS 任务、清空下游播放缓冲,并把对话历史截断到用户真正听到的位置,避免下一轮上下文与真实听感不一致。
语音 Agent 优化最先应该看哪个指标?
优先看用户最后有效语音到 Agent 首个可播放音频之间的 P95/P99,它最接近用户实际感知的对话空白,之后再按 endpointing、LLM、TTS 和播放缓冲拆解。