1. 从“能跑通”到“答得准”:RAG 应用的真实分水岭
很多人第一次搭 RAG(检索增强生成)的时候,心态都差不多:文档切一切、向量库塞进去、检索几条拼进 Prompt,模型能吐出答案,就算大功告成。我最早做的那套问答系统也是这样,Demo 阶段效果惊艳,老板问什么都能答上来,团队一度觉得这事成了。可一旦把知识库换成真实业务文档,问题就全冒出来了——问“退货政策里生鲜类怎么处理”,它给你背一段通用售后条款;问“这个接口的超时时间是多少”,它把三个版本的参数混在一起答;更离谱的是,明明知识库里没有的内容,它也能一本正经地编出一段。
这就是 RAG 应用最真实的分水岭:能跑通不等于答得准。检索增强生成这套架构,本质上是在“检索”和“生成”两个环节之间做接力,任何一棒掉链子,最终答案都会崩。而绝大多数人优化 RAG 时,眼睛只盯着大模型,觉得换个更强的模型就万事大吉,结果发现换了模型准确度也就涨了几个点,瓶颈根本不在那儿。
我后来复盘了大量问答 badcase,发现准确度问题基本可以归到三类:检索没召回对的(召回率问题)、召回了一堆噪音把模型带偏的(精度问题)、召回了正确内容但模型没用好(生成与上下文利用问题)。这三类问题的优化手段完全不同,如果你不先定位到底卡在哪一环,盲目调参就是碰运气。
这篇内容我想聊的就是怎么系统性地把 RAG 问答准确度往上提。适合已经跑通过基础 RAG、但被准确度问题折磨的开发者,也适合正准备把 RAG 从 Demo 推向生产的人。我会从检索质量、切分策略、重排序、上下文组织、评估体系这几个角度拆开讲,每个环节都给出我实际踩过坑之后总结的做法和判断依据。核心关键词就一个:RAG 问答准确度,所有内容都围绕怎么把它从“勉强能用”拉到“敢上线”。
2. 先别急着调模型:定位准确度瓶颈的排查链路
优化最忌讳的就是上来就改。我见过太多人一发现答不准,第一反应是换 embedding 模型、换大模型、调 temperature,改了一圈发现没方向。正确的做法是先建立一套能定位问题的排查链路,把“答不准”这个大问题拆成可观测的小问题。
2.1 把 badcase 分成“检索锅”还是“生成锅”
我的做法是给每条 badcase 打两个标签:检索是否召回了包含正确答案的片段,以及模型是否基于召回内容给出了正确答案。这两个标签一交叉,问题归属就清楚了。
| 检索是否命中 | 生成是否正确 | 问题归属 | 典型表现 |
|---|---|---|---|
| 否 | 否 | 检索环节 | 知识库里有答案,但没被召回 |
| 是 | 否 | 生成环节 | 召回对了,模型却答偏或答错 |
| 是 | 是 | 正常 | 无需处理 |
| 否 | 是 | 模型幻觉 | 没召回却蒙对了,最危险 |
这张表是我排查的起点。实测下来,大部分团队的 badcase 里“检索没命中”占比往往超过一半,但因为检索环节不像模型输出那么直观,很多人根本没意识到问题出在这儿。你只要老老实实把 50 条 badcase 按这张表分类,优化方向立刻就明确了。
提示:判断“检索是否命中”时,不要只看 Top1 结果,要把 TopK(比如 Top10)全部人工过一遍。很多时候正确答案在第 6、第 7 条,只是排序太靠后没进 Prompt,这属于排序问题而非召回问题。
2.2 用命中率指标量化检索质量
光靠人工看 badcase 效率太低,得有个可量化的指标。我常用的是Hit Rate@K:对一批标注了标准答案的问题,看正确答案所在片段是否出现在检索结果的前 K 条里。这个指标直接反映检索环节的天花板——如果 Hit Rate@10 只有 60%,那你后面生成环节再怎么优化,准确度也上不去。
计算方式很朴素:准备 100 到 200 条带标准答案的问题,跑一遍检索,统计命中比例。我一般会同时看 Hit Rate@1、@3、@5、@10 四个值。如果 @1 很低但 @10 很高,说明召回没问题、排序有问题,重点该放在重排序上;如果 @10 都很低,那就是召回本身出了问题,得从切分、embedding、查询改写入手。
2.3 检索和生成要分开评估,别混在一起看
一个常见的误区是只看端到端的答案准确率。这个指标当然重要,但它是个黑盒,涨了跌了你都不知道为什么。我的习惯是维护两套评估:检索侧看 Hit Rate 和 MRR(平均倒数排名),生成侧看答案正确率和忠实度。这样每次改动,你能清楚知道是哪个环节受益了。
举个我自己的例子。有段时间我把切分粒度从 512 token 调到 256 token,端到端准确率涨了 8 个点。如果只看端到端,我会以为“切小就是好”。但分开看才发现,Hit Rate@10 其实没怎么变,涨的是 Hit Rate@1——因为片段变小后,单个片段里的噪音少了,正确答案更容易排到前面。这个洞察直接指导了我后续的重排序策略,而不是盲目继续切小。
3. 检索召回率上不去,八成是切分和查询这两步出了问题
定位到检索环节是瓶颈之后,接下来就是动手优化。检索召回率低,原因通常集中在两个地方:文档切分不合理,以及用户查询和文档表述之间存在语义鸿沟。这两块是 RAG 准确度的地基,地基没打好,后面全是空中楼阁。
3.1 固定长度切分是最省事也最容易埋雷的做法
新手最常用的切分方式就是按固定字符数或 token 数硬切,比如每 500 字一段。这种做法实现简单,但问题很致命:它会把一个完整的语义单元拦腰截断。比如一份接口文档里,“超时时间”这个参数说明可能横跨两段,硬切之后前半段在片段 A、后半段在片段 B,检索时两条都召回了,但每条都只有半句话,模型拼起来容易出错。
我踩过最惨的一次坑,是一份产品规格书里有个表格,硬切之后表头和表体分到了不同片段,结果模型把“最大并发数”的值安到了“超时时间”上。这种错误特别隐蔽,因为答案看起来有模有样,不仔细核对根本发现不了。
后来我改用基于结构的切分:优先按文档的自然结构切,比如 Markdown 的标题层级、HTML 的标签、PDF 的段落。一个二级标题下的内容尽量保持完整,如果太长再按句子边界二次切分。这样每个片段都是一个语义自洽的单元,召回质量明显提升。
3.2 重叠窗口和语义切分的取舍
结构切分不是万能的,遇到没有明显结构的纯文本,还是得靠长度切。这时候重叠窗口(overlap)就很重要。我一般设置 10% 到 20% 的重叠,比如片段 500 token、重叠 80 token。重叠的作用是防止关键信息正好卡在切分边界上被割裂,代价是存储和检索成本略微上升。
再进阶一点是语义切分:计算相邻句子的 embedding 相似度,在相似度骤降的地方断开。这个思路很优雅,但实测下来计算成本不低,而且对相似度阈值的设定很敏感,阈值调不好反而切得更碎。我的建议是,除非你的文档语义跳跃特别大,否则结构切分加适度重叠已经够用,不必一上来就上语义切分。
注意:切分粒度没有标准答案,它和你的文档类型、问题类型强相关。FAQ 类文档适合按问答对切,技术文档适合按章节切,法律合同适合按条款切。别迷信某个“最佳 token 数”,要拿你自己的数据去试。
3.3 查询改写:让用户的问法和文档的写法对上
检索召回率低的另一个大头,是用户提问的措辞和文档里的表述对不上。用户问“这个功能要钱吗”,文档里写的是“本服务采用订阅制收费模式”。字面上几乎没重叠,纯向量检索也可能因为表述差异而漏召回。
解决这个问题的核心手段是查询改写(Query Rewriting)。我常用的有几种:
- 同义扩展:把用户问题扩展成多个表述,分别检索后合并结果。比如“要钱吗”扩展出“收费”“价格”“费用”“订阅”。
- HyDE(假设文档嵌入):先让模型根据问题“编”一段假设性的答案,再用这段答案去检索。因为假设答案的措辞更接近文档风格,召回率往往更高。
- 多查询生成:让模型把一个问题拆成几个子问题,分别检索。这对复杂问题特别有效。
我实测下来,HyDE 在技术文档场景提升明显,但在事实型问答上有时会引入偏差,因为模型“编”的假设答案可能本身就是错的。所以我的做法是多路召回 + 融合:原始查询、改写查询、HyDE 查询各召回一批,用 RRF(倒数排名融合)合并。这样既保留了原始查询的准确性,又借了改写查询的召回广度。
3.4 多路召回融合的实操配置
多路召回听起来复杂,落地其实不难。核心就是每路召回各取 TopK,然后用 RRF 算综合排名。RRF 的公式很简单:每个文档的得分等于它在各路结果中排名的倒数之和。
def rrf_fusion(result_lists, k=60): scores = {} for results in result_lists: for rank, doc_id in enumerate(results): scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)这里的 k 是个平滑参数,一般取 60。它的作用是削弱排名靠前文档的绝对优势,让多路结果更均衡地融合。我一般会跑三路:原始查询、同义扩展、HyDE,每路取 Top20,融合后取 Top10 进入下一步。这套配置在我手上的几个项目里,把 Hit Rate@10 从 70% 出头拉到了 85% 以上。
4. 召回了一堆却答不准:重排序与上下文精炼
假设检索召回率已经不错了,但答案还是不准,问题往往出在召回了太多无关内容,把模型带偏了。大模型的上下文窗口虽然越来越大,但塞进去的噪音越多,它抓重点的能力反而越差。这一步的核心任务是:从召回的候选里,把真正相关的那几条挑出来,并且组织成模型好用的形式。
4.1 向量检索的“语义相似”不等于“答案相关”
向量检索的本质是找语义相似的片段,但“语义相似”和“能回答问题”是两回事。用户问“如何重置密码”,一篇讲“密码安全策略”的文档语义上高度相似,但它不包含操作步骤,对回答问题毫无帮助。这种“相似但不相关”的片段,是拉低准确度的隐形杀手。
我做过一个统计,在纯向量检索的 Top10 结果里,真正对回答有帮助的片段平均只有 3 到 4 条,其余都是语义相关但信息冗余或无关的。这 6 到 7 条噪音,就是模型答偏的根源。
4.2 引入重排序模型做二次筛选
解决办法是加一个重排序(Rerank)环节。向量检索负责快速从海量文档里粗筛出候选(比如 Top50),重排序模型负责对这 50 条做精细打分,挑出真正相关的 Top5 送进 Prompt。
重排序模型和 embedding 模型的区别在于:embedding 是双塔结构,查询和文档分别编码,速度快但精度有限;重排序是交叉编码器(Cross-Encoder),把查询和文档拼在一起过模型,精度高但速度慢。所以典型架构是“向量粗筛 + 重排序精排”,兼顾速度和精度。
我常用的重排序方案有 BGE-Reranker 系列,也有基于大模型做相关性打分的。后者效果更好但成本高,适合对准确度要求极高的场景。实测下来,加一个重排序环节,端到端准确率普遍能涨 10 到 15 个点,是性价比最高的优化手段之一。
| 环节 | 模型类型 | 处理量级 | 主要作用 |
|---|---|---|---|
| 向量粗筛 | 双塔 embedding | 全库百万级 | 快速缩小候选范围 |
| 重排序精排 | 交叉编码器 | Top50 到 Top100 | 精准挑出相关片段 |
| 生成 | 大语言模型 | Top3 到 Top5 | 基于精选上下文作答 |
4.3 上下文不是越多越好,要按相关性裁剪
很多人有个误区,觉得上下文塞得越满,模型掌握的信息越全,答得越准。实际上恰恰相反。我做过对比实验,同样的问题,塞 10 条片段和塞 3 条精选片段,后者的准确率反而更高。原因是噪音片段会干扰模型的注意力,让它抓不住重点。
所以我的做法是按相关性动态裁剪:重排序得分高的片段完整保留,得分中等的截取核心句,得分低的直接丢弃。同时控制总 token 数,一般不超过模型上下文窗口的 60%,给生成留足空间。如果片段之间有冲突信息(比如不同版本文档),我会在 Prompt 里明确标注来源和版本,让模型知道该信哪个。
4.4 用“引用标注”倒逼模型忠于上下文
提升生成环节准确度,还有一个我特别推荐的小技巧:要求模型在答案里标注引用来源。比如“根据文档 A 第 3 节,超时时间是 30 秒”。这个要求有两个好处:一是模型为了标注来源,会更认真地读上下文,减少幻觉;二是出了问题你能快速定位是哪条片段导致的,方便排查。
实现上,我会在每条片段前加一个编号,Prompt 里明确要求“答案中涉及的事实必须标注对应的片段编号,无法从片段中找到依据的内容不要编造”。实测下来,这个简单的约束能把幻觉率降不少,尤其是对“知识库里没有的问题”,模型会更倾向于回答“根据现有资料无法确定”,而不是硬编。
5. 知识割裂与多跳问题:当单次检索不够用时
前面讲的都是单轮检索的优化。但真实场景里,有一类问题特别难搞:答案需要综合多个片段才能得出,或者需要先检索一步、再根据结果检索下一步。这就是所谓的“知识割裂”问题,也是 RAG 准确度提升到一定阶段后必然遇到的瓶颈。
5.1 多跳问题的典型表现
举个具体例子。用户问“我们用的那个数据库,它的备份策略是什么”。这个问题需要两步:先确定“我们用的数据库”是哪个(可能在另一份文档里),再去找这个数据库的备份策略。单次检索很难同时命中这两块信息,因为查询里根本没提数据库的名字。
再比如“对比 A 方案和 B 方案的优缺点”,答案分散在两份文档里,单次检索可能只召回其中一份,模型就只能答一半。这类问题的共同特征是:答案不在单个片段里,而在片段之间的关系里。
5.2 查询分解:把复杂问题拆成子问题链
应对多跳问题,最直接的手段是查询分解。让模型先把复杂问题拆成几个子问题,逐个检索,最后综合。比如上面那个数据库的例子,拆成“我们用的是哪个数据库”和“该数据库的备份策略是什么”两个子问题,第一个的答案作为第二个的检索条件。
这个思路落地时要注意:子问题的检索结果要能传递。我一般用一个简单的循环:先检索第一个子问题,把答案提取出来,拼进第二个子问题的查询里,再检索。对于更复杂的链式问题,可以迭代多轮,直到信息足够回答原问题。
5.3 用 GraphRAG 思路处理实体关系
如果知识库里实体关系特别密集,比如人物、组织、产品之间有大量交叉引用,那单纯的向量检索就很难处理了。这时候可以考虑GraphRAG的思路:先把文档里的实体和关系抽出来,构建一个知识图谱,检索时既走向量召回,也走图上的关系遍历。
GraphRAG 的优势在于它能回答“A 和 B 是什么关系”这类需要跨文档推理的问题。但它的代价也不小:构图需要额外的抽取和存储成本,而且抽取质量直接决定效果。我的建议是,如果你的问题里关系型查询占比很高,值得投入;如果大部分是事实型问答,那向量检索加查询分解就够了,不必为了追新而上 GraphRAG。
5.4 本体(Ontology)在 RAG 里的实际价值
最近“本体 RAG”这个词挺热,本质上是给知识库加一层概念体系,让检索能理解“这个术语属于哪个类别、和哪些概念相关”。它的价值在于处理同义词和上下位关系。比如用户问“笔记本”,知识库里写的是“便携式计算机”,有了本体就能建立映射。
但我要泼盆冷水:本体不是银弹。构建和维护本体的人力成本很高,而且一旦业务变化快,本体很容易过时。我的实际经验是,用轻量级的同义词表和类别标签就能覆盖 80% 的需求,没必要一上来就搞一套完整的本体体系。除非你的领域术语极其规范且稳定(比如医疗、法律),否则投入产出比不划算。
6. 评估体系:没有度量就没有优化
聊了这么多优化手段,最后必须落到评估上。我见过太多团队优化 RAG 全靠“感觉”,改一版测几个问题觉得好了就上线,结果线上翻车。没有稳定的评估体系,所有优化都是盲人摸象。
6.1 构建自己的评估集
评估集是优化的前提。我的做法是从真实用户问题里采样,覆盖不同类型(事实型、操作型、对比型、多跳型),每条标注标准答案和对应的正确片段。规模不用很大,100 到 300 条就能反映问题,关键是要持续维护,把线上新出现的 badcase 补进去。
评估集最忌讳的是自己造题。自己造的问题往往措辞规范、指向明确,和真实用户的“口语化、信息不全”的问法差距很大,测出来的准确率会虚高。一定要用真实问题,哪怕它们看起来又乱又碎。
6.2 检索和生成分开度量
前面提过,评估要分两层。检索侧看 Hit Rate@K 和 MRR,生成侧看答案正确率、忠实度(是否忠于上下文)、以及拒答率(该拒答时是否拒答)。这几个指标组合起来,能比较全面地反映系统状态。
我一般会维护一个自动化评估脚本,每次改动后跑一遍,输出这几个指标的对比。这样任何一次优化,是涨是跌、涨在哪个环节,一目了然。这套流程建立起来之后,优化效率会高很多,因为你不再靠猜。
6.3 用大模型做评估的注意事项
人工评估准确但慢,所以很多人用大模型做自动评估。这确实能提效,但有几个坑要注意。第一,评估模型和被评估模型最好不是同一个,否则容易“自己评自己”产生偏差。第二,评估 Prompt 要写得足够具体,明确评分标准,否则模型打分很随意。第三,自动评估结果要定期用人工抽检校准,防止评估模型本身漂移。
我的做法是自动评估跑全量,人工抽检 10%。如果两者差异超过阈值,就说明评估 Prompt 需要调整了。这套组合拳打下来,评估既快又相对可靠。
7. 我在实际项目里踩过的几个坑
前面讲的都是方法论,最后分享几个我实际踩过的坑,都是文档里不会写、但特别容易中招的。
第一个坑是过度依赖单一 embedding 模型。我早期所有项目都用同一个 embedding 模型,觉得通用模型哪都能用。后来发现,不同领域的文档,embedding 效果差异很大。技术文档和客服对话,用同一个模型,总有一边效果差。后来我改成按领域选模型,甚至对特定领域做微调,召回率提升明显。
第二个坑是忽略文档的时效性。知识库里有新旧两版文档,检索时两条都召回,模型不知道该信哪个,答案就自相矛盾。后来我在元数据里加了版本和生效时间,检索时优先返回最新版本,冲突才解决。这个坑特别隐蔽,因为单看每条片段都是对的,问题出在片段之间。
第三个坑是Prompt 里塞了太多指令。我一度在 Prompt 里写了十几条规则,什么“要简洁”“要分点”“要标注来源”“不确定要拒答”,结果模型顾此失彼,反而答得乱七八糟。后来精简到三条最核心的指令,效果反而更好。模型不是越约束越听话,指令太多它会“精神分裂”。
第四个坑是用测试集调参调到过拟合。我有一阵子反复在同一个评估集上调切分粒度、调 TopK,指标一路涨,结果上线后真实问题准确率没怎么变。后来才意识到,我是在对着评估集过拟合。解决办法是留一个“从未参与调参”的测试集,只在最终验证时用。
这些坑的共同点是:它们都不是技术难题,而是工程判断问题。RAG 优化到后期,拼的不是谁用的模型新,而是谁对数据和场景的理解深。把评估做扎实,把 badcase 分析透,比追任何新框架都管用。
最后再分享一个小技巧:给检索结果加上“为什么召回它”的解释。我在调试阶段会让系统输出每条片段的召回来源(是原始查询还是改写查询)和重排序得分。这样分析 badcase 时,能快速判断是召回问题还是排序问题,排查效率翻倍。这个习惯帮我省了大量时间,推荐你也试试。