实时语音链路
语音交互和文字交互的差别,不只是"多了录音和播放"。它把一条请求-响应式的链路,变成了一条有时序、可被打断、延迟累加的流水线。
大部分语音产品做得不好的原因不是模型不行,而是这条链路上的每一段都各自加了一点延迟,加起来就不可用了。
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
这条选择比"用哪家供应商"更容易做错,因为两者的失败模式完全不同。
| WebRTC | WebSocket | |
|---|---|---|
| 音频链路 | 浏览器原生采集/播放、抖动缓冲、丢包补偿 | 自己采 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 阈值按场景调过,区分"人声"和"对系统说话"
- 有明确的听觉状态反馈(在听 / 在想 / 在说)
- 输入与输出通道分开建模,支持状态并存
- 转写文本按不可信输入处理,不直接映射特权动作
- 失败时有可听的反馈,而不是静默
- 端到端延迟在真实网络下测过,不是本地
最后一条最常被跳过。本地测延迟永远是好的,真实网络下的往返和抖动才是用户听到的东西。