流式语音 Turn Detection 与背压:端点、打断和音频队列治理
只加 VAD 无法得到可靠语音轮次。本文用单调音频序列、端点状态机、预卷与尾静音、打断取消和有界背压控制实时语音链路。
本文要点
- 主题聚焦:AI、大模型、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
语音助手最常见的生产故障不是“完全听不见”,而是轮次边界错了:用户换气时被提前提交,话说完后系统迟迟不答,打断后旧 TTS 又冒出半句,或网络变慢时浏览器无限堆积 PCM,最终把十秒前的声音当成实时输入。VAD 只能判断一帧像不像人声,不能独自决定这一轮何时可提交。
语音/音频 AI 应用 已介绍 ASR、TTS、VAD 与打断,本篇把它们收敛成可观测的状态机和背压契约。传输层的事件与恢复原则也可结合 LLM 流式输出协议,但音频不能像文本 token 一样任意丢弃或重排。
每个音频块先有确定身份
会话协商 codec、采样率、声道、frame duration 和协议版本。每个块带单调 sequence、捕获时间、样本数与 turn generation;服务端拒绝重复、倒退和跨 generation 块,并用实际样本数重建媒体时间,而不是依赖不稳定的网络到达时间。
audio: {encoding: pcm_s16le, sampleRateHz: 16000, channels: 1, frameMs: 20}
runtime:
gatewayImage: registry.example/voice-gateway@sha256:88a0...
vadModelDigest: sha256:2f13...
asrModelRevision: asr-prod-2026-06
providerApiContractSha256: 510b...
turn:
speechStartFrames: 3
preRollMs: 240
silenceEndMs: 650
maxTurnMs: 30000
endpointGraceMs: 250
buffer:
clientHighWaterMs: 1000
serverHardLimitMs: 2000
resumeBelowMs: 400
参数绑定 VAD、ASR、语言、设备类型和回声消除配置,不能全站共享一个“650ms 最佳值”。客户端 clock 只用于诊断,服务端 session clock 负责超时;断线重连携带最后确认 sequence 和 generation,无法证明连续时开启新 turn,不把两段音频硬拼。
用状态机组合声学与语义端点
状态至少包含 idle、possible_speech、speaking、possible_end、committed 和 cancelled。连续若干语音帧才从 possible 进入 speaking,同时把 pre-roll 环形缓冲补回,避免吞掉首字;短静音只进入 possible_end,恢复人声就回 speaking。达到静音阈值、最大轮次或显式按键结束后,才生成唯一 turn_id 提交。
ASR interim 是可替换视图,不是业务事实。每次修订带 revision 和覆盖区间,只有端点确认后的 final transcript 才触发业务工具、写操作或按任务成功收费。网络字节、已接收音频毫秒、ASR/模型 token 和 GPU 时间从资源被接受时另行记账,取消也记录实际消耗,不能因未形成 final 就假装没有成本。若句子明显未完,可在硬上限内延长一次。状态转移可参考 Agent 运行时状态机,副作用仍由确定代码守门。
背压必须限制“音频年龄”
浏览器采集速度由真实时间决定,下游却可能因 ASR、网络或模型排队变慢。队列以毫秒和字节双重计量,并暴露 desiredSize 或显式 credit。达到高水位后仍让下游按可用速度持续 drain,同时把上游 credit 置零,暂停或拒绝新的 enqueue;暂停下游只会让积压更严重。服务端到硬上限后返回可分类过载,不能继续接受并假装实时。
已经进入活跃 turn 的 PCM 不能从中间随机丢帧。短拥塞使用有界缓冲;麦克风无法暂停且预测不能在硬期限内排空时,取消整个未提交 generation、计量已消耗资源、通知用户重说,并从新 sequence 恢复。只允许丢弃说话前的旧静音或可重取派生数据并记录 gap。Web Streams 背压只传播消费能力,不能让麦克风停止产生现实声音。
监控队列毫秒、最老帧年龄、credit 耗尽、gap、重复/乱序、端点耗时和取消释放时间。单看 WebSocket bufferedAmount 不够,它只显示一个发送端的字节,无法解释服务端 ASR 队列和媒体时长。Next.js AI 流式响应治理 中的取消传播适用于控制面,但这里还需清空音频与播放 generation。
Barge-in 是原子代际切换
AI 播放期间检测到用户持续语音,先用回声消除和短确认窗排除扬声器回灌,再原子增加 generation:停止本地播放、取消剩余 TTS、取消或冻结旧 LLM、关闭旧 ASR turn,然后提交含 pre-roll 的新用户音频。所有迟到的旧 TTS chunk、文本 delta 和工具候选因 generation 不匹配被丢弃。
如果旧回复已经触发不可逆工具,打断不能撤销事实,只能进入补偿或向用户说明;因此工具副作用不能由 interim transcript 直接触发。播放端记录实际播到的音频位置,下一轮上下文只包含用户已听见或系统明确标记未播的内容,避免模型误以为整段回复已经传达。
用噪声和拥塞回放发布
黄金集覆盖短句、长停顿、口头禅、咳嗽、背景人声、双人重叠、蓝牙抖动、回声、弱网、服务端暂停和连续打断。保存音频 hash、期望起止窗口、final 文本与允许修订,不把真实用户录音直接放测试仓库。门禁可要求首字截断为零、关键轮次漏检为零、端点 P95 在目标内、错误 generation 播放为零、取消后 500ms 内停止出声、队列不越硬上限。
新 VAD 或端点参数先影子计算,不同时驱动用户可见回复;再按设备与语言切 5%、20%、50%。若提前提交、尾延迟、重说率或队列丢弃越线,路由恢复旧配置 digest,新 generation 使用旧状态机,已有 turn 按绑定版本完成。回滚不能只改静音毫秒,还要一起恢复 VAD、ASR final 规则、缓冲与打断策略。
可靠 turn detection 的结果不是“多数时候接得上话”,而是每个音频块、端点决定、背压丢弃和代际取消都能解释,过载时宁可明确重试,也不把过期声音伪装成实时对话。
延伸阅读
Related