1. 为什么“RAG 烂大街”是个伪命题
这两年但凡跟大模型沾点边的团队,几乎人手一套 RAG 流水线。文档切片、向量化、存库、检索、拼 prompt、丢给模型生成——这套东西已经标准化到随便找个开源框架半天就能跑通 demo。于是圈子里开始流行一句话:“RAG 烂大街了”。
我一开始也这么觉得。直到去年接手一个企业知识库项目,客户丢过来 12 万份文档,涵盖产品手册、工单记录、合同模板、内部 Wiki,要求“问什么都能答准”。我拿最熟悉的 Naive RAG 跑了一版,Top-5 召回率勉强 60%,答非所问的比例高得离谱。那一刻我才意识到:烂大街的从来不是 RAG 本身,而是那条最粗糙的流水线。
真正拉开差距的地方,藏在六个关键分水岭上。这篇文章就把这六处逐一拆开,讲清楚每一处为什么是分水岭、怎么判断自己卡在哪、以及具体怎么改。不管你是刚搭完第一个 RAG demo 的新手,还是正在被召回率折磨的老手,应该都能找到能直接抄作业的东西。
先给个全局认知:一条完整的 RAG 链路,从数据进来到答案出去,大致经过数据解析、切片策略、索引构建、检索召回、重排过滤、生成编排六个环节。Naive RAG 在每个环节都用了最省事的默认方案,而生产级 RAG 需要在每个环节做针对性设计。下面逐个说。
2. 分水岭一:数据解析与切片策略
2.1 为什么切片是第一个分水岭
很多人把切片当成“预处理”,随便按 512 token 一刀切就完事。这是最致命的误区。切片质量直接决定了检索的上限——切错了,后面检索再花哨也救不回来。
我见过一个典型案例:一份产品故障排查手册,每个故障现象和解决方案是一一对应的。按固定长度切片后,某个故障现象的描述在 chunk 3 的末尾,对应的解决方案在 chunk 4 的开头。用户问“XX 报错怎么解决”,检索命中了 chunk 3,但解决方案根本没被召回,模型只能瞎编。
固定长度切片的根本问题在于:它假设语义边界和字符长度边界重合,而现实中这两者几乎从不重合。
2.2 分层切片的具体做法
我的做法是分层处理,不同文档类型用不同策略:
| 文档类型 | 切片策略 | 切片长度 | 重叠 | 理由 |
|---|---|---|---|---|
| 结构化手册 | 按标题层级切 | 按章节自然长度 | 无 | 章节本身就是语义单元 |
| 合同/法律文本 | 按条款切 | 按条款 | 条款号重叠 | 条款独立性极强 |
| 工单记录 | 单条工单为单元 | 整条 | 无 | 问答对天然完整 |
| 长篇文章 | 语义切片 | 256-512 token | 10%-15% | 平衡粒度与上下文 |
| 表格数据 | 整表 + 行摘要 | 按表 | 表头重复 | 表格脱离表头无意义 |
语义切片的核心思路是:先按句子切分,再计算相邻句子的语义相似度,在相似度骤降的地方断开。这样切出来的每个 chunk 内部语义连贯,边界落在真正的语义转折处。
具体实现上,我常用一个组合方案:先用规则做粗切(按标题、段落),再用语义相似度做细调。纯语义切片计算量大,纯规则切片又不够灵活,两者结合性价比最高。
2.3 实操心得:切片长度不是拍脑袋定的
切片长度怎么定?我的经验是看检索粒度需求。如果你希望检索返回的是“一个完整答案”,切片就要包含完整答案;如果你希望检索返回的是“相关线索”,切片可以更细。
一个可参考的计算方法:假设你的文档平均每 100 字包含一个完整信息点,用户提问通常需要 2-3 个信息点才能回答,那么切片长度应该在 200-300 字左右。太短则信息不完整,太长则噪声过多。
注意:切片重叠不是越多越好。重叠 10%-15% 足够覆盖边界信息,超过 20% 会导致检索结果大量重复,浪费上下文窗口。
3. 分水岭二:索引构建与向量化选型
3.1 向量模型不是越贵越好
索引构建环节,最大的坑是盲目追求“最强向量模型”。我见过团队用最大的 embedding 模型,结果检索效果还不如一个小模型——因为向量模型和你的数据领域不匹配。
通用向量模型在通用语料上训练,对专业术语、行业黑话的表示能力有限。比如医疗领域的“心梗”和“心肌梗死”在通用模型里可能距离较远,但在专业模型里应该很近。
选型时我会做三件事:
- 拿真实 query 做小规模评测:从历史提问里抽 100 条,人工标注正确答案,然后对比不同模型的召回率。这比看论文榜单靠谱得多。
- 考虑维度与成本:高维向量检索慢、存储贵。如果 768 维和 1536 维效果差不到 5%,果断选 768。
- 测试领域适配性:如果通用模型效果差,考虑用领域数据做微调,或者用领域预训练模型。
3.2 混合索引:向量 + 关键词 + 图
纯向量检索有个硬伤:对精确匹配不敏感。用户问“产品型号 XR-2000 的保修期”,向量检索可能召回一堆“保修期”相关但型号不对的文档。
我的方案是混合索引,三路并行:
- 向量索引:负责语义相似召回,解决“意思相近但用词不同”的问题。
- 关键词索引(BM25):负责精确匹配,解决型号、编号、专有名词的召回。
- 图索引:负责关系推理,解决“A 的上级部门是谁”这类需要多跳的问题。
三路结果用加权融合(Reciprocal Rank Fusion 是常用方法),权重根据业务调。一般场景下向量 0.5、关键词 0.3、图 0.2 是个不错的起点。
3.3 GraphRAG 到底解决了什么问题
GraphRAG 这两年很火,但很多人没搞明白它到底解决什么。简单说:当答案需要跨多个文档、多跳推理才能得出时,纯向量检索无能为力。
举个例子:文档 A 说“项目 Alpha 由张三负责”,文档 B 说“张三隶属于技术二部”,文档 C 说“技术二部的预算审批人是李四”。用户问“项目 Alpha 的预算谁审批”。纯向量检索可能只召回文档 A,答不出李四。GraphRAG 通过实体关系图,能从 Alpha 走到张三、走到技术二部、走到李四,完成多跳推理。
但 GraphRAG 不是银弹。构建知识图谱的成本很高,实体抽取、关系抽取都需要额外模型,而且图谱质量直接决定效果。我的建议是:只有当你的业务确实存在大量多跳推理需求时,才上 GraphRAG。否则混合索引 + 重排就够了。
4. 分水岭三:检索召回与 Contextual Retrieval
4.1 召回阶段的核心矛盾
召回阶段永远在解决一对矛盾:召回率与精确率的平衡。召回太少,答案可能不在候选里;召回太多,噪声会干扰模型判断。
Naive RAG 通常设 Top-K=3 或 5,这是拍脑袋的值。我的做法是动态 Top-K:根据 query 的复杂度调整。简单事实性问题 Top-K=3 足够,复杂分析性问题可能需要 Top-K=10 甚至更多。
判断 query 复杂度可以用一个轻量分类器,或者直接用规则:包含“对比”“分析”“为什么”等词的 query 判为复杂,扩大召回。
4.2 Contextual Retrieval 的关键改进
Contextual Retrieval 是这两年检索侧最重要的改进之一。它的核心思想是:切片在脱离原文后,会丢失上下文信息,导致检索和生成都受影响。
具体做法是:在切片入库前,用模型为每个切片生成一段上下文说明,然后把这段说明拼接到切片前面一起向量化。比如一个切片内容是“保修期为 24 个月”,单独看不知道是什么的保修期。加上上下文说明“本段来自产品 XR-2000 的售后条款”,检索时就能准确匹配到“XR-2000 保修期”的 query。
这个改动的成本很低——一次额外的模型调用——但效果提升明显。我在多个项目里实测,召回率能提升 10-20 个百分点。
4.3 实操:召回结果去重与多样性
召回阶段还有一个容易被忽略的问题:结果高度重复。尤其是切片有重叠时,Top-10 里可能有 5 条内容几乎一样,真正有用的信息只占 2-3 条。
我的处理方式是做 MMR(最大边际相关性)去重:在保证相关性的前提下,优先选择与已选结果差异大的候选。这样能在有限的名额里塞进更多样的信息。
具体参数上,MMR 的 lambda 设 0.7 左右比较合适——偏向相关性,但保留一定多样性。lambda=1 就是纯相关性排序,lambda=0 就是纯多样性,都不好用。
5. 分水岭四:重排与过滤
5.1 为什么需要重排
召回阶段用的是向量相似度,这是个“粗筛”。向量相似度高不代表真的相关——可能只是用词相似。重排阶段用更精细的模型(通常是 cross-encoder)对候选做二次打分,能显著提升精度。
我做过对比:同一批 query,召回 Top-20 直接给模型生成,准确率约 65%;经过重排后取 Top-5 再生成,准确率能到 82%。重排是性价比最高的精度提升手段之一。
5.2 重排模型的选型
重排模型分两档:
- 轻量档:小型 cross-encoder,延迟低,适合对响应速度敏感的场景。
- 重量档:大型 cross-encoder 或 LLM 打分,精度高但延迟大。
我的建议是:线上用轻量档做初排,离线用重量档做评测和调优。如果业务对精度要求极高且能接受延迟,再考虑线上用重量档。
5.3 过滤规则的设计
重排之后还需要过滤,把明显不相关或低质量的结果剔除。常用过滤规则:
- 相似度阈值:低于阈值的直接丢,避免“矮子里拔将军”。
- 时间过滤:如果业务对时效敏感,过期文档降权或剔除。
- 权限过滤:不同用户能看到的文档不同,检索前就要做权限隔离。
- 去重过滤:内容重复的只保留一条。
注意:过滤规则不要设得太激进。我见过团队把阈值设太高,导致召回结果被砍到只剩 1-2 条,模型反而没足够信息生成。阈值应该通过评测确定,而不是拍脑袋。
6. 分水岭五:生成编排与 Agentic RAG
6.1 从“一次检索”到“多轮编排”
Naive RAG 是“一次检索 + 一次生成”,但很多问题需要多轮检索才能回答。比如“对比 A 产品和 B 产品的保修政策”,需要分别检索 A 和 B 的信息,再对比。
Agentic RAG 的核心就是把检索变成 Agent 的一个工具,让 Agent 自主决定检索什么、检索几次、什么时候停止。这需要一套编排框架来管理流程。
6.2 LangGraph 在 RAG 编排中的角色
LangGraph 是目前做 Agentic RAG 编排比较顺手的框架。它把流程建模成图:节点是操作(检索、生成、判断),边是流转条件。相比链式编排,图编排能表达循环、分支、并行,更适合复杂 RAG 流程。
一个典型的 Agentic RAG 图结构:
- 入口节点:接收 query,判断复杂度。
- 检索节点:执行检索,可多次调用。
- 判断节点:判断当前信息是否足够回答。
- 生成节点:信息足够时生成答案。
- 回退边:信息不足时回到检索节点,调整 query 再检索。
这个结构的关键在于判断节点——它决定了要不要继续检索。判断逻辑可以用规则(比如“召回结果相似度都低于阈值就继续检索”),也可以用模型判断。
6.3 实操:控制 Agent 的检索轮次
Agentic RAG 最大的风险是无限循环——Agent 一直觉得信息不够,一直检索。必须设上限。
我的做法是设两个上限:最大轮次(比如 3 轮)和最大 token 消耗(比如 8000 token)。任一超限就强制进入生成节点,用现有信息回答,并在答案里标注“信息可能不完整”。
另外,每轮检索后要更新 query。第一轮用原始 query,第二轮用“原始 query + 缺失信息点”,第三轮再调整。这样每轮检索都有针对性,而不是重复检索同样的内容。
7. 分水岭六:评测与持续迭代
7.1 没有评测就没有优化
前面五处改进,怎么知道有没有效果?靠评测。没有评测的 RAG 优化就是盲人摸象。
RAG 评测分两个层面:
- 检索层:召回率、精确率、MRR、NDCG。衡量检索系统找到相关内容的能力。
- 生成层:答案准确率、忠实度(是否基于检索内容)、完整性。衡量最终答案的质量。
7.2 评测集的构建
评测集是评测的基础。我的构建方法:
- 从真实提问中采样:历史提问是最真实的评测数据。
- 人工标注答案:每条 query 标注标准答案和对应的源文档。
- 覆盖各类场景:简单事实、多跳推理、对比分析、否定查询都要有。
- 定期更新:业务在变,评测集也要跟着更新。
规模上,100-200 条 query 就能给出有参考价值的评测结果。太少波动大,太多标注成本高。
7.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 召回结果不相关 | 切片粒度过粗/过细 | 检查切片长度和边界 | 调整切片策略,加语义切片 |
| 答案答非所问 | 检索没命中正确文档 | 看召回结果里有没有正确答案 | 加混合索引,加 Contextual Retrieval |
| 答案编造内容 | 模型没基于检索内容 | 检查 prompt 和忠实度 | 强化 prompt 约束,加引用要求 |
| 多跳问题答不出 | 缺少关系推理能力 | 看是否需要跨文档推理 | 上 GraphRAG 或 Agentic RAG |
| 响应太慢 | 检索或重排太重 | 看各环节耗时 | 轻量化模型,加缓存 |
| 结果重复度高 | 切片重叠过多 | 检查重叠比例 | 降重叠,加 MMR 去重 |
7.4 持续迭代的节奏
RAG 系统不是一次搭好就完事。我的迭代节奏是:
- 每周:看线上 bad case,归类问题。
- 每月:跑一次完整评测,对比指标变化。
- 每季度:review 整体架构,看有没有新的技术值得引入。
迭代时一次只改一个变量,否则无法判断是哪个改动带来的效果。这是做实验的基本纪律。
8. 我在实际项目中的几点体会
踩了这么多坑,有几个体会特别深。
第一,不要迷信新技术。GraphRAG、Agentic RAG 都很火,但如果你的业务就是简单事实问答,上这些就是杀鸡用牛刀,徒增复杂度和成本。先把基础流水线做扎实,再考虑进阶方案。
第二,数据质量比算法重要。我见过太多团队在算法上反复调优,但源文档本身就有大量错误、重复、过期内容。垃圾进垃圾出,数据不清洗,算法再强也白搭。
第三,评测要趁早。很多团队做到一半才想起来评测,结果发现前面做的改动都没法量化效果。评测集应该和系统同步建设,甚至先建评测集再搭系统。
第四,简单方案往往够用。混合索引 + 重排 + Contextual Retrieval 这套组合,能解决 80% 的召回问题。剩下 20% 再考虑 GraphRAG 或 Agentic RAG。不要一上来就上最复杂的方案。
最后分享一个我常用的小技巧:给每个 chunk 打上来源标签(文档名、章节、更新时间),生成答案时要求模型标注引用来源。这样既能提升答案可信度,又方便排查问题——答案错了,直接看引用的 chunk 就知道是检索错了还是生成错了。这个习惯帮我省了大量排查时间。