Skip to main content

实时语音链路

语音交互和文字交互的差别,不只是"多了录音和播放"。它把一条请求-响应式的链路,变成了一条有时序、可被打断、延迟累加的流水线。

大部分语音产品做得不好的原因不是模型不行,而是这条链路上的每一段都各自加了一点延迟,加起来就不可用了。

VAD判断说没说完ASR语音转文字LLM生成回复TTS文字转语音播放边收边播延迟是累加的每一段都有自己的耗时,总延迟 = 各段之和 + 传输能省的地方VAD 越早判定结束越快(但太激进会打断),TTS 流式合成可边生成边播打断要能立刻停用户插话时立即停止正在播放的音频并清空队列
图:级联语音链路的四个阶段

1. 级联链路的四段​

目前主流的做法还是级联(不是端到端语音到语音),因为它可控、可替换、可排查:

VAD(语音活动检测) → 判断用户说完了没有 ASR(语音转文字) → 把语音变成文本 LLM → 生成回复 TTS(文字转语音) → 把回复变成音频

每一段都是独立的,可以单独替换供应商或模型。这个可替换性在实践里很值钱——ASR 换一家、TTS 换个音色,都不影响其他段。

1.1 延迟是累加的​

总延迟 = VAD 判停 + ASR + LLM 首字 + TTS 首音 + 网络往返

这意味着:优化某一段的效果有限,但任何一段变慢都会立刻被感知。所以语音场景的优化是从"找最慢的那一段"开始的,而不是笼统地"优化延迟"。

一个常被忽略的点:VAD 的等待时间经常和 ASR 一样长。用约 500ms 的静音判定"说完了"是常见配置,这段时间是纯等待——但它不能随便缩短,太激进会把用户的正常停顿当成说完,导致话说到一半被打断。

1.2 可省的地方​

流式 ASR:边说边出字,不用等说完才开始转写。这能让 ASR 的延迟和说话时间重叠。

流式 TTS:边生成边播,第一个句子合成完就开始放,不用等整段回复生成完。这一项通常是最明显的改善——用户听到的"第一个字"提前了整个生成时间。

LLM 首字优化:和文字场景一样,首字延迟是用户感知最强的指标。

2. 打断是最重要的交互​

语音对话里,用户会插话。这是语音和文字最本质的差异之一——文字对话里用户不会在你打字时打断你。

打断要立刻生效:检测到用户开始说话(VAD 触发)→ 立刻停止正在播放的音频 → 清空待播队列 → 取消还在进行的 TTS 生成 → 取消 LLM 生成(如果还没开始回复)。

但这里有个几乎一定会踩的坑:扬声器的声音会被麦克风收回,系统把自己的播放当成用户插话,于是自己打断自己。阈值救不了它——你放出来的语音本来就是"持续的人声"。

三条解法:播放期间做回声消除(getUserMedia 开 echoCancellation,或依赖 WebRTC 的 AEC)、播放期间门控上行(只上传、不做 VAD 判定,或把播放波形作为参考信号减掉)、以及耳机场景单独放宽。免提和耳机的行为差异极大,两种都要测。

采集端也别用已废弃的 ScriptProcessorNode,用 AudioWorklet:它在音频线程里处理,不会被主线程的渲染卡住。播放端的"停止"要落到 AudioBufferSourceNode.stop() 并清空调度队列,光把音量设 0 不算停。还有个上线才会发现的问题:AudioContext 必须由用户手势激活(autoplay policy),iOS 上尤其严格,首次进入必须有一次点击。

后面两步是钱的问题:不取消就继续为没人听的音频付费。这一点和《AbortController 与请求取消》里追加的那一节是同一个道理——取消要一路传下去。

2.1 但不要过度敏感​

反过来,VAD 太敏感会导致:用户清了下嗓子,AI 就停了;环境噪声一响,AI 就以为被打断了。

实践上通常要区分"有人声"和"用户在对我说话",并且对短促的声音做抑制(比如 300ms 以下的声音不算打断)。这个阈值要实测,且区分场景——安静的室内和嘈杂的室外应该不同。

3. 与纯文本链路的差异​

文本语音
输入边界明确(用户点发送)要靠 VAD 判断
可打断基本不需要核心交互
延迟构成主要是首字四段累加
中间态展示打字机波形 / 字幕
失败表现报错文本用户听到 silence

最后一行值得注意:语音链路失败时用户什么都听不到,而且不知道是网络问题、服务问题还是模型在想。所以语音界面需要"我在听 / 我在想 / 我在说"这类明确的状态反馈,比文字界面更需要。

4. 状态机​

一次语音会话的状态比文字复杂,因为说话和生成可以重叠:

  • 空闲(等用户说话)
  • 收音中(VAD 已触发,还在说)
  • 判停中(静音等待窗口内)
  • 转写中
  • 生成中(可能同时已经在播前半段)
  • 播放中
  • 被打断(回到收音中)

关键点是多个状态可以并存:TTS 在播放后半段的同时,LLM 可能还在生成;用户在播放中说话,系统要同时处理"停止播放"和"开始收音"。

用一个状态机表达不了这种并存,通常需要按"输入通道"和"输出通道"分开建模,各自有状态,再用一个协调层处理它们的交互(文字界面的状态机见《对话界面的状态机》)。

5. 安全:语音转写是不可信输入​

这一条容易被漏,而且后果不小。

ASR 转出来的文本是用户输入,也可能是环境里的任何人说的话。如果它被直接映射成特权动作,风险就来了:电视里的声音、旁边同事的对话、甚至用户自己念的一段文字,都可能触发工具调用。

ElevenLabs 在其语音 SDK 的建议里也明确提了这一点:把识别文本当作不可信输入,在服务端配确定性的护栏或意图白名单,不要让转写文本直接映射到特权动作或敏感工具调用。

这和《Agentic 的十类风险》里的 ASI01 是同一类问题——模型分不清指令和数据,而语音输入比文字更难界定"这是谁说的"。

6. 部署形态​

三种,按复杂度递增:

托管实时 API。 供应商提供 WebSocket 实时接口,前端直连(通过你的后端发临时凭证)。最快上线,延迟通常也不错,但数据要过供应商。

自建级联。 自己串 VAD + ASR + LLM + TTS,每段可换。可控性最好,也最费工。开源生态里有完整的模块化实现可以参考(兼容 OpenAI Realtime 协议的实现让前端可以复用标准客户端)。

端侧部分环节。 VAD 放浏览器端(它很轻),ASR/TTS 走服务端。这样能省掉上传静音片段的流量,也让打断检测更快。

第三种是常见的折中:把最轻、最需要实时性的一环放端上,重的仍在服务端。思路和《端侧推理能做什么》里说的一致。

6.1 WebRTC 还是 WebSocket​

这条选择比"用哪家供应商"更容易做错,因为两者的失败模式完全不同。

WebRTCWebSocket
音频链路浏览器原生采集/播放、抖动缓冲、丢包补偿自己采 PCM/Opus、自己排抖动
回声消除内建 AEC,echoCancellation 真正生效要自己做,效果取决于设备
网络穿透需要 STUN,对称 NAT 下要 TURN走标准 HTTPS/WSS,易过企业防火墙
可控性服务端难代理、难录制,调试链路长可以落盘、可以中间件改写、好排查
延迟通常更低且更稳取决于你的缓冲策略

结论不是"选一个",而是按网络环境分:公网、移动端、强调打断体验的走 WebRTC;企业内网、需要录音审计、需要经过自有网关的走 WebSocket。两者都要准备 TURN 或降级路径——企业网络封 UDP 是常态,只做 WebRTC 会在某些客户那里直接不可用。

还有一条与协议无关的趋势:2026 年托管实时 API 的主流形态已经是端到端语音到语音(原生音频进、原生音频出),不再是"ASR → LLM → TTS"的级联。它的延迟更低、能保留语气,但中间没有文本,护栏插不进去、音色与词表不可控。级联仍然是可控性优先场景的正解——两条路线并存,别把任何一条当唯一答案。

7. 开源与自托管的现实​

2026 年这一层已经有很多开源选择:Whisper 系列、SenseVoice、Qwen3-ASR/TTS、Kokoro、CosyVoice 等,覆盖不同语言和延迟档位。自托管的好处是数据不出网、可定制(比如插入敏感词过滤、定制标点)。

代价是你要运维 GPU 推理服务,而语音是实时的——它对 P99 延迟的要求比批处理严格得多,一次 GC 停顿用户就能听出来。

所以自托管语音更适合有明确合规要求或已经有 GPU 基础设施的团队。

8. 清单​

  • 四段各自测过延迟,知道最慢的是哪一段
  • TTS 是流式的(边生成边播)
  • 打断能立刻停播放、清队列、取消生成
  • VAD 阈值按场景调过,区分"人声"和"对系统说话"
  • 有明确的听觉状态反馈(在听 / 在想 / 在说)
  • 输入与输出通道分开建模,支持状态并存
  • 转写文本按不可信输入处理,不直接映射特权动作
  • 失败时有可听的反馈,而不是静默
  • 端到端延迟在真实网络下测过,不是本地

最后一条最常被跳过。本地测延迟永远是好的,真实网络下的往返和抖动才是用户听到的东西。