03 - 听:音频怎么变成文字
上一篇把用户那四秒录音切成了 200 个向量。现在要让它变成「我要一杯少冰的珍珠奶茶」,11 个字。
200 个向量对 11 个字。你手上的训练数据是几万条这样的录音,每条配一句文字 —— 注意,只配了整句。没有任何一条数据告诉你「第 37 个向量属于第 3 个字」。
你想训练模型说「看到这个向量就输出这个字」,可根本不知道每个向量对应哪个字。这种输入和输出数量对不上、又没人标出对应关系的情况,有个名字叫对齐(alignment)。
绕开它有三种办法,业界这三十年就在这三条路上来回走 —— 而且三条现在都还活着,各占各的场子。
一、对齐问题
假设你用最直白的办法:给每个向量配一个字。200 个向量、11 个字,那就得有 189 个向量对应「什么都不输出」—— 而训练数据没告诉你是哪 189 个。猜错了,模型学到的就是错的对应。
人工标注能解决,但标一小时语音要几十小时人工,训练却需要几万小时。成本上走不通。
所以三条路线是在回答同一个问题的两半:训练时怎么在没标注的情况下学、推理时怎么知道该吐字了。
这三条路线各有一个流传下来的缩写,先把名字拆开 —— 它们的名字本身就说明了各自的做法:
| 缩写 | 全称 | 中文 | 名字在说什么 |
|---|---|---|---|
| CTC | Connectionist Temporal Classification | 连接主义时序分类 | 「时序分类」=给时间轴上每一帧做一次分类。「连接主义」是上世纪对神经网络的旧称,属于历史包袱。所以字面意思就是「用神经网络逐帧分类」,它的创新在于额外发明了一个「空白」类别 |
| AED | Attention-based Encoder-Decoder | 基于注意力的编码器-解码器 | 名字就是结构本身:一个编码器把整段音频读成向量,一个解码器逐字往外吐,靠注意力机制自己决定每出一个字要回头看哪几帧 |
| Transducer (旧称 RNN-T) | Recurrent Neural Network Transducer | 循环神经网络转换器 | transducer 在信号处理里指「把一种信号转成另一种的装置」,比如换能器。这里指把声学序列转成文字序列。前面的 RNN 是历史包袱 —— 现在的实现骨干早就不是循环网络了,但名字沿用至今 |
三条路线不是替代关系,现在都还活着:CTC 至今仍是最省的关键词唤醒方案,Transducer 仍是手机端流式听写的主力,AED 则演化成了今天的 Audio LLM。
| CTC | AED(Whisper 系) | Transducer | |
|---|---|---|---|
| 怎么绕开对齐 | 枚举所有能折叠成目标句的路径求和 | 不显式对齐,靠交叉注意力自己找 | 限定每步只能出字或听下一帧 |
| 代价 | 帧与帧条件独立,没有内部语言模型 | 要听完整段才能开口,天生不流式 | 三个网络,工程复杂 |
三条路线的具体差别:
| CTC | AED(Whisper 系) | Transducer(RNN-T) | |
|---|---|---|---|
| 结构 | 编码器 + 每帧一个分类头 | 编码器 + 自回归解码器 | 编码器 + 预测网络 + 联合网络 |
| 解码步数 | 等于帧数,一次并行算完 | 等于输出字数,自回归 | 帧数 + 字数,交替推进 |
| 天然流式 | 是 | 否 | 是 |
| 内部语言模型 | 无 | 有(解码器就是) | 有(预测网络) |
| 典型失败 | 同音字选错、漏字 | 静音段幻觉、循环重复 | 删除错误偏多 |
| 现在还用在哪 | 唤醒词、强制对齐、蒸馏出的轻量模型 | 通用离线转写、多语言 | 端侧和电话流式听写 |
AED 的解码器本身就是一个完整的语言模型。即使编码器那边什么都没送过来,它照样能自己往下编 —— 表现出来就是一段没人说话的音频被转写成一句通顺的话,有时甚至是训练数据里高频出现的字幕组署名。
改不掉,因为这就是结构决定的。工程上只能靠前置 VAD(Voice Activity Detection,语音活动检测 —— 一个只判断「这段有没有人在说话」的轻量模型)把静音段直接掐掉,不让它进模型。
二、Whisper 的三十秒定长窗口
Whisper 是 AED 路线最有影响力的实现,它的很多工程约束都源自一个设计决定:编码器的输入长度是固定的 30 秒。
长音频切块这件事,SGLang-Omni 的 Qwen3-ASR 路径给了一组值得抄的默认值:模型本身能吃 1200 秒的单次输入,但服务端默认只送 30 秒一块(max_audio_clip_s),理由是短块的批处理效率更高,而且输出 token 预算会随片长自己膨胀;单个请求最多 8 块并发(max_concurrent_chunks),防止一个长上传把别人的请求挤光;整体上限 3600 秒,因为解码后的波形要整段留在内存里。还有一条容易忽略的:尾块短于 0.5 秒时,会把前一刀往前挪来把尾巴吸收掉 —— 模型对过短输入的行为不可靠。
三、编码器接大语言模型
Qwen3-ASR、Fun-ASR、MOSS-Transcribe-Diarize 走的都是同一个结构,可以概括为「把 AED 的解码器换成一个真正的大语言模型」。
官方给了这条链路的两张图。一张是流水线视角,从波形到 mel 到编码器到时间合并到投影,最后进 LLM:
docs/_static/image/moss-td-asr-pipeline.svg。中间那格 0.23 −1.47 0.89 2.01 ··· 是重点:交给 LLM 的是浮点向量,不是 token id。另一张是模型视角,来自模型自己的仓库:

代价也要说清楚:一个 1.7B 的 Audio LLM 干的是过去 300M 参 数模型的活。它换来的是多语言、热词、分离、时间戳这些能力打包在一起,但如果你的场景只是单语言、固定领域的短语音转写,传统 CTC 模型在同样硬件上能快一个量级,而且不会幻觉。不该用 Audio LLM 的判据很清晰:输出格式固定、领域封闭、对幻觉零容忍的场景(比如语音指令、车载唤醒、电话号码识别)。
四、流式识别与前瞻延迟
离线识别可以等用户说完再出结果,流式不行 —— 它要边说边出字。
矛盾在于:要听准当前这个字,模型得先听见它后面那一小段。汉语里「四」和「是」,光听那一个音节分不出来,得靠后面接的字。可「先听见后面」就意味着要等,而等待就是延迟。
这段为了听准而多等的音频,有个名字叫前瞻(look-ahead)。它有多长,基本就等于用户感觉到的卡顿有多久。
流式链路上还有两个常被低估的组件:
- VAD(语音活动检测)决定了「从哪到哪算一句」。它切得太碎会把一句话拆成几段,上下文断裂导致识别变差;切得太粗则延迟上升。绝大多数流式系统的体感问题出在 VAD 参数上,而不是识别模型上。
- 端点检测(endpointing)决定了「用户说完了没有」。这个判断直接触发下游的大模型开始回答,判早了打断用户,判晚了对话有空档。语音对话产品里,它比识别准确率更影响体感。
五、三个指标要一起看
供应商发来一句「我们的 WER 是 2.1%」,你该不该信?
信不了,因为这个数字单看没有意义 —— 换一套文本规整化规则(数字要不要转成汉字、标点算不算错、英文大小写怎么处理),同一个模型能测出相差一倍的 WER。
所以选型至少要同时问三个数,缺一个都能被糊弄过去:
| 指标 | 测什么 | 陷阱 |
|---|---|---|
| WER / CER | 转写准不准 | 依赖文本规整化规则。数字、标点、英文大小写怎么处理,能让同一个模型的 WER 相差一倍 |
| RTFx | 吞吐(一秒能转写多少秒音频) | 是并发下的总吞吐,不是单请求速度。RTFx 500 可能来自「并发 32 每路 RTFx 15」 |
| 延迟 p95 | 体感 | 均值毫无意义。ASR 的延迟分布是长尾的,长音频和短音频混在一起时尤其明显 |
下面是一组真实测量(SGLang-Omni 在一张 H100 上跑 Qwen3-ASR-1.7B,SeedTTS 英文集 1088 条),把吞吐和延迟画在一起就能看出问题:
卡闲着,堆并发却没用 —— 这个组合说明瓶颈不在算力。它在准入排队、请求构建、以及自回归每一步在 CPU 侧的调度开销上。
一组真实的数字(SGLang-Omni 在 H100 上跑 Qwen3-ASR-1.7B,SeedTTS 中文集 2020 条):并发 1 时 14.3 req/s、RTFx 66.9、延迟均值 70 ms;并发 32 时 126.4 req/s、RTFx 591.6、延迟均值 252 ms;并发 64 时吞吐不再上升(129.2 req/s),延迟却翻倍到 489 ms,还开始丢请求。这个「拐点」的成因和排查过程,是07 篇的主要案例。
这里先给结论中最反直觉的一条:在那个拐点上,GPU 利用率只有 33%~54%。ASR 服务的瓶颈通常不在 GPU 算力,而在这几处:请求排队等着被放进运行批(这一步叫准入)、把请求组装成模型输入、以及自回归每一步在 CPU 侧的调度开销。拿到一个「ASR 服务不够快」的问题时,先看 GPU 利用率,再决定往哪个方向查。
下一篇:04 - TTS 的两段式结构,反过来看文字怎么变成波形。