☰
RAG落地实践:从查询改写、混合检索到重排序的全链路调优
2026/10/26 13:17:03 网站建设 项目流程

第118期翻译选材的时候,我特意挑了TowardsArtificialIntelligence上这篇关于本地知识库问答系统的文章。不是因为它用了多新的模型,而是它把RAG(检索增强生成)这条技术路线的工程细节讲得比较透。国内很多团队聊RAG还停留在“向量数据库+LLM”这两个词上,真到落地的时候,查询怎么改写、分块怎么切、召回结果怎么排序,全是坑。这篇原文给了不少可复现的实验数据,我觉得值得好好拆开来讲。

1. 为什么本地知识库RAG成了值得深入拆解的主题

1.1 公网大模型和私有知识库之间的断层

TowardsArtificialIntelligence那篇文章开篇讲了一个很实际的矛盾:通用大模型确实能写诗、能聊天、能做通用推理,但问到企业内部制度、产品技术文档、某条业务线的历史决策记录,它就开始一本正经地胡说八道。这不是模型笨,而是训练数据里根本没有这些私有知识。原文给了一组数据,我记不太清具体数字,但结论很明确:在垂直领域问答场景下,通用模型直接回答的准确率往往不到50%,而加上检索环节之后能大幅提升。

这个问题的本质是知识来源不同。通用大模型的知识来自预训练语料,有截止日期、有覆盖盲区;企业内部知识库则是活的,每天都在更新。想让模型真正用上这些增量知识,两条路:一条是继续训练或者微调,成本高、周期长、知识更新还得重新训;另一条就是RAG,不改变模型参数,用检索把相关知识“喂”到上下文里,让模型基于给定材料作答。

原文的立场很明确:90%以上的企业内部知识问答场景,不应当走微调,而应当走RAG。这个观点我认同。微调适合改变模型的行为模式和输出风格,但不适合塞入大量事实性知识。你不可能把几千篇制度文档微调进模型参数里,就算硬塞进去,也会出现知识混叠和遗忘问题。RAG的优势在于知识外置、更新即时、可溯源,这在企业场景里太重要了。

1.2 原文实验里最反直觉的发现

这篇文章值得翻译的重要原因,是它没有停留在“RAG很好用”这个层面,而是给出了不少反直觉的结论。比如它对比了几种常见的检索方案,发现单独用向量检索的效果,在很多专业领域数据集上,并不比传统BM25关键词检索好多少,甚至在某些场景下更差。

我刚看到这个结论时也有点意外。仔细一想又合理:向量检索擅长捕捉语义相似,但语义相似不等于答案相关。比如用户问“打印机卡纸怎么办”,向量检索可能把“打印机的维护保养周期”排到前面,因为两句的语义向量距离很近,但后者根本不是用户要的解决步骤。反过来,BM25能精确匹配“卡纸”“怎么办”这些关键词,反而能命中真正的故障处理文档。

所以原文给的建议是混合检索:把BM25和向量检索的结果做融合,再用重排序模型兜底。这个方案后来我在自己的项目里验证过,确实稳定有效。这也是我决定翻译这篇文章的原因——它有实验数据支撑,不是那种堆概念的空谈文章。

2. 原文拆解:一整套RAG系统的五个核心模块

2.1 查询理解与改写模块

很多讲RAG的文章上来就说“把用户问题向量化,然后去向量库里查”,但TowardsArtificialIntelligence这篇文章花了不小篇幅讲查询改写。这是国内不少团队容易忽略的环节。用户真实输入的query往往是口语化的、有指代歧义的、长度也不可控的。直接拿原始query做向量检索,效果经常很飘。

原文提到几个改写策略。第一个是假设性文档生成(HyDE),让LLM先根据用户问题生成一个假设性的回答文档,再用这个文档去做向量检索。我当时看到这个方案觉得有点绕,但实测确实有效,因为假设性回答里包含了更完整的实体和语义信息,比简短的问题更容易匹配到知识库里的片段。

第二个是历史对话压缩。在多轮问答场景里,用户经常会说“那这个方案的成本呢”,如果只拿这句话去检索,系统完全不知道“这个方案”指的是什么。改写模块需要把多轮上下文压缩成一条完整的独立问句,再做检索。原文用的方法是让LLM做上下文重写,给一个模板,要求模型把指代词替换成具体的实体。

第三个是query拆解。复合问题要拆成多个单意图子问题,分别检索再合并结果。比如“请对比A方案和B方案在安全性和成本上的差异”,拆成A方案安全性、A方案成本、B方案安全性、B方案成本四路检索,再让LLM综合回答。这个思路不算复杂,但对检索精度提升非常明显。

2.2 文档解析与分块策略

原文用了一个我很喜欢的比喻:知识库里的PDF、Word、Markdown文档,对大模型来说是“一本没有目录、没有标点、纸张还粘在一起的书”。不做解析和分块,直接整篇塞进上下文,结果是模型读得云里雾里,检索也找不到精确位置。

文档解析这块,原文强调了两个容易被低估的环节。一是PDF里扫描件需要OCR,表格需要转成结构化的Markdown表格,双栏论文需要按栏重新排版,否则文本顺序都是乱的。二是页眉页脚、水印、参考文献这些噪声要滤掉,不然分块时会把不相关内容切进同一个块。

分块策略则是RAG里最考验经验的地方。分块太小,语义碎片化,检索到的内容缺乏上下文;分块太大,噪声多、向量表示被稀释,而且容易超出模型的上下文窗口。原文给了几个参考值:一般文本建议256到512个token,代码和技术文档建议512到1024个token,并且要配合重叠(overlap)区间。重叠的意义在于防止关键信息正好被切在分块边界上。我做过对比实验,没有重叠的情况下,检索命中率会掉5%到10%,这个差距在知识库场景里是很明显的。

分块还得考虑文档结构。原文提出的方案是按标题层级进行结构化分块:先识别H1、H2、H3标题,再把正文块归属于最近的标题之下。这样每个分块天然携带了章节上下文,检索时可以按“标题+正文”的组合送去重排序,效果比纯按字数切块好很多。我自己复现这个方法的时候,把分块字段设计成两级,父块存标题路径,子块存正文,检索时父块参与过滤、子块参与排序,整个系统的可用性提升了一个档次。

2.3 向量化与混合检索

向量化环节的选型决定了检索质量的上限。原文没有推荐具体模型,但给了很实在的选型原则:第一,Embedding模型的输出维度要和向量数据库支持的范围匹配;第二,中文场景不要盲目用英文模型,中文分词和语义理解要专门评估;第三,Embedding模型要跟你的领域数据分布做验证,用一组人工标注的问答对跑一遍召回率测试。

混合检索这块,原文给出了融合公式。不是简单的“向量结果和关键词结果各取一半”,而是用倒数排名融合(RRF)算法:每个文档在两种检索方式下都有一个排名,融合分数等于各排名倒数的累加。这样处理的好处是,某个文档在单一检索方式下排名靠前但在另一种方式下排名很靠后时,不会被直接丢弃,而是通过排名融合保留下来。

我也试过用加权分数融合,给向量检索0.7、BM25加权0.3,效果在部分数据集上可以,但换一个数据集就得重新调权重,泛化性不如RRF。原文还提到了一个细节:召回数量不要贪多,向量检索和BM25各取前50条做融合就够了,召回太多反而会引入噪声,增加重排序的负担。

2.4 重排序模块

重排序是RAG从“能用”到“好用”的关键分水岭。初检阶段,向量检索和BM25的目标是尽量别漏掉相关内容,所以召回范围会放宽,结果里混着不少不相关的片段。重排序模型的任务就是把真正有用的排到最前面。

原文对比了几种重排序方案。最简单的是用LLM本身做重排序,把候选片段和问题拼接后让模型打分,但成本高、延迟大,不适合在线场景。更实用的是专门的Cross-Encoder重排序模型,把问题和文档对拼在一起输入模型,输出相关性分数。这类模型精度高,但因为要对每一对做全量计算,速度比Bi-Encoder慢不少,所以只能对初检结果用。

工程上还要考虑重排序后的数量控制。原文建议重排序后只保留前3到5个片段送入LLM生成答案,再多了反而会分散模型注意力。我在实际测试中发现,保留5个片段、每个片段控制在500到800字,答案质量和检索延迟的平衡最好。如果把前20个片段全塞进去,不仅费token,模型还容易捡了芝麻丢西瓜。

2.5 生成模块与答案溯源

最后一步是把重排序得到的高质量片段和用户问题一起给LLM生成答案。原文强调了几点:一是在提示词里要求模型只依据给定的文档片段作答,不要使用内部知识补全,这样可以减少幻觉;二是要求答案中标注引用来源,也就是在生成结果里带上“信息来源:xxx.docx第3章第2节”这样的标注,方便用户溯源;三是当检索结果与问题明显不相关时,模型要敢于回答“知识库中未找到相关信息”,而不是强行编一段。

这个设计让RAG系统在企业场景下更可信。用户看到的每个答案都能对应到具体文档,出了问题可以回查,这在制度问答、合规审查、客服辅助这些场景里特别重要。

3. 照着原文落地:搭建一套轻量级可运行的知识库问答系统

3.1 技术选型:哪些组件可以直接抄作业

看完原文的思路,我照着搭了一套可以实际运行的系统,技术栈如下:

模块选型选型理由备注
Embedding模型BGE-large-zh中文效果好,开源可商用也可用m3e-base降低资源占用
向量数据库Milvus Lite单机部署简单,支持混合检索数据量大可换Qdrant或ES向量插件
关键词检索BM25(ES或自定义倒排索引)精确匹配能力强,与向量检索互补小规模可直接用rank_bm25库
重排序模型BGE-reranker-baseCross-Encoder结构,精度高对初检数量敏感,注意batch处理
生成模型Qwen2.5-14B-Instruct中文生成能力强,可本地部署如果显存有限,可换7B版本

这个组合不是唯一的答案,但胜在每一步都有开源实现,而且模型精度和资源消耗的平衡点比较合理。Embedding、Reranker、生成模型三件套都用中文优化的开源模型,整体效果不会比调闭源API差太多,但数据全程不出内网,对企业来说这点很重要。

3.2 索引构建的完整流程

索引阶段就是从原始文档到可检索向量的过程。我按原文的结构化分块思路写了个处理管线。

先看文档解析部分。我用的是PyMuPDF读取PDF,配合paddleocr做扫描件OCR。需要说明的是,普通PDF里也经常藏着图片型文字,不跑OCR就会丢内容。解析完成后,把文本按标题层级切块,代码大致是这样:

def chunk_by_headings(text): lines = text.split("\n") blocks = [] current_heading = "Root" current_buffer = [] for line in lines: if re.match(r"^#{1,4}\s+", line): if current_buffer: blocks.append({ "heading": current_heading, "content": "\n".join(current_buffer) }) current_buffer = [] current_heading = re.sub(r"^#{1,4}\s+", "", line.strip()) else: current_buffer.append(line) if current_buffer: blocks.append({ "heading": current_heading, "content": "\n".join(current_buffer) }) return blocks

这只是第一层切分。如果某个标题下面的内容太长,超过了设定的分块长度,还要做第二层滑动窗口切分,窗口大小取400个token、重叠区间80个token。这组参数我是在实际文档集上调出来的,纯文本偏多的时候效果不错;如果文档里表格多,可以把窗口调大到600,重叠保持100。

紧接着是把分块向量化。这一段有性能取舍:Embedding模型推理速度不算快,但比生成模型好很多,批量处理时一次喂32条文本,CPU上也能跑。向量化完成后,把ID、heading、content、vector四个字段写入Milvus,同时把ID和content写入ES做BM25索引。两份索引用同一个doc_id关联,方便后面做混合检索。

3.3 检索与生成的全链路参数

查询阶段,我按下面的配置跑过一轮压力测试:

  1. 用户输入query后,先用LLM做查询改写,把口语化问题转换为适合检索的表述。这里注意,改写要轻量,用一个7B模型就行,太重会影响整链路延迟。
  2. 改写后的query并行送进两个检索器:向量检索取top50,BM25取top50。
  3. 用RRF融合两个结果集,融合后取top20。
  4. 把top20片段交给BGE-reranker重排序,取前5个片段。
  5. 将query和5个片段拼进提示词,送Qwen2.5-14B生成答案。

每步的超参数我整理成了表格:

参数推荐值调整说明
向量检索召回数50太少容易漏,太多增加融合和重排压力
BM25召回数50与向量检索保持一致,便于RRF公平融合
RRF融合参数k60原论文推荐值,作用是把排名做平滑
重排序输入数20超过20个之后,reranker精度提升不明显
重排序输出数5生成阶段5个片段的信息量足够且不分散
生成温度0.2知识问答场景要低温度,减少随机性
最大生成token512答案超过512字的情况很少,足够用了

整条链路的端到端延迟,在单张A30显卡上大约是3到5秒,其中生成阶段占了差不多3秒。如果对延迟有更高要求,可以把生成模型换成量化版本,或者改用更小的7B模型,但答案质量会有轻微下降。原文对延迟的讨论相对少,但在实际落地时,这个指标往往比离线评测的准确率更影响用户体验。

4. 把检索质量做上去:从60分到90分的调优细节

4.1 查询改写到底是加分项还是负担

我把查询改写模块做了个对照实验:一组直接拿用户原始query去检索,一组先让LLM改写再检索。结论让我有点意外——改写并不总是加分。对于表述清晰、包含明确实体名的query,改写几乎没有收益,偶尔还会因为LLM多写了几句废话,把检索方向带偏。

真正让改写发挥价值的是两类query:一是口语化严重的长句,二是带有指代关系的多轮query。比如用户说“这东西安全吗”,不加改写,检索系统根本不知道“这东西”是什么;改写成“电力电缆护套材料的阻燃安全性”之后,召回质量立刻就不一样了。

所以我的建议是给查询改写加一个触发条件,而不是每个query都走一遍改写。用规则判断query长度、是否包含指代词、是否是多轮追问,满足条件才触发LLM改写。这样既提升了复杂query的精度,又不会给简单query增加无谓的延迟和成本。

4.2 重排序模型的参数陷阱

BGE-reranker这类Cross-Encoder模型,输入长度上限一般在512个token。如果分块长度超过了这个限制,直接塞进去会被截断,导致排序效果大打折扣。我在测试初期就踩过这个坑,当时分块设置得很大,想着多塞点上下文,结果reranker根本读不完整,排序质量反而下降了。

解决方案有两种:一是分块时就限制单个片段不超过512个token;二是在重排序前,把超过限制的候选片段再做一次滑窗切分,取最匹配的子片段参与打分。第一种方案更干净,我最终选择了在分块阶段就控制长度。

另一个容易忽略的点是batch大小。重排序模型虽然不需要生成,但全量计算pair的时候,显存占用并不低。候选20对时,batch size设为8比较稳妥,太大容易OOM,太小则推理时间翻倍。实测这一步对系统稳定性的影响比想象中大。

4.3 混合检索权重怎么设才不会打架

RRF融合的好处是不需要调权重,但它把“向量检索”和“BM25”当成平权渠道。某些场景下这样做并不合理。比如代码检索场景,精确匹配函数名和变量名极度重要,BM25的贡献应该更大;比如问题语义复杂、关键词和答案之间没有直接字面重合的场景,向量检索的贡献更大。

我在实际系统里做了一点改动:给RRF加一个渠道权重系数,向量结果的倒数排名乘1.2,BM25结果乘1.0,相当于在默认融合基础上稍微偏向语义检索。这个系数是通过一组行业QA对试出来的,整体效果稳定。如果你要复现,建议在自己的测试集上先跑一版等权的,再分别单独调向量权重和关键词权重,对比一下差异再定系数,不要凭空设。

5. 原文没展开但落地必踩的坑

5.1 重排序之后还是答非所问,问题多半出在提示词

RAG系统跑通了不代表效果好了。有一段时间我这边检索结果明明挺准的,但答案就是缺关键步骤。后来排查发现,问题不在检索,而在生成提示词写得太笼统。我给的指令是“根据以下文档回答问题”,模型并不知道它应该优先采信检索片段中哪部分信息,也不知道输出格式应该是什么样。

后来我把提示词改成了结构化格式,明确告诉模型:先找“问题对应的直接答案”,再找“支撑依据”,最后才考虑“相关背景信息”;如果没有直接答案,明确说“未找到”。同时给了一个输出模板,要求答案开头直接给结论,不要铺垫。改动之后,答案质量肉眼可见地提升了一大截。这个坑特别隐蔽,因为整条链路的日志看起来一切正常,只有人工看答案的时候才发现不对。

5.2 知识更新了,FAQ还是答旧的

RAG的核心优势是知识可以随时更新,但更新不是把新文档向量化入库就完事了。我在测试中发现,老版本制度文档删除后,向量库里相同内容的旧分块如果没同步清理,检索时新旧版本会同时被召回,模型经常选择新版本作答,但也有概率选中旧版本,答案就矛盾了。

解决方案是给文档做版本管理:每条文档入库时记录文档编号和生效日期,检索时按生效日期过滤,只保留当前有效版本。如果出现跨版本冲突,优先选生效日期最新的分块送入重排序。这个机制原文没有细讲,但实际业务场景几乎必然会遇到。

5.3 评测指标别只看一个召回率

原文末尾提到RAG系统的评测应该分成检索和生成两个环节分别评估。检索环节看召回率、MRR、NDCG;生成环节看答案正确性、忠实度、引用准确率。两个环节的指标要分开看,否则出了问题根本定位不了是检索的锅还是生成的锅。

我按这个思路建了一组评测集,大概200条真实历史工单和对应标准答案。检索环节的评测方法是:把标准答案拆成关键片段,看这些片段是否出现在检索结果里;生成环节的评测方法是:用规则检查答案中是否包含标准答案里的关键实体和数字。这套方法不依赖大模型打分,公平性和可重复性都比较好。每次调整分块参数或检索策略后,就跑一遍评测集对比数据,很快就能看出哪一步改动是正向的。

结尾不需要再叠什么总结,这一路从选材、拆解原文到落地复现,我最深的感受是:RAG不是把几个组件接起来就完事的技术,而是一个需要反复调优的系统工程。如果你正打算给团队搭建知识库问答,建议先按原文的思路把“分块质量、混合检索、重排序”这三个环节分别跑通,再做一个100条左右的小评测集,用数据说话。最后分享一个经验:优先把你的重排序模型效果做到极致,它带来的收益通常比换一个更大参数的生成模型明显得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询