1. 为什么“RAG 烂大街”是个伪命题
这两年但凡跟大模型沾点边的团队,几乎人手一套 RAG 流水线。文档切片、向量化、存库、检索、拼 prompt、丢给模型生成,六步走完,一个“知识库问答”就上线了。于是圈子里开始流行一句话:RAG 已经烂大街了。
我一开始也这么觉得。直到去年帮三个不同行业的团队做检索增强的调优,才发现一个扎心的事实:烂大街的从来不是 RAG 本身,而是那条被无数教程抄来抄去的最简流水线。真正决定一套 RAG 系统是“玩具”还是“生产力工具”的,是流水线之外那六处分水岭。这篇文章就把这六处掰开揉碎讲清楚,顺带把 Agentic RAG、Contextual Retrieval、GraphRAG、LangGraph 这些热词背后的真实用途和落地方式说透。不管你是刚跑通第一个 demo 的新手,还是正在为召回率发愁的老手,都能从里面找到能直接抄作业的东西。
先说结论,免得你看到一半觉得我在灌水。那六处分水岭分别是:切分策略、上下文补全、检索路由、图谱增强、Agent 编排、评估闭环。前三个决定你的召回质量,后三个决定你的系统能不能从“问答”进化成“干活”。下面逐个拆。
2. 分水岭一:切分策略决定召回天花板
2.1 固定长度切分为何是灾难的开始
几乎所有入门教程的第一步都是RecursiveCharacterTextSplitter,chunk_size 设个 500 或 1000,overlap 设个 50 或 100。这套配置在 demo 文档上跑得挺欢,一上真实业务就露馅。
问题出在语义完整性上。固定长度切分是纯字符维度的操作,它根本不知道自己在切什么。一个完整的操作步骤可能被拦腰截断,前半段在 chunk A,后半段在 chunk B。检索时只召回了 A,模型拿到半截指令,生成出来的答案要么缺步骤,要么直接编。
我踩过最典型的一个坑:一份设备维修手册,某个故障排查流程跨了三个自然段。固定切分后,关键的那句“若指示灯闪烁三次,则更换主板”被切到了下一个 chunk 的开头。用户问“指示灯闪三次怎么办”,检索命中的是上一个 chunk,里面只有故障现象描述,没有处置方案。模型很“聪明”地根据现象编了一个处置建议,差点造成误操作。
2.2 结构化切分的三种实战打法
真正靠谱的切分,得先理解文档的结构。我一般按文档类型分三套打法:
第一套,标题层级切分。适用于 Markdown、Word、PDF 里有明确标题层级的文档。用MarkdownHeaderTextSplitter或者自己写解析逻辑,按 H1/H2/H3 切,保证每个 chunk 自带完整的标题路径。这样检索出来的片段天然带上下文,模型一看就知道这段内容属于哪个章节。
第二套,语义切分。适用于没有明显结构的长文本,比如会议纪要、访谈记录。核心思路是计算相邻句子的 embedding 相似度,在相似度骤降的地方切一刀。LangChain 里有SemanticChunker可以直接用,但要注意阈值调参,太敏感会切得太碎,太迟钝又等于没切。
第三套,父子文档切分。这是我目前最推荐的方案。思路是:检索时用小的子 chunk 保证精准命中,生成时把子 chunk 所属的父 chunk 一起喂给模型保证上下文完整。LangChain 的ParentDocumentRetriever就是干这个的。子 chunk 可以切到 200 字左右,父 chunk 保留 2000 字,检索精度和生成质量同时兼顾。
提示:切分参数没有万能值。我一般会先拿 20 条真实用户问题做一轮召回测试,看命中率,再反过来调 chunk_size 和 overlap。凭感觉设参数,十有八九要返工。
2.3 表格和图片到底怎么处理
热词里有人问“rag 知识库能存储图片嘛”,这个问题问到了点子上。纯文本 RAG 确实存不了图片,但真实业务文档里表格和图片占比极高。
表格的处理思路是转成结构化文本再入库。比如一张设备参数表,不要直接 OCR 成一行行文字,而是转成“设备型号 X 的额定功率是 Y 千瓦,额定转速是 Z 转每分钟”这样的自然语言描述。这样检索时才能被语义匹配到。我一般用pandas读表格,然后按行拼成描述句。
图片的处理分两种。如果图片里有文字,走 OCR 提取后按文本处理。如果图片是示意图、流程图,那就得用多模态模型生成图片描述,把描述文本入库,同时保留图片路径。检索命中描述后,把图片一起返回给前端展示。这样用户既能看到文字答案,也能看到原图。
3. 分水岭二:上下文补全让检索不再“断章取义”
3.1 Contextual Retrieval 到底在补什么
Anthropic 提出的 Contextual Retrieval 是这两年 RAG 领域最实用的改进之一。它的核心洞察很简单:一个 chunk 脱离原文后,很多指代和背景信息就丢失了。
举个例子。原文是“该公司 2023 年营收增长 15%,主要得益于新产品的市场表现”。切分后,chunk 里只剩“营收增长 15%”。检索时用户问“哪家公司营收增长 15%”,这个 chunk 根本匹配不上,因为它压根没提公司名。
Contextual Retrieval 的做法是:在入库前,用大模型给每个 chunk 生成一段简短的上下文说明,然后把这段说明拼在 chunk 前面一起做 embedding。比如上面那个 chunk,补全后变成“本段来自 XX 公司 2023 年财报,该公司营收增长 15%”。这样检索时就能命中了。
3.2 补全上下文的实操流程
具体操作分四步:
- 把整篇文档和当前 chunk 一起喂给模型,让模型生成一段 50 到 100 字的上下文说明。
- 把说明拼接到 chunk 开头,形成“增强 chunk”。
- 对增强 chunk 做 embedding 入库。
- 检索时命中的是增强 chunk,但返回给生成模型时可以只返回原始 chunk 加说明,避免冗余。
这里有个成本问题:每个 chunk 都要调一次模型,文档多了 token 消耗不小。我的优化做法是批量处理加缓存。同一篇文档的所有 chunk 一次性喂给模型,让它批量输出上下文说明,比逐个调用省一半以上的 token。另外,对于结构清晰的文档,其实可以用规则生成上下文,比如直接把标题路径拼上去,不一定非要调模型。
3.3 元数据过滤是隐形的召回加速器
上下文补全之外,元数据是另一个被严重低估的环节。很多团队入库时只存文本和向量,把文档来源、创建时间、部门、版本这些信息全扔了。结果检索时没法做过滤,只能全库扫,既慢又不准。
我的习惯是入库时强制带上五类元数据:来源文件、章节路径、创建时间、文档类型、权限标签。检索时先按元数据做一轮粗筛,再在候选集里做向量匹配。这样召回率和响应速度都能明显提升。尤其是权限标签,多租户场景下没有它,检索结果可能把 A 部门的机密文档推给 B 部门的用户,这是要出大事的。
4. 分水岭三:检索路由决定系统聪不聪明
4.1 为什么单一向量检索不够用
向量检索擅长语义匹配,但有几个硬伤。第一,它对精确匹配不敏感。用户问“错误码 E5021 怎么解决”,向量检索可能召回一堆讲错误处理的通用文档,就是找不到那个具体错误码。第二,它对数值比较无能为力。用户问“功率大于 100 千瓦的设备有哪些”,向量检索理解不了“大于”这个操作。第三,它对多跳推理束手无策。用户问“A 产品的供应商的母公司是哪家”,这需要两步检索,单次向量匹配做不到。
4.2 混合检索加路由的落地方式
我的标准配置是三路召回加一个路由:
- 向量检索:处理语义相似的问题。
- 关键词检索:用 BM25 或 Elasticsearch,处理精确匹配和专有名词。
- 结构化查询:对数值、日期、枚举值,直接转成 SQL 或过滤条件查元数据。
路由层用一个小模型或者规则引擎来判断用户问题该走哪一路。简单问题走单路,复杂问题走多路然后融合排序。融合排序我一般用 RRF(Reciprocal Rank Fusion),它不需要调权重,对多路结果的合并效果很稳。
LangGraph 在这里特别好用。你可以把每一路召回定义成一个节点,路由判断定义成条件边,整个检索流程就是一张有向图。这样逻辑清晰,调试时也能看到每个节点的输入输出,比一坨 if-else 好维护得多。
4.3 查询改写让召回率再上一个台阶
用户的问题往往口语化、有歧义、缺上下文。直接拿原始问题去检索,召回率很难看。查询改写就是解决这个问题的。
我常用的改写策略有三种。第一种,指代消解。多轮对话里用户说“它多少钱”,得结合上文把“它”替换成具体产品名。第二种,查询扩展。用户问“怎么重启”,扩展成“重启方法、重启步骤、重启操作”。第三种,子问题拆解。复杂问题拆成多个简单问题分别检索,再合并结果。
这些改写动作都可以用 LangGraph 编排成流水线。改写节点、检索节点、融合节点、生成节点,各司其职,出问题也好定位。
5. 分水岭四:GraphRAG 补上关系推理的短板
5.1 GraphRAG 解决的是什么问题
传统 RAG 把文档切成孤立的 chunk,chunk 之间的关系全丢了。但很多业务问题的答案,恰恰藏在实体之间的关系里。比如“哪些供应商同时给 A 产品和 B 产品供货”,这需要跨文档、跨 chunk 做关系推理,纯向量检索根本做不到。
GraphRAG 的思路是:先从文档里抽取实体和关系,构建知识图谱,检索时同时在图谱上做遍历。这样就能回答“A 的供应商还有谁”“B 和 C 有什么关联”这类关系型问题。
5.2 图谱构建的实操要点
GraphRAG 落地最大的坑在实体抽取。用大模型抽实体,召回率还行,但准确率和一致性堪忧。同一个实体在不同文档里可能被抽成不同的名字,比如“华为”“华为技术”“Huawei”被当成三个实体。
我的做法是先定义本体(ontology),再让模型按本体抽取。本体就是实体类型和关系类型的清单,比如“公司”“产品”“供应商”是实体类型,“供应”“竞争”“合作”是关系类型。模型抽取时只能从本体里选,不能自由发挥。这样一致性大幅提升。
抽取完之后还要做实体对齐,把指向同一实体的不同名称合并。简单场景用字符串相似度加规则,复杂场景用 embedding 相似度加人工审核。这一步很费功夫,但省不得,图谱质量直接决定检索质量。
5.3 GraphRAG 和向量检索怎么配合
GraphRAG 不是要取代向量检索,而是互补。我的标准架构是双通道:向量通道负责语义匹配,图谱通道负责关系推理,最后把两路结果融合。
具体来说,用户问题先过路由。如果问题是“XX 是什么”“XX 怎么用”这类事实型问题,走向量通道。如果是“XX 和 YY 什么关系”“XX 还关联了哪些 ZZ”这类关系型问题,走图谱通道。如果是混合型问题,两路都走,结果合并。
这套架构在 LangGraph 里实现起来很自然。向量检索和图谱检索各是一个节点,路由是一个条件边,融合是一个汇聚节点。整个流程可视化,调优时一目了然。
6. 分水岭五:Agentic RAG 让系统从“问答”变“干活”
6.1 Agentic RAG 和普通 RAG 的本质区别
普通 RAG 是单轮的:用户问,系统检索,生成答案,结束。Agentic RAG 是多轮的:系统会自己判断要不要检索、检索几次、检索什么、结果够不够、要不要换个角度再检。它把 RAG 从“一次检索”变成了“一个检索决策过程”。
这个区别在简单问题上体现不明显,但在复杂任务上就是天壤之别。比如用户说“帮我对比 A 产品和 B 产品的技术参数,然后推荐一个适合户外场景的”。普通 RAG 可能只检索一次,拿到什么算什么。Agentic RAG 会先检索 A 的参数,再检索 B 的参数,再检索户外场景的选型建议,最后综合生成对比和推荐。
6.2 用 LangGraph 编排 Agentic RAG
LangGraph 是目前编排 Agentic RAG 最顺手的框架。它的核心概念是状态图:每个节点是一个处理步骤,边是步骤之间的流转条件,整个图共享一个状态对象。
一个典型的 Agentic RAG 图长这样:
- 入口节点:接收用户问题,初始化状态。
- 规划节点:判断问题类型,决定检索策略。
- 检索节点:执行检索,把结果写入状态。
- 评估节点:判断检索结果是否充分。不充分就回到规划节点换策略,充分就进入生成节点。
- 生成节点:基于检索结果生成答案。
- 工具调用节点:如果问题需要计算、查询数据库、调 API,在这里执行。
这个图的关键在于评估节点和回环。评估节点决定了系统会不会“再试一次”,这是 Agentic 的核心。没有回环,就还是单轮 RAG。
6.3 工具调用让 RAG 真正“下地干活”
热词里有个说法叫“让 AI 真的下地干活”,这说的就是工具调用。纯 RAG 只能回答文档里有的东西,工具调用能让它执行文档外的操作。
比如用户问“帮我查一下上个月 A 产品的销量,然后和 B 产品对比”。RAG 只能检索到产品介绍文档,查不到实时销量。但如果系统挂了数据库查询工具,它就能先调工具查销量,再检索产品文档做对比分析,最后生成完整答案。
LangGraph 的工具调用节点支持绑定任意 Python 函数。我的经验是工具描述要写得极其清楚,包括功能、参数、返回值、适用场景。模型靠描述来判断该不该调、怎么调。描述写得含糊,模型就会乱调或者不调。
注意:工具调用一定要加权限校验和参数校验。模型可能生成非法参数,也可能调用它不该调的工具。生产环境里,每个工具入口都要做一层防护,不能裸奔。
7. 分水岭六:评估闭环决定系统能不能持续进化
7.1 没有评估的 RAG 就是盲人摸象
我见过太多团队,RAG 上线后全靠用户反馈来发现问题。用户不反馈,就以为系统没问题。实际上召回率可能只有 60%,只是用户懒得说而已。
RAG 的评估要分检索质量和生成质量两层。检索质量看召回率、准确率、MRR(平均倒数排名)。生成质量看忠实度(答案是否基于检索内容)、相关性、完整性。两层都要评,只评一层会漏掉问题。
7.2 评估数据集的构建方法
评估的第一步是有一批带标注的问题-答案对。没有标注数据,一切评估都是空谈。
我的做法是:先从真实用户日志里抽 100 到 200 条问题,然后人工标注每条问题的标准答案和应该命中的文档片段。这个工作量不小,但一次投入长期受益。标注完之后,每次系统改动都跑一遍评估,看指标是涨是跌。
如果实在没有人工标注资源,可以用大模型做自动评估。让模型判断生成的答案是否忠实于检索内容、是否回答了问题。虽然不如人工准,但作为快速迭代的参考足够了。
7.3 用 LangSmith 做全链路追踪
评估之外,链路追踪是排查问题的利器。LangSmith 可以记录每一次调用的输入输出、耗时、token 消耗,出问题时能快速定位是检索环节还是生成环节的锅。
我一般会在关键节点打上标签:检索节点记录召回了哪些 chunk,评估节点记录判断结果,生成节点记录最终答案。这样一条链路看下来,问题出在哪一目了然。没有追踪,调优就是碰运气。
8. 常见问题与排查技巧实录
8.1 召回不准的排查顺序
召回不准是最常见的问题。我的排查顺序是:先看切分,再看 embedding,再看检索策略,最后看查询改写。
切分问题占召回问题的六成以上。chunk 太大,噪声多;chunk 太小,语义不全。先拿几条 bad case 看看命中的 chunk 长什么样,基本就能判断是不是切分的问题。
embedding 问题占两成。中文场景下,通用 embedding 模型对专业术语的区分度可能不够。换个领域微调过的模型,或者用多向量检索,往往能改善。
检索策略和查询改写各占一成。单一向量检索换成混合检索,或者加上查询改写,通常能再提几个点。
8.2 生成答案胡编的三种解法
模型胡编,本质是检索内容不足以支撑答案,但模型硬要答。解法有三种:
第一种,prompt 里明确要求“不知道就说不知道”。这能挡住一部分,但模型有时候会“假装知道”。
第二种,加忠实度校验。生成完答案后,用另一个模型判断答案是否基于检索内容。不忠实就重新生成或者返回“未找到相关信息”。
第三种,提高检索门槛。检索结果的相关性分数低于阈值时,直接不生成,返回“没有找到相关内容”。宁可说不知道,也不要胡编。
8.3 响应太慢的优化方向
RAG 响应慢,通常是三个原因:检索慢、模型慢、链路长。
检索慢一般是向量库索引没建好,或者候选集太大。加元数据过滤缩小候选集,或者换更快的向量库,能明显改善。
模型慢是 token 太多。检索回来的 chunk 不要全塞给模型,按相关性排序取 top 3 到 top 5 就够了。上下文补全的说明也可以精简。
链路长是 Agentic RAG 的通病。多轮检索加评估确实慢,但可以通过并行检索和缓存来优化。多路召回并行执行,常见问题的答案缓存起来,都能省时间。
| 问题现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 召回不准 | 切分不合理 | 查看命中 chunk 内容 | 调整切分策略 |
| 召回不准 | embedding 不适配 | 测试相似度分数 | 换模型或微调 |
| 答案胡编 | 检索内容不足 | 检查召回结果 | 加忠实度校验 |
| 答案胡编 | prompt 约束弱 | 检查 prompt | 强化“不知道”指令 |
| 响应慢 | 候选集太大 | 看检索耗时 | 加元数据过滤 |
| 响应慢 | 链路太长 | 看各节点耗时 | 并行化加缓存 |
8.4 几个容易忽略的细节
第一个,chunk 的去重。多路召回时,同一个 chunk 可能被多路命中。不去重的话,生成时同一段内容出现多次,浪费 token 还干扰模型。
第二个,检索结果的重排序。初步召回的结果排序往往不够准。加一个 cross-encoder 重排序模型,能把最相关的排到前面,生成质量提升明显。
第三个,对话历史的处理。多轮对话里,历史太长会挤占检索内容的 token 空间。我的做法是只保留最近三轮对话,更早的做摘要压缩。
第四个,冷启动问题。新文档入库后,embedding 可能和已有文档不在一个分布上。定期做全量重 embedding,或者用增量学习的方式更新,能缓解这个问题。
9. 我在实际项目中的几点体会
这套六分水岭的框架,我在三个项目里反复验证过。最深的体会是:RAG 的调优是个系统工程,没有银弹。切分改好了,召回可能还是不行,因为 embedding 不适配。embedding 换了,生成可能还是胡编,因为 prompt 没约束好。每一处都要调,每一处都要评估。
另一个体会是不要过度设计。不是每个场景都需要 GraphRAG,也不是每个场景都需要 Agentic。简单问答场景,把切分和上下文补全做好,效果就已经很能打了。图谱和 Agent 是给复杂场景准备的,用错了地方就是徒增复杂度。
最后分享一个实用技巧:建一个 bad case 库。每次发现召回或生成的问题,就把 case 记下来,标注问题类型和修复方式。这个库积累到几十条之后,你会发现大部分问题都是重复的,修复起来有章可循。这比每次从头排查高效得多。