一、问题背景
在做本地化 ASR 部署时,模型效果不能只看一次测试的准确率。
这次遇到的问题比较典型:前期测试Qwen3-ASR-MNN时,正常语音识别效果不错,尤其是中文连续语音,整体表现比较有吸引力。但在接入真实会议音频后,出现了一个比较棘手的问题:
模型有时候识别得很好,有时候会出现明显的“幻觉”文本。
所谓 ASR 幻觉,并不是普通的错别字,而是音频中没有说过某些内容,模型却输出了完整的文字。
例如原始音频基本处于静音状态:
[无有效语音]模型却可能输出类似:
好的,下面我们继续介绍今天的会议内容。或者在一句话结束之后继续生成:
谢谢大家的观看,今天的内容就分享到这里。这类结果对于会议转写来说比普通识别错误更加危险。
因为普通错字通常比较容易发现,而幻觉文本看起来往往非常通顺,用户反而可能认为它是真实会议内容。
最终的问题就变成了:
因此,这次没有继续围绕量化参数反复调优,而是重新评估了模型选型,最终切换到SenseVoice 非量化模型进行验证。
二、先搞清楚什么是ASR幻觉
ASR 幻觉可以简单理解为:
模型输出的文本与实际音频内容没有对应关系。
常见表现主要有三类。
1. 静音幻觉
音频没有明显语音:
音频 → 静音模型输出:
好的,我们开始今天的会议。2. 尾部重复
用户已经停止讲话:
我们今天主要讨论服务器部署问题。模型继续输出:
我们今天主要讨论服务器部署问题,谢谢大家。3. 上下文补全文本
用户只说:
这个问题我们后面再确认。模型却生成:
这个问题我们后面再确认,具体方案会在下一次会议中进行详细讨论。从语言表达上看非常自然,但实际上并没有对应的语音。
这也是 ASR 幻觉最麻烦的地方。
三、为什么“效果好”还不能直接上线?
模型上线之前需要考虑的不只是准确率,还包括:
- 稳定性;
- 长音频表现;
- 静音处理;
- 噪声环境;
- 重复输出;
- 边界处理;
- CPU/GPU占用;
- 内存稳定性;
- 推理速度。
特别是会议系统。
假设一场会议:
60分钟其中真正有人讲话的时间可能只有:
45分钟剩下:
15分钟可能是:
- 思考停顿;
- 翻页;
- 讨论间隙;
- 设备碰撞声;
- 空调声;
- 键盘声。
如果模型在这些区域产生文字,最终会议纪要就会混入不存在的信息。
因此实际工程中应该关注:
识别准确率 + 幻觉率 + 稳定性而不是只看 CER。
四、第一步:建立幻觉测试集
这次排查没有直接重新训练模型,而是先把问题固定下来。
建立了一组专门测试 ASR 幻觉的数据。
测试集 ├── 正常语音 ├── 短静音 ├── 长静音 ├── 背景噪声 ├── 键盘声 ├── 咳嗽 ├── 掌声 ├── 说话结束后的尾部静音 └── 长会议录音这样可以避免:
换一个音频 ↓ 模型表现不同 ↓ 无法判断问题是否真正解决五、用VAD把问题拆开
ASR 系统中,一个非常重要的前置组件就是VAD(Voice Activity Detection)。
VAD负责判断:
当前音频有没有人在说话?简单来说:
如果没有 VAD:
静音和噪声就可能增加模型产生异常文本的机会。
因此,在工程部署中,不能简单认为:
ASR模型效果好,所以直接把所有音频送进去。
应该先考虑完整的音频处理链路。
六、为什么最终选择SenseVoice非量化模型?
经过实际测试后,选择 SenseVoice 非量化版本进行替换。
SenseVoice 是一个支持 ASR、语言识别、情感识别和音频事件检测的语音基础模型;官方公开的 SenseVoiceSmall 支持中文、粤语、英语、日语和韩语。其官方实现采用非自回归端到端架构,并提供 FunASR 推理和部署方案。
这里特别强调:
选择 SenseVoice 并不是因为“量化模型一定不好”。
真正的原因是:
也就是说,这是一次工程上的模型取舍。
七、为什么先使用非量化版本?
量化的核心目的是:
但量化也可能带来一定精度损失。
对于普通识别错误:
服务器 → 服务器可能影响不明显。
但是对于一些边界场景:
静音 噪声 短语音 尾部截断模型输出行为可能发生变化。
因此在这次问题中,优先采用:
非量化模型 + VAD + 统一测试集先确认模型本身的稳定性,再考虑后续压缩。
八、SenseVoice推理测试
官方 FunASR 示例支持通过AutoModel调用 SenseVoice,并可以配合 FSMN-VAD 对长音频进行切分。官方文档还提供了use_itn、batch_size_s、merge_vad等参数用于不同推理场景。
一个基础测试代码如下:
from funasr import AutoModel model = AutoModel( model="iic/SenseVoiceSmall", vad_model="fsmn-vad", vad_kwargs={ "max_single_segment_time": 30000 }, device="cuda:0" ) result = model.generate( input="meeting.wav", language="zh", use_itn=True, batch_size_s=60 ) for item in result: print(item)这里有一个比较重要的参数:
max_single_segment_time=30000即最大单段约 30 秒。
长音频不要简单地一次性全部送进编码器,而应该结合 VAD 或窗口机制进行处理。SenseVoice 官方文档也明确提供了长音频分段方案。
九、增加幻觉检测层
实际项目中,不建议完全依赖模型自己解决幻觉。
可以在 ASR 后增加一个简单的规则检测层。
例如:
def check_result(text, audio_duration): if audio_duration > 3 and not text.strip(): return "empty" if len(text) > 80 and audio_duration < 2: return "suspicious" return "normal"当然,真正上线时规则不能这么简单。
可以进一步增加:
VAD语音占比 + 文本长度 + 平均置信度 + 重复率 + 时间戳形成综合判断。
十、增加重复文本检测
ASR 幻觉中还有一个比较常见的问题:
重复输出。
例如:
今天我们讨论系统部署方案 今天我们讨论系统部署方案 今天我们讨论系统部署方案可以使用简单的文本相似度进行检测:
def repetition_ratio(text): parts = text.split(",") if len(parts) < 2: return 0 same = 0 for i in range(1, len(parts)): if parts[i] == parts[i - 1]: same += 1 return same / (len(parts) - 1)如果:
repetition_ratio > threshold则进入异常结果处理。
实际工程中还可以使用:
- 编辑距离;
- N-gram;
- SimHash;
- embedding 相似度。
十一、最终测试方法
模型替换以后,没有直接看某一个音频,而是统一跑完整测试集。
重点比较:
| 指标 | Qwen3-ASR-MNN | SenseVoice非量化 |
|---|---|---|
| 正常语音CER | 测试 | 测试 |
| 静音幻觉次数 | 测试 | 测试 |
| 尾部幻觉次数 | 测试 | 测试 |
| 重复输出次数 | 测试 | 测试 |
| 平均推理耗时 | 测试 | 测试 |
| 峰值内存 | 测试 | 测试 |
| 长音频稳定性 | 测试 | 测试 |
这里不建议为了文章好看而虚构具体数字。
真实项目应该直接使用自己的测试数据填写。
十二、为什么最终没有继续优化Qwen3-ASR-MNN?
如果模型只是偶尔出现一个错字:
优化数据 → 继续微调是比较合理的。
但是当问题变成:
模型输出不存在于音频中的内容就需要换一个思路。
因为这已经从:
识别准确率问题
变成了:
系统可靠性问题。
尤其是会议场景。
假设一句不存在的话进入会议纪要:
会议决定下周一正式上线。如果实际会议从未说过这句话,后面的摘要、知识库、任务提取甚至检索系统都有可能继续使用这条错误信息。
因此:
幻觉可能会沿着整个数据链路继续传播。
十三、这次问题的技术沉淀
最终形成了一套比较适合实际项目的处理思路:
模型本身只是其中一个环节。
对于本地部署的智能会议产品而言,真正需要关注的是整个链路是否稳定,而不是某一个 benchmark 指标是否漂亮。
十四、总结
这次从 Qwen3-ASR-MNN 切换到 SenseVoice 非量化模型,核心并不是简单的“模型A比模型B好”。
真正解决的问题是:
当一个模型存在偶发幻觉,而且业务又对错误内容比较敏感时,应该优先解决稳定性,而不是继续追求单点指标。
最终采用的方案可以概括为:
SenseVoice 官方生态目前也提供了 VAD、标点、说话人相关组件以及 ONNX、Libtorch、CPU/边缘端等部署路径,因此后续如果需要进一步优化部署性能,可以在先保证识别稳定性的前提下,再逐步尝试量化和推理加速。
对于 ASR 工程来说,“识别得准”只是第一步,“没有莫名其妙生成不存在的内容”同样重要。
尤其是会议转写这种结果会继续进入摘要、知识库和业务系统的场景,稳定性往往比一次测试中的几个百分点更值得关注。