ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RAG从能跑到能打:检索优化、重排序与评估闭环的工程落地指南

RAG从能跑到能打:检索优化、重排序与评估闭环的工程落地指南

上次聊完RAG的基础流程,不少人照着文章把demo搭起来了,演示效果很惊艳,但一上业务数据就开始各种打脸——检索回来的片段沾点边但不对,大模型一本正经地引用错误信息,改个embedding模型又得全部重新索引。这篇是检索增强生成技术精讲的第二篇,核心只解决一件事:让RAG从“能跑”变成“能打”。如果你已经理解了RAG的基本链路(文档加载→切块→向量化→召回→生成),但卡在优化、评估和工程落地这一步,这篇就是为你写的。

1. 先把问题摊开:RAG从“能用”到“好用”差在哪

先泼盆冷水。RAG这个方向,本质上是把“大模型记忆不可控”的问题,转化成“检索质量可控”的问题。但很多人把整套流程搭完,发现效果并没有想象中好,原因往往不是模型不够强,而是链路中好几个环节都在“悄悄扣分”。

我一般把RAG生产落地的瓶颈归纳成三类:第一类是“召而不准”,用户问法一变化,向量检索就抓瞎,Top-K里一堆不相关内容,或者相关内容压根没被召回;第二类是“准而不精”,相关文档确实被捞回来了,但排序不对,最有用的一段被排在后面,大模型生成时被前几条干扰信息带偏;第三类是“精而不信”,检索到的片段之间互相矛盾,或者片段本身只覆盖了问题的一小部分,模型却把它当成完整答案来用,最后生成的结果看着流畅,实则经不起推敲。

这三类问题,第一类出在解析、切块和向量化,第二类出在召回后的重排序,第三类出在上下文编排和提示词设计。再加上贯穿全程的评估体系缺失,很多团队连“优化有没有效果”都说不清楚,只能凭感觉调参。这篇文章就按这条主线展开:先把检索侧的三个关卡逐个攻破,再讲清楚重排和查询改写怎么做,然后单独用一整章讲提示词与上下文编排,最后给出可落地的评估方案和工程避坑实录。如果你想直接看评估和避坑,可以直接跳到第5和第6章。

2. 检索质量的三道关卡:解析、切块与向量化

2.1 文档解析:别把PDF直接扔给切块器

很多RAG项目翻车,第一步就翻在文档解析上。市面上PDF转文本的库很多,但如果文档里全是表格、多栏排版或者扫描件,直接转出来的文本就是一团乱麻。你想想,一个原本结构清晰的销售对比表,被逐行抽出来,表格语义全部丢失,切出来的块自然是一堆孤立数字,这种数据再怎么调模型都救不回来。

我的建议是,不管你用什么工具,目标只有一个:把任意格式的源文档,转换成结构保留完整的Markdown或JSON。针对PDF,优先选择能识别版面结构的解析方案,把标题层级、表格结构、列表嵌套都保留下来;针对扫描件,先走OCR再做版面还原;针对Word和Markdown源文件,直接转换,别自作主张去拍平。为什么强调结构保留完整?因为后续切块时,结构本身就是最天然的边界——一个小节的内容往往比固定字符数切出来的块,语义内聚得多。

实操时还有个容易被忽略的细节:解析完成后,必须做人工抽检。我一般会按文档类型各抽5到10页,重点检查三件事:标题层级是否错乱、表格是否被截断、乱码和多余换行有多少。这个步骤花不了半小时,但能帮你提前发现一批“解析期埋雷”的数据,避免后面整条链路都被带偏。很多人图省事跳过这步,最后检索效果差,还以为是模型不行,其实根子在源头。

2.2 切块策略:块的大小真的能决定答案质量

切块是RAG里最“四两拨千斤”的环节。参数改一改,不用动模型,检索效果可能就天差地别。但很多人一上来就问“chunk_size设多少”,这其实是个伪问题——真正该问的是“我的文档适合哪种切块策略”。

主流的切块策略有四种:固定窗口切块、递归结构切块、父子分块和语义切块。它们的核心逻辑和适用场景,我整理成了一张表:

切块策略基本逻辑适用场景常见坑
固定窗口按固定字符数硬切,可带重叠文本结构均匀的日志、新闻、公告切在不该切的地方,语义断裂
递归结构切块按Markdown标题、段落等边界逐层切技术文档、手册、带标题层级的内容某些小节超长,需要二次切分
父子分块子块用于向量召回,召回后返回其父块给模型需要大上下文才能作答的复杂文档索引和存储成本翻倍
语义切块根据句向量相似度动态切分内容主题跳跃大、段落边界模糊的文本计算成本高,边界不一定符合直觉

我的经验是:技术文档优先用递归结构切块,标题层级就是天然的语义边界。参数上,chunk_size可以从500到800字符起步,overlap设50到100,先跑一版再调。如果是合同、论文这类段落内部逻辑紧密的内容,固定窗口也能凑合,但重叠窗口别省——模型需要看到上下文衔接,完全不重叠会把一句话拦腰截断。如果是客服对话、FAQ这类短文本,别切块,直接整条存入向量库就行,硬切反而破碎。

父子分块值得多说一句。它的思路是:用一个小子块去参与向量召回,因为小块向量更聚焦、相似度得分更准;但召回后不直接把子块丢给模型,而是把包含子块的父块返回。实战中,子块用200到300字符,父块用800到1500字符,效果通常不错。代价是索引时同一份内容以两种粒度各存一份,存储翻倍,但换来的是检索精度和生成信息量的双重提升,值得。

2.3 向量化:Embedding模型选型与维度权衡

很多人把Embedding模型当成一个“随便选选”的组件,实际上它对召回效果的影响,可能比大模型本身还大。近一两年开源社区出了很多中文向量模型,闭源API也不少,但千万别只看榜单分数。榜单测的是通用场景,你的业务文档里全是行业黑话和缩写时,通用模型照样哑火。

我之前帮某团队优化一套内部知识库问答,他们用的Embedding在公开评测集上成绩很好,但检索他们内部的设备运维手册时,Top-5里经常混进无关的文档。换成一个在领域语料上微调过的小模型后,不用改任何其他参数,命中率明显提升。原因很简单:向量模型习得的语义空间,越贴合你的语料分布,相似度排序越有意义。

实际选型时,我会建议至少准备两个候选模型,用后面第5章说的评估方法去比,别凭直觉。另外维度也要考虑:维度越高,向量越精细,但存储和计算开销越大。几百条几千条文档无所谓,等到了百万级向量,维度差个几百,内存和检索耗时差距就出来了。工程上,小数据集追求精度优先,大数据集建议做个降维测试,很多情况下降一半维度,效果损失很小、性能收益很大。

取舍建议:通用场景,直接选最新一代的主流中文开源向量模型,维度控制在1024以内;领域文档占比超过四成,优先找领域微调模型;存储成本敏感时,量化到8-bit甚至4-bit,精度损失通常可接受。还有个小技巧:Embedding模型最好和大模型用同一个tokenizer体系,切词习惯一致,能减少很多“检索得到但模型读不懂”的怪问题。

3. 召回只是开始:查询改写、混合检索与重排序

3.1 查询改写:把用户的问法翻译成“好检索的语言”

用户的提问方式,和文档的表述方式,往往隔着一条鸿沟。比如用户问“新政策对报销流程有什么影响?”,文档里写的是“本规定自2024年3月起调整差旅报销审批权限”。这两句话在语义上相关,但向量相似度可能不够高,导致漏召回。查询改写就是解决这个问题:通过一个小模型或提示词,把用户的原始问题,改写成更适合检索的句式。

常用手法有三种:指令化改写,把疑问句变成关键词或短句,比如“工伤认定需要哪些材料”改成“工伤认定材料清单”;多查询扩展,用大模型生成同一个问题的多个不同角度表述,分别检索再合并结果,能显著提升召回率;HyDE(假想文档),先让大模型基于问题生成一个“假设的理想答案”,再用这段假想答案去检索。这个操作听起来绕,但实测能把很多模糊问题的检索精度拉上来一截,代价是每个问题多消耗一次大模型推理,延迟大约多几百毫秒。

使用多查询扩展时,展开数量建议控制在3到5个。太少漏召回,太多会把不相关的噪声也带进来。还要注意合并策略:多个查询召回的候选片段要去重、再统一走重排,不能简单“先到先得”。

3.2 混合检索:稀疏加稠密,两手都要硬

纯向量检索有个天然缺陷:对专有名词、精确ID、代码变量名这类“字面匹配更重要”的请求,效果反而不如老牌的BM25关键词检索。反过来,BM25对语义改写无能为力。所以生产级RAG基本都会上混合检索——把BM25和向量召回的Top结果合并。

合并不是为了炫技,是因为两类检索的错误模式正好互补。向量检索擅长“意思对但用词不同”,BM25擅长“字面匹配但语义无关”。合并后,相关片段只要占任意一路的优势,就有机会进入候选池。融合的方法有很多,最简单的实用方案是RRF(Reciprocal Rank Fusion):把每个片段在两路结果中的名次取倒数,再加权求和。它不需要统一分数尺度,实现简单、效果稳定。如果你本身用的向量库支持分数归一化,也可以用加权求和,一般来说向量检索权重给0.6到0.7,BM25给0.3到0.4,具体比例建议按你的文档类型调一版对比测试。

很多人在这一步会忽略一个细节:混合检索的候选集大小要大于最终想给模型的片段数。比如模型上下文里最多放5段,混合检索至少合并召回20到30段,再通过重排挑出最合适的5段。跳过多路召回直接给模型,等于把重排的决策空间提前卡死了。

3.3 重排序:让最合适的片段浮出水面

重排序(Rerank)是RAG链路里性价比最高的一环。原理并不复杂:向量召回阶段用的双塔结构,query和doc分别编码成向量再算相似度,速度快但精度有限;重排阶段改用交叉编码器,把query和候选的doc拼接后一起过模型,直接输出相关性分数,精度高出一个档次。代价是推理开销大,所以它只作用于召回阶段选出的Top候选上。

我之前遇到一个典型案例:某个问答场景,向量召回Top-1经常是错的,但正确片段明明排在Top-5里。加了重排模型后,把向量召回的Top-30重排,取重排后的Top-3,准确率肉眼可见地涨了一截。原因就是交叉编码器能看到query和doc的完整交互,而不是靠两个独立向量碰运气。

工程上的建议:重排的候选窗口设在50到100条,重排后取Top-3到Top-5给大模型。重排模型不用选太大的,参数量几十亿就足够,兼顾精度和延迟。如果你用本地推理,记得给重排单独做并发优化,它是最容易拖慢链路的一环。另外重排分数也可以顺便做一次阈值过滤,低于阈值的片段直接丢弃,宁缺毋滥,能有效减少大模型被弱相关片段误导的概率。

4. 上下文编排与提示词构建:让大模型学会“按证据说话”

4.1 上下文窗口管理:塞得越满,效果越差

到了这一步,检索出高质量的Top-5片段,任务就结束了吗?还早。把5段内容一股脑拼进提示词,大模型的表现往往比你预期差得多。原因有两个:一是token长度膨胀后,模型注意力被稀释,关键信息被噪声淹没;二是多个片段之间存在信息重叠甚至冲突,模型无从判断。

我处理的方式是“先清洗、再编排”。清洗阶段做三件事:去重,把语义重复的片段合并或剔除;裁剪,对超长片段保留与问题最相关的句子,比如按与query的相似度对句级片段排序,保留前70%;冲突处理,如果两个片段在关键事实上冲突,优先保留来源更新或权威性更高的片段,并把冲突信息在提示词里显式标注。

编排阶段,则要按照“从具体到抽象”的顺序排列片段:先放最直接命中问题的段落,再放背景补充信息。不要以为“大模型能自己找重点”,它不会。你把答案线索埋在第五段,它前三段看得津津有味,最后生成的就是前几段的复述,精确漏掉答案。

4.2 提示词模板设计:三个角色,三套话术

用RAG的提示词,和普通对话提示词最大的区别在于:模型必须学会区分“依据”和“常识”。它不能因为知识库里没有就直接闭嘴不答,也不能没有依据却硬编。

我习惯把提示词拆成三个角色来设计。第一个是“检索忠实者”:告诉模型只能依据提供的上下文回答,如果上下文没有依据,必须明确回答“资料中未提及”,禁止编造。第二个是“证据引用者”:要求模型在回答时标注依据来源,比如用“[来源1]”这样的标记,把答案与检索片段一一对应。第三个是“谨慎推理者”:当检索片段的信息不完整时,要求模型先说明已知范围,再给出有限条件下的结论,降低误导性。

一个简单的模板可以长这样:

你是一个企业知识库问答助手。请仅依据以下资料回答用户问题。 资料: [资料1] ... [资料2] ... 规则: - 严格基于资料作答,禁止使用未出现在资料中的信息; - 若资料不足以回答问题,请说“现有资料无法完整回答”,并列出已获得的相关信息; - 输出格式:先给结论,再列依据编号,最后做必要的补充说明。 用户问题:...

这段模板看着简单,但每条规则都有实际作用。“禁止使用未出现在资料中的信息”是防幻觉;“现有资料无法完整回答”是给模型一个合法的“认怂”出口,不认怂它就会编;“先结论后依据”是方便用户直接看答案,不信再去查来源。实际用的时候,你可以根据场景再加一个“默认以资料中日期最新的内容为准”之类的规则,处理文档版本冲突。

4.3 引用溯源与置信度:不止是加分项,是保命项

引入引用标注,表面上是给用户看的,实际上是给RAG系统自己“上保险”的。一旦生成的内容可以回溯到具体检索片段,你就能快速定位是哪个环节出了问题:是切块切碎了、向量召回错了、重排排偏了,还是生成环节说漏嘴了。没有引用的RAG,排查问题基本靠猜。

更进一步的方案是给每个检索片段算一个置信度标签,结合重排分数和来源优先级,打上“高置信/中置信/低置信”的标记。低置信片段对应的用户问题,可以强制走“资料不足”的兜底话术,而不是硬答。还有一个实用技巧:在提示词里让模型对“依据不足但凭常识作答”的部分,主动标注“(此为推测,未在资料中找到直接依据)”。这一步真的能大幅减少用户在重要场景下的误判。

5. 没有评估就没有优化:量化RAG的每一步

5.1 三层指标体系:检索、生成、端到端各盯各的

RAG的优化,最怕“凭感觉”。这一版重排好像好一点了?那一版提示词似乎更流畅了?但问具体好在哪里,说不出所以然。要摆脱这种状态,必须把RAG拆成可量化的评估维度。

我建议至少盯三个指标。第一是上下文相关性(Context Relevance):评估检索回来的片段中,有多少比例和问题真正相关。太低了,说明召回和重排有问题,改了也白改。第二是答案忠实度(Faithfulness):评估生成的答案是否严格基于检索片段,有没有引入幻觉。这是RAG最核心的指标,答案再流畅,不忠实就等于零。第三是答案相关性(Answer Relevance):评估最终回答是否真正回应了用户的问题,这是端到端体验的最终校验——经常出现答案完整但答非所问的情况,只看忠实度发现不了。

这三个指标各有侧重,建议分开记录,不要混成一个综合分。调参时也要分开归因:上下文相关性差,问题大概率出在切块、向量模型或混合检索权重上;忠实度低,问题大概率出在提示词约束不够或检索片段冲突;答案相关性低,问题大概率出在查询改写和上下文编排上。这样归因定位,优化方向才清晰。

5.2 评估数据集:没有高质量题目,评估就是耍流氓

做评估,最难的不是指标计算,是题目集怎么来。很多人拿几个自己编的问题就开测,测出来的结论天然带偏差。我建议用两种方式构建评估集:一种是从真实线上日志里抽样,把用户实际问过的问题作为测试样例,再针对每个问题,人工标注“标准答案”和“依据片段”;另一种是构造“难度梯度”题目,故意加入模糊提问、多跳问题、陷阱问题,测出系统的边界在哪。

构造方式上,现在有一种省力的做法:先从知识库里随机抽一些片段,让大模型基于这些片段生成问题,再让模型反向校验问题的可回答性。这样能快速批量生成一批“有标准答案”的测试集,但注意要人工抽查,避免模型自洽循环导致题目太简单。测试集规模不用太大,50到100条精心设计的题目,就足够支撑一轮迭代评估了。

5.3 一个可落地的评估脚本思路

真正落地时,评估流程就是一个自动化脚本:输入测试题目集,跑RAG全链路,分别记录检索结果、重排分数、最终答案,再调用评判模型给三个指标打分。关键点是评判模型本身要设定评分标准,比如“忠实度1分到5分,3分以上视为达标”,避免大模型打分时忽高忽低。

我用Python写了个简化版的评估循环思路,核心逻辑大概是这样:

def evaluate_rag(test_set, rag_pipeline, judge_model): results = [] for item in test_set: question = item["question"] golden_context = item["golden_context"] golden_answer = item["golden_answer"] # 跑完整RAG链路 retrieved = rag_pipeline.retrieve(question) answer = rag_pipeline.generate(question, retrieved) # 计算指标 context_corr = judge_model.score_context_relevance(retrieved, question) faithfulness = judge_model.score_faithfulness(answer, retrieved) answer_rel = judge_model.score_answer_relevance(answer, question) results.append({ "question": question, "context_corr": context_corr, "faithfulness": faithfulness, "answer_rel": answer_rel, "hit": any(doc.id == item["golden_doc_id"] for doc in retrieved) }) return results

这里的hit(召回命中率)是个很好的辅助指标,但它只测召回不测排序,所以要和三项指标一起看。评估脚本建议每次调参后统一跑一遍,把结果存下来做对比,形成版本记录。别今天测5条,明天测8条,数据不齐没法比较,优化就成了撞大运。每次调参后统一跑一遍,把结果存下来做版本对比。

6. 工程落地中的真实场景与避坑实录

6.1 实时更新与权限隔离:两个容易被拖成“黑洞”的工程问题

第一个是知识库更新。业务文档每周都在变,线上问答系统不可能每次全量重建索引。工程上要设计增量更新机制:新文档进来,解析切块后直接写入向量库;旧文档变更,要能识别同一源文档,做软删除(旧向量标记为失效,不再参与召回),避免旧版和新版同时出现、互相打架。增量更新的关键在于“文档级去重”,不能依赖向量去重——向量去重只能发现完全相同的文本,发现不了更新版本。

第二个是权限隔离。企业知识库天然分权限,不同角色能看不同文档。权限过滤最安全的做法是在召回前做:根据用户身份,在索引层面就限定检索范围。但如果你的向量库底层不好做细粒度权限,次优方案是在召回后做加严过滤——检索回来先按用户权限过滤,再送重排。千万别指望大模型自己能知道哪些内容不能看,它在生成时只会忠实复述上下文,不会替你考虑合规。

6.2 去重与缓存:小技巧带来大提升

RAG系统在线上运行时,有一批问题是高频重复的。同一条政策,一百个人问一百变,每次都跑完整检索加生成,成本高不说,响应还慢。一个简单的缓存层就能解决:把查询改写后的规范化问题作为key,把最终的答案、引用片段、生成结果作为value,设置合理的有效期。注意缓存key不要用用户的原始输入,要做同义改写和归一化处理,不然“报销流程是什么”和“怎么报销”就缓存不到一起去。

另外召回片段本身也有去重问题。向量召回时,相似的片段经常一起被捞回来,反而相互覆盖。我建议在重排前加一步“局部去重”:计算候选片段两两之间的相似度,超过阈值就保留排名高者。这个操作对生成质量的提升,比想象中明显——它减少了提示词里的冗余信息,让关键线索更容易被模型捕捉到。

6.3 延迟与成本平衡:资源不只是算力,还有你的耐心

RAG链路里,延迟主要耗在三个环节:检索、重排、生成。检索可以使用HNSW索引并合理调参,比如在百万级向量库上控制候选扫描数量,能显著提速。重排是纯加餐环节,可以单独做个降级策略:低峰期全量重排,高峰期为了保延迟只重排前20条,效果损失其实不大。生成的延迟只能靠控制上下文长度来压——这就是为什么第4章强调裁剪和过滤,每剪掉一段无关内容,用户等待的秒数就少一段。

成本上还有一个容易忽视的点:Embedding索引的构建经常被反复重跑。每次调完模型或切块策略,全量索引就要重建。这本身没问题,但如果每次重建都全量跑,成本很可观。建议在开发环境先对抽样子集(比如10%的文档)建索引、跑评估,效果稳定后再全量重建。这样调试阶段的算力开销能降一个量级。

6.4 我踩过的一堆坑

最后分享几个真实的翻车现场。第一个坑是切块太小。曾经把合同文档按300字符切块,检索精确度很高,但问“这份合同包含哪些违约责任条款”时,答案永远不完整——因为条款分散在好几个块里,Top-K根本放不下全部相关块。换成父子分块后,检索精度和答案完整性总算两头兼顾。第二个坑是上下文塞太满。为了“充分利用大模型能力”,把Top-10的片段全塞进提示词,结果模型被前三段冗长的背景信息带偏,连最核心的结论都没答出来。后来强制裁剪到Top-5,效果立竿见影。第三个坑是重排全量做。一开始图省事,把向量召回的200条全部送去重排,延迟直接翻倍。限制到Top-50后再重排,效果没降,延迟稳了。第四个坑是拿Web UI自带的测试集自嗨。界面点点点看几个结果觉得效果不错,一上压测就崩。后来老老实实搭了自动化评估脚本,才算真正掌握了系统的真实水平。

做RAG这一年多,最大的体会是:RAG的难点从来不在模型,而在数据工程和评估闭环。模型再强,检索数据一塌糊涂也白搭;效果再好,说不清好在哪里就没法持续优化。另外记住,重排和查询改写是两个最容易被忽略、但性价比最高的优化点,很多人绕过它们直奔“换更大的模型”,其实是在绕远路。按这套链路一步步把基础打牢,你的RAG离稳定的“生产可用”就不远了。

RELATED READING

延伸阅读

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