转载本文请注明出处:https://yudonglee.me/streaming-asr-explained/  |  作者:yudonglee

2024 年是流式 ASR 的「文艺复兴」之年——OpenAI 发布 Realtime API、Google 推出 Gemini Live、Kyutai 开源 Moshi(端到端语音 LLM,理论延迟仅 160 ms)。Voice Agent 一夜之间从科幻变成产品形态,而支撑这一切的根基依然是低延迟流式语音识别:必须在用户讲话的当下就把字一个一个吐出来,否则后面的 LLM、TTS、对话系统都失去意义。

本文是 CTC 系列WhisperRNN-TConformerSSL 三部曲 这条语音线索的第 8 篇,从工程视角彻底拆透「怎么把一个为离线设计的 ASR 模型改成可流式」。读完HE将能回答:

  1. 把一个 Conformer-Transducer 改成流式,需要动哪些模块?分别有什么代价?
  2. Whisper / Conformer / RNN-T 三种架构在流式部署上的取舍是什么?什么场景该选谁?
  3. Moshi / GPT-4o Realtime 这类端到端语音 LLM 的「200 ms 延迟」是怎么算出来的?流式 ASR 在里面扮演什么角色?

1. 背景:流式 ASR 究竟有多难

「流式 ASR」最朴素的定义是:用户说话的同时,模型实时输出文字,不必等用户说完整句。这听起来似乎只是把离线模型加个 partial output 接口,但工程上要解决三个互相耦合的问题:

  1. 算法延迟 (Algorithmic Latency)。模型为了产出某一帧的预测,需要等多久的「未来」音频?严格因果模型 = 0;带 1 秒 look-ahead = 1000 ms。
  2. 计算延迟 (Computation Latency)。算这一帧需要多少 wall-clock 时间?取决于模型大小、硬件、批处理策略。两者之和是用户感知到的「首字延迟」(First Token Latency)。
  3. 端点检测 (Endpointing)。怎么判断「用户说完了」,从而让对话系统进入 LLM 思考阶段?早了截断、晚了反应慢,体验差异巨大。

工业LD对 voice agent 的体验门槛是首字延迟 < 500 ms、端点延迟 < 300 ms。Moshi 这类端到端语音 LLM 把整链路压到 200 ms 以内才算「自然对话」。下面我们逐层拆解,看哪些模块在阻碍我们达到这个目标。

2. 流式三大「天敌」

把一个为 LibriSpeech 离线训练的 Conformer-Transducer 拿到流式环境跑,会立刻撞上三个结构性障碍:

  • 双向 Self-Attention。Encoder 中每帧 attention 默认看全序列,未来帧也算在里面。意味着模型必须等整段音频才能算出第一帧的输出——算法延迟 = 音频总长
  • 双向卷积。Conformer 的 Convolution Module 用中心 padding(kernel=31 时 padding=15),意味着算 t 时刻输出要用到 t+15 帧的未来——算法延迟 = 15 帧 × 10 ms = 150 ms 一层;17 层叠加最坏可达 2.5 s
  • BatchNorm。Conv Module 内的 BN 统计依赖整段序列,流式时无法稳定计算 mean/var——会导致输出分布漂移

三个问题各有对应解法:把 self-attention 改成chunked attention,把双向卷积改成causal/streaming conv,把 BatchNorm 换成LayerNorm。这三个改造每一个单独都能让模型跑起来,但一起做才能把延迟和精度同时压到可用区间——它们是耦合优化,不是独立选择。

更微妙的是,RNN-T 和 CTC 这两种损失函数天然适合流式——CTC 每帧独立输出、RNN-T 严格单调对齐,二者都不依赖未来 token。而 attention-based seq2seq(如 Whisper、LAS)的解码器是非单调的,cross-attention 可以任意跳到未来 encoder 帧,根本不存在「流式解码」的自然语义。这也是为什么工业流式 ASR 几乎全是「Conformer encoder + RNN-T decoder」的组合——损失函数侧已经自带流式属性,只需要把 encoder 改造成 chunk-aware。

3. Chunked Attention:把序列切块、各看各的未来

Comparison of full self-attention, causal mask, and chunked attention with look-ahead — the three attention patterns relevant to streaming ASR
图 1:流式 ASR 中三种 attention mask 的对比。Full attention 看全序列(非流式),causal mask 严格只看过去(0 延迟),chunked + look-ahead 在 chunk 内允许双向、跨 chunk 屏蔽未来——这是 WeNet U2++ 流式 Conformer 的默认做法。

Chunked attention 是把 self-attention 的「全序列查看」限制成「当前 chunk 内自由 + 历史 chunk 完整 + 未来 chunk 屏蔽」。chunk 大小 C 是延迟与精度的核心权衡——C=4 帧 (40 ms) 几乎无延迟但精度差,C=160 帧 (1.6 s) 接近离线效果但延迟大。WeNet 的 U2U2++ 框架定义了一个聪明的解法:

Dynamic Chunk Training——训练时每个 batch 从一个均匀分布里采一个 chunk size(比如 [1, max_chunk]),让同一个模型同时学到所有 chunk 尺寸的能力。推理时按业务延迟需求选不同的 C:实时字幕用 C=8(80 ms 延迟),离线转录用 C=∞。一份模型权重,多档延迟可调。这是 WeNet 在工业LD大规模流行的关键工程武器。

import torch

def chunked_attention_mask(T: int, chunk_size: int, history: int = -1) -> torch.Tensor:
    """生成 chunked-attention 的布尔 mask。
    True = 允许 attend;False = 屏蔽。
    Args:
        T          : 序列总长(帧数)
        chunk_size : 当前 chunk 大小
        history    : 可见的历史 chunk 数;-1 表示无限(看所有过去)
    Returns: shape (T, T) 的布尔矩阵
    """
    # 每个位置 i 所属的 chunk id
    chunk_id = torch.arange(T) // chunk_size                  # (T,)
    qid, kid = chunk_id.unsqueeze(1), chunk_id.unsqueeze(0)   # (T,1), (1,T)
    can_attend = kid <= qid                                   # 未来 chunk 屏蔽
    if history >= 0:
        can_attend &= (qid - kid) <= history                  # 限制历史长度
    return can_attend

# 用法示例:8 帧序列、chunk=2、看 1 个历史 chunk
mask = chunked_attention_mask(T=8, chunk_size=2, history=1)
# 在 attention 内部:scores.masked_fill_(~mask, float("-inf"))

4. Causal Convolution + LayerNorm:消除卷积侧延迟

Conformer 的 Convolution Module 默认使用 中心 padding 的 depthwise conv(kernel=31, padding=15),对每一帧都依赖两侧各 15 帧上下文。流式部署有两个改造方案:

  1. 纯因果卷积 (Causal Conv)。把 padding 从 `(15, 15)` 改成 `(30, 0)`——左侧 padding 全部加在前面,右侧不允许 padding。算 t 时刻输出只用到 [t-30, t] 帧。算法延迟 = 0,但精度会掉。
  2. Chunk-aware Conv (Dynamic Chunk Convolution)。让卷积也按 chunk 切——chunk 内允许双向,跨 chunk 屏蔽。这与 chunked attention 行为对齐,是 WeNet U2++ 的默认做法。论文实测显示比纯因果卷积 WER 低约 0.5。
import torch.nn as nn

class CausalConvModule(nn.Module):
    """流式版 Conformer Convolution Module。
    把 BatchNorm 换成 LayerNorm,DepthwiseConv 改为左侧全 padding。
    """
    def __init__(self, d_model: int, kernel_size: int = 15):
        super().__init__()
        self.ln       = nn.LayerNorm(d_model)
        self.pw1      = nn.Conv1d(d_model, 2 * d_model, kernel_size=1)
        # 关键:左 padding = kernel - 1,右 padding = 0 → 因果
        self.dw       = nn.Conv1d(d_model, d_model, kernel_size,
                                  padding=0, groups=d_model)
        self.pad_left = kernel_size - 1
        self.norm     = nn.LayerNorm(d_model)        # 替换 BatchNorm
        self.act      = nn.SiLU()
        self.pw2      = nn.Conv1d(d_model, d_model, kernel_size=1)

    def forward(self, x, cache: torch.Tensor = None):
        """x: (B, L, d). cache: 上一次输出末尾的 pad_left 帧, 用于跨 batch 续推。"""
        x = self.ln(x).transpose(1, 2)                  # (B, d, L)
        x = nn.functional.glu(self.pw1(x), dim=1)       # (B, d, L)
        if cache is None:
            cache = torch.zeros(x.size(0), x.size(1), self.pad_left,
                                device=x.device, dtype=x.dtype)
        x_padded = torch.cat([cache, x], dim=2)         # 拼接历史 → (B, d, pad+L)
        new_cache = x_padded[:, :, -self.pad_left:]     # 保留末尾给下次
        x = self.dw(x_padded)                           # (B, d, L)
        x = x.transpose(1, 2)                           # back to (B, L, d)
        x = self.act(self.norm(x))
        x = self.pw2(x.transpose(1, 2)).transpose(1, 2)
        return x, new_cache

BatchNorm 替换为 LayerNorm 是流式部署里另一个常被忽略的细节。BN 在训练时累计的均值/方差是整段序列的统计,推理时这些统计在流式 chunk 上不可用,模型输出分布会漂移。LayerNorm 沿特征维归一化,与序列长度无关,是流式天然友LLO的选择。NeMo 与 k2/icefall 都把 Conformer 的 Conv Module BN 默认换成了 LN(变体「LN-Conformer」)。

5. KV Cache:流式推理的内存帮手

chunked attention 在训练时是 O(L²) 计算量,但推理时每来一个新 chunk 都从头算所有过去 attention 就太浪费了——前面 chunk 的 key/value 不会变,可以缓存。KV Cache 的核心思想是:

class StreamingMHA(nn.Module):
    """简化版流式 multi-head attention,支持 KV cache。"""
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.h = n_heads
        self.qkv  = nn.Linear(d_model, 3 * d_model, bias=False)
        self.out  = nn.Linear(d_model, d_model)

    def forward(self, x, k_cache=None, v_cache=None, max_history=128):
        """x: (B, chunk, d). k_cache/v_cache: 上次保留的历史 K/V。"""
        B, T, C = x.shape; D = C // self.h
        q, k, v = self.qkv(x).chunk(3, dim=-1)          # 每个 (B, T, C)
        # 拼接历史 K/V
        if k_cache is not None:
            k = torch.cat([k_cache, k], dim=1)
            v = torch.cat([v_cache, v], dim=1)
            # 限制历史长度,丢掉过老的
            k = k[:, -max_history:]
            v = v[:, -max_history:]
        q = q.view(B, T, self.h, D).transpose(1, 2)     # (B, h, T, D)
        k_h = k.view(B, -1, self.h, D).transpose(1, 2)
        v_h = v.view(B, -1, self.h, D).transpose(1, 2)
        out = nn.functional.scaled_dot_product_attention(q, k_h, v_h)
        out = self.out(out.transpose(1, 2).reshape(B, T, C))
        return out, k, v                                 # 返回新 cache

有了 KV cache,每个 chunk 的增量计算量从 O(L²) 降到 O(C·L_max)——C 是 chunk 大小(<10)、L_max 是历史上限。在 L_max=128 帧 (1.28 s) 设定下,单 chunk 推理延迟在 A10 GPU 上能压到 5 ms 量级。这一点对 voice agent 体验至关重要。

实战中还有一个被严重低估的优化:把 KV cache 维护在 CPU 端,每个 chunk 临时上传 GPU。在多路并发(如客服中心 1000 路同时通话)场景下,把所有路的 cache 都常驻 GPU 显存会迅速爆 OOM;放在 CPU 内存里则单路只占几 MB,瓶颈反而落在 PCIe 上行带宽。sherpa-onnx 在 GPU 推理服务里专门优化了这一路径,实测 1000 路并发下显存占用比朴素实现降 8 倍。

6. Whisper 的流式化:把非流式模型「包装」成流式

Whisper 训练时是 30 秒固定窗口 + 双向 attention,本质上是非流式架构。但工业LD仍然有大量将 Whisper 部署成实时字幕的需求,主要靠下面三种工程包装:

方案 核心思路 延迟 典型实现
滑窗 + 部分提交 每 1–3 秒推理一次,覆盖式输出,保留前面已确认部分 1–3 s whisper-streaming
VAD 切片 + 整段推理 用 Silero VAD 检测说话段,每个段独立解码 ~段长 + 500 ms WhisperX / faster-whisper-server
LocalAgreement 多次滑窗推理后投票,重叠区域一致的部分提交 1–2 s WhisperLiveKit

这些都属于「外部 chunking + 内部完整推理」策略——模型本身不动,靠把音频切片实现「假流式」。代价是延迟下限约 1 s,达不到 voice agent 的 200 ms 体验。要做到真正的低延迟,必须在模型架构层面支持流式,这就是为什么 Google / Apple / Kyutai 在 voice agent 场景没有采用 Whisper,而是自研流式架构。

7. 流式 ASR 工业架构:VAD + Encoder + Decoder + Endpoint

Industrial streaming ASR architecture: Mic → VAD → Streaming Encoder with KV cache → Streaming Decoder → Endpoint detection → partial+final text
图 2:典型生产环境流式 ASR 架构。VAD 预过滤静音、Streaming Encoder 带 KV cache、RNN-T 或 CTC 在线解码、Endpoint 判定句末——五个模块串成的 latency budget。

典型生产架构由 5 个模块串成(图 2)。每个模块对延迟的贡献:

  1. Mic buffer:硬件最小一次 20 ms(系统层 WebRTC 标准)。
  2. VAD (Voice Activity Detection):典型 30 ms 决策延迟。Silero VAD 几乎是 2024 后的事实标准——开源、CPU 友LLO、误检率 < 1%。WebRTC 自带 VAD 太老不推荐。
  3. Streaming Encoder:取决于 chunk size。chunk=80 ms 时编码延迟约 80–120 ms。
  4. Streaming Decoder:RNN-T greedy < 10 ms / chunk;CTC beam search 略高。
  5. Endpoint detection:根据连续 N 帧 blank/silence 判定句末,典型阈值 700 ms。这是个独立的策略层,可以基于 VAD、RNN-T blank 比例、或专门训练的小模型来做。
Timeline comparison of RNN-T token-by-token streaming output and Whisper chunk-by-chunk bulk output
图 3:RNN-T 天然流式(token 逐个吐出,延迟 80–200 ms)vs Whisper 切窗流式(每个 chunk 完整解码,延迟 = chunk 长度 1–10 s)。这是为什么 voice agent 通常选 Conformer-Transducer 而非 Whisper 的根本原因。

8. 性能指标与对比

方案 模型 算法延迟 FTL (RTX 3090) WER (Libri test-other)
Whisper offline large-v3 (1.55B) 30 s ~30 s 3.9
WhisperLiveKit large-v3 + chunked ~2 s ~2.5 s 4.5
WeNet U2++ (chunk 16) Conformer-Transducer (100M) 320 ms ~120 ms 5.5
WeNet U2++ (chunk 4) 同上 80 ms ~50 ms 6.3
icefall Zipformer-Streaming Zipformer-T (70M) 320 ms ~80 ms 4.4
Moshi (end-to-end) 7B Temporal Transformer + Mimi codec 160 ms ~200 ms

这张表清晰说明了流式 ASR 的三重权衡——延迟、精度、模型大小相互掣肘。Voice agent 业务通常选 chunk 320 ms 档:综合 WER ~4.5、FTL ~100 ms、模型 ~70M 可在 CPU/GPU 边缘端部署。Whisper 这类大模型只在「精度第一、延迟次要」的离线转录场景才划算。

chunk 大小的选择有一条经验规律:把它设为「最短词的发音时长」的 2 倍左右。中文常见单字发音 150–250 ms,两个字组合(如「打开」「停止」)约 400 ms,因此中文 voice agent 取 chunk=320–480 ms 几乎都能 work;英文最短词如 “no”、”yes”、”OK” 也是 200–300 ms,同样适用。chunk 过小(< 80 ms)模型来不及形成稳定上下文,WER 退化超过 1;chunk 过大(> 800 ms)会让首字延迟超过用户感知阈值,对话体验明显迟钝。

9. 2024–2026 业LD:从级联到端到端

2024 年最重要的范式转变是端到端语音 LLM 的崛起。OpenAI 的 GPT-4o Realtime API、Google 的 Gemini Live、Kyutai 的开源 Moshi——它们把 ASR、LLM、TTS 三个传统级联模块融合到单个自回归模型,输入是音频 token,输出也是音频 token。这种架构的延迟下限不再受级联累加约束,理论上只受 codec 帧大小限制。

Moshi 为例,其全双工延迟拆解:

  • Mimi codec 帧大小:80 ms(12.5 Hz token rate)。
  • 声学到 token 延迟:80 ms(模型 forward)。
  • 理论总延迟:160 ms;实际 L4 GPU 上约 200 ms。

这种架构里,「流式 ASR」不再是独立模块——它被吸纳进语音 LLM 的输入端,与文本 token 共享同一个 Transformer。Moshi 的 “Inner Monologue” 设计让模型先预测时间对齐的文本 token、再预测 Mimi 音频 token,相当于把 ASR 隐式做在前几个 token 上。这是端到端语音建模的最新范式,但对单纯流式 ASR 场景仍然 overkill——大多数业务还是会选「VAD + Conformer-T + 端点」这套经典架构,因为它在 CPU 上就能跑、可控性高、调优工具链成熟。

另一个值得关注的趋势是「半流式」体验设计。OpenAI Realtime API 实际上不是严格 200 ms 端到端流式——它内部用了 server-side VAD 切片 + 流式 ASR + LLM 生成首 token 的延迟优化组合,对外暴露的「response.audio.delta」事件平均触发延迟约 320 ms(OpenAI 自己公布的数据)。Gemini Live 类似。这告诉我们:真正的端到端流式语音模型仍属研究前沿,主流产品依然是「经过精细工程优化的级联架构」。理解流式 ASR 各个组件的延迟贡献,比追求端到端语音 LLM 更有工业实用价值。

10. 工程化的几个深水坑

  1. 训练-推理不一致。dynamic chunk 训练时随机采样 chunk size,但推理时往往固定一档。如果训练时 chunk size 分布没覆盖到推理用的那档,会有 0.3–0.8 WER 退化。把推理目标 chunk 加进训练采样分布是必要的工程动作。
  2. SpecAugment 与 chunk mask 冲突。SpecAugment 的时间 mask 段如果跨越多个 chunk 边LD,会让模型学到「跨 chunk 引用」的错误模式。WeNet 的做法是把 SpecAugment 改成 chunk-aware 版本——mask 严格在单个 chunk 内。
  3. KV cache 内存爆炸。设 max_history 太大(> 256 帧)单流就要几十 MB,1000 路并发就爆。生产部署常把历史上限设为 96–128 帧(即 1 s 上下文),实测精度下降 < 0.2 WER 但 GPU 显存利用率从 95% 降到 30%。
  4. Endpoint 与 LLM 触发的耦合。在 voice agent 里,端点过早触发 = LLM 在用户没说完就开始想;过晚 = 反应迟钝。LiveKit AgentsPipecat 这类框架引入了「turn detection model」——专门训练一个小 BERT 判断语义上是否说完,比纯静音阈值聪明得多。这是 2024 年后 voice agent 工程的新标准做法。
  5. partial vs final。流式系统要同时输出「partial hypothesis」(每来一帧就刷新当前最佳推断)和「final hypothesis」(端点检测后的最终结果)。前端 UI 通常订阅 partial 做实时滚动字幕、订阅 final 触发下游动作。两路输出的格式约定、字符闪烁优化都需要专门设计——直接把每次 partial 全文重画会导致 UI 抖动严重。

11. 总结

流式 ASR 的本质是把一个为「全局信息」设计的网络,逼着它在只看局部时也能做出合理预测。Chunked attention 让 self-attention 看的更少;causal conv 让卷积只用过去;KV cache 让推理省一半算力;LayerNorm 替代 BatchNorm 让流式统计稳定——这些技术互相耦合,缺一不可。WeNet U2++ 的 dynamic chunk training 把这些组合优雅地统一到「一份权重多档延迟」的范式里,是当下工业 ASR 部署最成熟的方案。

对工程师而言,2026 年的现实路线选择大致这样:

  • 实时字幕 / 同传场景:WhisperLiveKit (large-v3) 精度高、调优少;延迟 1–2 s 可接受。
  • 语音助手 / 客服机器人:WeNet / icefall + Conformer-Transducer-Streaming,CPU 也能跑,FTL ~100 ms。
  • 对话式 AI Agent:要么 ASR + LLM + TTS 经典三件套(用 Silero VAD + Streaming Conformer),要么端到端语音 LLM(Moshi / GPT-4o Realtime)。
  • 移动端 / 边缘:whisper.cpp 或 sherpa-onnx 把上述模型量化部署。

把这篇放在我之前 Whisper / RNN-T / Conformer / SSL 三部曲 一起读,HE应该已经能从「算法 → backbone → 训练范式 → 工程部署」四个维度,完整理解 2026 年端到端 ASR 的整张地图。

参考资料

  1. Yao, Z. et al. WeNet: Production Oriented Streaming and Non-streaming End-to-End Speech Recognition Toolkit. arXiv:2102.01547, Interspeech 2021.
  2. Zhang, B. et al. WeNet 2.0: More Productive End-to-End Speech Recognition Toolkit. arXiv:2203.15455, Interspeech 2022.
  3. Wu, D. et al. U2++: Unified Two-pass Bidirectional End-to-end Model for Speech Recognition. arXiv:2106.05642, 2021.
  4. Défossez, A. et al. Moshi: a speech-text foundation model for real-time dialogue. arXiv:2410.00037, 2024.
  5. Sainath, T. et al. A Streaming On-device End-to-end Model Surpassing Server-side Conventional Model Quality and Latency. ICASSP 2020.
  6. Macháček, D. et al. Turning Whisper into Real-Time Transcription System. arXiv:2307.14743, 2023.(whisper-streaming)
  7. k2-fsa / icefall:github.com/k2-fsa/icefall (Zipformer-Streaming 参考实现)
  8. Silero VAD:github.com/snakers4/silero-vad
  9. WhisperLiveKit:github.com/QuentinFuxa/WhisperLiveKit
  10. LiveKit Agents:github.com/livekit/agents (turn detection model)