打断后半天不接话,多数情况下先把怀疑点放在端点检测一侧比放在模型本身更有效率:插话通常只有一两个字、起始音偏低、与助手自己的播报重叠,很容易被判成噪声、被判成还没开始说话,或者在静音判定窗口里被切碎。但也有一类情况是上行音频在采集阶段就没拿到,还有一类是客户端本地交互层根本没发出打断事件。合理的排查顺序是采集、本地打断事件、端点检测参数,三者都排除了再去怀疑对话侧或模型侧。
插话后愣住或长时间不应答,先按三步走:用 ffmpeg 或系统录音同时录下麦克风,回放确认插话时刻是否真有波形;再在开发者工具或客户端日志里找本地打断、静音、会话状态事件及其时间戳,确认事件是否发出;最后单独调整静音判定时长与最短语音段长度做对照。若同一段录音换网络或换客户端后现象消失,优先怀疑客户端麦克风模式或链路丢包;两端都复现,才需要结合服务端日志讨论模型侧。诊断期间不要同时改多个参数,否则无法归因。
确认插话时上行音频真的被采到
这一步的目的很单纯:先证明麦克风在插话那一刻确实拾到了声音。很多“没接话”其实是设备选错、被独占、增益过低或系统降噪把短促语音吃掉了,后面所有参数调整都没有意义。
做法是让录音和应用同时占用麦克风。macOS 上可以用 BlackHole 之类虚拟声卡把输入复制一份,Linux 用 PulseAudio 的 loopback 模块,Windows 用 ffmpeg 的 dshow 输入直接录设备。下面是一个与平台无关的调用思路,设备名需要按 ffmpeg -devices 或系统列表替换:
# 录制系统默认输入,时长 60 秒,保留原始采样率便于对齐
ffmpeg -f avfoundation -i ":0" -t 60 -c:a pcm_s16le mic_check.wav
# 或 Windows:ffmpeg -f dshow -i audio="Microphone (USB Audio)" -t 60 -c:a pcm_s16le mic_check.wav录完之后把 wav 拖进 Audacity 一类工具看波形,找到应用日志里那条“开始播报”的时间点,向后对齐,确认插话位置是否有可见波形和合理幅度。判断分成两种:波形里有清晰插话、但应用没有产生任何对应的识别结果或状态变化,说明声音采到了、在后续处理里丢了;波形里插话位置几乎是平直线,或者幅度极低,说明采集侧就没拿到,优先查设备选择、静音开关、系统级降噪与自动增益、是否有其他程序独占输入。
查看客户端是否发出了打断事件
音频采到之后,第二处要确认的是本地交互层有没有把“打断”这件事表达出来。这里只观察本机可见的东西:事件名、时间戳、方向,不去推断服务端字段。
浏览器端打开开发者工具,Network 面板按 WS 过滤,逐帧看时间轴;Console 里按关键词过滤本地打印的事件,常见的是 speech_start、speech_end、barge_in、interrupt、mic_mute、session_state 这类命名,具体名称取决于所用客户端。桌面或移动客户端则找本地日志文件,按时间戳排序看同一时刻前后的状态迁移。
结果通常落在两种情形。一种是从头到尾没有打断类事件:可能打断按钮或快捷键没绑定,也可能播报期间麦克风被半双工逻辑主动静音,或者 AEC 把重叠语音当成回声压制了,这属于本地交互层没触发。另一种是事件已经打出,但随后没有新的上行音频帧、也没有会话状态回落,属于上游没响应,需要把这条时间戳连同录音一起交出去定位。
调整端点检测的静音判定与最短语音时长
如果音频在、事件也在,就要怀疑插话被端点检测吞掉了。两个方向最值得先调:静音判定时长决定多久无声算一句结束,偏长会让插话迟迟不封口;最短语音段长度决定多短的语音才被当作“开始说话”,偏大会把一两个字的插话直接判为噪声。字段名取决于所用客户端或框架,可能是 silence_duration、end_of_speech_timeout、min_speech_duration、vad_threshold 之类的形态,需要以实际配置为准。
对比方法要克制:一次只改一个参数,用下一节的固定脚本跑同一组用例,记录响应与否和延迟档位。先把最短语音段长度往小调一档,看短插话是否开始被接受;再把静音判定时长往下调一档,看是否还会出现长时间挂起。两个参数同时改,结果无法归因。
用固定脚本做重复插话复现
偶发问题必须变成次数才有讨论价值。下面是框架无关的骨架:固定文本、固定语速、固定插话位置、固定重复次数。朗读可以用本地 TTS 预生成 wav 再回放,避免每次语速不同。
# 伪代码骨架,按实际客户端替换 play 和 inject 两步
TEXT="请介绍一下端到端语音交互的常见延迟来源"
N=10
for i in $(seq 1 $N); do
play assistant_or_prompt.wav # 或在页面里触发一次提问
sleep 3 # 等助手开始播报
t0=$(date +%s.%N)
inject user_bargein_16k.wav # 固定插话音频,走虚拟麦克风回放
wait_for_response || echo "no-response"
t1=$(date +%s.%N)
echo "$i,$t0,$(echo "$t1-$t0"|bc)" >> run.csv
done记录表至少包含这几列:序号、插话时刻(相对播报开始的秒数)、是否响应、响应延迟档位(小于 0.5 秒 / 0.5 至 2 秒 / 2 至 5 秒 / 大于 5 秒 / 无响应)、插话段是否出现在上行音频、本地打断事件是否发出、备注(改动过的参数)。十次里若“无响应”稳定出现,问题可复现;若只在个别次数出现,就在备注里记录当时的网络与设备状态。
用同一条录音换客户端或网络对比
最后一步是把变量换成环境。把“提问加插话”整段录成一个 wav,用虚拟麦克风回放,保证每次送给客户端的音频完全一致。虚拟麦克风在 Linux 可用 PulseAudio 的 loopback,macOS 用 BlackHole,Windows 用 VB-Cable 一类方案,把播放输出接到录音输入。
然后在两组条件下重跑同一套脚本:一组有线网络、一组弱网或移动热点限速;一组浏览器客户端、一组桌面客户端。归纳时看现象落在哪一侧。只在弱网出现,倾向上行拥塞、音频帧被丢或连接重连,属于链路行为;只在某个客户端出现,倾向该客户端的麦克风模式(半双工、播放时静音)、打断事件绑定或 VAD 参数,属于客户端行为;两种网络、两种客户端都复现,才有必要带着录音、本地事件时间戳和服务端日志去讨论对话侧或模型侧的响应问题。