1. 为什么“RAG 烂大街”是个伪命题
这两年但凡跟大模型沾点边的项目,十有八九会挂上 RAG 三个字母。招聘 JD 里写“熟悉 RAG 检索增强”,技术分享里讲“我们基于 RAG 搭建了企业知识库”,开源社区里各种 RAG 框架层出不穷。于是很多人得出一个结论:RAG 已经烂大街了,没什么技术含量。
我一开始也这么觉得。直到真正把一套 RAG 系统从 demo 推到生产环境,被各种脏数据、召回不准、答非所问、上下文超长、多跳推理失败反复教育之后,我才意识到一个事实:烂大街的从来不是 RAG 本身,而是那条“文档切块 → 向量化 → 存向量库 → 相似度检索 → 塞进 prompt”的流水线。这条流水线谁都能搭,网上教程一搜一大把,零基础半天就能跑通一个能问答的 demo。
但 demo 和生产之间,隔着六道真正的分水岭。这六处分水岭决定了你的 RAG 是“玩具”还是“能下地干活的工具”。这篇文章我就围绕这六处分水岭,把每一处的核心原理、实操要点、踩坑经验掰开揉碎讲清楚。涉及到的关键词包括 RAG、Agentic RAG、Contextual Retrieval、GraphRAG、LangGraph,我会尽量用从业者之间交流的口吻,把“为什么这么做”讲透,而不是只丢一堆配置。
先说清楚这篇文章适合谁看:如果你已经跑通过最基础的 RAG demo,但发现效果不稳定、召回质量差、复杂问题答不上来,想搞清楚下一步该往哪优化,那这篇就是写给你的。如果你是完全零基础,也能看懂,因为我会在关键处补基础概念,只是信息密度会比较高,建议边看边对照自己的项目。
2. 分水岭一:检索质量,从“能召回”到“召回得准”
2.1 朴素向量检索为什么经常翻车
最基础的 RAG 用的是稠密向量检索:把文档切块,用 embedding 模型编码成向量,查询时把问题也编码成向量,算余弦相似度,取 top-k。这套逻辑听起来很合理,但实际用起来问题一大堆。
第一个问题是语义漂移。向量相似度高,不代表内容真的相关。我做过一个测试,问“公司年假怎么申请”,检索出来的 top-3 里居然有一条是“公司年会安排通知”,因为“年假”和“年会”在向量空间里离得很近。这种错误在中文场景里特别常见,因为中文的 embedding 模型对细粒度语义的区分能力参差不齐。
第二个问题是关键词失配。用户问的是“报销流程”,但文档里写的是“费用核销指引”,如果 embedding 模型没学好这两个词的语义关联,就召不回来。反过来,用户问一个专有名词、产品型号、错误码,向量检索经常抓瞎,因为这些词的语义信息很稀疏。
第三个问题是top-k 的取舍困境。k 取小了,召回不全;k 取大了,噪声一大堆,塞进 prompt 反而干扰模型。我见过太多项目把 k 设成 10 甚至 20,结果模型被无关内容带偏,答非所问。
2.2 混合检索:稠密加稀疏才是正解
解决上面这些问题,最直接的手段是混合检索(Hybrid Retrieval):稠密向量负责语义匹配,稀疏检索(BM25、TF-IDF 这类)负责关键词精确匹配,两路结果融合。
BM25 的原理不复杂,它本质上是基于词频和逆文档频率的打分,对专有名词、型号、错误码这类“低频但关键”的词特别敏感。向量检索抓语义,BM25 抓字面,两者互补。融合方式常见的有两种:一种是加权求和(比如 0.7 向量 + 0.3 BM25),另一种是 RRF(Reciprocal Rank Fusion,倒数排名融合)。
RRF 我实测下来更稳,因为它不依赖分数归一化。不同检索器的分数尺度完全不一样,向量相似度是 0 到 1,BM25 分数可能是 0 到几十,直接加权很容易被某一方主导。RRF 只看排名,公式是score = Σ 1/(k + rank),k 一般取 60。这样两路检索的排名信息都能公平地贡献进来。
# RRF 融合的简化实现 def rrf_fusion(vector_results, bm25_results, k=60): scores = {} for rank, doc_id in enumerate(vector_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) for rank, doc_id in enumerate(bm25_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)提示:混合检索的收益在“查询包含专有名词或精确术语”时最明显。如果你的知识库全是通用叙述性文本,收益可能没那么大,但加上基本不会亏。
2.3 重排序:把真正相关的顶上来
混合检索解决了“召回”的问题,但 top-k 里还是会有噪声。这时候需要重排序(Rerank)。重排序模型(比如 BGE-Reranker、Cohere Rerank 这类)是一个交叉编码器,它把 query 和每个候选文档拼在一起打分,精度比向量相似度高得多,但速度慢,所以只对 top-k 的候选做精排。
我的标准流程是:混合检索召回 top-50,重排序精排取 top-5,再塞进 prompt。这样既保证了召回率,又保证了精度。实测下来,加了重排序之后,答案准确率能提升 15% 到 30%,具体取决于知识库的噪声程度。
重排序的代价是延迟。交叉编码器对 50 个候选打分,可能要几百毫秒到一两秒。如果对延迟敏感,可以只对 top-20 做重排,或者用更小的重排模型。这里没有标准答案,得根据你的场景权衡。
2.4 查询改写:别让用户的问法限制你
用户的问题往往很口语化、很模糊,直接拿去检索效果很差。查询改写(Query Rewriting)就是让 LLM 先把用户问题改写成更适合检索的形式。
常见的改写策略有几种。第一种是扩展:把“年假怎么请”扩展成“年假申请流程、年假审批步骤、年假天数规定”。第二种是分解:把“A 和 B 的区别以及各自适用场景”拆成两个子查询分别检索。第三种是消歧:结合对话历史,把“它怎么用”里的“它”替换成具体指代的对象。
查询改写用 LLM 做,成本不高,但收益明显。我一般会在检索前加一步改写,把口语化问题转成 2 到 3 个检索友好的查询,分别检索后合并结果。这一步在 Agentic RAG 里会更进一步,后面会讲。
3. 分水岭二:切块策略,从“按字数切”到“按语义切”
3.1 固定长度切块的三个致命伤
最省事的切块方式是按固定字符数切,比如每 500 字一块,重叠 50 字。这种方式实现简单,但问题很多。
第一个致命伤是切断语义单元。一个完整的段落、一个表格、一段代码,被硬生生从中间切开,前半块和后半块各自都不完整。检索到前半块,模型看到的是半句话,自然答不好。
第二个致命伤是块内主题混杂。500 字里可能前半段讲 A 功能,后半段讲 B 功能,向量编码后变成一个“四不像”的向量,跟 A 和 B 都不太像。
第三个致命伤是忽略文档结构。Markdown 的标题层级、PDF 的章节、HTML 的标签,这些都是天然的语义边界,固定切块完全无视它们。
3.2 结构感知切块:顺着文档的骨架切
结构感知切块(Structure-aware Chunking)的核心思路是:优先按文档的天然结构切,结构太大再按语义切,语义还太大才按长度切。
具体做法是:先解析文档结构,Markdown 按标题层级切,PDF 按章节和段落切,代码按函数和类切。每个块尽量保持语义完整。如果某个章节太长,再用语义切块模型(比如基于句子 embedding 相似度的切分)把它切成子块。最后才用固定长度兜底。
这样做的好处是每个块都是一个完整的语义单元,向量编码更准确,检索命中的内容也更完整。代价是实现复杂一些,需要针对不同文档格式写解析逻辑。
3.3 Contextual Retrieval:给每个块补上下文
Anthropic 提出的Contextual Retrieval是我这两年觉得最有价值的改进之一。它的核心洞察是:一个块被单独拿出来看,往往丢失了它在原文中的上下文,导致检索和生成都受影响。
举个例子,文档里有一句“该功能默认关闭,需要在配置文件中开启”。单独看这个块,你不知道“该功能”是什么功能。Contextual Retrieval 的做法是:在切块之后,用 LLM 给每个块生成一段简短的上下文说明,把块放回原文语境里,然后把这段说明拼在块前面一起编码。
# Contextual Retrieval 的 prompt 模板示意 context_prompt = """ 以下是整篇文档: <document> {document} </document> 以下是文档中的一个块: <chunk> {chunk} </chunk> 请用一两句话说明这个块在整篇文档中的位置和上下文, 以便这个块被单独检索时仍能被正确理解。只输出说明,不要重复块内容。 """这样做之后,每个块都带上了“我是谁、我在哪”的信息,检索准确率提升很明显。Anthropic 的实测数据是,结合上下文检索和 BM25 混合检索,检索失败率能降低 49%,再加上重排序能降低 67%。这个数字我自己的项目里没有精确复现,但方向是对的,提升确实可观。
代价是切块阶段要为每个块调一次 LLM,成本和时间都上去了。优化手段是用便宜的小模型做这一步,或者用 prompt caching 缓存整篇文档的编码,只让块内容变化。
3.4 块大小和重叠的取舍
块大小没有万能值。我的经验是:叙述性文档 300 到 500 字,技术文档 500 到 800 字,代码块按函数粒度。块太小,上下文不足;块太大,噪声多且检索精度下降。
重叠(overlap)的作用是防止边界信息丢失,一般设块大小的 10% 到 20%。但重叠不是越多越好,重叠太多会导致重复内容被多次召回,浪费上下文窗口。我一般设 10%,结构感知切块的话重叠可以更少,因为边界本身就是语义边界。
注意:块大小要和你的 embedding 模型的最大输入长度匹配。有些模型只支持 512 token,你切 1000 字可能被截断,后面的内容等于没编码。
4. 分水岭三:从 Naive RAG 到 Agentic RAG
4.1 Naive RAG 的天花板在哪
Naive RAG 的流程是线性的:检索一次,生成一次,结束。这个流程能处理“事实型问答”,比如“XX 政策的申请条件是什么”。但遇到下面这些情况就歇菜了。
多跳问题:“A 公司的 CEO 毕业于哪所大学,这所大学在哪个城市?”这需要先查 A 公司 CEO,再查这个人的毕业院校,再查院校所在城市,至少三次检索。Naive RAG 一次检索搞不定。
需要判断的问题:“这份合同有没有风险条款?”这需要先检索合同相关条款,再判断,可能还要检索公司风控标准做对比。
检索失败需要重试的问题:第一次检索没找到相关内容,Naive RAG 就直接说“我不知道”,但其实换个查询词就能找到。
4.2 Agentic RAG 的核心:把检索变成工具
Agentic RAG的核心思路是把检索封装成一个工具(tool),让 Agent 自己决定什么时候检索、检索什么、检索几次。Agent 有了自主决策能力,就能处理上面那些复杂场景。
用 LangGraph 来实现的话,基本结构是一个带条件边的状态图:Agent 节点负责决策,工具节点负责执行检索,条件边根据 Agent 的输出决定是继续检索还是生成答案。LangGraph 的好处是它把 Agent 的执行流程显式建模成图,每个节点的输入输出、状态流转都清清楚楚,调试起来比黑盒 Agent 容易得多。
# LangGraph 构建 Agentic RAG 的骨架示意 from langgraph.graph import StateGraph, END def build_agentic_rag(): graph = StateGraph(AgentState) graph.add_node("agent", agent_node) # LLM 决策 graph.add_node("retrieve", retrieve_node) # 执行检索 graph.add_node("generate", generate_node) # 生成答案 graph.set_entry_point("agent") graph.add_conditional_edges( "agent", should_retrieve, # 判断是否需要检索 {"retrieve": "retrieve", "generate": "generate"} ) graph.add_edge("retrieve", "agent") # 检索完回到 agent 再判断 graph.add_edge("generate", END) return graph.compile()这个图的关键在于should_retrieve这个条件函数。它根据 Agent 的输出决定下一步:如果 Agent 认为需要检索,就走 retrieve 节点;如果认为信息够了,就走 generate 节点。检索完回到 agent,Agent 看到新检索的内容,再决定是继续检索还是生成。这样就实现了多轮检索和自主决策。
4.3 工具设计:检索工具不止一个
Agentic RAG 里,检索工具可以有很多个,让 Agent 按需选择。常见的工具有:
- 向量检索工具:处理语义查询
- 关键词检索工具:处理精确匹配
- SQL 查询工具:处理结构化数据
- API 调用工具:查实时数据
- 计算工具:做数学运算
Agent 根据问题类型选择工具。比如问“上个月销售额是多少”,Agent 应该选 SQL 工具而不是向量检索。问“产品手册里怎么配置”,应该选向量检索。这种“工具路由”能力是 Agentic RAG 相比 Naive RAG 的一大优势。
4.4 循环控制与成本控制
Agentic RAG 最大的风险是无限循环。Agent 可能反复检索,每次都觉得信息不够,停不下来。必须设置最大迭代次数,一般 3 到 5 次就够了。超过就强制生成,哪怕信息不全。
另一个风险是成本失控。每次检索加一次 LLM 调用,多轮下来 token 消耗是 Naive RAG 的好几倍。我的做法是:简单问题走快速路径(一次检索直接生成),复杂问题才走 Agent 路径。判断复杂度可以用一个轻量分类器,或者让 Agent 自己判断。
实操心得:Agentic RAG 不是越复杂越好。我见过有人把所有问题都走 Agent 多轮检索,结果延迟从 2 秒涨到 15 秒,用户体验反而下降。先用简单流程覆盖 80% 的问题,剩下 20% 再上 Agent。
5. 分水岭四:GraphRAG 与知识图谱的引入
5.1 向量检索搞不定的“全局性问题”
有一类问题,向量检索天生搞不定,叫全局性问题。比如“这份报告的主要主题有哪些”“整个知识库涉及哪些产品线”“A 和 B 之间有什么关联”。这类问题需要综合整个语料的信息,而不是检索几个相关块。
向量检索的机制决定了它只能找到“和 query 最像的几个块”,无法做全局汇总。你问“主要主题”,它给你几个碰巧包含“主题”二字的块,答非所问。
5.2 GraphRAG 的思路:先建图,再检索
GraphRAG的思路是:先用 LLM 从文档里抽取实体和关系,构建知识图谱,再对图谱做社区检测和摘要,检索时既可以用图结构做多跳推理,也可以用社区摘要回答全局性问题。
具体流程分几步。第一步是实体和关系抽取:让 LLM 读每个块,抽出实体(人、组织、产品、概念)和它们之间的关系。第二步是图谱构建:把实体作为节点,关系作为边,构建图。第三步是社区检测:用 Leiden 这类算法把图划分成社区,每个社区是一组紧密相关的实体。第四步是社区摘要:让 LLM 给每个社区生成摘要。检索时,局部问题走实体检索,全局问题走社区摘要。
这样做的好处是,全局性问题可以通过读取社区摘要来回答,多跳问题可以沿着图的边做推理。代价是构建成本高,一个中等规模的知识库,建图可能要跑几个小时甚至更久,token 消耗也大。
5.3 GraphRAG 和向量 RAG 怎么选
不是所有场景都需要 GraphRAG。我的判断标准是:
| 场景特征 | 推荐方案 |
|---|---|
| 事实型问答为主 | 向量 RAG + 混合检索 |
| 需要多跳推理 | GraphRAG 或 Agentic RAG |
| 需要全局汇总 | GraphRAG |
| 知识库频繁更新 | 向量 RAG(图维护成本高) |
| 实体关系密集 | GraphRAG |
| 预算和延迟敏感 | 向量 RAG |
GraphRAG 不是银弹,它的构建和维护成本很高。如果知识库更新频繁,图很难保持最新。我的建议是:先用向量 RAG 加混合检索加 Agentic 把效果做到位,确实遇到全局性和多跳问题解决不了,再考虑上 GraphRAG。
5.4 Ontology RAG:给图谱加一层本体
Ontology RAG是在 GraphRAG 基础上更进一步,引入本体(ontology)来规范实体和关系的类型。比如定义“产品”是一个类,“属于”是一种关系,“产品 A 属于产品线 B”就是一个符合本体的三元组。
本体的好处是让图谱结构更规范,检索时可以做类型约束的查询。比如“查询所有属于产品线 B 的产品”,有本体的话可以直接按类型查,没有本体就只能靠字符串匹配。代价是需要预先设计本体,这需要领域知识,不是自动能搞定的。
6. 分水岭五:评估体系,没有度量就没有优化
6.1 为什么 RAG 评估这么难
RAG 的效果评估比传统 NLP 任务难得多。传统任务有标准答案,对错分明。RAG 的答案往往是开放式的,而且涉及检索和生成两个环节,出错了你都不知道是检索没召回到,还是召回到了但生成没用好。
我见过太多团队凭感觉调 RAG,今天觉得这个 chunk size 好,明天觉得那个 rerank 模型强,调来调去没有方向。根本原因是没有建立评估体系。
6.2 检索和生成分开评估
评估要拆成两块:检索质量和生成质量。
检索质量的核心指标是 Recall@k 和 MRR(Mean Reciprocal Rank)。Recall@k 衡量前 k 个结果里有没有包含正确答案所在的块,MRR 衡量正确答案排在第几位。这两个指标需要标注数据:每个测试问题,人工标出哪些块是相关的。
生成质量的指标包括忠实度(Faithfulness)和答案相关性(Answer Relevance)。忠实度衡量答案是否完全基于检索到的内容,没有编造;答案相关性衡量答案是否切题。这两个指标可以用 LLM 来打分,也就是所谓的 LLM-as-a-Judge。
# 用 LLM 评估忠实度的 prompt 示意 faithfulness_prompt = """ 基于以下检索内容回答问题,然后评估答案是否完全基于检索内容。 检索内容: {context} 问题:{question} 答案:{answer} 请判断答案中的每个陈述是否都能在检索内容中找到依据。 输出格式:{{"faithful": true/false, "unsupported_claims": [...]}} """6.3 构建评估集的实操方法
评估集不用很大,50 到 100 个问题就能反映问题。构建方法是:从真实用户问题里采样,或者让领域专家出题。每个问题标注标准答案和相关的块。
我一般会分几类问题:简单事实型、多跳推理型、全局汇总型、边界情况(知识库里没有答案的)。每类都要有,这样才能全面评估。
评估要定期跑,每次改动(换 embedding 模型、调 chunk size、加重排序)都跑一遍,看指标变化。没有评估集,所有优化都是盲调。
6.4 常见评估陷阱
第一个陷阱是测试集和训练集重叠。如果你用知识库里的内容出题,又用同样的内容检索,指标会虚高。测试问题应该用真实用户问法,而不是文档里的原句。
第二个陷阱是只看平均分。平均分高不代表没问题,可能简单问题全对,难问题全错。要分类看指标。
第三个陷阱是LLM 评委的偏见。LLM 打分会有偏好,比如偏好长答案、偏好流畅的表达。最好用多个 LLM 交叉验证,或者人工抽检校准。
7. 分水岭六:工程化与生产落地
7.1 从 demo 到生产的鸿沟
demo 能跑通,不代表能上线。生产环境要考虑的东西多得多:并发、延迟、成本、监控、降级、数据更新。
我见过太多项目 demo 惊艳,上线就崩。用户一多,检索延迟飙升;知识库一更新,索引重建要几个小时;模型一抽风,整个服务不可用。这些都是工程问题,不是算法问题,但决定了 RAG 能不能真正落地。
7.2 索引更新策略
知识库不是静态的,文档会增删改。索引更新有几种策略:
- 全量重建:简单粗暴,但耗时长,适合小知识库或低频更新
- 增量更新:只处理变化的文档,需要追踪文档变更
- 双索引切换:维护新旧两套索引,重建完成后原子切换,避免服务中断
我的建议是:小知识库用全量重建加定时任务;大知识库用增量更新加双索引切换。切换时要注意 embedding 模型版本一致,否则新旧向量不在同一空间,检索会乱。
7.3 缓存与降级
缓存能大幅降低延迟和成本。常见的缓存层有:
- 查询缓存:相同问题直接返回缓存答案
- 检索缓存:相同查询直接返回缓存检索结果
- embedding 缓存:相同文本不重复编码
降级策略是保命用的。检索服务挂了,降级到关键词检索;LLM 服务超时,降级到返回检索原文让用户自己看。有降级总比整个服务挂掉强。
7.4 监控指标
生产环境必须监控这些指标:
| 指标 | 说明 | 告警阈值建议 |
|---|---|---|
| 检索延迟 P99 | 检索环节的尾部延迟 | > 2s |
| 生成延迟 P99 | 生成环节的尾部延迟 | > 10s |
| 检索命中率 | 有检索结果的查询占比 | < 90% |
| 空答案率 | 返回“不知道”的占比 | > 20% |
| token 消耗 | 每查询平均 token | 突增 50% |
| 用户反馈 | 点赞点踩比例 | 点踩率 > 15% |
这些指标能帮你快速定位问题。检索延迟高,可能是向量库负载高;空答案率高,可能是检索召回差;token 消耗突增,可能是 Agent 循环失控。
7.5 一个完整的生产架构参考
把前面讲的整合起来,一个生产级 RAG 架构大概是这样:
查询进来,先过查询改写,生成多个检索查询。然后走混合检索,向量和 BM25 并行召回,RRF 融合。融合结果过重排序,取 top-k。如果是复杂问题,走 Agentic 流程,多轮检索。检索结果塞进 prompt,调 LLM 生成。生成结果过忠实度检查,不通过就重试或降级。全程有缓存、有监控、有降级。
这套架构不便宜,但能扛住生产环境的考验。具体要不要全上,看你的场景和预算。核心是:每一层都要有明确的收益,不要为了架构而架构。
8. 六处分水岭的优先级与落地建议
讲了这么多,最后说说落地顺序。如果你现在手上有一个效果一般的 RAG 项目,想优化,我建议按这个优先级来:
第一优先是评估体系。没有评估,后面所有优化都是盲调。先花一周时间建评估集,把检索和生成的指标跑起来。
第二优先是混合检索加重排序。这是投入产出比最高的一步,改动不大,效果提升明显。先把召回和精度做上去。
第三优先是切块策略优化。结构感知切块加 Contextual Retrieval,能显著提升检索质量。这一步需要一些工程投入,但值得。
第四优先是查询改写。用 LLM 改写查询,成本低收益高,尤其是用户问法口语化的场景。
第五优先是Agentic RAG。处理复杂问题需要,但会增加延迟和成本,建议在简单流程跑通后再上。
第六优先是GraphRAG。构建成本最高,只在确实需要全局汇总和多跳推理时上。
这个顺序不是绝对的,得根据你的具体场景调整。但核心逻辑是:先度量,再优化召回,再优化切块,再优化查询,最后才上复杂架构。很多团队一上来就搞 GraphRAG、搞 Agent,结果基础没打好,效果反而更差。
我在实际项目里踩过最大的坑,就是过早引入复杂架构。当时觉得 Agentic RAG 很酷,把所有查询都走 Agent 多轮检索,结果延迟爆炸,用户流失。后来退回来,先用简单流程覆盖大部分场景,只对复杂问题走 Agent,体验才回来。RAG 的优化是个渐进过程,每一步都要有数据支撑,别凭感觉上架构。
最后分享一个小技巧:如果你的知识库里有大量表格和图片,纯文本 RAG 会丢信息。表格可以转成 Markdown 或结构化文本再切块,图片可以用多模态模型生成描述再入库。这块展开又是另一个话题了,但值得单独花时间处理,因为很多企业知识库的核心信息就在表格里。