文章

LLM 实时语音输入生产实战:用 VAD 分段、Partial/Final 对账与音频契约稳住低延迟识别

实时语音应用的难点不只是模型准确率。本文从音频格式契约、VAD 分段策略、Partial 与 Final 状态对账、时间戳回放到真实噪声评测,系统拆解如何构建可观测、可回滚的低延迟语音识别管线,适合 LLM、生成式 AI 与实时 Agent 技术读者。

背景:实时语音故障往往发生在模型之前

实时语音应用常被简化成”麦克风音频送给模型,再把文字交给 LLM”。真正上线后,故障通常不只来自识别模型:浏览器送来 48 kHz 立体声,服务端按 16 kHz 单声道解析;网络重连导致音频片段重复;VAD 过早结束一句话;前端把尚未稳定的 Partial Transcript 当成最终文本;不同语音段的完成事件乱序到达,最终出现字幕倒序、内容重复或工具误调用。

因此,语音输入管线应被视为一个具有明确协议和状态机的生产系统。核心不是追求单一的最低延迟,而是在以下四个目标之间建立可测量的平衡:

  1. 边界正确:不漏掉句首、轻声和尾音,也不过度吞入长时间噪声。
  2. 状态一致:增量文本可以修改,最终文本只能按唯一语音段提交一次。
  3. 格式确定:采样率、声道数、编码、字节序和时间基准均可追踪。
  4. 延迟可解释:能区分采集、网络、VAD、识别首字和最终确认各阶段耗时。

核心原理一:先建立音频契约,再谈模型效果

音频契约必须版本化

每个音频会话至少应记录以下字段:

{
  "audio_contract_version": "audio-v3",
  "codec": "pcm_s16le",
  "sample_rate_hz": 16000,
  "channels": 1,
  "frame_duration_ms": 20,
  "sequence_origin": 0,
  "clock": "monotonic",
  "vad_profile": "call-center-noisy-v2",
  "transcription_profile": "realtime-balanced-v4"
}

这些字段不是普通日志,而是重放条件。没有它们,离线回放时无法判断质量变化究竟来自模型、重采样器、VAD 参数还是客户端编码升级。

Whisper 官方实现将音频重采样为 16 kHz 单声道 PCM,并以 30 秒为标准块处理;代码中的音频帧步长为 10 ms,时间戳 Token 粒度为 20 ms。这说明格式转换和时间基准是模型输入的一部分,而不是无关紧要的外围处理。但 16 kHz 不能被泛化为所有模型的固定要求,应由具体模型适配器声明能力。

建议把格式转换放在一个位置

客户端、边缘网关和识别服务同时做重采样,会产生难以追踪的重复处理。较稳妥的做法是:

  • 客户端只负责采集、轻量封包和递增序号;
  • 音频网关负责解码、声道归一化、重采样和幅度检查;
  • 模型适配器只接收已经符合声明格式的帧;
  • 原始音频是否保留,由隐私策略和故障采样策略决定。

音频网关应为每一帧附加 session_idstream_epochsequence_nocapture_tsduration_ms。发生重连时递增 stream_epoch,避免新旧连接使用相同序号造成误去重。

核心原理二:VAD 是可调的延迟—完整性决策器

OpenAI Realtime 文档区分两类转折检测:Server VAD 根据静音切分,Semantic VAD 根据话语是否语义完整判断用户是否说完。Server VAD 常见参数包括激活阈值、句首预留时长和静音结束时长;Semantic VAD 则通过 eagerness 控制更快切段还是给用户更多停顿时间。

这意味着 VAD 不是简单的”有声音 / 无声音”开关,而是在持续回答三个问题:

  1. 这段信号是不是语音?
  2. 语音从哪里开始,句首要向前回补多少?
  3. 当前停顿是句内犹豫,还是一次真正的发言结束?

不要只调一个静音时长

生产 VAD 至少要按场景分 Profile:

场景特点VAD 策略建议
近讲麦克风噪声低可以更积极结束
电话窄带音频压缩失真、双讲处理压缩失真和双讲
会议室远场多人和回声提高句首回补
车载或户外持续噪声提高抗噪要求
长思考问答长停顿允许更长句内停顿

NVIDIA Riva 同时提供流式中间结果、神经 VAD、End-of-Utterance 和 Two-Pass EOU。Two-Pass 思路很有价值:先产生可供下游预处理的早期转写,再等待更可靠的最终端点确认。它揭示了实时系统的关键设计——早结果和最终结果应当是两个不同状态,而不是同一个文本字段不断覆盖

VAD 的上线指标

除了识别错误率,还应记录:

指标含义
speech_start_detection_ms真实句首到检测句首的延迟
front_end_clipping_ms被截掉的句首长度
end_of_utterance_wait_ms真实句尾到最终提交的等待
false_turn_rate噪声被提交为语音段的比例
split_utterance_rate一句话被错误拆成多段的比例
merged_utterance_rate多句话被错误合并的比例
empty_final_rate最终事件为空的比例

只看平均端点延迟,会掩盖 P95/P99 下的长等待和特定口音、噪声场景中的截断。

核心原理三:Partial 和 Final 必须通过状态机对账

实时转写通常同时返回增量文本和最终文本。OpenAI Realtime Transcription 文档明确区分 delta 与 completed 事件,并提示不同语音段的完成事件不保证按顺序到达,需要使用 item_id 进行匹配。

正确的数据模型不是一个可变字符串,而是语音段状态机:

CAPTURING -> SPEECH_STARTED -> PARTIAL_ACTIVE -> AUDIO_COMMITTED
    -> FINAL_RECEIVED -> DOWNSTREAM_COMMITTED

异常分支:
    -> CANCELLED -> TIMED_OUT -> SUPERSEDED -> REPLAY_REQUIRED

Partial 只用于展示和预计算

Partial Transcript 可以用于:

  • 实时字幕;
  • 提前做语言识别、意图候选和实体预热;
  • 预取可能需要的工具或知识;
  • 估计后续文本 Token 预算。

但它不应直接触发不可逆动作,例如提交订单、发送消息、修改数据库或确认身份。因为后续音频可能修正前面的词,数字、否定词和专有名词尤其容易变化。

Final 也不能”到达即执行”

最终事件应先通过以下门禁:

  1. session_id + stream_epoch + item_id 是否唯一;
  2. 是否已经处理过相同完成事件;
  3. 音频序号区间是否连续,是否存在缺帧;
  4. Final 是否对应当前有效会话,而不是重连前的旧流;
  5. 是否需要等待更早的 item 完成,以恢复逻辑顺序;
  6. 是否满足业务侧最小置信度或关键实体复核规则。

一个简化的处理示例如下:

type SegmentState = {
  itemId: string;
  epoch: number;
  partial: string;
  final?: string;
  status: "partial" | "final" | "committed" | "cancelled";
};

function onTranscriptEvent(event: TranscriptEvent) {
  const key = `${event.sessionId}:${event.epoch}:${event.itemId}`;
  const state = segmentStore.getOrCreate(key);

  if (event.type === "delta" && state.status === "partial") {
    state.partial += event.delta;
    publishEphemeralCaption(key, state.partial);
    return;
  }

  if (event.type === "completed") {
    if (state.status === "committed" || state.status === "cancelled") return;
    state.final = event.transcript;
    state.status = "final";
    finalReorderBuffer.offer(event.logicalOrder, state);
  }
}

其中 publishEphemeralCaption 与最终业务提交必须使用不同通道。前者允许覆盖,后者要求幂等。

工程落地:把实时语音拆成六个可替换模块

1. Capture Adapter

负责浏览器、移动端、SIP 或设备音频接入。统一输出帧序号、采集时间和格式声明,不负责猜测模型格式。

2. Audio Normalizer

完成解码、声道转换、重采样、幅度归一化和异常帧检查。应暴露重采样耗时、削波率、静音比例和输入电平分布。

3. Turn Detector

封装 Server VAD、Semantic VAD 或本地神经 VAD。配置必须按 vad_profile 版本发布,不允许在线直接覆盖无审计参数。

4. Streaming Transcriber

负责向供应商或自建 ASR 发送有序音频块,接收 Partial、Final、时间戳、置信度和说话人标签。对于模型不支持的可选字段,要明确降级,而不是返回伪造默认值。

5. Transcript Reconciler

负责乱序恢复、Partial 替换、Final 幂等、重连 Epoch 隔离和时间戳归一化。它是语音管线中最容易被低估的模块。

6. Downstream Gate

把最终文本交给 LLM 或业务系统前,执行关键实体规则、会话状态检查和可逆 / 不可逆动作分级。语音识别结果只是输入证据,不应绕过业务授权。

时间戳与重放:别只保存一段文本

长音频采用滑动窗口或缓冲分段时,容易发生时间戳漂移、重复和幻觉。WhisperX 通过 VAD Cut & Merge 与强制音素对齐生成更精确的词级时间戳,其研究也指出长音频缓冲或滑窗方式容易出现漂移、重复和时间戳不准。

生产回放包建议包含:

{
  "trace_id": "voice-01J...",
  "audio_contract_version": "audio-v3",
  "vad_profile": "call-center-noisy-v2",
  "model_profile": "realtime-balanced-v4",
  "audio_sha256": "...",
  "frame_ranges": [[0, 181], [182, 346]],
  "turn_events": [
    {"type": "speech_started", "at_ms": 420},
    {"type": "speech_stopped", "at_ms": 3180}
  ],
  "partial_events": [],
  "final_events": [],
  "client_clock_offset_ms": 0,
  "server_received_at": "2026-07-12T23:01:57Z"
}

对隐私敏感业务,可以只保存经过授权的抽样音频、不可逆音频指纹和完整事件元数据。没有真实音频时,至少保留帧范围、格式指纹、事件时间线和最终文本,便于定位协议错误。

评测方法:同时测字词、边界和交互延迟

仅用 WER 或 CER 评估实时语音不够。建议建立四层指标:

识别层

  • WER/CER;
  • 数字、日期、金额、邮箱、产品名等关键实体准确率;
  • 空转写、截断转写和重复转写比例。

边界层

  • 句首截断与句尾拖延;
  • 错误拆句和错误合句;
  • 噪声误触发;
  • 打断和双讲场景下的恢复。

状态层

  • Partial 被 Final 修改的字符比例;
  • Final 乱序率;
  • 重复完成事件率;
  • 重连后旧 Epoch 污染率;
  • 时间戳回退或重叠比例。

体验层

  • 首个 Partial 延迟;
  • 最终确认延迟;
  • 用户说完到 LLM 开始回答的延迟;
  • 字幕抖动次数;
  • 用户因系统抢话而重复表达的比例。

OpenAI 当前实时转写文档也建议使用真实麦克风、电话音频、口音、背景噪声、代码切换、领域词汇和长会话进行测试,并分别跟踪空文本、截断和延迟,而不是只看 WER。

适用场景

该架构适合:

  • 实时客服与电话机器人;
  • 会议字幕、纪要和说话人归属;
  • 车载、穿戴设备与语音助手;
  • 语音驱动的 Agent 或工具调用;
  • 医疗、保险、金融等需要关键实体复核的语音录入;
  • 多供应商 ASR 切换与自建模型灰度。

离线播客转写若没有实时交互要求,可以简化 Partial 状态机,但仍应保留音频契约、分段和时间戳回放。

常见误区

误区一:VAD 越快,体验越好

过快切段会截断句内停顿并增加文本碎片。端点等待应按场景调优,不能只追求最小值。

误区二:Partial 只是 Final 的前缀

增量结果可能重写前文,不能假设只追加字符。UI 和存储都要支持替换。

误区三:完成事件天然有序

网络、并行识别和服务端调度都可能导致不同语音段的完成事件乱序。必须按稳定标识和逻辑序号对账。

误区四:所有音频统一 16 kHz 就够了

采样率只是契约的一部分。声道、编码、字节序、帧长、重连和时间基准同样影响结果。

误区五:真实噪声可以用白噪声代替

电话压缩、回声、双讲、远场混响、键盘声和移动网络丢包具有不同结构。合成白噪声只能覆盖有限场景。

上线检查清单

  • 每种客户端都上报明确的编码、采样率、声道和帧长。
  • 音频契约、VAD Profile、模型 Profile 均有不可变版本号。
  • 重连时递增 Stream Epoch,并隔离旧连接事件。
  • Partial 与 Final 使用不同存储语义和下游权限。
  • Final 按 item_id 幂等,并支持乱序缓冲。
  • 关键实体有独立准确率和人工复核策略。
  • 测试集包含真实设备、电话、噪声、口音、代码切换和长会话。
  • 分别监控首个 Partial、最终确认和下游响应延迟。
  • 对 VAD 配置变更执行影子流量、回放和小比例灰度。
  • 保留可审计的事件时间线,并按隐私策略限制原始音频留存。
  • 模型不支持时间戳、置信度或说话人标签时,有明确降级路径。
  • 出现空转写、持续噪声、乱序或超时风暴时,可以快速切换到安全 Profile。

更多 FAQ

VAD 应放在客户端还是服务端?

两者都可以,但只能有一个组件负责最终语音段提交。客户端 VAD 适合降低带宽和交互延迟,服务端 VAD 更便于统一治理。常见方案是客户端提供提示性事件,服务端根据统一 Profile 做最终裁决。

什么时候可以让 Partial 提前触发 LLM?

只适合可撤销的预计算,例如意图候选、检索预取或模型预热。任何不可逆工具调用都应等待 Final 和业务门禁。

如何判断一次 VAD 调整是否真正变好?

同时比较端点等待、句首截断、错误拆句、噪声误触发、Final 质量和用户打断率。只降低延迟但增加截断,不算改善。

参考资料

  1. OpenAI, Voice activity detection (VAD): https://developers.openai.com/api/docs/guides/realtime-vad
  2. OpenAI, Realtime transcription: https://developers.openai.com/api/docs/guides/realtime-transcription
  3. OpenAI Whisper, audio.py: https://github.com/openai/whisper/blob/main/whisper/audio.py
  4. Radford et al., Robust Speech Recognition via Large-Scale Weak Supervision: https://arxiv.org/abs/2212.04356
  5. Bain et al., WhisperX: Time-Accurate Speech Transcription of Long-Form Audio: https://arxiv.org/abs/2303.00747
  6. NVIDIA Riva, ASR Overview: https://docs.nvidia.com/deeplearning/riva/user-guide/docs/asr/asr-overview.html

常见问题

VAD 阈值是否越灵敏越好?
不是。阈值过低会把噪声识别成语音,阈值过高又会截掉轻声或句首。生产配置必须同时观察前端截断、误触发、端点等待时间和转写质量。
为什么 Partial Transcript 不能直接写入最终业务记录?
增量文本可能被后续音频修正,而且不同语音段的完成事件未必按到达顺序返回。应使用稳定的会话、语音段和 item 标识完成对账后再落最终结果。
统一转成 16 kHz 单声道是否适用于所有模型?
不适用。16 kHz 单声道是 Whisper 等模型的典型输入约定,但不同实时模型可能支持其他格式。系统应按模型能力声明转换,而不是把某个采样率写死成全局真理。