用声音提问的检索 — 误识别·反问·拒答,以及通过了依据检查的错误答案
一句话总结
语音查询很短、很口语化,而且带着 ASR 转写错误进来。所以在检索之前要先整理查询(去掉口头赘词、规整数字词、把依赖前一个问题的追问补全),用检索分数先拒绝没有答案的问题,而模型给出的回答,要用所引用的文档检查之后才能说出口。检查最多只能看到数字是否在文档里——要知道,仍会留下通过了依据检查的错误回答,并以此来评估(模块 9)。
为什么需要它
LLM 工程课程中的 RAG 接收的是书面写下的问题。电话则不同。本模块的 12 条查询,是合成语音经这个镜像的流式 ASR 实际转写出来的结果(fixtures/voice/rag/queries.jsonl)。“What time do you guys open on Saturday?”变成了“OHI WHAT TIME DO YOU GUISE OPEN ON SATURDAY”,“How long does a refill take?”变成了“HOW LONG DOES ORIFYL TAKE”。而第二个问题是“O K AND WHAT ABOUT SUNDAY”——没有前一个问题,就不知道它在问什么。
工作原理
段落级嵌入。电话里要念出来的回答只有一两句话,所以把文档切成段落,每个段落单独做嵌入。这个镜像的嵌入模型是 all-MiniLM-L6-v2(qint8 ONNX)。按模型卡的做法,用 attention mask 遮盖后对 token 向量取平均(mean pooling),再归一化为长度 1,内积就是余弦相似度。
查询改写。三条规则就能处理这份数据的大部分情况。(1)去掉口头赘词列表(um · uh · oh · like · okay · o k ……)。列表里也放进了这个 ASR 实际输出的“ohi”——把观察到的误识别汇集成词典,是现场的做法。(2)把数字词改成数字(twenty four → 24)。文档里写的是“24 hours”。(3)像“and what about Sunday”这样的追问,要取来前一个问题,只替换星期几。像“ORIFYL”这样的误识别,规则无法纠正——只能期待嵌入凭语义找到相近的段落。
测量一下。在这 14 份文档(41 个段落)上,改写之前,top-1 召回率就是 10 个中的 10 个。嵌入把包含“ORIFYL”的问题放在了处方续开段落的附近。但是改成与 BM25 混合的混合检索(RRF,k=60)后,top-1 对原始查询降到了 10 个中的 8 个,对改写后的查询降到了 7 个,变差了——“VITIO”“ORIFYL”这样的误识别词不在任何文档里,给不了 BM25 任何信号,剩下的常见词(how、long、the)把排名搅乱了(在这个镜像中测得的值)。混合检索并不总是赢。改写帮上大忙的地方,不是检索,而是问题的含义。如果把“and what about sunday”原样交给模型,就不知道问的是什么。
拒答阈值。对没有答案的问题(推荐披萨店、Wi-Fi 密码),检索也会找回点什么。如果最接近的段落的分数低于阈值 τ,就连模型也不问,直接拒答——不给小模型编造的机会。在这份数据中,有答案的问题的最低分是 0.345,没有答案的问题的最高分是 0.260,所以它们之间存在阈值。为什么要小心改写规则,在这里也看得出来。有一次加入了“you → the clinic”的替换,“can you recommend a good pizza place”就变成了“can the clinic recommend…”,分数升到 0.397,无论什么阈值都无法把它与有答案的问题区分开了。所以去掉了这条规则。
格式靠语法,内容靠检查。如果对 0.5B 模型说“把出处写成 [kb-hours] 这样”,它连格式都会违反。用 llama-server 的 response_format(json_schema)把输出限定为 {"answer": …, "source": …},并把 source 设为只允许这次找到的文档 id 和 none 的 enum,模型就无法引用没有找到的文档。然后由代码检查:回答里的数字(时间、金额、天数)在所引用的文档中是否原样存在。实际上,这个模型给出了“refill takes 10 business days”,却引用了血液检查文档——文档里写的是 3 business days。没有通过检查的回答不说,而是把引用文档中与问题最接近的句子原样念出来(提取)。作为代价,它不可能出错,但不那么自然。
通过了依据检查的错误回答。数字检查会让“星期六几点开门”得到工作日的时间(8:30 a.m. to 6 p.m.)也通过。因为数字在文档里。实际上,这条流水线对星期六的问题和星期日的追问,都回答了工作日的时间。这样的错误回答,验证器拦不住,必须用标准答案表来评估才看得出来(模块 9 的事实准确率)。
在现场相遇的样子
语音助手的引用与界面上的脚注不同。不能把“[kb-hours]”念出声来(在模块 7 的生成朗读文本中会被删掉)。相反,引用要留在日志和评估中,以便事后追问“是依据什么说的”。
下一项实验要做什么
把 KB 文档切成段落并做嵌入,制作余弦检索函数。让查询改写规则接受隐藏用例的测试,用原始查询和改写后的查询测量召回率和分数,然后确定区分没有答案的问题的阈值。只把越过阈值的问题以 JSON 形式询问 LLM,用验证器过滤,把没有通过的回答换成提取,生成最终的语句。