首页 / 资讯中心 / 文章详情

语音检索实战:从语音转写到RAG问答的完整链路

语音检索实战:从语音转写到RAG问答的完整链路 ★ FEATURED ARTICLE
“语音检索”这个词乍一听像是“对着数据库喊一句话系统自动把相关内容捞出来”。实际拆开看它就是把一段语音变成文字再把文字送进向量检索链路里最后把结果喂给大模型做问答或者摘要。我在带团队做某内部知识库项目时早期只做了纯文本的RAG问答后来业务方提了一个新需求用户想直接用语音提问特别是现场巡检的场景手脏、不方便打字张口就问“上次那台设备的保养记录是什么时候”。这就逼着我把语音识别、文本检索、对话生成三条链路串在一起。LangChain在这里扮演的是编排角色把语音转写的输出、文本切片、向量召回和大模型问答像流水线一样串起来。这套方案做完之后我踩了不少坑也总结了一些能直接用的经验这一篇就把它完整记录下来。1. 先想清楚语音检索到底在解决什么问题1.1 核心链路拆解从声波到答案的四步跳跃任何语音检索系统本质上都是在完成同一件事把一段没有结构的音频信号变成有结构的、可检索的文本知识再通过语义匹配找到最相关的内容。整个过程拆成四步就非常清晰了。第一步是音频采集与预处理。这一步决定了后续所有环节的天花板。麦克风距离说话人一米的收音质量和手机贴嘴边说出来的音频转写准确率差距可以非常大。实际项目中我见过太多人一上来就调模型参数结果发现根本问题是录音文件采样率只有8kHz、背景噪音巨大语音识别模型连人声都分不清。所以我会在系统设计稿里把音频预处理单独画一个模块包含降噪、音量归一化、格式转码、采样率统一这几个动作。第二步是语音转文本Speech-to-TextSTT。这是语音入口最关键的一环。转写质量直接决定下游检索的召回上限——如果音频里说的是“液压系统”转写成了“叶压系统”那后面无论向量模型多强都永远找不回正确的知识片段。我在选型时会格外关注专业词汇的识别能力而不是只盯着通用准确率。第三步是文本切片与向量化。语音转写出来的文本通常是一整段口语带很多“嗯”“啊”“那个”之类的填充词和书面语RAG的处理方式不太一样。需要先做口语文本的清洗再把长文本切分成适合检索的语义块最后用嵌入模型转成向量。第四步是检索与生成。通过向量相似度召回最相关的文本片段再结合大模型生成最终答案。LangChain在这个环节提供的Retriever抽象、LCEL表达式和问答链封装可以让前面三步的结果很自然地汇入文本检索的成熟链路中不需要重复造轮子。1.2 为什么不能跳过转写直接做“语音向量检索”我经常被问到这个问题能不能用语音编码器直接把音频转成向量绕过文本环节技术上确实有人在做端到端的音频检索比如给一段音乐、一段环境音做向量匹配。但放到“语音问答”这个场景里直接做音频向量会有三个绕不开的问题。第一个问题是语义粒度不匹配。音频向量擅长表征“这段声音像不像另一段声音”但不擅长表征“这句话问的是什么意思”。用户问“保养记录什么时候到期”和“保养记录哪一天截止”是两段完全不同发音的音频但语义完全一致。纯音频向量的相似度计算很难捕捉这种同义改写关系文本向量则天然为语义匹配优化。第二个问题是可解释性差。文本检索召回某一段内容你可以直接把原文展示给用户看音频检索召回了一段音频你还得再转写一遍才能让用户确认“系统是不是真的get到了我的问题”。中间多了不可控的一层出了问题很难排查。第三个问题是工程成本。音频向量化需要更大的模型、更长的推理时间存储和检索的基础设施也不是常规向量数据库能直接cover的。所以我最终的方案是坚定地走“语音转文本再检索”的路线。这个思路不是最炫酷的但它是工程上最稳、最容易调试、效果最可控的。2. 技术选型每一环都在权衡什么2.1 语音转写本地模型和云端API怎么选语音转写是整个链路里最需要权衡的一环选型时主要看三样东西准确率、数据隐私、单路成本。云端API的方案优势很明显识别准确率高尤其在中文普通话、带口音的语音上商用服务做了大量数据积累。部署成本几乎为零拿密钥调接口就行。但它的瓶颈在于音频数据要上传到第三方服务。我做内部知识库的时候客户明确要求音频不能出内网因为里面可能涉及设备参数、人员信息等敏感内容。这个限制直接pass掉了云端方案。本地方案我选择了开源语音识别模型Whisper系列是当时最主流的选择此外还有其它国产模型可以横向对比。本地部署的最大优势是数据不出域推理在一台带GPU的服务器上就能跑。缺点是中文效果需要额外调优尤其是专业术语。我的经验是如果业务场景有固定的术语表一定要准备一个“热词表”或者“提示词模板”在推理时传给模型能显著提升专有名词的识别率。参数选择上Whisper的large模型准确率最高但推理速度慢small和medium模型速度快准确率略低。我当时的折中方案是离线批量转写用large追求质量实时语音交互用medium甚至small追求延迟。这不是一个固定的答案要根据你的机器配置和并发量来定。2.2 嵌入模型通用向量还是领域微调文本向量化这一环很多人会直接选一个榜单上分数最高的通用嵌入模型然后就没有然后了。但在语音检索场景里嵌入模型的选择要跟着“文本类型”走。语音转写出来的文本有两个特点一是口语化严重和我们平时RAG用的文档书面语差异很大二是可能存在转写错误甚至带着“同音字替换”的问题。我实测过某些在标准语义相似度数据集上评分很高的通用嵌入模型遇到“嗯嗯啊啊”开头、语序凌乱的口语文本时召回效果会明显下降。原因很简单模型的训练数据主要是规范文本对口语噪声的鲁棒性不足。解决方向有三个按成本从低到高排列换模型直接对比多个开源嵌入模型在同一条口语转写文本上的召回效果选一个最稳的。清洗文本在送嵌入之前先去除语气词、重复词、修正明显的同音错别字把口语“翻译”成半书面语。领域微调如果数据量够至少几万条带标注的问答对可以用领域数据对嵌入模型做继续训练这是效果上限最高的方案但工程量也最大。我给大多数项目的建议是先做文本清洗再横向对比两三个通用模型如果还不能满足业务指标再考虑微调。不要一上来就微调因为语音转写的错误分布不稳定模型微调完之后语料一换效果可能会反弹。2.3 向量存储轻量索引还是服务化数据库向量存储的选择取决于并发量和数据规模。数据量在百万级以下、并发请求不高、不需要分布式部署的场景本地轻量索引就足够了。我第一版Demo用的是FAISS这类本地向量索引优点是可以嵌入到Python进程里零运维成本开发调试的时候特别方便。缺点是它不是一个“数据库”没有增删改查的事务语义也没有权限管理数据更新时需要重建索引或者做增量合并。如果系统要上线并且会有多个客户端并发访问、数据需要频繁更新那我会建议用服务化的向量数据库比如Milvus、Qdrant、Chroma等。服务化数据库的好处是数据持久化、支持过滤条件、有API可以做动态的管理。代价是多了一套服务要运维架构复杂度上去了。我的习惯是分阶段演进原型阶段用本地索引能跑通就赶紧验证业务效果一旦确认业务方向正确再平滑迁移到服务化向量库。LangChain的向量存储抽象层把这两类存储都统一成了同样的接口切换时只需要改一行代码这算是用框架的一个隐性收益。2.4 LangChain在链路里的真实作用不是“万能胶”很多人对LangChain有一个误解觉得它是个“万能胶”什么都能粘。实际上LangChain最核心的价值是把“输入 → 转换 → 检索 → 生成”这一套流水线的模式固定下来让你不用再反复写那些结构几乎一样的胶水代码。在语音检索场景里LangChain帮了我三个具体的忙它提供了统一的Retriever抽象我可以在“向量检索”和“多路召回”之间无缝切换不需要改动问答链的代码。它的LCEL语言LangChain Expression Language让“语音转文本的结果直接作为下一条链路的输入”这件事变得非常直观流程的每一步都可以单独调试和替换。它内置了多种文档加载器、文本分割器、输出解析器的组合省去了我调研和封装的时间。但我也要提醒一句LangChain不解决算法效果问题。它不能让一个糟糕的嵌入模型变好也不能让一个错误百出的转写结果变聪明。它的价值在于工程效率在于让团队把精力集中在调数据、调模型上而不是反复写链路代码。3. 从零搭建一套可运行的语音检索Demo3.1 环境准备先跑通最小闭环我把整套系统的Demo用一个Python脚本就能跑通组件配置如下语音转写用开源Whisper模型向量索引用FAISS嵌入模型用一个本地开源的通用模型问答模型接一个兼容OpenAI接口格式的本地推理服务。这样整条链路完全不依赖外部API数据全程本地处理。依赖安装的核心是三行命令pip install openai-whisper pip install langchain langchain-community pip install faiss-cpu如果机器有NVIDIA GPU并且装好了CUDAWhisper会自动用GPU推理速度会快很多。没有GPU也能跑就是慢一些。FAISS安装的时候注意选择cpu版本还是gpu版本原型阶段cpu就够用。3.2 语音转写模块把音频变成干净的文本转写模块要做两件事调用模型转写清理转写结果。我的实现思路是先加载Whisper模型再把音频文件路径传进去模型会返回一个包含文本和分段时间戳的结果对象。import whisper model whisper.load_model(medium) # 加载音频并转写 audio_path query_audio.wav result model.transcribe( audio_path, languagezh, initial_prompt以下是普通话的语音记录可能包含设备维护、保养记录等专业术语。 ) raw_text result[text] print(raw_text)这里有几个细节要注意。languagezh参数可以强制Whisper按中文识别避免它把中文夹着英文的情况识别错。initial_prompt是提升专业术语识别率的关键技巧我在里面放了业务相关的提示词相当于给模型“预热”了一下领域语境。实测下来加了这行提示之后术语识别准确率有明显提升大家可以试试。转写之后需要清洗。语音转写文本里常见的“嗯”“啊”之类的语气词以及重复的字都会干扰后续嵌入模型的效果。import re def clean_transcript(text: str) - str: # 去除常见的语气词和填充词 text re.sub(r[嗯啊呃诶哦], , text) # 去除连续重复的字符 text re.sub(r(\w)\1{2,}, r\1, text) # 将连续空白压缩为单个空格 text re.sub(r\s, , text).strip() return text clean_text clean_transcript(raw_text)注意清洗规则不要做太激进否则会改变原意。比如把“对对对就是那个”里的“对对对”全删掉虽然文本干净了但丢失了用户强烈的确认语气如果下游要做情感或意图分析就会受影响。在检索场景下我保留这些重复词按原样处理不在清洗阶段做语义压缩。3.3 文本切片与向量化口语文本的切分不能照搬文档规则语音转写出来的文本通常是一段对话或者一段独白没有标题、没有段落、没有markdown结构这和RAG文档切片面临的输入完全不同。文档切片有标题分割、段落边界可以利用语音文本切片只能靠长度和语义结束符来切。我的做法是先按句子边界切句再用LangChain的文本分割器做块合并。from langchain_text_splitters import RecursiveCharacterTextSplitter # 先做句子级切分 splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[。, , , , \n], ) chunks splitter.split_text(clean_text)chunk_size和chunk_overlap这两个参数是检索效果的关键。chunk_size太大一个块里包含多个主题召回时精确度下降太小一个块的信息量不足以支撑回答。我测试下来300字左右的中文文本块在多数场景下效果比较平衡。chunk_overlap的存在是为了避免句子被从中间切断导致信息残缺通常设为chunk_size的10%-20%。语音文本切成块之后紧接着就是向量化。我在这里强调一个通用做法把嵌入模型的调用封装成一个函数方便后续替换。from langchain_community.embeddings import HuggingFaceEmbeddings embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, encode_kwargs{normalize_embeddings: True}, )normalize_embeddingsTrue这行很关键它会对向量做L2归一化。归一化之后向量的内积就等于余弦相似度这样做FAISS的IP内积检索时结果排序和余弦相似度排序是一致的少踩一个“为什么检索结果和相似度分数对不上”的坑。3.4 构建向量索引并实现检索向量化之后用FAISS存成本地索引文件。from langchain_community.vectorstores import FAISS # 假设 knowledge_chunks 是已清洗并切片后的文档块列表 vectorstore FAISS.from_texts( textsknowledge_chunks, embeddingembedding_model, ) vectorstore.save_local(faiss_index)检索的时候我直接用FAISS的similarity_search_with_score方法拿到相似度分数方便后面做阈值判断。# 加载索引 vectorstore FAISS.load_local( faiss_index, embeddingsembedding_model, allow_dangerous_deserializationTrue, ) query clean_text docs_with_scores vectorstore.similarity_search_with_score(query, k5) for doc, score in docs_with_scores: print(fscore: {score:.4f}, content: {doc.page_content})上面加了allow_dangerous_deserializationTrue这是LangChain新版FAISS加载的安全参数。本地加载自己的索引文件时没问题但如果索引文件来自外部绝对不要加这个参数会有反序列化风险。3.5 用LangChain串联完整问答链路检索这一步跑通之后再用LangChain把转写、检索、问答串成一条链。这里我用LCEL的写法比老式的RetrievalQA链更加直观调试也更方便。from operator import itemgetter from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough # 假设 llm 是你已经配置好的大模型对象 # 例如通过 langchain_openai 的 ChatOpenAI 指向本地推理服务 retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 5}, ) prompt ChatPromptTemplate.from_messages([ (system, 你是知识库助手。根据检索到的资料回答问题。如果资料中没有相关内容请明确说明无法回答。), (user, 检索到的资料\n{context}\n\n用户问题{question}), ]) def format_docs(docs): return \n\n.join(doc.page_content for doc in docs) chain ( { context: itemgetter(question) | retriever | format_docs, question: itemgetter(question), } | prompt | llm | StrOutputParser() )这个链路的写法核心是输入一个带question字段的字典先从question取文本作为检索的关键词然后经过retriever得到文档集合再格式化后和原问题一起填入Prompt模板最后交给大模型生成答案。整套流程串起来之后最终使用方式非常简单def voice_search(audio_path: str) - str: # 第一步转写 result model.transcribe(audio_path, languagezh) clean_query clean_transcript(result[text]) # 第二步检索 问答 answer chain.invoke({question: clean_query}) return answer这个函数就是整个语音检索Demo的核心入口。给它一个音频文件路径它返回一个文本答案。从产品角度看后端到这一步就完成了剩下的是接前端语音采集和播放。提示Demo阶段把整个链路写在一个脚本里没有问题但一旦要上线强烈建议把转写、检索、问答拆成独立服务因为三者的资源需求差异太大了——转写吃GPU检索吃内存和磁盘问答既吃GPU又吃显存。混在一个进程里其中一个服务的波动会拖垮全部。4. 检索质量优化的五个关键细节4.1 转写质量是第一道闸门改提示词比改模型更划算很多团队在做语音检索时一开始就把精力花在微调语音识别模型上这是成本最高的路径。我的经验是先优化提示词和输入音频质量往往投入最小、见效最快。对于Whisper模型initial_prompt参数就像一个“上下文预热器”。我测试过在提示词中加入领域常见术语比如设备名、故障代码、业务缩写转写准确率能提升好几个百分点。原理是Whisper在解码时会在语言模型层面偏向提示词中出现的词序列相当于给模型一个词表先验。音频质量优化上一个非常实用的小技巧是如果音频是远场麦克风录的先用高通滤波器滤掉低频环境噪声再做一次响度归一化。我发现用ffmpeg做简单的highpass滤波之后转写准确率提升比换更大的模型还明显。原因很简单——人声的主要能量集中在300Hz到3400Hz之间低频的空调声、设备嗡鸣声滤掉之后信噪比立刻上来了。4.2 分段策略直接决定召回上限文本切片是整个检索系统里最容易被忽视、但对效果影响极大的部分。我在语音检索项目里试过三种切片策略结果差异非常大。第一种是固定长度硬切比如每200字一切完全不管句子边界。这种方式实现最简单但效果最差因为大量语义完整的句子被拦腰截断检索时匹配到的文本块往往是一句半截话不见头不见尾大模型回答时只能靠猜。第二种是RecursiveCharacterTextSplitter在分隔符列表上做递归切分优先按句子边界切切出来的块基本能保持语义完整。这是我推荐的基础方案。第三种是针对语音文本的“意图完整切换分”在第二者的基础上加入主题判断一个文本块尽量只包含一个主题。这个方法效果好但需要额外的分类模型或者规则成本偏高。我的建议是先上第二种在评估指标不达标时再升级到第三种。切片参数的调优有一条实用原则chunk_size设置成“回答一个常见问题需要的最少上下文长度”而不是“模型上下文窗口能塞下的最大长度”。这个认知帮我少走了很多弯路。设得太小回答缺少背景设得太大召回的块里一半内容跟问题无关反而稀释了答案质量。4.3 用元数据过滤解决“范围漂移”问题在语音检索的一个实际场景里我踩过一个很典型的坑用户问“上个月的操作记录”系统把“上个月”“这个月”“去年”的操作记录全部召回了。原因很简单——向量检索只认语义相似度它会找到所有跟“操作记录”相关的文本而对时间的过滤完全无感。这个问题本质上是纯向量检索的范围漂移问题。解决方案是给文本块加上元数据比如时间、来源、文档类型在检索时用LangChain的过滤器做条件限定。LangChain的多向量检索和SelfQueryRetriever可以解析问题里的结构化条件并自动拼过滤条件是解决这个问题的标准思路。具体做法切片的文本不仅保留内容本身还要带上它的元数据字段比如“时间”、“来源部门”、“文档标题”。然后检索时先从问题里解析出“上个月”这个时间条件转成时间范围再作为元数据过滤条件传给向量检索引擎。这样就能保证“上个月”这个条件在召回阶段就被严格过滤而不是等向量召回之后再人工筛。这个思路也适用于语音检索的其他常见场景比如“设备A的维修记录”“去年第三季度的故障报告”本质都是用元数据把问题的限定条件拆出来缩小检索范围。4.4 重排序是提升答案质量的兜底方案向量检索的召回结果只是一个粗糙的候选集排序质量并不完美。我更推荐在召回之后加一个重排序Rerank环节用交叉编码器对候选文档和问题做精细化打分再按新分数重新排序只把Top-K送给大模型。为什么需要重排序因为双塔式向量模型Embedding模型把问题和文档分别编码后算相似度这种“先独立编码再匹配”的方式速度快但牺牲了语义交互的精度。交叉编码器把问题和文档拼在一起同时对编码能看到两边的词级交互精度高很多但速度慢。重排序正好用在召回后这个阶段只需要对少数的候选文档做精排成本可控。我加完重排序之后几个标准测试问题的答案完整度有了明显提升。具体到LangChain里可以通过继承Retriever封装一个重排序的Retriever或者直接接第三方的Rerank服务。工程实现不复杂但对用户体验的提升是实打实的。4.5 语音交互特有的问题口语省略和指代消解语音检索和键盘输入检索有一个巨大差异口语里充满省略和指代。用户不会像打字一样说完整句更多是“那个呢”、“上次说的那个方案”、“后面那个螺丝拧一下”。如果直接把这种话丢给检索系统大概率什么都搜不到。我当时的解决方案是引入一轮“口语改写”中间层在做检索之前先让大模型把用户的问题改写成完整、指代清晰的书面描述。比如“那个呢”结合上一轮对话上下文改写成“当前正在讨论的液压系统维护方案是什么”。这一步可以用LangChain的链轻松实现把改写逻辑串联到语音检索链路里。另外多轮对话场景还要维护一个会话状态。语音交互天然是逐轮进行的用户总会在下一轮去指代上一轮提到的内容。我的做法是把最近几轮的用户问题和系统回答摘要一起传给“口语改写”环节让改写模型有上下文可用而不是每次孤立地理解当前一句。这个设计和搜索场景的“查询补全”是同一个思路但在语音场景里尤为关键。5. 实测中踩过的坑和排查清单5.1 常见报错与处理速查表我在跑通这套语音检索方案的过程中整理了出现频率最高的几个问题和对应的处理方法。现象可能原因处理方法Whisper转写结果全英文拼音音频采样率过低或原始音频被压缩转码时统一为16kHz或更高检查是否有二次压缩转写文本中出现大量重复“嗯嗯啊啊”音频背景噪声过大增加前端降噪使用高通滤波器再重新转写向量检索召回文本明显不相关嵌入模型对口语文本不敏感做口语文本清洗或者换一个在对话语料上训练过的嵌入模型检索结果排序不稳定相同问题结果时好时坏没有固定随机种子或者文本分割器chunk_overlap设置为0固定分割参数减少召回时的随机性大模型回答和检索内容对不上Prompt中context占用太长模型注意力被稀释减小k值召回个数或者对答案加约束“只根据资料回答”FAISS加载本地索引报反序列化错误使用了旧版LangChain保存的索引重新构建索引或者用allow_dangerous_deserializationTrue仅限本地可信文件这张表是我在实际调试中一点点积累的每一条都真实踩过。其中“检索结果排序不稳定”最容易被忽视很多团队以为是大模型幻觉其实是文本分段的chunk_overlap设为0导致边界句子信息丢失引起的切换不同批次数据时结果自然不稳定。5.2 语音场景特有的三个隐蔽坑第一个坑是“同音字替换”对向量检索的连锁影响。语音识别把“智能运维”转成“智囊运维”时后面的嵌入模型会被误导。我的解决方法是在转写模块后面加一个“领域词表纠偏”步骤把高频术语的同音错别字映射回正确写法。这个纠偏词典来自对错题样本的统计维护成本不高但效果很可观。第二个坑是“用户说话习惯”导致的提示词污染。用户提问时习惯带口头禅比如“那个我想问一下”“就是说有没有可能”。这些前缀对语义没有贡献但会拉低检索的精确度。我在清洗函数里专门维护了一个停用词列表把这些开场的口头禅删除后再检索。这个经验是A同学在测试反馈中无意中发现的——他发来一批用户query检索效果普遍偏差后来发现几乎所有query都有一个共同的前缀删掉之后效果立刻好了。第三个坑是“静音段和卡顿段”干扰转写时间戳。很多语音检索系统会依赖Whisper返回的分段时间戳做定位比如“跳转到音频的第三句话”。但实际录音中有大量短暂停顿Whisper会把停顿识别成独立的空片段导致时间戳错位。我最后的处理是直接按文本内容呈现不做时间戳定位除非音频本身是干净的逐句录制。5.3 性能与成本实测数据参考最后给一组我在实际项目中的实测数据供参考。环境是单张消费级GPU显存24GB。使用Whisper medium模型转写一段30秒的中文语音大约需要1.5秒到3秒取决于音频清晰度。嵌入模型对每段300字文本的编码时间约为30毫秒FAISS检索单条query的耗时不追求极限优化也就在10毫秒以内。整套链路从音频输入到答案输出大约2秒到4秒其中转写占了大头。如果要降到1秒以内瓶颈清晰指向转写模型。两条路可以走换更小尺寸的模型small甚至base或者用流式转写边说边转不需要等全句说完。但这会带来准确率的下降需要业务方权衡。我个人的习惯是在Demo阶段不要急着优化延迟先把准确率跑达标再针对瓶颈做性能优化。因为很多性能优化手段会降低准确率如果一开始就追求快后面排查效果问题时会很痛苦。注意语音检索的评估体系要跟纯文本RAG区分开。文本RAG可以评测“召回命中率”但语音检索的误差会在转写环节就被放大所以最终评估必须结合端到端的问答效果而不是只看某一环的指标。否则会出现“每一环单独看都很好整体效果却不尽人意”的情况。我个人在实际操作中的体会是语音检索链路的复杂度不在任何单个环节而在环节之间的衔接质量。转写、切片、向量化、检索、生成每一项都有成熟的开源组件但把它们串成一个稳定可靠的整体需要的是对每个衔接点都保持敬畏。做完这套方案之后我再接任何新的语音类需求都会先问一句这段音频是安静的室内录的还是嘈杂的真实环境录的这个问题的答案直接决定了系统的架构走向。最后再分享一个小技巧如果你暂时没有足够的音频数据来测试可以先手动把一批常见问题用浏览器自带的语音转文字功能转成文本再用这些文本去验证下游检索链的效果。这样可以把“语音识别”和“检索生成”两个环节解耦测试快速定位问题出在哪一层。把这两个环节分开调通了再合到一起效率会高很多。
阅读完成 · 觉得有帮助?
咨询建站