ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG系统详解:从知识检索增强到混合检索与重排序实战

RAG系统详解:从知识检索增强到混合检索与重排序实战

先说一个我最近切身感受到的场景:某问答项目的线上日志里,用户反复问同一个问题——“你们支不支持批量导入”,而大模型的回答每次都是“请提供更多上下文”或者干脆绕着圈子答。模型没有说错话,但它根本不知道答案。这种时候最直接的办法不是换更大的模型,而是给模型一本可以翻的“说明书”。这就是RAG(Retrieval-Augmented Generation,检索增强生成)要解决的事。

这篇文章围绕“8.3 RAG系统:知识检索增强”来展开,适合三类人看:一是刚接触大模型应用、想给模型外挂私有知识的开发者;二是已经在做RAG但检索效果不理想、想知道问题出在哪儿的工程师;三是想从原理层面理解“为什么要分块、为什么要重排序、为什么纯向量检索不够”的产品和技术负责人。我会把RAG从离线索引到在线查询的完整链路拆开讲,穿插可复现的参数配置、选型思路和实际踩坑记录,最后聊聊这类系统在真实项目里最容易翻车的几个环节。

1. 大模型记不住的东西,交给外部知识库去补

1.1 为什么只靠大模型不够

先看一个大模型自身没法绕开的约束:知识截止时间。模型训练完之后,它的世界就固定在那一天。一个去年发布的新政策、一份最近更新的操作手册、一批内部产品的规格变更,模型都不可能知道。就算是在截止时间之前的内容,模型也只能记住“被训练语料覆盖到的部分”,那些低频的、冷门的、只在企业内部文档里出现的知识,它基本没有机会见过。

另一个问题是幻觉。模型在不确定答案时,倾向于用流畅的语言“编”一个说得通的回答,而不是承认不知道。这在闲聊场景里还能接受,在客服、医疗、金融、设备运维这些领域就是事故。早期的问答系统靠写规则做兜底,维护成本极高;后来有人开始做提示词约束,让模型说“我不清楚”,可模型并不知道自己哪些不清楚,所以这个方案也不彻底。

RAG的思路其实很朴素:模型不知道的,我们先把答案找出来塞给它。在用户提问的时候,系统先去检索知识库,把最相关的几段内容拿出来,连同原始问题一起交给大模型,让它根据这些资料来回答。这样一来,模型不需要“记住”答案,只需要“读懂”我们给的资料,幻觉问题被大幅压低,知识也有了明确的出处,可以追溯、可以更新。

1.2 RAG和微调的取舍

我在不同项目里经常被问到:要不要做个微调?这个问题的标准回答是:看你想让模型变什么。微调改变的是模型的“能力”和“行为习惯”,比如让它输出固定格式、学会某种语气、掌握某个专业术语的表达方式。但微调很难把“事实”塞进模型。你拿一千份文档去微调,模型最多记住几个高频结论,面对同样内容的另一个问法,它照样可能答错,而且即使答对了,你也说不出它是真知道还是蒙对了。

RAG改变的是模型“可访问的信息范围”。它不改变模型的权重,也不改变模型的表达能力,只控制每一轮回答时模型能看到什么。文档更新之后,重建索引马上生效,不需要重新训练。微调一个模型动辄几天几周,而且还需要准备高质量的训练数据,相比之下RAG一周内就能从零搭出可用版本。

不过RAG不是万能的。如果模型本身理解能力太差,给它资料它也读不懂;如果任务需要模型具备某种“推理习惯”而不是“查找答案”,RAG帮助有限。现实项目里两个手段经常混用:先用RAG解决知识来源问题,再用微调解决输出风格和格式问题。微调改个性,RAG补知识,两者不是替代关系。

1.3 什么场景值得上RAG

不是所有项目都需要RAG。判断标准可以归纳为三条:第一,答案依赖的内容是否存在一个“外部来源”,比如FAQ、产品手册、政策文件、实验报告;第二,这些内容是不是经常变化,需要及时反映到回答中;第三,回答是否正确有没有可追溯的验证需求。如果三条都满足,RAG就是当前性价比最高的方案。

反过来说,如果问题本身就是开放性的,比如“帮我想几个创意”,或者用户只想要一个简短寒暄,那RAG没有太大意义。另外,如果知识库很小、问题也很固定,直接把FAQ写进提示词可能更简单,没必要引一条完整链路进来。项目里有个错误倾向:一上来就搭建完整RAG系统,做完之后发现80%的问题靠简单的规则匹配就能解决。判断“复杂度边界”在哪里,比盲目上技术栈更重要。

2. 一条查询从发起到返回:RAG的完整链路拆解

2.1 离线侧:从原始文档到可检索的索引

RAG系统的离线侧目标只有一个:把非结构化的文档变成“计算机能快速检索的结构化索引”。这里又拆成三个步骤:解析、分块、向量化。

解析阶段处理的是格式问题。PDF要提取文字并保留段落顺序,Word要做标题层级识别,网页要去掉导航和页脚噪声。很多项目在第一步就会踩坑:扫描版PDF解析出来是一堆乱码,表格数据被截断成半行文字,代码片段被误当正文切碎。我的建议是:在解析阶段宁可保守一点,先保住文本完整性,再考虑怎么切片。文本都是错的,后面做得再好也没有意义。

分块是把长文档切成小段,每段作为一个独立检索单元。这个步骤直接影响检索精度,后面会单独展开。向量化则是用Embedding模型把每一段文字转换成一串浮点数向量,让“语义相似的句子在向量空间里距离更近”。这些向量和对应的原文一起存入向量数据库,同时维护一个从向量到原文ID的映射关系。在线查询的时候,把用户问题也变成向量,去向量库里找最近的若干条记录。

2.2 在线侧:召回、重排、生成三步走

用户提一个问题之后,系统内部的流程大致分三步。

第一步是召回。把用户问题向量化和知识库里的向量做相似度计算,取Top-K个候选段落。K的选择通常在5到20之间,太小容易漏掉关键信息,太大会让后面的模型输入过长、噪声增多。纯向量召回的方式是“只按语义距离排序”,它有一个天然缺陷:忽略关键词精确匹配。比如用户搜“服务条款第3条”,语义上最接近的可能是“服务条款概述”,而不是真正的第3条原文。为了避免这个问题,越来越多系统采用“混合召回”:向量检索跑一遍,同时用BM25这类传统词法检索跑一遍,把两路结果合并去重,再进入下一步。

第二步是重排。召回阶段拿到的是几十条候选,不能全塞给大模型,所以要精挑。重排器(Reranker)会以“问题-段落”配对为单位逐一打分,判断这段文字对回答当前问题到底有多大帮助。这一步和召回阶段的向量相似度不同,重排器是专门为“相关性判断”训练的模型,精度更高,但也更贵,所以只用来处理召回后的少量候选。

第三步是生成。我们把重排后的得分最高的几段内容拼接起来,按照“系统提示词+参考资料+原始问题”的结构发给大模型,并在提示词里明确要求:只能基于参考资料回答,如果资料中没有答案就说不知道。这一步是RAG效果好坏的最后一道闸门,提示词的约束力直接决定模型会不会发挥过度。

2.3 一条实际请求的日志视角

为了讲得更具体,我给你看一个简化版的实际流程日志,字段做了脱敏:

[query] 批量导入支持哪些格式? [expand] 批量导入, 支持格式, CSV, Excel, API [recall: vector] 命中 doc_345 段落2 (score 0.83), doc_102 段落5 (score 0.79) [recall: bm25] 命中 doc_345 段落2 (score 8.5), doc_208 段落3 (score 7.2) [merge] doc_345_p2, doc_102_p5, doc_208_p3 [rerank] 1. doc_345_p2 (0.92), 2. doc_208_p3 (0.68), 3. doc_102_p5 (0.45) [prompt] 参考资料: doc_345_p2, doc_208_p3 / 问题: ... [answer] 支持CSV和Excel,API导入正在灰度中...

如果你在一个真实系统里看到这种日志,就能一眼定位问题出在哪一段。检索阶段没有命中有效文档,说明索引或分块有问题;召回命中多但重排后分数普遍很低,说明候选质量不行;前三步都正常但生成结果胡扯,说明提示词或模型本身要调。RAG问题排查的核心思路就是“逐段分离定位”,别一上来就怀疑模型。

3. 分块、嵌入与向量检索:决定检索质量的三个环节

3.1 分块策略:不同文本结构要区别对待

分块是RAG系统里最容易被轻视、实际影响最大的环节之一。分块太粗,一段文字塞进了多个主题,向量表示会被稀释,检索时相似度分数不高不低,找不准;分块太细,语义不完整,比如一个条件句被从中间切断,模型看到的是残缺信息。经验法则是:块大小要根据“最小完整语义单元”来定,而不是统一拍一个数字。

我个人的实践是按文档结构分块——先解析出标题层级,标题下的小节作为一个候选块,块超过阈值的再递归拆分,同时保留上下文的标题作为前缀。这个策略在有明确结构的文档(说明书、规章制度、论文)上效果很好。但像聊天记录、工单描述这类弱结构化文本,就得靠固定窗口大小加重叠:比如每块512个token,相邻块之间重叠64个token。重叠的目的是防止同一个语义单元恰好被截断落在两块里,给检索留点缓冲。

还有一个细节经常被忽略:分块之后要不要保留元信息。建议每一段带上文档标题、章节路径、更新时间、来源URL这些字段。检索结果里这些东西既是给重排器用的信号,也是给用户展示来源的依据。没有元信息的RAG系统,做出来像是个黑箱,回答对了不知道哪儿查到的,回答错了也没法追溯。

3.2 嵌入模型选型:通用与领域之间的平衡

选Embedding模型时,第一反应是看榜单分数,这个方向对,但不完全对。我更建议先做一组“领域样本测试”:把你业务里典型的20个问题拿出来,人工标注每个问题应该命中哪几个文档段落,然后用不同Embedding模型跑一遍,看各自的召回命中率有多少。分数高的通用模型不一定擅长你的领域,尤其当文档里充满缩写、术语、数字型号时,通用模型的向量空间很可能不认识这些词。

举个例子。某设备维修场景的文档里大量出现“P-2311泵组”“联锁跳变”“DCS点位”这些词。用通用中文Embedding模型做检索,“P-2311泵组”经常命中不了;换用针对工业文档微调过的Embedding模型之后,相关段落能稳定排进Top-5。这不是说通用模型不行,而是说在专业领域里,词表和语义分布差异太大,稍有偏科就会影响召回。

使用Embedding模型还有一个容易忽略的点:文本长度限制。模型通常只接受512个token以内的输入,超过会被截断。所以分块大小的上限,本质上是由Embedding模型的输入窗口决定的,不是你想切多大就切多大。

3.3 向量检索为主还是混合检索为主

纯向量检索的问题刚才提过:它对精确词匹配不敏感。用户问“CVE-2024-1234”这个漏洞编号,如果知识库里写的是“CVE-2024-1234 存在远程代码执行漏洞”,向量检索大概率能命中;但如果你问的是“编号1234的漏洞”,向量检索可能就懵了。词法检索在“精确值匹配”上有天然优势,编号、型号、人名、条款序号这些场景是它的强项。

所以现在的RAG系统主流做法是混合检索:向量召回做“语义模糊匹配”,BM25召回做“词法精确匹配”,两路结果合并后做归一化打分。归一化方法各家常不一样,简单的做法是分数分别做Min-Max缩放到0到1,再加权求和;稍微复杂一点的做法是把两路结果交给重排器统一排序,让模型自己学习“什么时候更信向量、什么时候更信词法”。

从成本角度看,混合检索多出来的BM25几乎不增加计算开销,索引是文本级的,不需要额外模型。我建议所有RAG项目从一开始就做混合检索,不要先做纯向量、发现问题再加。一步到位省下来的重构时间,比省下的那点开发量值钱得多。

4. 从Demo到生产:一次可复现的选型与部署记录

4.1 基础链路选型清单

下面这套选型是我在一个内部知识问答项目里实际跑过的组合,目标是快速验证RAG在业务数据上的效果,选型原则是“够用就好、方便替换”。

环节选型理由
文档解析自研解析服务加第三方PDF库文档格式杂,统一走一套解析接口便于后续扩展
分块标题结构优先,固定窗口兜底兼顾结构文档与弱结构文本
Embedding中文通用Embedding模型加领域微调版本对比用领域样本测试决定最终使用哪个
向量库支持过滤、混合检索的向量数据库避免后期因元信息过滤需求而迁移
词法检索BM25精确匹配兜底,与向量结果合并
重排轻量级Cross-Encoder重排模型平衡精度与性能
大模型通用对话模型,上下文窗口稍大依赖提示词约束完成生成

这里要强调一点:选型清单里的任何一项都不是固定的。向量库完全可以换成开源自建的;Embedding模型迭代很快,两三个月出一个新的候选。关键是每一层都留好接口,不要把业务逻辑耦合死在某个具体实现上。我在项目里吃过这个亏:早期用了某个向量库的特有排序语法,后续要切换成别的库时,检索逻辑要重写一遍。

4.2 一套可直接落地的配置参考

给一份可复现的分块与检索参数配置,项目里可以直接拿去做基线测试,再根据自身数据调整:

chunking: strategy: hierarchical max_tokens: 512 overlap_tokens: 64 min_chunk_tokens: 128 metadata_fields: [doc_id, title, section_path, update_time, source_url] embedding: model_name: text-embedding-3-small-classic max_input_tokens: 512 batch_size: 32 normalize: true retrieval: top_k_candidates: 20 vector_weight: 0.7 bm25_weight: 0.3 rerank_top_n: 5 min_score_threshold: 0.35 generation: model_context_window: 8192 reference_docs: 5 temperature: 0.2 max_output_tokens: 1024 system_prompt: "只依据参考资料回答..."

几个参数的解释:min_chunk_tokens: 128表示小于128个token的碎片段落会被并入相邻块,避免出现大量半句话块;temperature: 0.2是经过测试后确定的,降到0以下容易让回答生硬,调到0.5以上幻觉概率明显升高;min_score_threshold: 0.35的含义是重排结果低于这个分数时,系统直接回答“知识库中未找到相关信息”,不让模型硬答。

4.3 部署之后先看这三个信号

系统上线之后,不要急着调提示词,先看三件事:第一,知识库索引覆盖率——到底有多少文档成功进入了向量库,多少文档解析失败或切片后为空;第二,检索Top-K命中率——随采样一批线上问题,看正确文档是否出现在召回列表里,这是检索质量最直接的反映;第三,生成环节的拒答率——有多少问题被系统判定为“知识不足”,如果拒答率太高,说明检索环节漏掉了太多信息,不是模型的问题。

这三个信号能帮你把问题快速分层:索引覆盖率低,问题在解析和入库;检索命中率低,问题在分块和Embedding;命中率正常但拒答率高,问题在重排阈值或提示词。逐层排查比盲目调参高效得多。我见过有人花了两周调Embedding模型,最后发现是PDF解析阶段把一半文档的表格丢了。

5. 检索增强系统的短板与优化方向

5.1 索引更新:RAG最容易被遗忘的运维负担

RAG系统的知识不是一次建好就永远不变的。业务文档每周更新,过时内容如果不及时从索引里移除,模型会拿旧信息回答新问题。这在客服场景里尤其致命——用户问“最新价格”,系统还答上个月的报价。

索引更新策略一般分三种:定时全量重建、增量更新、源文件触发更新。全量重建最简单,但文档量大时耗时很久;增量更新需要追踪文件变更状态;触发更新最可靠,又依赖文档管理平台能提供事件通知。我的建议是:前期先做定时全量重建,每天凌晨跑一次,等系统稳定后再按需上增量。不要把增量更新设计得太复杂,很多团队在上线第一个月就被“索引和源数据不同步”这个运维问题拖住了。

5.2 多跳问题与查询改写

RAG系统有个很典型的能力边界:当问题需要综合多个段落甚至多篇文档的信息时,单次检索往往不够。比如问“A型号设备和B型号设备的保修政策有什么区别”,答案可能分散在A设备的介绍页和B设备的售后条款里,一次召回很难同时拿到两个信息源。这种问题被称为多跳(Multi-hop)问题。

应对思路是查询改写加多轮检索。把原始问题拆解为多个子查询,先后检索,再把第一次检索到的内容作为上下文去生成第二次查询。另一种更轻量的做法是“检索前改写”:把问句中的指代词还原成完整描述,比如“它们的保修政策”改写成“A型号设备和B型号设备的保修政策”,能明显提升召回质量。这里可以用一个小模型做改写,不必每次检索都动用大模型。

5.3 另一个容易被低估的方向:重排序模型要单独评估

很多RAG项目会在检索环节花大力气,对重排器却只随便选一个模型接上就不管了。但重排器是“从50条候选里挑出5条”的关键,一旦它选错,前面的召回做得再好也没用。我习惯把重排器当作一个独立模块来评估:构造一组“问题-候选段落-相关与否”的测试集,计算它的NDCG和Precision@5,而不是只凭感觉看几个例子。

选重排模型时要特别注意它的输入长度限制。Cross-Encoder类重排器一般限制在512个token内,段落过长会被截断,导致相关信号丢失。遇到长段落要重排,先按块召回再对块重排,最后把命中的块还原成完整章节片段给生成模型,这个策略在实践中效果不错。

5.4 从RAG到Agent:检索增强的下一步

写这篇文章时,RAG系统的边界正在被进一步推宽。传统RAG是“检索一次,生成一次”,遇到复杂任务时能力有限。现在很多系统把检索能力包装成一个工具,交给Agent去规划调用——模型先判断要不要查知识库,决定查几次,查完再总结回答。这让系统具备了自主分解问题和多次检索的能力,也能根据中间结果判断“信息够不够,要不要换一个角度再查”。

这个方向很有前景,但对应的工程复杂度也上来了。Agent会自己决定调用动作,日志追踪、超时控制、成本预估都要重新设计。我的观点是:先把单轮RAG做到满意,再考虑Agent化。别让Agent的灵活性掩盖了底层检索能力不足的问题。

6. 一些实操建议:少走弯路的经验之谈

6.1 先手工验证,再自动化评测

搭建评测集是件很花时间的事,但从项目第一天就该做。选50到100个真实问题,每道题标注出正确答案对应的文档段落,这就有了最基础的“问题-答案-证据”三元组。之后每次改动分块参数、换Embedding模型、调重排器,都用这套数据跑一遍,效果一目了然。

基于这套人工标注的数据,可以逐步扩展自动化评测。生成回答之后,用“答案里是否包含关键事实”作为自动判分依据,例如检查答案中是否出现了正确答案里的关键实体和数值。这种自动判分的准确率不是100%,但用来对比不同版本的相对好坏完全够用。没有评测集的情况下调参,基本等于猜。

6.2 检索质量比生成质量更难优化,也更值得投入

项目中后期的大部分问题都出在检索上:召回的文档不相关,答案再怎么润色也是错的。我建议把优化精力按6:3:1分配——六成投入检索优化,三成投入生成优化,一成投入工程运维。很多人把精力放在提示词调优上,因为反馈直接、看得见变化,但检索的每次提升都是系统性的。

具体到检索优化优先级:先解决分块合理性,再检查Embedding模型是否适应领域,然后加混合检索,最后调重排器。这个顺序基于“影响范围和隔离难度”排列,每一步都能单独验证,不会因为同时改了多个变量而搞不清是谁起的作用。

6.3 最后一个小技巧

给提示词里加入一层“参考答案格式”约束,会明显提升回答的可用性。比如要求“先直接给结论,再简单说明依据,最后标注来源”。这种方式让用户一眼看到核心答案,又保留了追溯出处的路径。在实际项目中,同一个RAG系统换了这个提示词结构之后,用户对回答的满意度提升了不少。提示词别贪多,越简洁的指令,模型执行得越稳定。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进