ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

自研RAG前必看:逆向六款开源产品的架构启示

自研RAG前必看:逆向六款开源产品的架构启示 1. 为什么自研RAG前值得先啃六款开源产品以及我的逆向思路过去大半年里我一直在折腾自研RAG的事情。市面上关于RAG的教程、框架、demo多到看不过来但真正用到生产环境时问题一个接一个文档切分不合理导致检索不到关键段落、用户问法稍微口语化召回率就崩、明明召回了正确内容生成时却引用错地方。回头看最让我少走弯路的不是哪一篇论文而是认认真真逆向分析了六款开源RAG产品。所谓逆向并不是盯着源码一行行复刻而是通过阅读文档、追踪源码结构、跑通demo、对比默认参数和失败场景反推每个产品背后的设计决策它为了解决什么问题做了哪些约束哪些模块是可以拆出来复用的哪些地方其实做得并不好。为什么非要看开源产品因为RAG本质上不是一个算法问题而是一个系统工程问题。单独看论文能知道RAG的流程是检索—增强—生成但论文不会告诉你一个扫描版PDF进来之后怎么变成可检索的片段用户多轮对话里的指代怎么处理检索结果以什么格式交给大模型才能避免它胡编。这些恰恰是开源产品迭代了无数版本、被真实用户用脚投票出来的经验。我选的六款产品覆盖了不同思路LangChain和LlamaIndex偏向通用框架Dify和FastGPT偏向完整应用平台RAGFlow偏向深度文档理解Haystack偏向生产级pipeline。它们不是同一类东西正因如此逆向之后拼出来的蓝图才够完整。先说我的逆向方法。我给自己定了三个步骤第一读官方文档里的设计理念和架构图搞清楚每个产品宣称解决什么问题第二动手跑demo重点观察默认参数、配置项和失败场景比如故意上传一份版式复杂的合同看看它在切分和召回上的表现第三去GitHub翻源码目录结构和关键类的命名比如看它怎么抽象Loader、Retriever、Reranker这些组件组件之间的接口长什么样。这三步走完基本就能画出一张产品架构意图图——它和实际代码可能不完全一致但设计者的取舍会非常清晰地暴露出来。下面这张表是我当时选的六款产品和它们最值得借鉴的部分产品核心定位最有借鉴意义的设计适合谁参考LangChainLLM应用开发框架Loader/Splitter/Retriever抽象Chain编排想用统一框架快速搭建原型的人LlamaIndex数据索引与查询框架节点化索引、查询引擎、父文档/子文档检索想精细控制索引和检索逻辑的人DifyLLM应用开发平台知识库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在知识库的检索设置里明确提供了三种模式向量检索、全文检索、混合检索。混合检索默认同时执行向量召回和全文召回再用加权或RRFReciprocal 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融合接RerankDify、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而是一套可以持续迭代的工程架构。
RELATED READING

延伸阅读

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