语音接大模型:转写全对,为什么回答还是错的
版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何破解、盗版软件,也不推荐任何规避地区限制的访问方式,亦不对任何收益结果作承诺。转载请注明出处。
一、抽检全对,回答全错
先说一个很常见的场景。设备巡检的师傅按住说话,说三五秒,语音先过识别,再送大模型,模型返回一条操作建议或一次数据查询结果。上线之后,业务侧的反馈是「它老答错」。技术侧先查识别,每天抽一批录音,人耳复听,跟转写逐字比对。
比对的结果是:字基本都对。偶尔有标点差异,偶尔少一个语气词。连续看两周,字面错误少到可以忽略。于是结论很快定下来——识别没问题,问题在模型,去调提示词、换模型、加检索。
这个结论通常是错的。错不在判断,在抽检本身看的维度。
抽检看的是「字对不对」,而下游要的是「结构对不对」。这两件事之间隔着一整条链路:识别吐出片段,工程代码把片段拼成一段文本,这段文本才是模型的输入。拼接这一步删掉了大量字段,而删掉的那些字段,恰好是模型判断语义边界、指代对象、数值归属的依据。字一个没错,结构全丢了。
更麻烦的是,抽检样本天然偏向容易核对的样本:人会下意识挑安静环境、单人、短句、口音轻的录音,因为这种样本好比对。而线上出错的,往往是多人、长句、带型号和数字、说到一半改口的那一类。抽样偏了,指标就永远好看。
| 抽检时看的 | 实际漏掉的东西 | 在下游的表现 |
|---|---|---|
| 每个字是否与复听一致 | 片段之间的边界落在哪 | 一句被拆成两个意图,或两句并成一句 |
| 整段读起来是否通顺 | 数字与单位的搭配是否被改 | 数值算错,但句子读起来毫无破绽 |
| 单条样本本身是否正确 | 多轮之间说话人是否搞混 | 指代指向了错误的人 |
| 有没有明显错字 | 出错位置对应第几秒 | 无法复现,无法回归,无法回听 |
还有一个组织层面的原因:「转写→模型输入」这一段不属于任何人的职责。做识别的人交文本,做应用的人收文本,中间那层拼接代码往往是某个下午顺手写的,没有测试,没有对照集,也没人评审。结构性的信息就丢在这段没人认领的代码里。
二、识别交出来的是片段,不是一段文字
先看识别实际交出来的东西。以 Whisper 系实现为例,返回值不是一个字符串,而是一串片段,每段至少带这些字段:起始时间、结束时间、文本、对应的 token、平均对数概率、非语音概率。切分依据是声学停顿和窗口长度——很多实现按三十秒滑窗处理,段间还有重叠。
工程里最常见的写法,是一行"\n".join(seg["text"] for seg in segments)。这一行之后,五个字段里只剩文本,另外四个连同片段边界一起消失。消失的东西可以列清楚:
- 边界:停顿落在哪、一段有多长、段与段是不是同一句话;
- 说话人:这段是谁说的(如果做了分离的话);
- 把握程度:哪一段模型自己也拿不准,哪一段是硬猜的;
- 对齐:哪个字对应第几秒;
- 覆盖关系:流式场景下哪些是中间结果、哪些是最终结果。
这五类信息里,只有「字」被抽检覆盖了。其余四类,从删除的那一刻起就再也没有人看过。
这里要说清一件事,免得把方向搞反:通用语音识别确实做得足够好,字面错误率低到抽检看不出来——这不是错觉。Radford 等人在Robust Speech Recognition via Large-Scale Weak Supervision(arXiv:2212.04356)的摘要里写的是:在 68 万小时多语言、多任务弱监督数据上训练的模型,在标准基准上零样本迁移即可与先前的全监督结果相比,并且「When compared to humans, the models approach their accuracy and robustness」。注意用词是 approach——接近,不是超过;比较对象是论文选定的基线与测试集,不是你车间里那台设备、你那批型号、你那位带口音的师傅。摘要里没有给出任何可以直接搬到具体业务上的准确率数字,本文也就不引用这类数字。
这句话的真正用处,是解释为什么整条链路会集体误判:通用识别足够好,字面错误足够少,注意力自然转向大模型。但「字对」和「意思对」之间隔着一层结构,而结构在拼接那一行被删掉了——这正是后面五层失真的共同源头。
| 层次 | 丢掉的结构 | 典型表现 | 下游直接后果 |
|---|---|---|---|
| 一 | 分句边界 | 长句被切成两段,或两句并成一段 | 意图被拆开或合并,槽位串到别的请求上 |
| 二 | 音到形的唯一性 | 四点七变四点一,毫安变毫安时 | 数值、单位、型号进计算或检索,整条答案错 |
| 三 | 语句完整性 | 尾词还在路上就发起请求 | 模型按先验补全,答非所问 |
| 四 | 说话人归属 | 两个人说的话被拼成一段 | 指代无解,立场混成一个人 |
| 五 | 时间对齐 | 只剩整段文本,没有秒数 | 出错无法定位、无法回归、无法复核 |
三、第一层:分句边界丢失
现象:识别给出的边界是声学的,不是语法的。标点由后处理模型补——补标点的模型看到的是切好的片段,只能尽力把逗号句号塞进去,没有办法还原说话人本来在哪里断句。于是专业长句一到,边界必错。
三种错法最典型:
- 并列枚举被切断。「一号泵、二号泵、三号泵的轴承温度」中间换一口气,前半段就单独成段,后半段的「的轴承温度」失去修饰对象,变成一个孤立短语。
- 条件句被切断。「如果压力超过零点六兆帕,就先把旁路打开」,前半句单独送进去,模型读到的是一个陈述句而不是条件句,它会去解释「压力超过零点六」是什么状态,而不是给出「打开旁路」这个动作。
- 两句被并成一句。说完「三号泵怎么样」紧接着说「上周的记录调出来」,中间停顿短,被合成一段,模型读到的是一个语义混乱的长请求。
为什么下游会答错:意图分类和槽位抽取的输入单位是「句」。边界一错,一个请求被拆成两个不完整的请求,或者两个请求被合成一个语义混乱的请求。前者的表现是「只答了一半」,后者的表现是「答了另一件事」,两种在日志里都长得完全正常。
再往深一层,是指代范围变了。中文里「它」「这个」「那个」的先行语通常在同一句或上一句里,句子边界一错,搜索范围就错。用户说「三号泵振动偏高。它上周才换的轴承」——如果把两句并成一句,模型很可能把「它」理解成轴承而不是泵,后面整段建议就围绕轴承展开。
第三层原因是框架假设:编排框架把一次请求当成一条消息,不会回头质疑「你这句话是不是被切了」,切错了也照单全收。
修法方向:不要拿识别的切分当句子边界,而是用「停顿阈值 + 句末特征 + 最长长度」重新聚合,并且把每段的秒数一起留着。下面这段只用标准库,把片段按 1.2 秒以上的停顿聚成句,输出带时间范围:
segs=[{"start":0.0,"end":2.4,"text":"三号泵的振动值"},{"start":2.4,"end":4.1,"text":"上周是四点七"},{"start":4.1,"end":5.0,"text":"毫米每秒"},{"start":6.8,"end":9.2,"text":"今天复测变成四点一"},]defregroup(segs,pause=1.2):out,buf,t0,prev_end=[],[],None,Noneforsinsegs:ifprev_endisnotNoneands["start"]-prev_end>=pause:out.append({"start":t0,"end":prev_end,"text":"".join(buf)})buf=[]ifnotbuf:t0=s["start"]buf.append(s["text"])prev_end=s["end"]ifbuf:out.append({"start":t0,"end":prev_end,"text":"".join(buf)})returnoutforsinregroup(segs):print(f"[{s['start']:05.2f}-{s['end']:05.2f}]{s['text']}")输出两行:[00.00-05.00] 三号泵的振动值上周是四点七毫米每秒和[06.80-09.20] 今天复测变成四点一。
第二句没有单位。这就是问题本身——单位在上一段里,聚合规则按停顿切,把「毫米每秒」留在了前一句。所以聚合之后还得补一步「单位与量词向后继承」,或者干脆把相邻两段一起送。很多人做到聚合就停了,结果单位仍然错位,只是错得更隐蔽。
四、第二层:同音与专名,错得很像对的
现象:同音替换不会破坏句子的通顺度,反而常被语言模型「顺」得更通顺。这让它成为人工抽检里最难发现的一类错误,也是下游最致命的一类。
| 类型 | 说的是 | 转写成 | 下游怎么错 |
|---|---|---|---|
| 数字近音 | 四点七 | 四点一 | 进趋势判断,结论从上升变下降 |
| 单位漏字 | 四千八百毫安时 | 四千八百毫安 | 按容量推续航,差一个电压倍数 |
| 型号混读 | A 型密封圈 | 诶型密封圈 | 库存检索查不到,模型改为泛泛而谈 |
| 专名同音 | 淬火 | 退火 | 工艺参数整段给错,且看不出错 |
| 数字写法混用 | 十二点五 | 12.5 或十二点五 | 抽取规则漏掉一种写法,字段为空 |
为什么下游会答错,四个不同的机制:
数值会进二次计算。模型不是在复述这个数,而是拿它算差值、算变化、和阈值比较。输入错一位,输出整段结论反向,而措辞依然自信。「由 4.7 降到 4.1」和「由 4.1 升到 4.7」都通顺,只有一个是对的。
单位决定换算系数。「毫安」和「毫安时」差一个「小时」,模型的常识会替它选一个看起来合理的解释,错误被包装成正确答案。这类最难查,因为数值本身对,错的是量纲。
专名决定检索命中。型号、物料名、工艺名是知识库检索的键,键错一位就召回一堆不相干的文档,模型只能基于错误的上下文作答;回答内部逻辑完全自洽,只有对照原始录音才会发现出发点就错了。
最麻烦的一点:平均对数概率帮不上忙。同音替换在声学上完全合理,模型自己并不觉得不确定,置信度不会掉。想靠「置信度低就重试」兜住这一类错误,基本兜不住。
修法方向:术语热词表前置,在解码阶段做偏置,而不是事后让大模型猜——提示词里写「请纠正错别字」是没有用的,模型手里没有音频,它只能按语言习惯猜,而语言习惯恰恰是错的来源。再加一道关键数字与单位的二次确认。下面这段演示抽取「数字 + 单位」并与期望值比对,能把「毫安 / 毫安时」这种差异直接暴露出来:
importre NUM=re.compile(r"(\d+(?:\.\d+)?)\s*(毫安时|毫安|毫米每秒|毫米|伏|安)")EXPECT=[(4.7,"毫米每秒"),(4800.0,"毫安时")]defnumbers(text):return[(float(v),u)forv,uinNUM.findall(text)]defconfirm(text):got=numbers(text)return{"ok":got==EXPECT,"missing":[eforeinEXPECTifenotingot],"extra":[gforgingotifgnotinEXPECT],}say="振动 4.7 毫米每秒,容量 4800 毫安"print(numbers(say))print(confirm(say))输出是[(4.7, '毫米每秒'), (4800.0, '毫安')],比对结果为ok: False,missing里是(4800.0, '毫安时')。
这里有个坑要提前说:这段只处理阿拉伯数字写法。真实录音里「四千八百毫安时」是中文数字,不先归一化就整段漏抽。漏抽比抽错更危险,因为抽错会触发比对失败,漏抽只会安静地返回一个空列表,看起来一切正常。
五、第三层:流式半句
现象:为了压延迟,常见做法是语音活动检测一判定「说完了」就立刻发请求,或者每隔几百毫秒把增量文本推给模型。三个隐患:
- 判「说完」的依据是静音时长,不是语义完整。用户换气、思考、翻页、看仪表,都会触发判停。
- 中间结果会被最终结果覆盖。如果下游缓存了中间结果生成的内容,就会出现同一轮对话里前后两个回答互相矛盾——前一个基于半句,后一个基于整句。
- 尾音被切。最后一个字还没说完就被截断,识别成另一个字,且这个错字在中间结果里不会自我纠正。
为什么下游会答错:大模型面对不完整指令时的默认行为是补全,不是追问。补全方向由先验决定:一句话停在「三号泵的振动值跟上周比」,模型很可能自己补上「上升了多少」并给出一个数。它答得流畅、结构完整、格式合法,只是答在了一个用户没问的问题上。这类错误在日志里毫无异常,因为请求和响应都是合法 JSON,没有报错,没有超时。
| 策略 | 附加延迟 | 完整性 | 适用场景 |
|---|---|---|---|
| 见静音就发 | 最低 | 差,换气即断 | 单词级命令,开、关、上一首 |
| 固定间隔发增量 | 低 | 差,且中间结果会被覆盖 | 实时字幕,不接推理 |
| 静默窗口 + 句末特征 | 中,数百毫秒 | 较好 | 一问一答的语音助手 |
| 等最终结果再发 | 高 | 最好 | 长指令,含数字与型号 |
修法方向:设静默窗口,同时给最长等待兜底——超过上限就强制放行,但在请求里标注「本段可能不完整」,让下游走追问分支而不是硬答。下面这段把三个条件写在一起:
ENDINGS=("吗","呢","吧","多少","怎么样","是不是")defready(text,tail_silence_ms,window_ms=600,max_wait_ms=2500,held_ms=0):iftail_silence_ms>=window_ms:returnTrue,"静默窗口达标"iftext.endswith(ENDINGS):returnTrue,"句末特征命中"ifheld_ms>=max_wait_ms:returnTrue,"超时强制放行,标注不完整"returnFalse,"继续等"cases=[("三号泵的振动值是多少",120,0),("三号泵的振动值",700,0),("三号泵的振动值跟上周比",300,2600),]fortext,silence,heldincases:ok,why=ready(text,silence,held_ms=held)print(f"{ok!s:5}{why:18}{text}")三个用例分别命中「句末特征」「静默窗口」「超时强制放行」。最后一条是关键:宁可让模型知道这段不完整,也不要让它以为这段是完整的。一个显式的「不完整」标记,会把模型的默认行为从补全切换到追问,这一句话的差别,比换模型大。
这份语音链路的对照材料:里面是片段字段清单、热词表模板和一份「半句与整句」的对照样例,和本章的静默窗口判断配套使用。放在资料包里,扫码即可获取:
六、第四层与第五层:说话人丢失、时间戳脱钩
第四层,说话人丢失。
多人场景——会议室、坐席与客户、两个人配合巡检——识别输出的是一段连续文本,没有角色标签。工程上的常规做法是把整段作为一条用户消息送进去。
下游会因此答错,三个具体机制:
指代无解。「他说的那个阈值」里的「他」在纯文本里没有对应实体,模型只能就近挑一个名字,或用「对方」含糊带过。前者可能挑错人,后者让回答失去可执行性。
立场被合并。客户说「我担心这个方案撑不住」,工程师说「撑得住」,两句话拼成一段,模型读到的是自相矛盾的一段陈述,输出往往是折中的废话——既说有风险,又说可行。
指令与复述混淆。「改成零点六」这句话是谁说的,决定了它是一条待执行的指令,还是一条转述。没有说话人标签,这两种解释在文本上完全一样。
修法:先做说话人分离,再按「角色:内容」拼上下文;分离结果不确定时,保留「未知说话人」标签,不要静默合并。
第五层,时间戳与原文脱钩。
拼接时只留文本,起止时间丢掉;有的实现连片段顺序都不留,只剩一个整段字符串。后果有四条,逐条都影响能不能把系统调好:
无法定位。用户投诉「它答错了」,你手里只有整段文本,找不到对应第几秒,也就无法回听。想确认是识别错还是模型错,第一步就卡住。
无法回归。改了热词表、调了聚合规则,没有「同一段音频 + 同一套期望输出」的对照集,改完不知道是变好还是变坏。这类改动经常是修好一个、弄坏两个。
无法归因。错误可能来自识别、聚合、检索或生成。没有秒数,就没法把责任分到具体环节,只能整条链路一起怀疑。
无法切片。长会议要按时间窗口送模型,没有时间戳只能按字数硬切,切在句子中间的概率很高。
这两层的共同点是:它们不影响「字对不对」,所以永远不会出现在字准确率的报表里。但它们决定了这个系统能不能被调试。一个不能定位、不能回归的系统,第一次上线是什么水平,往后大概就一直是这个水平。
七、五条工程判据
把前面五层收成一张表,再给五条可以落地的判据。这张表建议直接贴进项目文档,逐行对,缺哪行补哪行。
| 失真点 | 现象 | 兜底做法 |
|---|---|---|
| 分句边界 | 长句被切断,条件句变陈述句 | 按停顿与句末特征重新聚合,保留秒数 |
| 同音专名 | 四点七变四点一,型号检索不到 | 热词表前置偏置,关键数字回读确认 |
| 单位与量词 | 毫安与毫安时混用 | 单位向后继承,单位白名单校验 |
| 流式半句 | 尾词未落地就发请求 | 静默窗口,超时强制放行并打标 |
| 说话人 | 多人拼成一段,指代无解 | 先分离再拼「角色:内容」 |
| 时间戳 | 只剩整段文本 | 片段与秒数落盘,随请求一起传 |
判据一:保留原始片段与时间戳,不要只留拼接后的整段文本。
改哪里:识别适配层。落地形态是一张片段表,字段至少包含会话标识、片段序号、起始毫秒、结束毫秒、文本、说话人、平均对数概率;整段文本只是这张表的一个视图,随时可以重建。
怎么验收:随便挑一条线上请求,看能否在十秒内答出「这句话在第几秒」。答不出来就是没落地。
判据二:术语热词表前置。
改哪里:解码配置,不是提示词。把型号、物料名、工艺名、单位做成词表,喂给识别的偏置参数。事后让大模型纠错是无效的,它没有音频。
怎么验收:拿一批含专名的历史音频重跑,对比专名命中情况。没有变化,说明词表没真正进解码。
判据三:关键数字与单位二次确认。
改哪里:识别之后、生成之前。可以是一条规则校验,也可以是一次回读:「您说的是四点七毫米每秒,对吗?」成本是一次交互,收益是把一整类静默错误变成显式确认。
怎么验收:构造「毫安 / 毫安时」「四点七 / 四点一」的对照集,看拦截情况。这类对照集几十条就够。
判据四:流式设静默窗口。
改哪里:语音活动检测与请求发送之间。窗口长度按业务调,一问一答的场景通常数百毫秒可接受;同时给最长等待兜底并打标。
怎么验收:统计「超时强制放行」的占比,和这部分请求的下游错误情况。占比高说明窗口设小了;占比低但错误仍多,说明问题不在这一层。
判据五:多人场景先做说话人分离。
改哪里:识别之后、拼上下文之前。分离有错时保留「未知」标签,不要静默合并。
怎么验收:取一段双人对话,逐句检查拼出来的上下文里归属是否正确。错归属的比例,就是这条判据的验收值。
| 判据 | 改动位置 | 验收方式 |
|---|---|---|
| 保留片段与时间戳 | 识别适配层 | 任一请求能定位到秒 |
| 热词表前置 | 解码配置 | 含专名的历史音频重跑对比 |
| 数字二次确认 | 识别与生成之间 | 对照集上的拦截情况 |
| 静默窗口 | 检测与发送之间 | 超时放行占比及该部分错误情况 |
| 说话人分离 | 拼上下文之前 | 双人对话的归属正确情况 |
这五条有一个共同特点:都不需要换模型。换模型解决的是「字更准」,这五条解决的是「结构不丢」。前者有上限,上限由音频质量决定;后者是你自己就能补的。
顺序上,先做判据一和五,它们不改延迟,只加字段和标签;再做判据二和三,需要重跑对照集;最后做判据四,要压测着调。反过来做,就会一边调延迟一边改准确率,最后说不清是哪个改动起的作用。
语音问答的排查清单:一份按「片段字段 / 热词表 / 数字回读 / 静默窗口 / 说话人标签」五段整理的检查表,可以直接对着自己的链路逐项勾。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 文中位置 | 事实或结论 | 出处 | 核对方式 |
|---|---|---|---|
| 第二章 | 论文标题、作者、编号与提交日期 | Radford、Kim、Xu、Brockman、McLeavey、Sutskever,Robust Speech Recognition via Large-Scale Weak Supervision,arXiv:2212.04356,2022-12-06 提交 | 打开 arXiv 摘要页逐字核对标题与作者列表 |
| 第二章 | 68 万小时多语言多任务弱监督;零样本迁移可与全监督结果相比;与人类相比为「approach their accuracy and robustness」 | 同上,摘要原文 | 逐字核对摘要,引用处保持原措辞,未改写 |
| 第二章 | 摘要未给出可直接迁移到具体业务的准确率数字 | 同上,摘要范围内 | 摘要中不存在此类数字,故本文不引用任何识别准确率 |
| 第三至六章 | 五层失真的机制描述与场景举例 | 本文基于语音问答链路的工程整理 | 属机制分析,不含实测数据集,不出现准确率类数字 |
| 第三、四、五章 | 三段示例代码 | 纯标准库 python,本地运行通过 | 按代码块原样复制即可复跑,输出见各章正文 |
| 全文 | 涉及现状的表述 | 截至 2026-10-24 的公开信息 | 逐处标注时间限定,不用「近日」「眼下」替代 |
附表 B:术语速查表
| 术语 | 含义 | 出现位置 |
|---|---|---|
| ASR | 自动语音识别,把音频转成文本 | 全文 |
| 片段 | 识别输出的一段带起止时间的文本单元 | 第二、三章 |
| 中间结果与最终结果 | 流式识别的过程中间文本与收尾文本,后者会覆盖前者 | 第五章 |
| 语音活动检测 | 判断有没有人说话、说完了没有,常被当成判停依据 | 第五章 |
| 静默窗口 | 尾部静音达到一定时长才认为一句说完 | 第五、七章 |
| 热词偏置 | 解码时提高指定词的命中倾向,需要词表前置 | 第四、七章 |
| 平均对数概率 | 片段的对数概率均值,可粗略看作模型对这段的把握 | 第二、四章 |
| 说话人分离 | 判断哪一段是谁说的,输出角色标签 | 第六、七章 |
| 时间戳对齐 | 保留词或片段对应的秒数,用于定位与回归 | 第六、七章 |
| 二次确认 | 对关键数字与单位做回读或反问,不直接进结论 | 第四、七章 |
写在最后:这篇用到的资料
写这篇文章时,把语音链路的几个环节又重新拆了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
如果这篇对你有用,评论区说说你卡在哪一步,我按留言再补后续的拆解。