做知识库问答的朋友应该都有同感:模型能力再强,只要检索链路掉链子,回答就会前言不搭后语。RAG(检索增强生成)本身不是什么新鲜概念,但真正把它调到好用、稳定、让人敢上线,中间隔着一大堆细节。这篇笔记是系列第51篇,专门梳理我在实际项目中反复踩过坑、对比过方案的RAG优化技术,从检索侧的query处理、切分策略、embedding选型,到召回后的重排压缩,再到生成端约束和评估驱动的优化闭环,把这些内容拆开揉碎讲清楚。
1. 不急着优化,先把RAG链路拆开看
很多团队一上来就换embedding模型、堆向量库,结果指标纹丝不动。原因很简单:你不知道问题出在哪一环。RAG是一条完整的流水线,任何一个环节拖后腿,都会让最终答案质量崩掉。所以我建议,动手优化之前先画一张链路图,把每个环节可能出错的方式列清楚。
1.1 RAG的基本工作路径
一个标准的RAG系统,工作路径通常是这样的:
- 知识文档清洗,去掉页眉页脚、广告、重复段落;
- 文档切分成chunk,每个chunk单独做embedding;
- 把向量写入向量数据库,同时保留原文id和元数据;
- 用户提问后,对query做同样的embedding,在向量库做相似度检索;
- 取topK个结果,一并塞给大模型,让它基于这些片段生成答案。
这个链路看起来简单,但每一环都有隐形的坑。比如切分太粗,一个chunk里混了多个主题,召回的语义就不聚焦;切分太细,上下文被切断,模型拿到的信息残缺。再比如query里的口语化表达和文档里的书面语根本不是一套词向量空间的邻居,检索结果自然偏。所以优化不是单点替换,而是系统性调整。
1.2 四个最常出问题的瓶颈
根据我的实操经验,RAG效果不好,九成以上卡在下面四个地方:
| 瓶颈位置 | 典型症状 | 根因方向 |
|---|---|---|
| 召回精度 | 检索结果和问题主题无关 | query表达差、embedding不适配、chunk粒度过大 |
| 信息碎片化 | 答案只覆盖问题一半 | 切分破坏了上下文,或topK取值太小 |
| 上下文噪音 | 模型被无关片段带偏 | 检索结果混入语义相似但无用的内容 |
| 生成幻觉 | 模型编造知识库没有的内容 | 指令约束太弱、上下文压缩丢失关键信息 |
这四个瓶颈是相互影响的。召回精度差,后面所有环节都会放大错误;上下文噪音大,模型就算有正确答案也会被误导。所以下面几节,我会按"检索前—检索后—生成端—评测"的顺序展开,这些优化手段层层递进,组合使用效果最好。
2. 检索前优化:让提问和文档先对上话
检索前优化是整个RAG优化里性价比最高的一环。改动成本低,效果提升最明显。核心思路就一句话:把用户的问题,变成更容易和文档匹配的形式。
2.1 query改写,别让口语化问题直接去检索
用户提问很少是标准书面语。比如用户问"那个会员咋退啊",知识库文档里写的可能是"退订流程"。如果直接用口语去检索,embedding的相似度往往偏低,召回的自然不是最优文档。这时候query改写就是个必要的预处理步骤。
我常用的做法是用大模型做改写,prompt大概长这样:
query_rewrite_prompt = """ 你是一个检索助手。用户的问题可能比较口语化或不完整, 请将其改写为适合知识库检索的、语义完整且与文档表达习惯一致的关键词句。 只输出改写结果,不要解释。 用户问题:{query} 改写结果: """改写的时候要注意两点:第一,不要让模型过度发挥,改写答案里不要夹带原问题的补充信息,否则会引入幻觉;第二,改写后的query宁可更"中性化",也不要太口语太情绪化,情绪词对检索没有任何帮助。这个环节我用下来,检索命中率能提升5到8个百分点,成本只是每次多一次轻量级模型调用。
2.2 多轮对话的场景,必须做意图归一化
如果是对话型RAG,还有一个更隐蔽的问题:用户的提问经常是省略主语或指代不明的,比如"那它的价格呢"——"它"指代的是上一轮提到的产品,但如果直接把"它的价格"拿去检索,结果基本就是乱抓一气。
这种情况需要做一轮对话状态整理,把多轮上下文压缩成一条独立的检索query。我的实践经验是,在改写好query之后,把最近两三轮的关键信息拼接进去再检索,比让模型直接生成完整query更稳定。示例改造逻辑:
- 判断当前query是否包含指代词,如"它""这个""那个";
- 如果包含,则抽取之前对话中用户明确提到的实体;
- 用模板拼接:实体 + 当前问题去掉指代词;
- 如果实体抽取置信度不高,就退回原始query检索。
我在真实项目里遇到过"多轮指代丢失"导致检索完全跑偏的情况,用这个方案后相关度明显提升,而且不需要每次多轮都重写整段历史,成本和延迟都可控。
2.3 切分策略微调,效果立竿见影
chunk切分直接影响检索的精细度和上下文完整性,但很多团队直接沿用一个固定值比如512或1024就不动了。其实切分长度应该根据文档类型动态调整。
| 文档类型 | 推荐chunk大小 | 重叠长度 | 理由 |
|---|---|---|---|
| 问答对/FAQ | 200-400字 | 20-50字 | 强独立语义,切分过大容易混主题 |
| 产品说明/操作文档 | 500-800字 | 80-150字 | 需要保留操作步骤完整性 |
| 制度条款/合同文本 | 800-1200字 | 150-200字 | 条款上下文关联强,过细会断裂 |
| 技术论文/研究报告 | 600-1000字 | 100-200字 | 兼顾方法描述和结论引用 |
这里的关键不只是chunk大小,还有用来切分的分隔符优先级。先按标题结构切,再按段落切,最后按句子切,这样能最大限度保留文档自带的语义边界。纯按固定字符数硬切是最差的做法,会在句中截断语义。
我自己的习惯是,对一批知识文档先跑一遍结构分析,看看标题分布情况,再决定切分层级。实际操作中,为了省事可以先统一用递归字符切分器,设定一个较大的初始块大小,然后根据检索命中率和人工抽检结果做两三次迭代调整。每次只改一个参数,记录指标变化,不要同时动多个变量。
2.4 混合检索:向量和关键词不是二选一
只做向量检索,在专业术语、代码片段、专利号这类场景往往效果不好,因为这些内容在语义空间中可能靠得很近,但实际含义完全不同。BM25这类稀疏检索在精确词匹配上反而有优势。所以我在正式项目中都会用混合检索方案:
results_hybrid = ( vector_search(query_embedding, top_k=20) + bm25_search(query, top_k=20) ) # 分数归一化后按加权融合排序 rerank_scores = 0.6 * vector_score + 0.4 * bm25_score具体权重不是固定的。如果业务场景是标准文档问答,向量权重可以给高一点;如果场景是代码库或型号代码查询,BM25的权重就要往上调。我建议做两组小规模实验对比,用实际数据定权重,而不是拍脑袋。
另外,混合检索的召回量可以适当放大,比如单路各取20条,合并后再过滤。因为最后往往还有重排环节把关,召回阶段多捞一些"疑似相关"的候选不算浪费。
3. 召回后处理:从"找到相关"到"找到真正有用的"
召回之后的内容只是"候选",还不能直接用。它可能包含大量与问题不直接相关的片段,或者关键信息埋在一个超长的chunk里。聪明地筛选和组织这些候选,就是召回后处理要解决的事。
3.1 Rerank重排:从向量到精排的跨越
embedding模型的相似度本质上是双塔结构,两个文本各自编码之后再算相似度,这个过程中query和文档之间的交互信息是丢失的。所以才会有"向量相似度高但实际不相关"的诡异情况。而rerank模型通常是交叉编码器结构,query和文档拼接在一起过一遍模型,能做更精细的相关性判断。
在设计重排时,我给候选片段排序后的截断逻辑定了几个准则:
- 如果重排后的前3条已能覆盖问题的关键实体,可以直接截断topK=3;
- 如果前3条存在明显语义断层,即信息之间有跳跃,可以放宽到topK=5,再用生成端压缩;
- 每条候选的得分差距小于0.05时,说明候选之间区分度不够,可以多条并传给大模型自行判断。
从成本看,rerank确实比retrieval阶段贵,但它的收益也很明显。以我经历的一个客服知识库项目为例,加了rerank之后,答案正确率直接提升约12个百分点,且被模型引用的上下文更精准了。如果不是延迟极度敏感的场景,rerank应该作为必备组件。
3.2 父文档检索:解决碎片化问题
切分太细导致的上下文断裂,是RAG的经典难题。我的处理思路是"用小片段召回,用大片段拼装"。具体做法分两步:
- 把一个较大段落(比如一个三级标题下的内容)作为父文档,同时从其中拆分出若干简短子片段,子片段做embedding用于检索;
- 检索时如果子片段命中,就顺藤摸瓜找到父文档,把整段父文档内容交给模型。
这个方案的好处是检索的"灵敏度"和生成时的"上下文量"兼得。子片段因为粒度细,更容易和query精确匹配;父文档因为保留了上下文,让模型看到完整逻辑而不是残句。
需要注意控制父文档的大小。如果父文档超过1500字,交给模型会占用大量上下文窗口,反而可能稀释关键信息。可以把太长的段落再切成多个逻辑小节,各小节独立当父文档。
3.3 上下文压缩,给模型减负
RAG中一个常见的坑,是topK取太大,把大量语义相近但信息冗余的内容丢给模型,导致模型抓不住重点。我试过一种简单但有效的压缩思路:先把所有候选按段落开头一句话做摘要提取,如果模型觉得候选片段中已经包含答案信息,就不再需要重复的片段。这相当于做了一次"去冗余"。
更进一步,可以参考LLMLingua这类长文本压缩思路,在不破坏关键实体的前提下压缩prompt中的低信息密度内容。不过我在实操中发现,很多知识库场景其实用不着上专门压缩模型,一句话指令就够了:
在回答之前,请忽略与问题无关的检索段落;如果有多个段落重复表达同一事实,只保留信息最完整的那一段。这类指令不需要额外模型调用,直接降低幻觉率。注意如果项目对成本敏感,把提示工程做扎实,比上重型压缩模型更划算。
3.4 去重与排序策略
候选片段经常出现重复信息,比如FAQ和产品说明文档讲了同一件事,两个都召回了,模型把它当两条事实叠加,轻则啰嗦,重则自相矛盾。所以我习惯按句子级别的编辑距离做一次去重:
- 比较各候选片段之间重叠的句子比例,超过一定阈值(比如50%),按rerank得分保留最高一条;
- 保留下来的片段再按"与query实体重合度"二次排序,而不是完全依赖向量相似度。
去重这个操作常被忽略,但它能直接降低生成的重复感和矛盾概率。在处理几十条候选时效果尤为明显。
4. 生成端优化:大模型输出质量的最后一道闸门
前面所有工作,最终都要通过生成环节呈现给用户。很多优化做得不错的人,最后都栽在"答案表达"上:要不就是答非所问,要不就是引用错乱。生成端的优化不只是写Prompt,而是要把模型行为彻底约束在任务目标以内。
4.1 用指令约束模型"能说什么"
RAG场景最常见的幻觉,是模型对检索不到的内容强行补充。解决方式不是告诉它"不要编造",而是给它明确的边界。我惯用的生成指令模板:
请根据以下检索片段回答问题。要求: 1. 如果检索片段包含答案,直接用片段中的信息作答; 2. 如果检索片段不满足回答条件,明确回复"知识库中未找到相关内容"; 3. 回答中标注引用的片段编号,例如[1][2]; 4. 不要输出检索片段之外的任何推测性内容。这种指令在绝大多数基础大模型上都有效。但要注意"不要推测"这个约束在有些模型上执行得并不可靠,需要后续加校验环节补漏。只写Promt不做验证,等于把安全系数压在模型的自觉性上。
4.2 引用溯源,让答案可验证
引用溯源不只是给用户看的,更是给排查用的。我强烈建议在RAG系统的返回结果里带上原始文档id、chunk编号、页码等元数据。这样当答案出错,你能追溯到是检索召回错了,还是模型表述错了。
我这里有一个习惯:在问题输入大模型之前,先记录检索返回的候选片段集合快照。问题回答出现问题的时候,我首先排查的是"快照中的片段是否包含正确答案",如果包含,说明是模型生成环节的问题,反之则是检索环节的问题。这个区分极大节省了定位成本。
4.3 答案校验,兜底防幻觉
如果业务对错误零容忍,那就要在生成之后加上一层校验。通常做法是把生成的答案重新编码,和检索片段做一次相关性判断,如果答案的核心实体无法在片段中找到对应关系,就判定为可疑结果,转人工或回到检索链路重新处理。
更轻量级的做法是"关键实体检查":提前用NER抽取出问题中的核心实体,再检查生成的答案里是否覆盖了这些实体。如果缺失了某个核心实体,说明模型在生成时遗漏了检索片段中的关键信息。这个方法实现成本低,但能拦掉不少明显错误。我在金融场景的项目里会配合使用这两种思路,准确率和运维成本都更可控。
5. 评测体系:没有指标的优化都是碰运气
RAG优化最怕的是"感觉变好了"。感觉不可靠,必须用数据说话。但RAG的评测不能只盯一个指标,检索质量和生成质量要分开看。
5.1 检索质量指标,只看有没有找到
检索质量衡量的是"正确的内容有没有进入候选",常用指标包括检索命中率(Recall@k)、平均排序倒数(MRR)、归一化折损累计增益(NDCG@k)。这三个指标看两个维度:正确内容是否被召回、被召回时是否排在前面。
实操中我一般先固定k=10来测Recall@k。如果Recall率低,说明问题出在检索前或者检索本身,后续的rerank和prompt优化都白搭。反之如果Recall率高但最终答案质量差,才需要往压缩和生成端排查。
我自己在项目里最常用的方法是构建一个"黄金问答对"测试集:从知识库中抽200个问题,每个问题标注唯一正确答案所在文档。然后跑一遍检索,统计topK的命中率。这样能快速判断检索环节的健康度。
5.2 生成质量指标,忠实度比流畅性更重要
生成质量可以从两个角度衡量:
- 忠实度(Faithfulness):答案里的每一条关键信息,能否在检索片段中找到依据;
- 答案相关度(Answer Relevance):答案是否针对用户问题,是否答非所问。
这里有一个实操提醒:忠实度评测不能完全依赖大模型打分,建议先用程序化的规则验证"答案中的实体是否出现在检索片段中",然后再用大模型评估语句层面的忠实度。程序化规则快速粗暴,大模型判断细致,两者搭配才能既高效又准确。
5.3 评估驱动的调优循环
我通常会在RAG上线前搭一套"自动评测+人工抽检"的结构,具体包括:
- 自动化评测:每天定时对黄金测试集跑一次全流程,输出RAG质量各项指标变化趋势;
- 人工抽检:每天抽20条真实用户问题,人工判断答案是否可用,记录反馈到样本库;
- 指标告警:当某一项指标低于预设阈值时,自动提示对应环节负责人做归因分析。
在这种循环下,每次模型参数调整、切分策略改动、重排模型版本替换,都能被量化追踪。推荐每个优化动作后都跑一次评测再决定是否上线,而不是"顺手改完就部署"。
6. 一次完整的实战优化记录
所有的理论如果不落到项目里,都只是纸面功夫。下面分享一次我在某企业客服知识库场景里的实战优化过程,完整记录问题和调优步骤,供读者参考。
6.1 原始状态与问题清单
初始系统的状态很典型:文档约5000份,包含FAQ、操作手册、售后政策;直接接了一个开源向量模型和向量库,默认chunk大小500,首版效果很差。问题清单如下:
| 问题编号 | 表现 | 初步判断 |
|---|---|---|
| P1 | 检索结果大量无关或弱相关片段 | 切分粒度不当,query未改写 |
| P2 | 答案经常引用重复段落 | 候选序列存在大量内容冗余,缺少去重机制 |
| P3 | 用户口语化提问召回效果差 | 欠缺query改写归一化处理 |
| P4 | 涉及文档更新的信息,回答不准确 | 缺少文档元数据过滤,旧版本文档也被召回 |
6.2 逐项优化动作
- 对所有query增加改写步骤,调用轻量级模型把口语化问题转换为关键词句;
- 调整切分策略,操作文档改用800字大小、120字重叠,FAQ改用300字大小、40字重叠;
- 引入混合检索,向量检索与BM25各取前20条候选,加权融合排序;
- 候选序列加入交叉编码器重排,统一取topK=5;
- 增加冗余片段去重策略,超过重叠阈值的按得分保留一条;
- 生成端强化引用约束,要求答案标注片段编号。
这些动作按顺序逐步上线,每上线一步跑一次评测,确保每一步的效果可以单独归因。
6.3 效果对比
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 检索命中率 Recall@10 | 56% | 89% | +33% |
| 答案忠实度 | 62% | 91% | +29% |
| 答案相关度 | 68% | 93% | +25% |
| 用户反馈可接受率 | 52% | 85% | +33% |
从数据能看出,检索命中率提升是全局改进的杠杆;命中率上去了,后面生成端的忠实度和相关度自然跟着涨。如果当时只换一个大模型去调生成,可能涨不到这个效果。
6.4 过程中的三个意外发现
优化过程中有几个不在预期内的发现,写出来给各位提个醒:
第一,文档更新不及时造成的错检,比模型本身的问题更大。有一批旧版本产品条款没下线,检索时和新版内容混在一起,答案经常互相打架。加上元数据过滤后,问题立刻消失。
第二,去重逻辑在处理"多个文档段落内容雷同"的场景时太激进,会把不同来源但都有价值的段落删掉。后来调整阈值,从50%提高到65%重叠才判定为重复,效果更均衡。
第三,query改写如果目标格式定得太死,模型会把一些短语改写扭曲,反而降低检索效果。后来给了改写指令更多自由度,只要求语义完整、不要过度展开,效果恢复并超过原始方案。
7. RAG优化的三个常见误区
最后集中说说我在项目评审和带人过程中反复见到的误区,特别是那些看起来很有道理、实际却吃力不讨好的优化方向。
7.1 误区一:一味追求更大模型
RAG场景里,大模型只是最终的口语表达层,真正决定"上限"的是召回质量。可以做一个简单实验:固定检索结果和指令,把模型从7B换成70B级别,回答质量的提升往往有限。反而是在检索命中率不高的时候,换再大的模型也只会让"错误答案听起来更流畅"。
所以优先把小模型配合优化好的RAG链路跑通,再考虑模型大小。我见过不少团队直接上超大模型做RAG,延迟和成本飙升,最终因为检索没优化,答案依然不可用。
7.2 误区二:过度依赖向量相似度
向量相似度在语义相关性判断上有天然优势,但它无法捕捉事实性的精确匹配。代码里的函数名、产品型号、法规条文编号,这些场景下,向量检索常常把同义词当成同事实,却把精确术语之间的细微差别忽略掉。
混合检索、加上精确匹配规则,甚至必要时维护一个专门的关键词-文档映射表,都能弥补这个短板。优化是系统工程,不是某个模型的一己之力。
7.3 误区三:跳过评测直接上线
没有评测体系就上线,等于蒙着眼睛开车。尤其很多RAG系统刚上线时看起来还行,一遇到真实复杂问题就会翻车。这时候如果只有用户抱怨,没有任何可以定位问题的中间数据,排查成本会成倍增长。
我强烈建议,从第一天开始就保存检索日志和答案日志,并建立持续反馈机制。有了数据,优化才有方向。
8. 避坑清单速查
结合这几年做RAG项目的经验,我整理了一份高频问题速查表,每次调优遇到瓶颈时先对照一遍。
| 现象 | 排查方向 | 推荐解法 |
|---|---|---|
| 检索结果相关度低 | 先看query是否被正确改写,再看chunk是否过大 | 增加query改写、调整切分粒度 |
| 答案重复啰嗦 | 候选片段冗余 | 增加去重逻辑、调低topK |
| 答案完整但引用错乱 | 生成端指令弱 | 强化引用约束,限定只能引用给定片段 |
| 新文档上线后回答仍旧旧 | 元数据过滤缺失 | 为chunk增加版本号,过滤过期文档 |
| 专业术语检索不准 | 纯向量检索的局限 | 用混合检索,融合BM25或关键词匹配 |
| 多轮对话中对象指代不明 | 上下文意图丢失 | 做多轮指代消解,把实体按模板拼回query |
| 候选片段互相矛盾 | 文档内容陈旧或冲突 | 建立文档修订机制,淘汰旧版本内容 |
| 评测分数和用户体感不一致 | 测试集构建脱离真实场景 | 持续补充真实用户问题到测试集,定期人工校准 |
这些坑我基本都踩过一轮,每次排查到根因后复盘,发现绝大多数问题源头都非常"常规"——query处理不到位、文档治理不干净、评测没有跟上。RAG优化的本质,是系统工程能力的比拼,而不是单点技术的堆砌。
我个人在多次折腾后的最大体会是:别急着追新框架和复杂套路,先把链路各环节的数据可视化,让每个环节的瓶颈暴露出来,再针对性地做一点点修改,效果往往比一次大改动更稳。优化的节奏其实和修老房子类似,一次只动一面墙,出了问题就知道该拆哪里,住起来才踏实。