LSP 最长稳定前缀:一个范式,同时治好流式翻译和流式 ASR 的「改口」病

LSP 最长稳定前缀:一个范式,同时治好流式翻译和流式 ASR 的「改口」病

论文(原理):Dynamic Lagging using Stable-Prefix Training for Simultaneous Translation

开源项目(落地):netease-youdao/Confucius4-R2T2(核心文件 r2t2/r2t2_asr.py


1. 前言:「改口」是所有流式任务的通病

你有没有盯着直播字幕看过?说话人还没说完,字幕先蹦出来一版,过两秒又被悄悄改掉——”我们打算” 变成 “我们不打算”,屏幕闪一下。这种改来改去在很多场景是致命的:字幕要给同传译员看、要喂给下游的 NLP / LLM Agent、要立刻触发操作,只要前面吐出去的字还可能反悔,下游就没法马上动手。

所以真正的流式系统有个很硬的要求,叫 append-only(只增不改):吐出去的每一个字都是已经拍板、永不回改的,后面只能往后接,不许回头修。

有意思的是,这个「改口」病不是某一个任务独有的,它同时出现在两类看似不相关的任务里:

  • 流式翻译(同声传译):源文一个词一个词地来,你要是每来一个词就把当前这段整句重译,早期的译文经常和最终译文对不上,被推翻重写。
  • 流式 ASR(语音识别):音频一块一块地来,边听边转的中间结果会随着后续音频不断被回改。

这篇文章想讲清楚一件事:这两个病其实是同一个病,而且有一套统一的解法,叫 LSP(Longest Stable Prefix,最长稳定前缀)。我会先用一篇 arXiv 论文把 LSP 的原理讲透(它在翻译上落地),再用网易有道开源的 R2T2 把 LSP 的工程实现拆开(它在 ASR 上落地),最后看看 ASR 接上 LSP 之后,效果到底怎么样。

2. LSP 是什么:一个跨任务的范式

先给个总的心理模型(下图)。LSP 不是网络里的某个模块,而是「训练范式 + 推理协议」的组合,它和具体任务无关,MT 和 ASR 都能套。

LSP 一个范式两处落地

  • 训练侧:用一个离线的强模型当「老师」,把「源前缀里迟早不会变的那段」标出来(这段就叫 stable prefix / 稳定前缀),拿这些数据去微调模型,让它学会一件事——给定目前收到的输入,我到底能确定到哪、该提交多少、剩下的该等
  • 推理侧:已经提交出去的前缀被回灌当上下文,模型只能在它后面接着写;再配合模型自己标出的稳定边界,只提交稳定的那一段,不稳定的尾巴丢掉。

两侧合起来,就保证了 append-only:吐出去的绝不回改。翻译上,这套东西由一篇 arXiv 论文给出了完整的原理和实验;ASR 上,由 R2T2 给出了完整的代码落地。下面分别拆。

3. 论文视角:stable prefix 训练到底怎么实现

先看 LSP 的原理。论文《Dynamic Lagging using Stable-Prefix Training》做的是同声传译(simultaneous translation:输入没说完就开始翻),但它把「稳定前缀怎么造、怎么训、怎么推」讲得很完整——可以直接当作 R2T2 那份还没放出来的技术报告的学术版来读

3.1 先看「改口」在翻译里长什么样

论文给了一个特别形象的德语例子(下图上半部分)。源句是英文 “Does he playfully tease you?”,完整正确的德译是 “Neckt er dich spielerisch?”。可你要是一个词一个词喂进去看模型怎么翻:

翻译改口与稳定前缀对比

读到 “Does” 译成 “Macht”,读到 “Does he” 变 “Tut er das”,读到 “Does he playfully” 又变 “Tut er das spielerisch”,直到读到 “Does he playfully tease”,才第一次吐出以 “Neckt er” 开头、和最终译文对得上的结果。前三步全是错的方向,得推翻重写——这就是「改口」。

3.2 stable prefix:只训练「迟早不会变」的那段

论文的解法:既然部分翻译会抖,那就只拿「迟早不会被推翻」的部分去训练。构造方法(假设手头有个翻得不错的 MT 系统 t 当老师):

  1. 老师把整句 s 翻出来得全句译文 f = t(s);
  2. 老师把每个源前缀 s_1..i 也翻一遍得 p_i = t(s_1..i);
  3. 算 p_i 与 f 的最长公共前缀长度 ℓ_i(英译德按词、英译中日按字符);
  4. 源前缀 s_1..i 的稳定前缀 = f 的前 m_i 个单位,m_i = max(ℓ_1..ℓ_i)(运行最大值)

第 4 步的运行最大值是精髓:它保证稳定前缀单调不缩——一旦某段和全句译文对上、提交了,后面再嘈杂的部分翻译也撤不回它。对照下表最右列(就是抽出来的 stable prefix):

德语稳定前缀抽取示例

前三步没有公共前缀,稳定前缀是空的(学到的是”这时候啥都别急着提交”);第 4 步 “Neckt er” 对上了才第一次有内容。把这种「只留稳定段、砍掉抖动尾巴」的语料和全句译文混起来微调,模型就学会了克制。这也正是 R2T2 README 里说的三类特制数据之一——stable-prefix data。

3.3 推理:forced prefix 把「说过的不许改」变成硬约束

训练数据有了,推理时怎么保证不改口?论文用 forced prefix(强制前缀)(下图):

forced prefix 增量提示流程

源文分块到来,每一轮把上一轮已提交的译文作为 forced prefix 强制解码(等于告诉模型”开头这些照抄、不许改”),模型只往后补新的续写。比如 “Scientists discovered a new” 先译出 “Wissenschaftler entdeckten eine neue”,下一块 “species” 到了就锁定前面、只补 “Art”。这叫 flicker-free by construction(结构上就不可能改口)——模型能看到之前的译文当上下文,但改不了它。

记住这两个词:stable prefix(训练侧)和 forced prefix(推理侧)。 待会你会看到 R2T2 的代码里,一模一样的两件事换了个马甲又出现了一遍。

3.4 三个延迟旋钮:一份权重滑出一整条曲线

同一个模型怎么在「快但糙」和「慢但准」之间调档?论文给了三个推理期旋钮:wait-k(先读 k 个词再开译)、target suffix deletion(提交前砍掉末尾 d 个不可靠 token)、confidence(置信度阈值 τ)。其中置信度机制最关键、也最通用:

Δ = P(最可能的非结束符) − P(结束符 EOS)

Δ ≥ τ 就 COMMIT(提交),Δ < τ 就 STOP(停下,去读更多输入)。τ 往 −1 拧越激进、延迟越低;往 +1 拧越谨慎,直到退化成等整句的离线翻译。「提交还是再等一等」这个二选一,是所有流式任务的核心——记住它,第 4 节 R2T2 里那个 <wait> 就是同一个决策。

论文的实验也证明了范式有效:微调后模型在质量-延迟前沿上大幅优于基线,而且最坏情况的部分翻译质量(Worst COMET)被大幅托住(英译德 80.9→85.1、英译日 81.3→90.0)——中途任何时刻都不崩。这条曲线长这样(越靠左上越好,蓝线是稳定前缀模型):

质量-延迟权衡前沿

4. R2T2 视角:把 LSP 落到 ASR 代码里

原理清楚了,来看工程落地。R2T2(Real Real-Time Transcription,那个重复的 Real 就是在强调「吐出去就算数」)是网易有道开源的真流式 ASR,核心问题是:能不能让一个离线强模型(Qwen3-ASR),学会边听边稳定吐字、且吐出去绝不反悔?

4.1 先认识底座:Qwen3-ASR

Qwen3-ASR 三段式底座

Qwen3-ASR 是把音频接到大语言模型上的那类 ASR,三段式一条链:音频先进 AuT 音频编码器(Audio Transformer,一个单独预训练好的编码器,把 16kHz 波形算成 128 维 Fbank、卷积 8 倍降采样,吐出 12.5Hz 的音频 embedding,即每 80ms 一个向量;它用动态注意力窗 1–8s,所以天生既能吞长音频做离线、也能只喂一小块做流式);音频特征过 projector(投影层,一个学出来的线性映射) 对齐到 LLM 词向量空间;最后塞进 Qwen3 LLM 解码器自回归续写文字,输出带元信息格式 language Chinese<asr_text>今天天气不错

4.2 R2T2 改结构了吗?——没有,纯微调

结论先摆这儿:这张图一块都没动。R2T2 是 Qwen3-ASR 的一个微调 checkpoint + 一层纯 Python 推理调度。 四条证据:

  • r2t2/r2t2_asr.pyclass R2T2ASRModel(Qwen3ASRModel) 直接继承,离线接口 transcribe() 一行透传父类;整个文件没定义一个新 nn.Module、没碰 attention、没改 config,加的全是 init_streaming_state / streaming_transcribe 这类推理调度方法。
  • 模型类、config、processor 全来自阿里官方 qwen-asr 包(依赖直接写着 qwen-asr[vllm]),仓库自己一个模型定义文件都没有。
  • llama.cpp 路线用标准 mmproj + *.gguf 两件套,跟任何 Qwen3-ASR/Omni 的 GGUF 拆分一样——结构改了这套通用工具链就接不上。
  • 最有说服力的:benchmark 里 Qwen3-ASR base 160ms(没做流式微调、硬塞进 160ms 分块)和 R2T2 跑的是同一套结构、同一套推理包装,结果一个惨不忍睹、一个接近离线。提升只可能来自训练范式 + 权重,不是来自改网络。

4.3 LSP 在 R2T2 里怎么落地:emit 还是 wait

R2T2 用了三类特制数据训练(stable-prefix data、forced time-alignment data、token-level audio segmentation),配合 LSP 范式,让解码器每走一步做一个二选一决策(下图):

LSP 决策 emit 还是 wait

要么 emit 吐一个真 token(今 / 天 / 气),要么吐一个 <wait>——意思是”当前卡在音频的模糊过渡区(interference region),先别急,等下一块”。这个 emit/wait 决策,就是第 3.4 节论文里 Δ≥τ 就 commit、否则 stop 的同一件事,只是 ASR 把它显式化成了一个 <wait> token。图里右边那条回灌线是重点:已提交的稳定前缀会被 tokenize 后重新塞回解码器当 context——这正是论文的 forced prefix 换了个马甲:已拍板的字当既定事实喂回去,模型物理上没机会重写前面。

推理侧还有个边界符:模型被训练成会主动吐一个 |(或 #)标记”我确信到这为止”,| 之后是对没听全部分的抢跑猜测,推理时直接丢。代码里满地的 .split("|")[0] 就是干这个的(llama_native_backend.py:446)。(|/# 具体怎么监督出来的是从代码行为反推的,技术报告没出。)

4.4 流式推理五步走

一块音频进来,streaming_transcriber2t2_asr.py:302)做五件事,对着下面这张时序图看(灰=锁定前缀、绿=本轮新吐、红=被 | 切掉的抢跑):

流式推理时序

  1. 攒音频:新 PCM 追加进 buffer,每攒够一个 chunk(chunk_size_sec,低延迟配 0.16 秒)就消费掉,追加到 audio_accum从流开始到现在的全量音频,每步重喂)。
  2. 拼前缀:把上一轮已提交文本当续写起点喂回去(这就是 forced prefix);在模型给的稳定前缀上再手动回退最后 k 个 tokenunfixed_token_num,边界处最抖的几个字,默认 1)兜底;unfixed_chunk_num 管冷启动(前几块太短就不给前缀)。
  3. 喂 vLLM 出结果:模型从 prefix 之后开始续写——已提交的字被钉进 prompt,只能往后接。
  4. 切稳定前缀prefix + 新生成 拼成完整假设,按 | 砍掉抢跑尾巴,返回本轮新确定、可安全 append 的那段。
  5. 节流max_new_tokens 给得很小(=4,约对应 12.5Hz 帧率下一块音频的产出预算),让嘴巴始终追着耳朵走,不跑到音频前面瞎编。

长音频再套滑窗(streaming_transcribe_no_reset:累计超 16s 就丢最早 8s 及对应已提交文本);真上线还套一层 VAD(Voice Activity Detection,语音活动/静音检测,这里用 FireRedVAD:检测到一句话说完就收尾 + 状态清零,避免上下文无限拉长),每条只推增量文本给前端,天然 append 不闪。

5. ASR × LSP:效果到底如何

这是大家最关心的部分。R2T2 的 README 摆了一张很有说服力的对照表,我抽几个有代表性的数据集,列出四种配置的错误率(英文 WER / 中文 CER,单位 %,越低越好,即”要改动多少词/字才能把识别结果变成标准答案”):

英文 · WER Qwen3-ASR base 160ms(硬切) R2T2 160ms X-ASR 160ms(最强其它开源) Qwen3-ASR 2s(伪流式,允许回改)
LibriSpeech-clean 22.30 2.13 3.86 1.67
LibriSpeech-other 25.74 4.88 9.64 3.54
AMI(会议) 24.79 11.37 14.41 9.25
TED-LIUM 19.18 3.34 3.75 2.33
EN-RealSI(真实同传) 13.75 8.40 8.97 6.54
中文 · CER Qwen3-ASR base 160ms R2T2 160ms X-ASR 160ms Qwen3-ASR 2s(伪流式)
WenetSpeech-net 19.79 5.87 8.81 4.94
WenetSpeech-meeting 20.38 7.27 11.33 5.97
CN-RealSI(真实同传) 39.72 3.48 4.92 3.34

几个要点:

  • 同结构同框架,差别只在权重Qwen3-ASR base 160ms 和 R2T2 是同一套模型、同一套推理包装,硬切基线 LS-clean 崩到 22.30,R2T2 微调后回到 2.13,差十倍;CN-RealSI 从 39.72 干到 3.48。这个十倍的差距,纯粹来自 LSP 训练范式。
  • 逼近伪流式 / 离线:最右列 Qwen3-ASR 2s 是用 2 秒大块、且允许回改的伪流式参考(不满足 append-only)。R2T2 在 160ms、严格 append-only 的约束下,精度已经很贴近它了。
  • 吊打其它真流式开源模型:160ms 这一档里,R2T2 稳稳压过 X-ASR、Nemotron、Voxtral 等(后两者在中文上基本不能打),部分数据集追平商用系统。
  • 延迟与代价:平均延迟 200–600ms,chunk 80ms–2s 可配(对应第 3.4 节那条延迟-质量曲线,R2T2 也画了 accuracy-latency Pareto 前沿,和论文那张 quality-latency 前沿是一个味道);而且加了流式不掉离线精度

顺带一提:EN-RealSI / CN-RealSI 这俩「真实同传」测试集,和论文的同传任务正好对上——这不是巧合,下一节说。

6. 合起来看:LSP 把 ASR→MT 串成不改口的流水线

现在把两半拼起来。R2T2 是流式 ASR(LSP 落在 ASR),论文是流式 MT(stable prefix 落在翻译),它们用的是同一套「稳定前缀 + 提交即锁定」的思想。于是可以很自然地级联(下图):

ASR→MT 全链路不改口流水线

语音流 → R2T2 流式 ASR → 稳定转写前缀 → 流式 MT → 稳定译文,每一段都 append-only。关键在于:上游 ASR 不改口,下游 MT 才不会被迫连锁重译——如果 ASR 的中间结果反复回改,MT 每次都得推翻重来,整条链路根本没法低延迟。两段都用 LSP,整条链路才真正 flicker-free。这也不是硬凑:R2T2 的 README 自己就把 Simultaneous Speech Translation(同声传译)列为目标场景,还专门做了 EN-RealSI / CN-RealSI 真实同传测试集。

7. 收尾:LSP 这套思路到底聪明在哪

回到开头那个「改口」病。翻译和 ASR 看起来八竿子打不着,但它们的流式版本卡在同一个点上:输入没到齐时,早期输出不可靠、会反悔。LSP 给的解法有两个可复用的内核:

  1. 把稳定性构造进训练数据:不去发明复杂的流式网络结构,而是用一个离线强模型当老师,把「迟早不会变」的稳定前缀从数据里框出来,让模型从数据里学会克制。R2T2 那张对照表最能说明问题——对 LLM-based 的模型,”教会它流式行为”比”给它改个流式结构”性价比高得多(改结构要重训、要改推理引擎、生态全断;R2T2 底座、权重格式、部署工具链全复用了 Qwen3-ASR 的现成生态)。
  2. 用模型自身的信号做读写决策:不管是论文的 Δ≥τ、还是 R2T2 的 <wait> token + | 边界符,本质都是让模型自己判断”这段能提交了没”,而不是靠外部硬规则去猜。

对做流式语音 / 实时任务的同学,我觉得最值得抄的就是这两点。任何”边接收边输出、又不希望反复改口”的任务,都可以先问自己一句:能不能用一个离线的强模型,把”稳定的那部分”从数据里先框出来,再教模型自己判断什么时候该吐、什么时候该等?

当然也有代价:全量音频重喂在长音频上有算力浪费(所以要滑窗),| 边界符 + 手动回退这套启发式的稳定性上限,最终还是取决于模型被训得多好——这也是为什么大头都压在训练范式上。等 R2T2 的技术报告和论文的更多细节出来,LSP 的训练目标值得再回来补一篇。


扯句题外话:我平时的研究方向是 LLM 推理效率和模型压缩,跟这篇的”流式推理调度”算是隔壁工位。顺带说一下,我们把 AutoML / NAS 到大模型压缩这条线整理成了一本《动手学 AutoML:从 NAS 到大语言模型优化实战》——说实话它不覆盖 ASR、也不讲流式推理框架,跟本文不是一个话题;但如果你对”模型效率”这个大方向本身感兴趣(LLM 的剪枝 / 量化 / 架构搜索这些让模型更小更快的手段),书里有完整章节和从零实现,角度不同、目标相近,可以当个延伸。

动手学AutoML书籍封面

Flag Counter