Skip to content

实时语音 Agent 怎么做:延迟、打断和状态一致性

第一次把 ASR、LLM 和 TTS 串起来,通常很快就能听到一句完整回复。但“能说话”和“能对话”之间,还隔着延迟、抢话、打断、弱网、回声和工具状态这一整套工程问题。

用户不会关心后面接了几个模型,只会感受到它是不是停顿太久、自己插话后旧声音还在播、口头说“已经完成”但设备毫无变化。

  • 先做稳定的半双工,再追求全双工;基本轮次都不稳时,真人音色没有意义。
  • 每段链路都记录时间戳和 turn ID,否则只能知道“慢”,不知道慢在哪里。
  • 打断必须同时停止生成、播放和待执行动作,不能只把播放器静音。
  • 工具执行结果要和语音、屏幕、设备状态一致,不能让模型自己宣布成功。
flowchart LR
  MIC["麦克风"] --> PRE["降噪 / AEC / 重采样"]
  PRE --> VAD["VAD / Turn Detection"]
  VAD --> ASR["Streaming ASR"]
  ASR --> AGENT["Agent Runtime"]
  AGENT --> TOOL["Tools"]
  TOOL --> AGENT
  AGENT --> TTS["Streaming TTS"]
  TTS --> PLAY["播放 / 设备"]
  PLAY -. "打断反馈" .-> VAD

每一段都应独立记录时间戳,否则只能知道“很慢”,不知道慢在哪里。

一次自然回应可以拆成:

阶段指标
用户停顿到 turn 结束endpointing latency
最后一帧到 ASR finalASR finalize latency
Agent 收到文本到首个输出 tokenmodel time-to-first-token
工具调用tool latency
文本到首个音频包TTS time-to-first-audio
网络和缓冲transport / jitter buffer

真正影响体感的是:

用户结束说话
→ 第一段可听回复到达

不要只记录模型总耗时。

VAD 判断“有没有人在说话”,Turn Detection 判断“一轮话是否说完”。两者不能完全等同。

停顿过短:

  • 用户思考时被抢话;
  • 中文逗号停顿被当作结束;
  • ASR 只得到半句话。

停顿过长:

  • Agent 迟迟不回应;
  • 用户以为系统没听见;
  • 对话节奏拖沓。

推荐组合:

  • 音频 VAD 提供低延迟信号;
  • ASR partial 提供语言信息;
  • 标点、语义完整性和最大等待时间辅助判断;
  • 用户按键或显式结束作为可靠兜底。

Partial transcript 适合:

  • 实时字幕;
  • 预热意图分类;
  • 提前准备可能需要的上下文;
  • 判断用户是否仍在说话。

不要基于每个 partial 直接执行有副作用工具。识别结果会反复修正:

“帮我取消…”
“帮我取消明天的提醒”
“不要,改成后天”

高风险动作必须等待稳定 turn、参数确认和必要审批。

用户在 TTS 播放时重新说话,系统需要同时:

  1. 检测用户语音;
  2. 立即降低或停止本地播放;
  3. 取消尚未播放的音频缓冲;
  4. 通知服务端取消当前 TTS;
  5. 尽可能取消仍在生成的模型输出;
  6. 标记实际已经播放到哪里;
  7. 把新一轮用户语音送入 ASR。
SPEAKING → INTERRUPTING → LISTENING

只停止扬声器是不够的。服务端如果继续生成和计费,下一轮上下文还可能误以为整段话已经说给用户听。

建议显式建模:

IDLE
CONNECTING
LISTENING
THINKING
CALLING_TOOL
SPEAKING
INTERRUPTING
RECONNECTING
ERROR

UI、音频层和服务端都围绕同一组状态和 session/version 更新。

迟到事件必须携带 turn ID:

{
"session_id": "voice_123",
"turn_id": 18,
"event": "tts.audio.delta",
"sequence": 42
}

如果用户已经进入 turn 19,turn 18 的迟到音频不能继续播放。

工具超过一两秒时,完全沉默会让用户困惑。但也不要先说“已经完成”。

使用与实际状态一致的反馈:

“我正在查询订单状态。”
“这个操作需要你的确认。”
“查询暂时失败,我还没有修改任何内容。”

工具结果返回后再给完成回执。

链路推荐
浏览器或移动端实时双向音频WebRTC
服务端文本/事件流WebSocket 或 SSE
设备控制与轻量状态同步MQTT
内部服务调用HTTP/gRPC

WebRTC 提供实时媒体、拥塞控制和抖动处理,但不替代业务状态机。MQTT 适合设备消息,但命令必须包含目标设备标识、版本、过期时间和幂等 ID。

音频媒体流与业务控制流可以分离:

WebRTC:音频
WebSocket:turn、字幕、工具和 UI 事件
MQTT:设备动作与状态

需要区分:

  • 暂时抖动;
  • 媒体断开;
  • 控制通道断开;
  • ASR/TTS Provider 断开;
  • App 进入后台;
  • 设备切换网络。

重连时不要直接创建新会话并丢弃旧状态。使用:

  • 稳定 session_id
  • 单调递增 turn_idsequence
  • 已确认事件游标;
  • 短期音频缓冲;
  • 服务端 session TTL;
  • 明确的 resume / restart 响应。

如果无法安全续传,应告诉用户“连接已恢复,请重新说刚才一句”,不要偷偷拼接可能缺帧的音频。

至少明确:

  • 采样率和声道数;
  • PCM、Opus 等编码;
  • 帧长;
  • 自动增益、降噪和回声消除;
  • 浏览器是否处于安全上下文;
  • 麦克风权限;
  • 蓝牙设备切换;
  • 扬声器回采对 VAD 的影响。

浏览器中首先检查:

if (!window.isSecureContext) {
throw new Error("麦克风需要 HTTPS 或 localhost 安全上下文");
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});

模型或 WebSocket 之前就拿不到麦克风时,不要继续排查 ASR。

  • 麦克风开启状态始终可见;
  • 录音、转写和长期保存分别征得同意;
  • 原始音频设置最短保留期限;
  • transcript 和 trace 做脱敏;
  • 高风险动作需要视觉或语音确认;
  • 不仅依靠声纹进行身份认证;
  • 外部音频内容按不可信输入处理;
  • 工具权限与文本 Agent 一样遵循最小化。

至少覆盖:

  • 首次连接成功率;
  • 首音频延迟 P50/P95;
  • ASR 字错率及业务实体准确率;
  • turn 提前结束和过晚结束比例;
  • 打断生效时间;
  • 被打断后旧音频泄漏时长;
  • 工具成功率与错误回执准确率;
  • 重连恢复率;
  • 单分钟成本;
  • 不同设备、网络和噪声环境。

真实验收应使用:

  • 安静室内;
  • 背景电视或多人说话;
  • 扬声器外放;
  • 蓝牙耳机;
  • 弱网和网络切换;
  • 快速插话;
  • 长停顿;
  • 方言、数字、英文缩写和业务专有词。
  1. 单设备半双工,先打通 ASR → Agent → TTS;
  2. 增加可观测时间戳和 turn ID;
  3. 实现本地与服务端一致的打断;
  4. 接入工具和状态反馈;
  5. 增加重连和 session 恢复;
  6. 做噪声、弱网和多设备测试;
  7. 最后再优化情绪、音色和更复杂的全双工体验。

语音 Agent 的“真人感”首先来自节奏、状态一致性和可打断性,而不是更夸张的角色 Prompt。