1. 为什么自研RAG前值得先啃六款开源产品,以及我的逆向思路
过去大半年里,我一直在折腾自研RAG的事情。市面上关于RAG的教程、框架、demo多到看不过来,但真正用到生产环境时,问题一个接一个:文档切分不合理导致检索不到关键段落、用户问法稍微口语化召回率就崩、明明召回了正确内容生成时却引用错地方。回头看,最让我少走弯路的不是哪一篇论文,而是认认真真逆向分析了六款开源RAG产品。所谓逆向,并不是盯着源码一行行复刻,而是通过阅读文档、追踪源码结构、跑通demo、对比默认参数和失败场景,反推每个产品背后的设计决策:它为了解决什么问题做了哪些约束,哪些模块是可以拆出来复用的,哪些地方其实做得并不好。
为什么非要看开源产品?因为RAG本质上不是一个算法问题,而是一个系统工程问题。单独看论文能知道RAG的流程是"检索—增强—生成",但论文不会告诉你:一个扫描版PDF进来之后怎么变成可检索的片段,用户多轮对话里的指代怎么处理,检索结果以什么格式交给大模型才能避免它胡编。这些恰恰是开源产品迭代了无数版本、被真实用户用脚投票出来的经验。我选的六款产品覆盖了不同思路:LangChain和LlamaIndex偏向通用框架,Dify和FastGPT偏向完整应用平台,RAGFlow偏向深度文档理解,Haystack偏向生产级pipeline。它们不是同一类东西,正因如此,逆向之后拼出来的蓝图才够完整。
先说我的逆向方法。我给自己定了三个步骤:第一,读官方文档里的设计理念和架构图,搞清楚每个产品宣称解决什么问题;第二,动手跑demo,重点观察默认参数、配置项和失败场景,比如故意上传一份版式复杂的合同,看看它在切分和召回上的表现;第三,去GitHub翻源码目录结构和关键类的命名,比如看它怎么抽象Loader、Retriever、Reranker这些组件,组件之间的接口长什么样。这三步走完,基本就能画出一张"产品架构意图图"——它和实际代码可能不完全一致,但设计者的取舍会非常清晰地暴露出来。
下面这张表是我当时选的六款产品和它们最值得借鉴的部分:
| 产品 | 核心定位 | 最有借鉴意义的设计 | 适合谁参考 |
|---|---|---|---|
| LangChain | LLM应用开发框架 | Loader/Splitter/Retriever抽象,Chain编排 | 想用统一框架快速搭建原型的人 |
| LlamaIndex | 数据索引与查询框架 | 节点化索引、查询引擎、父文档/子文档检索 | 想精细控制索引和检索逻辑的人 |
| Dify | LLM应用开发平台 | 知识库ETL、分段模式、混合检索、Rerank配置 | 想做产品化知识库问答的人 |
| RAGFlow | 深度文档理解RAG | 版面分析、模板化切分、逐字引用 | 被PDF、扫描件、复杂表格折磨的人 |
| FastGPT | 知识库问答与工作流 | 可视化工作流编排、多节点灵活组装 | 想自定义问答流程和分支逻辑的人 |
| Haystack | 生产级RAG pipeline框架 | Pipeline组件化、YAML定义、评估体系 | 重视可测试性和可维护性的团队 |
2. 第一层逆向结论:文档解析与切分,决定了RAG的检索上限
在检索之前,几乎所有RAG系统都要回答一个问题:一份原始文档如何变成一个个可以检索的片段。这一步看起来简单,却是整个RAG链条里最容易埋雷的地方。很多人把RAG等同于"把文档丢进去,自动切一切,再向量化",但切分这个动作本身就有完全不同的层次,六款产品在这个问题上的差异非常明显。
2.1 LangChain和LlamaIndex的"通用切分器"思路
LangChain对文档的处理路径是Loader加载 + TextSplitter切分。Loader负责把PDF、Word、网页等不同格式变成统一的Document对象,TextSplitter负责把Document切成块。最常用的是RecursiveCharacterTextSplitter,默认chunk_size约1000字符、chunk_overlap约200字符,它按换行符、句号、逗号这些分隔符做递归切分。这套设计的优点是通用,任何文本都能切;缺点也很直接:它对文档的"版面结构"完全无感。一份合同里的条款标题、表格单元格、页眉页脚,在它眼里都是平等的字符流,切着切着就把语义完整的段落拦腰斩断。
LlamaIndex比LangChain更进一步的地方在于引入了"节点"(Node)的概念。它在文本片段之外保留了丰富的元数据,比如来源文件名、页号、章节标题、自定义属性等。它还支持父文档/子文档结构:先把文档切成较细的块用于精确匹配,检索到之后自动返回父文档级别的更大上下文。这个设计给我启发很大:切分不一定是"一次性定死"的,可以多粒度共存,用层级关系来兼顾"找得准"和"读得全"。
但这两款产品本质上都是"通用文本切分"的思路,默认值也只是一个安全起点。我实际测试的时候发现,对排版规整、Markdown风格的网页或者技术文档,固定大小切分加适当重叠是够用的;一旦换成扫描版PDF、带合并单元格的Excel、两栏排版的论文,这种思路就失灵了。这也是为什么RAGFlow出现之后迅速获得关注。
2.2 RAGFlow的"版面感知解析"思路
RAGFlow最核心的差异化设计是DeepDoc,一套包含版面分析、OCR、表格识别的文档解析引擎。对接下来的自研蓝图影响深远的一点是:它不把PDF当作纯文本抽取,而是先做版面分析,识别出标题层级、段落边界、表格区域、页眉页脚,然后再决定怎么切。比如一份带"3.2 违约责任"标题的合同,它会尽量把"3.2 违约责任"下面的条款作为一个整体切块,而不是按字符数硬切。它甚至支持模板化切分,可以针对固定格式的文档定义切分规则。
我自己测过一份真实的采购合同:用LangChain的RecursiveCharacterTextSplitter切分时,"违约责任"条款被切成两半,前半段进了一个chunk,后半段带着表格去了另一个chunk;后续检索"逾期交货的违约金比例"时,只召回了前半段,大模型生成的答案缺了后半段的违约金数字,非常尴尬。同样的文档走RAGFlow的版面感知切分,按条款边界切出来的chunk包含完整的表格和后续说明,一次检索就命中了所有关键信息。这个对比让我确认了一个判断:文档解析和切分的质量,直接决定了后面所有环节的上限,检索模型再强也弥补不了源头的信息断裂。
2.3 Dify和FastGPT在知识库工程上的工程化处理
Dify的知识库模块对切分这件事做了更贴近产品的封装。它在分段设置里提供了自定义分段长度、分隔符、以及"父子分块"模式。所谓父子分块,可以理解为用小块做检索、用大块做上下文:子块负责精确命中用户问题里的关键词,命中后把父块的整体内容送进大模型,这样既保证召回精度,又避免上下文过于碎片化。这是我在自研项目里第一个抄下来的设计。
FastGPT在导入知识库时也把文件解析、预处理、向量化拆成了独立节点,允许在可视化工作流里随时调整。比如你可以先对文档做一轮"结构清洗",去掉页眉页脚和无关广告,再按标题层级切分,最后统一向量化。它和Dify让我意识到一个共性:成熟的开源RAG产品都不会只给一个"切分长度"参数,而是会把清洗规则、分段策略、索引模式暴露成可配置项,因为真实文档的多样性决定了没有任何一个默认参数能通吃。
这里顺带说一个常见问题:RAG知识库能不能存图片?能,但路径不止一条。一是把图片OCR成文本后参与检索,这是最实用的方式;二是把图片作为独立chunk存入向量库,用图文多模态embedding模型做向量化;三是保留图片路径,让大模型生成答案时以Markdown图片形式引用。自研时如果涉及产品手册、广告素材这类带图文档,至少要把第一种做扎实,否则图里的信息对RAG来说等于不存在。
3. 第二层逆向结论:混合召回与重排,是RAG瓶颈的破局点
RAG翻车最集中的环节就是召回。很多人以为"向量检索"万能,实际跑起来完全是另一回事:精确的合同编号、产品型号、人名地名,向量检索经常给不出稳定结果;用户问题很短很口语的时候,embedding模型又容易把语义扯偏。我在六款产品里普遍观察到的解法是四个字:混合召回,外加一个重排环节。这几乎是所有能扛住真实问题的RAG系统的标配。
3.1 只靠向量检索为什么一定会碰壁
向量检索擅长的是语义匹配:你问"手机续航差怎么办",它能召回"电池不耐用"相关的段落。但RAG场景里有大量问题是字面匹配型的,比如"查询订单号PO-2024-0813的状态""《劳动法》第四十一条说了什么"。这类问题里的关键信息是编号、条款号、专有名词,它们的语义向量区分度很低,embedding模型未必能精确对应。而且embedding模型有一个很现实的问题:训练语料覆盖不到的冷门术语,向量表达本来就不靠谱,召回结果自然忽上忽下。
更麻烦的是,很多知识库问题的答案分散在多段文档里,用户问题又往往很简短,单靠一次向量检索很难覆盖全。单独跑一个稠密向量检索,命中率可能只有六成;单独跑一个BM25关键词检索,覆盖率也不高;但两者做混合,很多时候能把命中率拉到九成以上。这也是六款产品几乎都把"混合检索"当作默认进阶选项的底层原因。
3.2 六款产品各自的召回策略
Dify在知识库的检索设置里明确提供了三种模式:向量检索、全文检索、混合检索。混合检索默认同时执行向量召回和全文召回,再用加权或RRF(Reciprocal Rank Fusion)机制融合两路结果。全文检索走的是类BM25的稀疏检索思路,对专有名词、编号这类字面匹配场景特别有效。
LlamaIndex的思路更灵活。它提供了QueryFusionRetriever这样的组合检索器,可以把多个Retriever的召回结果合并并重新排序。你甚至可以把BM25检索、向量检索、知识图谱检索各跑一遍,然后用权重融合。它让我看到"多路召回"在框架层面可以做得非常干净,每一路检索器都实现同一个接口,融合逻辑只做一件事:收集各路结果,按融合策略计算最终分数。
Haystack在架构上就为混合检索预留了位置。它的DocumentStore统一管理向量索引和全文索引,EmbeddingRetriever和BM25Retriever是两个平级的组件,你可以在Pipeline里把它们串起来。FastGPT的知识库搜索节点同样支持配置语义检索、全文检索或两者的混合模式,而且每个知识库可以独立设置搜索策略,互不影响。
这些产品解决同一个痛点的方式各不相同,但设计意图是一致的:召回阶段先广撒网,别急着把候选数量压到很小;重排阶段再精挑细选。如果一上来就只用top5的纯向量结果,大概率会把正确答案挡在门外。
3.3 重排:说服我加进自研清单的第一个组件
召回之后为什么必须接重排?因为混召回来的top50里,往往只有几个真正有用,直接截断前5个会漏掉正确答案,全塞进上下文又会稀释注意力、浪费token。重排模型的作用就是把这批候选按相关性重新打一次分,让真正相关的段落浮到最前面。
Dify里Rerank是一个独立配置项,可以接入开源的bge-reranker或商业rerank模型;RAGFlow同样把重排作为检索链路的一环;FastGPT则在知识库搜索节点后面挂了重排节点。它们的共同逻辑是:把"快速过滤"和"精细排序"分到两个不同的能力层次。embedding模型负责快速找到"大概相关"的候选,重排模型负责从候选里找出"确实相关"的段落。
选择重排模型时要注意一个坑:重排模型的输入长度和得分尺度跟embedding模型不是一回事。我最初直接拿一个文本分类模型当重排器用,结果得分完全不可比,差点走偏。后来老老实实用了配置简单、社区验证多的bge-reranker系列,效果稳定很多。重排之后到底保留多少上下文,我建议根据重排得分分布来定,而不是写死top5:得分断层明显的地方就是召回质量开始下降的位置。
3.4 查询改写与知识图谱的补充地位
除了多路召回和重排,还有一个让RAG从"能用"变"好用"的细节:查询改写。第二轮对话里用户说"那它贵不贵",这里的"它"指代上一轮提到的某款设备,直接用原始问题去检索基本是徒劳。Dify的对话流程里通常会先让大模型结合历史记录改写当前问题,再用改写后的query去检索。FastGPT的工作流里也可以加一个"问题优化"节点,专门负责对用户输入做意图理解和改写。
还有一种情况是"多跳问题"。比如用户问"XX公司财报里提到的主要风险有哪些",文档里可能一个章节讲收入,另一个章节讲风险,向量检索单次召回很难把跨章节的信息凑齐。更极端的场景是跨文档实体关系的追踪,比如"A公司投资了哪些B公司旗下的项目",这已经接近知识图谱问答的范畴了。社区里讨论的GraphRAG、ontology RAG,本质上就是给RAG加一层实体关系图谱。我自己的判断是:向量库、关系型知识库、知识图谱不是二选一的关系,而是互补关系。向量库擅长语义模糊匹配,知识图谱擅长实体关系跳转,结构化数据库擅长精确统计查询;在哪一层用什么,取决于你的知识库内容到底以什么形态为主。
4. 第三层逆向结论:生成与编排,让RAG从demo走向可用系统
检索做得再好,最终用户面对的还是大模型生成的答案。这一层最容易翻车的不是大模型的生成能力,而是三点:答案有没有引用依据、流程能不能灵活编排、多轮对话有没有把历史语境用好。六款产品在这三方面各有高招。
4.1 引用溯源:RAG可信度的底线
如果你做大模型问答,最怕的就是"看似合理但其实是编的"。RAGFlow在这方面的设计值得单独拿出来讲。它在生成答案时不是让大模型自由发挥"根据参考资料回答",而是强制要求答案里的关键论断可以逐字回溯到原文片段,界面上直接展示引用的原文来源。这种"逐字引用"理念,本质上是在回答用户"你凭什么这么说"。
Dify的引用机制也做得很完整,答案后面可以附带引用的知识库段落和文档来源。LangChain层面虽然也能通过source_documents拿到来源,但默认体验没有把"引用"当产品能力来做,更多是开发者自行组装。FastGPT同样支持在回答中展示来源和引用。
我在自研里学到的落地姿势是:切分chunk时保留doc_id、page_num、section_id等元数据;生成prompt时明确要求大模型在关键句后面标注片段编号;后端拿到编号后映射回原文展示。这样即使大模型生成出错,用户也能快速点开引用定位问题,而不是对着一个莫名其妙的答案干瞪眼。
4.2 编排层:从Chain到可视化工作流的进化
LangChain早期通过Chain把检索和生成串起来,后来演进出LCEL语法,核心思路是"声明式地定义流程"。但用久了你会发现,Chain一旦复杂起来,调试和分支处理都很痛苦,因为整个流程是代码里写死的。Haystack选择了另一条路:一切皆组件,Pipeline可以用YAML描述,组件可以单独替换,测试时也能单独跑一个组件看输出。这种设计非常对我胃口:检索、重排、生成、评估各是各的模块,改一个不影响其他。
Dify和FastGPT更进一步,把编排做成了可视化工作流。FastGPT里你可以看到这样的节点链条:用户输入 → 历史记录整理 → 问题分类(判断是闲聊还是知识库问题)→ 知识库搜索 → 重排 → 大模型回答 → 输出引用。每个节点都可以单独配置、单独测试。Dify同样把知识库检索、重排、变量提取、条件分支做成了可视化的块。
编排层的选择往往决定了系统后期的维护成本。我的实际体会是:如果只是做原型,代码内直接串联检索和生成就够了;如果要做成一个长期维护的产品,强烈建议把检索、重排、生成拆成独立组件,再用一个轻量编排层去定义流程。这样做的好处是,哪天你觉得某个向量库不好用了,或者想换一个重排模型,只需要替换对应组件,上层的Prompt和交互逻辑完全不用动。
4.3 多轮对话与上下文管理
多轮对话是RAG落地时绕不开的坎。第一次提问用户往往说得很完整,第二次就开始各种省略和指代。处理方式主要有两派:一是在检索前改写当前问题,把用户的"它贵不贵"改写成"XX设备的售价是多少",这样检索query永远保持完整;二是在生成阶段把最近的对话历史拼进上下文,让大模型自己理解指代。成熟产品基本都是两者结合的,但要注意一个边界:对话历史不能无限堆,否则token会很快耗尽。
我踩过的坑是,对话历史里的"上一轮答案"本身可能就有错误。如果大模型上一轮已经回答错了,这一轮又拿着错误答案当上下文继续生成,错误会被放大。后来我加了一条规则:历史记录里只保留用户原始问题和经过改写后用于检索的query,不把大模型的旧答案当事实背景塞回去,明显减少了错误累积。
4.4 Agent化RAG:是不是所有场景都需要
标题对应的热词里反复出现"RAG智能体",这也是今年绕不开的话题。引入Agent之后,RAG不再是被动地"搜一次答一次",而是可以主动决定要不要搜索、搜索哪个知识库、要不要调用外部工具。FastGPT的复杂工作流事实上就是一种简化版的Agent框架。
但我的观点是:Agent化是期权,不是必选项。对大多数知识库问答场景,一个"意图路由 + 知识库检索 + 重排 + 生成"的固定流程已经能解决90%的问题。加入Agent能力会增加不确定性,比如模型选错工具、来回调用导致延迟上升。我建议先把固定流程做到稳定,再逐步在特定分支里引入Agent决策,而不是一开始就让所有请求都走一个自由发挥的Agent。
5. 逆向整合:一套可复用的自研RAG分层蓝图
逆向完六款产品之后,我给自己整理了一张自研RAG的分层蓝图。它的核心思想是"每层只干一件事,层与层之间用标准数据结构通信"。下面这张表就是这套蓝图的全貌,每个模块都标注了我参考的开源产品:
| 架构分层 | 核心职责 | 推荐实现思路 | 主要借鉴来源 |
|---|---|---|---|
| 数据接入层 | 多格式文档解析、OCR、版面分析 | 按文档类型选择解析策略,PDF/扫描件走版面分析 | RAGFlow |
| 文本处理层 | 结构化切分、元数据提取、清洗 | 标题感知切分 + 父子分块 + 元数据标注 | RAGFlow、Dify、LlamaIndex |
| 存储层 | 向量索引、全文索引、可选知识图谱 | 向量库 + BM25全文索引,按需加图谱 | Dify、Haystack、LlamaIndex |
| 检索层 | 混合召回、重排、查询改写 | 向量+BM25并行召回,RRF融合,接Rerank | Dify、Haystack、FastGPT |
| 生成层 | 提示词组装、引用溯源、流式输出、记忆管理 | 结构化Prompt + 强制引用编号 + 多轮上下文管理 | RAGFlow、FastGPT |
| 评估层 | 离线指标、在线反馈、回归测试 | 建标注集,跑召回率和答案质量指标 | Haystack |
这套蓝图落地时最关键的细节是分层之间的接口设计。我用Python的dataclass描述过一份简单的核心结构:
@dataclass class Chunk: id: str text: str metadata: dict # 来源文档ID、页号、章节标题等 embedding: list[float] | None = None score: float = 0.0 @dataclass class RetrievalResult: chunk: Chunk score: float source_doc_id: str rerank_score: float = 0.0只要全链路都用这套统一结构,各层之间就是彻底解耦的。比如存储层从向量库A换成向量库B,只需要改存储层的适配器,检索层、生成层完全无感。
往这套蓝图里填充实现的时候,我还总结出了一条最小可行路径,避免一上来就陷入复杂架构:
第一步,先做一个最简版本:文档加载 → 固定长度切分 → 向量化 → 检索topK → 拼Prompt给大模型。目标是端到端跑通,哪怕效果一般也没关系。
第二步,把全文检索加进来,和向量召回做RRF融合。你会发现精确匹配类问题的回答质量立刻上一个台阶。
第三步,把切分策略从"固定长度"换成"标题感知切分 + 父子分块",条件允许时给PDF接入OCR和版面分析。
第四步,在检索之后加重排模型,并只把重排得分最高的几个chunk送进上下文。
第五步,补上评估环节:人工标注几十条问题,记录每条问题应该命中哪些chunk,之后每次改动都用这组标注验证,确认没有回退才上线。
我自己就是按这条路径走的,每一步改动都能看到稳定收益。相反,如果一开始就照着最完整的架构图把所有模块全部搭出来,很容易陷入"什么都做了但什么都调不好"的困境。
6. 落地自研RAG时最容易踩的三个隐形坑
最后分享三个我在落地自研RAG过程中真正踩过的坑。它们不会出现在任何教程的架构图里,但每一个都实实在在影响了项目成败。
第一个坑是没有建立测试集就急着调参。我最早做RAG项目时,改一个chunk_size就上线,效果忽好忽坏,改着改着自己都分不清到底哪次改动有效。后来我花了两天时间,从知识库里整理出100条高频问题,每一条都人工标注了正确答案所在的chunk ID。这之后事情变得简单:每次改动先跑一遍测试集,看命中率指标,再决定要不要上线。评估体系的建立,比任何一次参数调优的长期收益都大。
第二个坑是文档更新导致的索引一致性问题。知识库不是一次导入就结束的,合同会修订、产品手册会更新。最初我只做了全量重建,每次重建要跑几个小时,中间用户搜到的全是旧数据。后来我把更新拆成了增量同步:根据文档哈希判断是否变化,只删除和重建变化的部分。还要注意文档版本冲突的问题,两版同名文件同时存在会让检索结果混乱,需要在元数据里显式标注生效时间或版本号。
第三个坑是上下文超限和检索结果冗余。一套方案刚上线时我习惯把top10的chunk全部塞给大模型,结果经常超长,而且检索回来的chunk里有大量重复信息,反而把关键内容冲淡了。后来我调整了策略:先重排,再从重排结果里截取得分明显高的前3到5个chunk,同时限制每个chunk的字符长度;超过长度限制的chunk做摘要压缩,而不是原样塞进去。回答的准确率不降反升,token消耗还少了一大半。
我自己的体会是,自研RAG最大的敌人不是模型不够强,而是把RAG当成一条"取文档—拼Prompt—丢给LLM"的直线流程。真正好用的系统,几乎都在架构上做了大量防御性设计:切分阶段保护语义完整性,检索阶段多路召回,生成阶段强制引用,上线之后持续用测试集盯着回归。六款开源产品的差异化设计,本质上都在围绕"可验证""可替换""可兜底"这九个字做文章。把这些思路真正落到自己的代码里,你得到的就不只是一个能跑的demo,而是一套可以持续迭代的工程架构。