ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RAG效果提升的六处分水岭:从文档切分到评估体系的工程实践

RAG效果提升的六处分水岭:从文档切分到评估体系的工程实践

1. 那条流水线为什么谁都能搭,但谁都用不好

RAG 这个词现在确实被说烂了。打开任何一个技术社区,搜一下 RAG,你能看到几百篇标题几乎一模一样的文章:加载文档、切分、向量化、存库、检索、拼 Prompt、丢给大模型。七步走完,Demo 跑通,截图发出来,评论区一片“学到了”。但如果你真的拿这套东西去跑一个稍微正经点的业务场景,比如企业内部的制度问答、技术文档检索、客服知识库,你会发现一个很尴尬的事实:Demo 里那套流水线,上线之后基本没法用。

问题出在哪?不是 RAG 这个思路不行,而是大多数人把 RAG 当成了一条固定的流水线来理解。流水线的意思是:输入进去,经过几个固定工位,输出出来。你只要每个工位买对设备,装好传送带,它就能跑。但 RAG 不是这样的东西。RAG 更像是一个需要根据原料、产品、客户反馈不断调整配方的生产过程。文档怎么切、检索怎么召回、召回之后怎么排序、排序之后怎么组装上下文、组装之后怎么让模型用——每一个环节都有大量的决策点,而这些决策点之间是互相影响的。

我见过太多团队在 RAG 上踩的坑,本质上都是同一个原因:他们把注意力全放在了“用哪个向量库”“用哪个 Embedding 模型”“用哪个框架”这些选型问题上,却忽略了真正决定 RAG 效果的那几个分水岭。选型当然重要,但选型是入门级问题。真正拉开差距的,是下面这六处。这六处每一处都对应着一个具体的工程决策,而且每一处的决策逻辑都不是“哪个更先进就用哪个”,而是“你的场景需要什么”。

这篇文章不打算再给你讲一遍 RAG 的基础流程,那种内容到处都是。我想做的是把这六处分水岭拆开,讲清楚每一处到底在分什么、为什么大多数人的默认做法会出问题、以及在实际项目里应该怎么判断和取舍。如果你正在做 RAG 相关的项目,或者准备做,这六处值得你花时间想清楚。

2. 第一处分水岭:文档切分不是切菜,是在重建语义边界

2.1 固定长度切分为何几乎总是错的

几乎所有 RAG 教程的第一步都是“把文档按固定字数切分”。500 字一段,或者 1000 字一段,加个 overlap 防止切断句子。这个做法在 Demo 里没问题,因为 Demo 的文档通常很短、结构很简单。但真实场景里的文档是什么样的?技术手册里有嵌套的章节和小节,合同里有条款和子条款,产品文档里有表格和代码块,FAQ 里有问答对。你拿一把固定长度的刀去切这些东西,切出来的片段在语义上往往是残缺的。

举个具体的例子。假设你有一份产品配置文档,里面有一段话:“当系统检测到内存占用超过 80% 时,会自动触发垃圾回收。如果垃圾回收后内存仍然高于 70%,则开始拒绝新的请求。”如果你按固定长度切分,恰好把“如果垃圾回收后内存仍然高于 70%”这句话切到了下一个片段,那么第一个片段就变成了一个不完整的条件描述,第二个片段则失去了上下文。检索的时候,用户问“内存高的时候系统会怎样”,第一个片段被召回了,但模型看到的是一个没有后续的规则,回答就会不完整甚至错误。

这不是极端情况,这是常态。固定长度切分的问题在于,它假设文档的语义边界和字符边界是对齐的,但事实几乎从来不是这样。

2.2 结构感知切分的实际做法

正确的做法是结构感知切分。什么意思?就是先理解文档的结构,再根据结构来决定切分点。Markdown 文档按标题层级切,HTML 按 DOM 结构切,PDF 先做版面分析再按段落和表格切,代码文件按函数和类切。切出来的每个片段应该是一个语义上自洽的单元,而不是一个被字符数截断的碎片。

具体操作上,我通常会用这样的策略:首先用文档解析工具把原始文档转成结构化格式,比如 Markdown 或者带层级信息的 JSON。然后按照标题层级做第一层切分,每个最小标题单元作为一个候选片段。如果某个单元太长,超过了 Embedding 模型的最佳输入长度,再在这个单元内部按段落做第二层切分。如果某个单元太短,比如只有一句话,就把它和相邻的同类单元合并。这样切出来的片段,每个都是一个完整的语义单元,检索的时候命中率会高很多。

这里有一个容易被忽略的细节:切分的时候要保留层级路径。什么意思?就是每个片段除了自身的文本内容,还要带上它在文档中的位置信息,比如“产品手册 > 配置管理 > 内存配置 > 垃圾回收策略”。这个路径信息在检索和组装上下文的时候非常有用,后面会讲到。

2.3 切分粒度与检索精度的权衡

切分粒度是另一个需要仔细考虑的问题。切得太粗,一个片段里包含多个主题,检索的时候容易召回不相关的信息,而且会浪费上下文窗口。切得太细,一个片段只包含一两句话,虽然精度高了,但模型可能缺乏足够的上下文来理解这句话的含义。

我的经验是,切分粒度应该和你的查询粒度匹配。如果你的用户查询通常是“XX 功能的配置方法是什么”这种中等粒度的问题,那片段就应该是一个完整的功能说明单元,大概几百字。如果你的用户查询是“XX 参数的默认值是多少”这种细粒度的问题,那片段可以更小,但最好还是保留所在的段落作为上下文。

实际操作中,我建议做一次切分粒度的评估。拿一批真实的用户查询,分别用不同粒度的切分方案跑检索,看召回率和准确率的变化。这个评估不需要很复杂,几十条查询就能看出明显的趋势。很多人跳过这一步直接拍脑袋定一个 chunk size,结果上线之后发现检索效果不好,又回头调,浪费的时间远比做一次评估多。

3. 第二处分水岭:检索不是一次向量搜索就完事

3.1 纯向量检索的盲区

大多数 RAG 流水线里的检索环节就是一句话:把查询向量化,然后在向量库里做相似度搜索,取 Top-K。这个做法的问题在于,向量检索擅长的是语义相似,但它不擅长精确匹配。用户问“ERR-4032 错误码是什么意思”,向量检索可能会召回一堆关于错误处理的通用文档,但真正包含 ERR-4032 这个具体错误码的文档反而可能排在后面。因为从语义上讲,“错误码”和“错误处理”确实很相似,但用户要的是那个具体的码。

这就是纯向量检索的盲区:它对精确的术语、编号、名称不敏感。而在企业知识库场景里,大量的查询恰恰是包含这类精确信息的。产品型号、错误码、API 名称、配置项名称,这些东西用向量检索往往效果不好。

3.2 混合检索的工程实现

解决方案是混合检索,也就是把向量检索和关键词检索结合起来。关键词检索可以用 BM25 或者全文索引来实现,它对精确匹配非常有效。把两路检索的结果合并,再做一个重排序,通常能显著提升召回质量。

具体实现上,我通常会用这样的架构:查询进来之后,同时走两路。一路做向量化然后查向量库,另一路做分词然后查全文索引。两路各取 Top-20 左右,然后用一个重排序模型或者简单的分数融合策略合并成一个 Top-10 的列表。分数融合可以用加权求和,向量相似度和 BM25 分数各占一定权重,权重根据你的场景调。如果精确匹配更重要,BM25 权重大一些;如果语义理解更重要,向量权重大一些。

这里有一个实操细节:BM25 的分词策略对中文场景特别重要。中文不像英文有天然的空格分隔,分词质量直接影响关键词检索的效果。我一般会用 jieba 或者 HanLP 做分词,同时把一些领域专有名词加到自定义词典里,避免被切碎。比如“垃圾回收”如果被切成“垃圾”和“回收”,检索效果就会打折扣。

3.3 重排序模型到底值不值得上

重排序是另一个经常被讨论的点。简单说,就是在检索召回一批候选片段之后,用一个更精细的模型对它们重新打分排序。常用的重排序模型有 Cohere Rerank、BGE Reranker 等。这个东西值不值得上?我的答案是:如果你的检索召回质量直接影响到最终回答质量,而且你有一定的延迟预算,那值得。

重排序的价值在于,它可以用比向量检索更精细的方式来判断查询和片段的相关性。向量检索是双塔结构,查询和文档分别编码,交互发生在最后一步,信息损失比较大。重排序模型通常是交叉编码器,查询和文档一起输入,能做更细粒度的交互,判断更准。实测下来,加一层重排序通常能把 Top-3 的准确率提升 10 到 20 个百分点。

但重排序也有代价。它增加了延迟,因为要对每个候选片段做一次模型推理。如果候选有 20 个,每个推理 50 毫秒,那就是 1 秒的额外延迟。所以要不要上重排序,取决于你的延迟预算和精度要求。如果延迟敏感,可以考虑用轻量级的重排序模型,或者只对 Top-10 做重排序而不是 Top-20。

4. 第三处分水岭:上下文组装决定了模型能看到什么

4.1 拼接 Top-K 片段的隐患

检索完成之后,下一步是把召回的片段组装成上下文,塞进 Prompt 里让模型回答。大多数流水线在这里的做法是:把 Top-K 个片段按顺序拼在一起,加个分隔符,然后前面加一句“根据以下内容回答问题”。这个做法看起来没问题,但实际上有很多隐患。

第一个隐患是信息冗余。Top-K 个片段里可能有大量重复内容,因为相关文档往往会在不同位置重复同样的信息。这些重复内容不仅浪费上下文窗口,还可能让模型产生困惑,因为它不知道哪个版本是权威的。

第二个隐患是信息冲突。不同片段可能包含矛盾的信息,比如旧版本文档和新版本文档对同一个配置的描述不同。如果直接把冲突信息拼在一起,模型可能会给出一个模棱两可的回答,或者随机选一个。

第三个隐患是上下文顺序。模型对上下文中的位置是有偏好的,通常开头和结尾的信息更容易被注意到,中间的信息容易被忽略。如果你把最重要的片段放在中间,模型可能反而没用好。

4.2 上下文压缩与去重的实操

解决这些问题的方法之一是上下文压缩。具体做法是,在组装上下文之前,先对召回的片段做一轮处理:去掉重复内容,合并相似片段,保留最相关的部分。可以用一个小的语言模型来做这件事,也可以用基于规则的策略。

我通常的做法是:先用相似度去重,把内容重叠度超过阈值的片段合并,保留信息量最大的那个版本。然后按相关性分数排序,把最相关的放在最前面和最后面,次相关的放中间。如果总长度超过上下文窗口的限制,就按分数从低到高截断,优先保留高分片段。

还有一个技巧是给每个片段加上来源标注。比如“来源:产品手册 v2.3,第 4 章”。这样模型在回答的时候可以引用来源,用户也能验证信息的可靠性。这个做法在企业场景里特别有用,因为用户往往需要知道答案的出处。

4.3 让模型学会说“我不知道”

上下文组装的另一个关键点是:要让模型知道什么时候该说“我不知道”。如果召回的片段里没有足够的信息来回答问题,模型应该明确说“根据现有资料无法回答”,而不是硬编一个答案。这个行为需要通过 Prompt 来引导。

我通常会在 Prompt 里加这样的指令:“如果以下内容不足以回答问题,请直接说明无法回答,不要编造信息。”同时,在组装上下文的时候,如果检索的置信度很低,比如最高分低于某个阈值,可以在 Prompt 里加一句提示:“注意:以下内容的匹配度较低,请谨慎使用。”这样模型会更倾向于保守回答。

这个做法看起来简单,但在实际项目里非常关键。企业用户对错误答案的容忍度远低于对“不知道”的容忍度。一个编造的答案可能导致严重的决策失误,而一个“不知道”至少不会误导人。

5. 第四处分水岭:Agentic RAG 把单次检索变成多步推理

5.1 单次检索为何搞不定复杂问题

前面讲的都是单次检索的优化。但有些问题,单次检索根本搞不定。比如用户问:“我们系统里哪些配置项会影响垃圾回收的行为,这些配置项的默认值分别是什么,修改它们有什么风险?”这个问题需要多个步骤:先找到垃圾回收相关的配置项,然后查每个配置项的默认值,再查每个配置项的风险说明。单次检索只能召回一批相关片段,但模型需要在这些片段之间做推理和整合,而且可能需要多轮检索才能收集齐所有信息。

这就是 Agentic RAG 要解决的问题。Agentic RAG 的核心思想是:把 RAG 从一个单次检索-生成的流程,变成一个多步的、有规划的、可以调用工具的智能体流程。智能体可以先分析问题,决定需要哪些信息,然后一步步检索、验证、整合,最后给出答案。

5.2 用 LangGraph 搭建多步检索流程

实现 Agentic RAG 的一个常用工具是 LangGraph。LangGraph 是 LangChain 生态里的一个库,专门用来构建有状态的、多步骤的智能体流程。它的核心概念是图:节点代表一个操作,边代表操作之间的流转。你可以定义一个图,里面有检索节点、推理节点、工具调用节点,然后根据条件决定下一步走哪个节点。

举个具体的例子。假设我们要搭建一个多步检索的 RAG 流程。可以这样设计图:第一个节点是“分析问题”,用一个语言模型判断这个问题需要哪些类型的信息。第二个节点是“生成检索计划”,把问题拆成几个子查询。第三个节点是“执行检索”,对每个子查询分别检索。第四个节点是“评估结果”,判断检索到的信息是否足够回答问题。如果不够,回到第二个节点重新规划;如果够了,进入第五个节点“生成回答”。

这个流程比单次检索复杂得多,但它能处理的问题类型也丰富得多。对于简单的事实性问题,它可能一步就搞定了;对于复杂的多跳问题,它可以反复检索和推理,直到收集齐所有需要的信息。

5.3 工具调用让 RAG 真正“下地干活”

Agentic RAG 的另一个重要能力是工具调用。检索只是获取信息的一种方式,有些问题需要调用其他工具才能解决。比如用户问“上个月我们的 API 调用量是多少”,这个问题需要查数据库,而不是查文档。如果 RAG 系统只能检索文档,它就回答不了这个问题。但如果它有一个“查询数据库”的工具,就可以先检索文档了解数据库的结构和查询方式,然后调用数据库工具获取实际数据,最后整合成回答。

LangGraph 对工具调用的支持很自然。你可以把每个工具定义成一个节点,然后在图里根据条件决定是否调用。工具的输入输出可以用结构化格式定义,这样模型能清楚地知道每个工具需要什么参数、返回什么结果。

这里有一个实操经验:工具的描述非常重要。模型是根据工具的描述来决定是否调用以及怎么调用的。如果描述写得含糊,模型可能在不该调用的时候调用,或者传错参数。我通常会把工具描述写得非常具体,包括它适合解决什么问题、需要什么输入、返回什么格式的结果。这看起来是小事,但对工具调用的准确率影响很大。

6. 第五处分水岭:GraphRAG 处理的是关系,不是文本

6.1 向量检索搞不定多跳关系

有些问题的难点不在于找到相关文本,而在于理解文本之间的关系。比如用户问:“A 系统的故障会不会影响 B 服务?”这个问题需要知道 A 系统和 B 服务之间的依赖关系。如果文档里分别描述了 A 系统和 B 服务,但没有明确说它们之间的关系,向量检索就无能为力了,因为它只能找到分别提到 A 和 B 的片段,但无法推断出它们之间的关系。

这就是 GraphRAG 要解决的问题。GraphRAG 的核心思想是:从文档中抽取实体和关系,构建一个知识图谱,然后基于图谱做检索和推理。当用户问一个涉及多跳关系的问题时,系统可以在图谱上做路径查询,找到实体之间的关联路径,从而回答问题。

6.2 实体抽取与图谱构建的实际难点

GraphRAG 听起来很美好,但实际做起来有几个难点。第一个难点是实体抽取。从非结构化文本里抽取实体和关系,需要用到信息抽取模型或者大语言模型。抽取的质量直接决定了图谱的质量。如果抽取不准,图谱里就会有很多噪声,检索效果反而更差。

第二个难点是实体消歧。同一个实体在不同文档里可能有不同的名称,比如“垃圾回收”“GC”“garbage collection”指的是同一个东西。如果不做消歧,图谱里就会有多个节点表示同一个实体,关系就断了。消歧可以用规则做,也可以用模型做,但都需要领域知识。

第三个难点是图谱的维护。文档更新了,图谱也要更新。如果文档频繁变动,图谱的维护成本会很高。所以 GraphRAG 更适合文档相对稳定的场景,比如法规、标准、技术手册。

6.3 GraphRAG 和向量 RAG 的混合架构

在实际项目里,我通常不会纯用 GraphRAG,而是把它和向量 RAG 结合起来。具体做法是:先用向量检索召回一批相关片段,然后从这些片段里识别出关键实体,再在知识图谱里查询这些实体的关系和属性,把图谱查询的结果作为补充上下文一起给模型。这样既能利用向量检索的语义匹配能力,又能利用图谱的关系推理能力。

这种混合架构的实现复杂度比纯向量 RAG 高不少,但它能处理的问题类型也更丰富。如果你的场景里有大量的关系型查询,比如“哪些服务依赖于这个组件”“这个变更会影响哪些下游系统”,那 GraphRAG 值得投入。

7. 第六处分水岭:评估体系决定了你能不能持续优化

7.1 没有评估就没有优化

前面讲了五处分水岭,但如果没有第六处,前面五处你都做对了,也不知道自己做对了。这就是评估体系。RAG 系统的一个特点是,它的效果很难用单一指标衡量。检索召回率、回答准确率、回答完整性、延迟、成本,这些指标之间往往有 trade-off。你需要一个评估体系来帮你判断每次改动到底是变好了还是变差了。

大多数团队在 RAG 评估上的做法是:找几个人,问几个问题,看看回答得怎么样。这个做法的问题在于,它不可复现、不可量化、不可持续。今天觉得回答不错,明天换个问题可能就崩了。而且人的判断标准不一致,同一个人在不同时间段的判断也可能不同。

7.2 构建 RAG 评估集的实操方法

构建一个可用的 RAG 评估集,需要做几件事。第一,收集一批真实的用户查询。这些查询应该覆盖你的主要场景,包括简单的事实性问题、复杂的多跳问题、需要精确匹配的问题、需要关系推理的问题。数量不用很多,几十到几百条就够,但要有代表性。

第二,为每个查询标注标准答案和相关的文档片段。标准答案可以是人工写的,也可以是从现有文档里摘的。相关文档片段的标注很重要,因为它能帮你评估检索环节的效果,而不仅仅是最终回答的效果。

第三,定义评估指标。检索环节可以用召回率、准确率、MRR 等指标。生成环节可以用答案准确率、完整性、忠实度等指标。忠实度指的是回答是否完全基于检索到的内容,没有编造。这个指标在企业场景里特别重要。

第四,建立自动化评估流程。每次改动之后,自动跑一遍评估集,生成指标报告。这样你就能清楚地看到每次改动的影响。如果某个改动提升了准确率但降低了忠实度,你就需要判断这个 trade-off 是否值得。

7.3 在线反馈与离线评估的闭环

离线评估集是基础,但它不能覆盖所有情况。真实用户的查询分布可能和你的评估集不一样,而且用户的行为会随着时间变化。所以还需要在线反馈机制。比如在回答下面加一个“这个回答有帮助吗”的按钮,收集用户的反馈。这些反馈可以用来发现新的问题类型,补充到评估集里,形成闭环。

我通常会把在线反馈和离线评估结合起来用。离线评估用来做快速迭代,每次改动跑一遍,看指标变化。在线反馈用来做长期监控,发现离线评估没覆盖到的问题。两者结合,才能持续优化 RAG 系统的效果。

8. 这六处之外,还有一些零散但重要的经验

上面六处分水岭是主线,但实际做 RAG 项目的时候,还有一些零散的经验值得分享。

关于 Embedding 模型的选择,我的建议是不要盲目追求排行榜上的最高分。排行榜上的评估集和你的场景可能差别很大。更好的做法是拿你自己的评估集,跑几个候选模型,看实际效果。另外,Embedding 模型的维度、推理速度、成本也是要考虑的因素。有时候一个中等效果但速度快的模型,比一个效果好但慢的模型更适合你的场景。

关于向量库的选择,如果数据量不大,几万到几十万条,用 FAISS 或者 Chroma 就够了,部署简单,性能也够。如果数据量到了百万级以上,或者需要分布式部署,再考虑 Milvus、Qdrant 这些。不要一上来就上重型武器,维护成本很高。

关于 Prompt 的设计,一个常见的错误是把 Prompt 写得太长太复杂。模型对长 Prompt 的遵循能力是有限的,太长的 Prompt 反而容易让模型忽略关键指令。我通常会把 Prompt 控制在几百字以内,把最重要的指令放在最前面和最后面。

关于缓存,RAG 系统里有很多可以缓存的地方。Embedding 结果可以缓存,检索结果可以缓存,甚至最终回答也可以缓存。对于重复查询比较多的场景,缓存能显著降低延迟和成本。但要注意缓存的失效策略,文档更新之后,相关的缓存要及时清理。

关于多语言场景,如果你的文档和查询涉及多种语言,Embedding 模型的选择会更关键。有些模型在多语言上表现好,有些只擅长英文。另外,检索的时候可能需要做跨语言检索,这又是另一个复杂话题了。

最后说一点关于 RAG 和微调的关系。经常有人问,RAG 和微调哪个更好。我的看法是,它们解决的是不同的问题。RAG 解决的是知识获取的问题,让模型能访问外部知识。微调解决的是行为对齐的问题,让模型按照特定的风格或格式输出。两者不冲突,可以结合使用。但在大多数场景下,先做好 RAG 的收益比先做微调大得多,因为 RAG 的迭代成本低,效果提升也直接。

我在实际项目里的体会是,RAG 的效果提升不是线性的,而是阶梯式的。你可能花了很多时间调切分、调检索,效果提升不明显,但突然某一处做对了,整体效果就上了一个台阶。这六处分水岭,每一处都可能是那个让你上台阶的点。关键是不要只盯着流水线,要理解每一处背后的原理和取舍,然后根据你的场景做判断。

RELATED READING

延伸阅读

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