六款开源RAG产品摆在仓库里,看起来都是开箱即用的完整方案,可真到我们需要把RAG能力嵌进自家后台系统的时候,问题就来了。我们的场景要求知识库按部门拆空间、按文档密级控制检索权限,还要对接已有的账号体系和审核流程,直接部署开源项目等于让业务改造成本去适配软件边界。所以我定了一条路线:先把Dify、RAGFlow、FastGPT、LlamaIndex、QAnything、txtai这六款有代表性的开源RAG拆开研究,再根据它们的工程取舍提炼出一套真正适合自研的落地蓝图。这篇文章就是完整逆向工程过程的记录,包括我拆解的路径、提炼出的通用设计规律,以及自研时踩过和避开过的坑。
在这个领域折腾一段时间你会发现,RAG项目失败率高的原因不在模型,而在系统架构。模型决定的是基础能力下限,而分块、索引、检索、重排、更新、权限这几个环节的配合,决定了回答质量的上限。开源项目之所以值得拆,是因为它们在真实企业需求里滚过,很多设计决策是无数用户反馈逼出来的。读懂了这些决策,自研就不用再交一遍学费。
1. 为什么放着现成的开源RAG不用,非要从零自研
1.1 直接改开源项目的隐性成本
直接拿一个开源RAG项目部署上去,遇到的第一个问题往往不是功能不够,而是“适应业务”的压力被转移给了团队。Dify把工作流作为平台的中心,默认设计是面向运营人员搭建对话应用的,你要把它嵌入企业内部审核流、审批流、权限流,就得在不少地方做兼容改造。RAGFlow的深度文档解析能力确实强,但它的模型依赖、GPU资源占用和中间件部署方式,在边缘环境或纯CPU服务器上会造成很大的资源负担。FastGPT的运营体验很好,可一旦需要定制调用链,你不得不去理顺它的前后端耦合。
这些项目本身都做得不错,但它们不是为你这个特定业务设计的。你业务的附加条件——比如文档密级、用户分群、审计日志、对接统一登录——每一项都可能落在开源项目不支持或者需要改源码的位置。与其到处改人家的核心逻辑,不如把自己需要的架构梳理清楚,参考开源项目已经验证过的设计,写一套边界干净的实现。
1.2 逆向工程不是抄代码,是提取“设计约束”
我理解的逆向工程,不是把别人的代码搬过来改改,而是从源码和运行时行为里反推每个项目的设计约束:它面临的是什么问题?为什么做成这样?在什么条件下这样做是对的?
举个例子,拆RAGFlow的时候你会发现它的解析链路特别重:布局识别、OCR、表格结构分析、版面还原,为什么?因为它假设输入文档是扫描件、复杂排版的PDF、带表格的合同。设计约束是“文档质量差、版式复杂、必须保留原始版面语义”。那么自研时如果你的文档来源很干净,比如内部规范生成的Markdown,这套重解析链路就可以大幅简化,只保留分块和元数据提取就够了。
提取设计约束的意义在于知道什么该丢、什么该留。这比直接把某个模块的代码抄过来有价值得多。项目里很多看起来很复杂的模块,其实只是特定场景下的特定答案。
2. 选品:六个RAG项目各自代表一种工程路线
2.1 选品标准:不看名气,看差异化
我选参考对象的时候有三个硬标准。一是架构差异要足够大,不能拿六个同类型的工作流编排平台;二是部署形态要多样,得有单体应用、有嵌入式库、有对外服务平台;三是项目要继续维护,代码和社区讨论还有参考价值。按这个标准我选了下面六款:
| 产品 | 路线关键词 | 核心设计关注点 | 对我启发最大的部分 |
|---|---|---|---|
| Dify | 工作流编排型RAG平台 | 知识库作为平台的抽象资产、可视化编排、数据集管理 | 检索节点的过滤链路和知识库数据模型 |
| RAGFlow | 深度文档解析型RAG | 布局识别、表格处理、父子分块、解析状态机 | 文档解析流水线和任务状态管理 |
| FastGPT | 企业轻量运营型RAG | 应用级权限树、低成本配置、多知识库路由 | 知识库路由与应用权限隔离 |
| LlamaIndex | 开发框架型RAG | Node抽象、子索引、查询转换、高阶检索 | 复杂查询拆解与子问题重组 |
| QAnything | 企业搜索型RAG | 两阶段检索:倒排索引+向量召回,重排器融合 | 重排模块的工程化落地方式 |
| txtai | 嵌入式轻量RAG | YAML配置、嵌入式索引、低资源运行 | 把RAG嵌入独立产品的封装思路 |
我选项目的时候特意避开了只做Demo展示的仓库。那种项目一般是固定分块加向量库加大模型,链路很短,缺少真实工程约束。我要拆的是那种已经经历了私有化部署、企业定制、多人使用反馈的项目,这类项目的代码里带着大量生产环境的痕迹。
2.2 六个项目各自回答什么问题
Dify回答的是:一个以工作流为中枢的RAG平台,如何组织知识库与其他节点之间的数据依赖。它不需要让知识库直接暴露给最终用户,而是把它当作工作流中的一个抽象资源,权限、版本、调度放在节点层统一管理。
RAGFlow回答的是:当文档复杂度上升时,解析方式对RAG效果的影响到底有多大。它用事实证明,文档解析不是简单的文本提取,而是要把版面结构、表格、标题层级都保留下来,这是一条完整且独立的工程链路。
FastGPT回答的是:在没有专职算法团队的条件下,一个RAG知识库如何用极低的运营成本落地。它对多知识库路由、权限隔离、共享链接的处理,很值得做SaaS或内部服务化的团队参考。
LlamaIndex回答的是:把检索组织成可编程的抽象层时,如何支持子查询、摘要索引、邻居召回这些高级玩法。它的抽象层级比前面几个产品更高,适合我这种喜欢自己组装模块的工程师。
QAnything回答的是:企业搜索场景下,如何用两阶段检索获得稳定的质量提升。倒排索引和向量召回的结果如何合并、交给重排模型时保留哪些信息,这些工程细节非常具体。
txtai回答的是:嵌入式场景里,单进程、YAML配置、不依赖外部向量库的情况下,RAG能轻到什么程度。它的设计提醒我,自研的时候不要把系统边界定义得过重,要留轻量部署的选项。
2.3 拆解前的心理准备
动手之前我给自己定了一条规矩:前七天先在每个项目里跑通“构建知识库-查询-返回”的最小链路,不追求完全吃透所有代码,先把主干锚定下来。锚定的办法是给每个项目写一张链路地图。Dify的链路是:上传文档-分割-生成向量-写入向量库-检索-重排-返回。RAGFlow的链路更长:上传-解析-版面识别-表格识别-过滤-分块-入索引-检索-重排-返回。链路地图能帮我在几千个文件里快速定位对应模块,而不是被代码庞大的结构带着走。
这一步很重要。RAG项目的代码量通常不小,不画地图直接扎进去,很容易在某个细节里绕不出来。
3. 拆源码方法论:沿着调用链抠关键决策
3.1 推荐的通读路径:入口-链路-数据模型
拆个项目不用从头读到尾,效率最高的顺序是这样的:
- 先看README和项目架构说明,确认它官方声明的主要特性是什么。
- 部署一次,用符合自己业务形态的文档跑通全流程。
- 找到“创建知识库”的API或者命令行入口,沿着SDK代码追到后端处理入口。
- 从后端入口继续追踪“文档解析-分块-嵌入-写入索引”这条链路。
- 接着看“检索”接口,反向找“召回-过滤-重排-生成”这条链路。
- 两条链路交汇的地方,往往就是核心数据模型,比如文档表、分块表、索引表。
- 最后读数据模型的迁移文件和权限校验代码,往往能看出设计者真正的意图。
比如Dify,它的入库链路比想象中长,但核心很好找。知识库被抽象成Dataset,文档上传之后走DocumentService,关键函数是process_document,它会先用DocumentSplitter把文本切成块,再通过embedding写入向量库。从函数命名就能看出设计思路:文档不是直接进索引,而是先经过“处理”变成已分割的块,每个块单独向量化。
RAGFlow更明显。它有一个TaskExecutor在后台异步跑任务,解析任务会被分发给document_parser组件。我读到这里就理解了,解析异步加任务状态流转,正好对应它们的文档状态机设计。
LlamaIndex是库形式,没有统一的入库链路,它的核心是通过VectorStoreIndex包了一层又一层的抽象。你顺着用下去,最终会落到基础模块里的Document和Node两个类。理解了这两个类,后面所有高级检索都是在Node对象上做变换。
3.2 必看的四类文件:模型、服务、检索、更新
我整理了一下拆解RAG项目必看的四类文件,不用全翻代码,看这四处就能抓住骨架:
- 文档和分块的数据模型定义。比如Dify的DocumentSegment、RAGFlow的Chunk,它们定义了分块之后的数据结构。
- 向量入库的写入服务。看它是同步还是异步,是覆盖写还是增量写,是否需要先清理旧索引。
- 检索和召回接口实现。看它如何组合词法检索、向量检索和元数据过滤。
- 更新任务调度。看它如何监听新文档、如何处理失败任务、如何触发重试。
把这四类抓出来,一个项目的RAG设计就基本清楚了。外围的工作流引擎、前端页面、部署脚本,按需求延伸去看就好。
3.3 拆解时绕开“模型封装层”的干扰
六款产品里五款都做了模型无关的封装层,比如BaseEmbedding、LanguageModel抽象,接口设计得很漂亮。但拆代码的时候如果陷进这一层,容易被接口命名误导,以为某个功能很复杂,其实是套了好几层转发。
我的经验是,拆解重点应该放在模型被调用前后的数据变换上。嵌入之前文本做了什么清洗?嵌入结果有没有做缓存?检索返回之前有没有去重?这些和模型无关的数据处理才是RAG的心智。
例如,我在Dify里看到同一条文本会经过多条prompt模板做格式统一,目的可能就是让后续检索结果更稳定。在RAGFlow里能看到它对纯文本片的清洗规则非常细,包括特殊符号、表格边框字符的归一化处理。这些细节都埋在解析模块里,不在模型接口里,绕开封装层才能看到。
4. 六款RAG拆完后的共性:可复用的设计规律
把六个项目放在一起对比,求同存异,能看到一些惊人的共性。这些共性就是我后来做自研时挂在墙上反复对照的基本盘。
4.1 分块策略:都在向结构感知进化,而不是固定窗口
六个项目里,即使是最轻量的txtai,也不会只用固定长度分块。它们的策略可以归成三类:固定窗口加重叠,这是FastGPT和txtai在部分场景保留的兜底方案;递归结构分块,Dify在设置里提供了这样的选项;深度结构感知分块,RAGFlow的布局树和LlamaIndex的文档树都归这一类。
核心洞察是:分块不纯粹是文本切分问题,本质是“检索单元完整性”的问题。一段语义完整的长文如果被强切,后面检索再强也召不回完整的上下文。所以这些产品都尝试把“段落”或“语义单元”作为默认分块基础,窗口长度只是兜底。
自研建议:先按文档结构切,标题、段落、表格分别处理。如果某个块超过token上限,再用递归方式继续分割。不要所有文档都用同一个splitter,应该给不同类型文档注册不同的解析器。
4.2 检索:多路召回是标配,关键词检索重新被重视
除了偏抽象层的LlamaIndex,其他五个项目在检索阶段都显式或隐式地加了词法检索。Dify提供混合检索,底层用全文检索加向量检索融合;RAGFlow的默认检索走的是全文搜索引擎的BM25召回;QAnything更直接,把两阶段检索做成默认架构。
为什么大家都回头加词法检索?因为实际业务文档里专有名词、编号、产品型号特别多,向量检索有时候就是区分不了“RAG-101”和“RAG-201”这种差一个数字的关键信息,词法检索能精确命中。自研时要做一路BM25或自维护倒排索引,别只靠embedding一条路走到底。
4.3 重排:几乎成了标准动作
从QAnything把交叉编码器重排模型做成默认,到Dify标准的重排节点,再到RAGFlow的排序融合,重排在六个项目里都是重要角色。早期做RAG的时候我也觉得两步够用,召回一下生成一下就完事,但召回返回的TopK片段里只要有一两个不相关的内容,大模型的回答就开始失控。
加上重排之后,质量提升非常明显。我看到的工程实现共性是用轻量级交叉编码器做重排,同时对多路召回结果做合并去重。成本可控,收益很大。
4.4 文档生命周期是状态机,不是一次性导入
六个项目处理文档的方式其实都隐含一个状态机:上传、解析、分块、索引入库、可用,中间还有失败、重试、过期。RAGFlow对状态管理做得特别重,Dify也支持文档重新处理。这个状态机带来的好处是任务可以断点续跑,哪个文档失败了一眼就能看出来。
如果自研的时候图省事,上传后同步处理、处理完直接入库,等文档量上来、任务失败率出现,会发现失败任务根本定位不到,只能全部重新导入一遍,代价极高。
4.5 权限和检索要能前置过滤,不能在召回之后才想起权限
企业里做RAG必须考虑权限,这点FM开源项目做得比较透。Dify对知识库做权限隔离,FastGPT设计了应用级授权,都包含用户在提问时只能检索到他有权限看的文档。
常见的工程做法是:在向量库的存储字段里写入空间标识、文档标识、允许访问的用户组列表这些元数据,检索时先把权限过滤条件作为硬过滤传给存储层,再进入向量相似度计算。因为向量检索只能算语义,没有权限概念,所以权限过滤必须放在召回之前,作为硬约束下推。这已经是我做自研时雷打不动的原则。
4.6 可观测性和评测闭环:开源项目隐藏最深的一层
这六个项目里,很少能看到完善的评测套件,但它们都做了大量的隐式对账。比如日志里打了chunk_id、检索分数、重排分数、耗时,这些信息串联起来就是一次完整检索链的追踪。没有这些日志,线上出问题根本不知道是召回漏了,还是重排把该排前面的排后面了,还是生成阶段露馅。
自研RAG的时候,从第一天就要留出中间产物的追踪接口:这个请求召回了哪些块?每块分数多少?重排之后顺序如何?最后送给大模型的是哪几段?把这些记下来,才能让系统具备自我诊断能力。
5. 逆向工程产物:我的自研RAG四层蓝图
拆完六款产品之后,我把它们的设计约束重新组合,沉淀成了一套适合私有化部署的自研RAG蓝图。这套蓝图已经在我们项目里跑过了,它没有照抄任何一个开源产品,但每层设计都能在那些开源项目里找到原型。
5.1 总体分层:接入层、索引层、检索层、服务层
我简化试过三层,也试过五层,最后稳定在四层:
- 接入层:负责上传、解析、格式转换,把PDF、Word、Markdown等不同格式变成统一的标准化段落结构。
- 索引层:负责分块、向量化、写入向量库和倒排索引,也负责增量更新和任务重试。
- 检索层:负责多路召回、权限过滤、重排、上下文组装。
- 服务层:负责API网关、会话管理、链路追踪、配置中心。
5.2 各层核心接口定义
接入层暴露两个核心接口:一个是上传文档生成文档ID,一个是解析文档返回结构化解析结果。解析结果统一成区块列表,包含标题、段落、表格、代码块类型,这个阶段不做向量化。这样做的好处是下游模块只和区块打交道,不感知PDF和Word的差异。
索引层接口做两件事:对区块分块并生成向量,然后写入统一存储接口。存储接口做抽象层,不直接依赖某个具体的向量数据库,否则哪天要换存储引擎,会牵连半层代码重写。
检索层接口需要三个能力:混合召回、重排、组装上下文。混合召回接收查询和从服务层传下来的权限过滤条件,返回候选列表;重排对候选做交叉编码器打分;组装上下文根据重排结果截取、拼接给大模型。
服务层对外只暴露四个API:对话能力、检索能力、文档导入能力、监控能力。内部做一个路由表,映射不同知识库到不同的数据配置。
5.3 数据流设计:一份文档的完整旅程
拿一份产品说明书的PDF来说明:
- 网关收到导入请求,创建文档记录,状态置为已上传。
- 接入层解析这份PDF,标题、段落、表格分别提取,表格单独标记为表格区块。
- 索引层对区块做分块,超过token上限的子块递归分割。
- 每个分块做向量化,同时写入倒排索引,做分词和归一化。
- 全部入库成功后,文档状态置为可用。
- 用户发起查询,请求自带所属空间信息,检索层先把空间权限作为硬过滤条件传给检索器。
- 向量召回和关键词召回并行执行,返回结果合并去重。
- 候选交给重排模型打分排序。
- 按最大token预算截取上下文,组装成提示词发给大模型。
每一步都有日志,解析耗时、分块数量、召回分数、重排分数全部落盘。这样出了问题可以直接定位到具体环节。
5.4 技术选型的约束条件
基于逆向工程得到的经验,我在技术选型上给自己定了几个硬条件。
向量数据库必须支持元数据过滤条件。不支持过滤的直接排除。现在很多向量库把过滤下推做得很好,这一条能省掉大量性能开销。
重排模型不需要上几十亿参数的大模型,一个交叉编码器架构的轻量重排模型就够用。如果涉及跨语言场景,选多语言重排模型更保值。
嵌入模型一定要看中文场景效果,不要只看英文榜单,并且要测自己领域的长文本相关性。
整个入库链路必须走异步任务队列,不能同步阻塞在API请求里。文档多起来以后,同步处理是活不下去的。
6. 把蓝图变成能跑的工程:关键落地细节
蓝图是骨架,真正让系统和开源项目拉开差距的是工程细节。这一节写的是我把蓝图实现成可运行系统时,反复调试出来的几个细节。
6.1 分块重叠与边界完整性
自研里我保留了重叠窗口,但没有简单粗暴地从头再切一遍。主块大小控制在800到1000个token,重叠留100到150个token。切块的时候优先保证边界完整:一个句子、一个列表项、一个代码块尽量完整留在同一个块里,绝不让一句话被切开。
如果遇到超长句子,比如产品文档里常见的五六十个中文字的注释,我会选择让块超出一点预算也要保句子完整。超预算问题用最终的硬切兜底,即token超过预算的110%时才强制切断。
6.2 多路召回分数的归一化融合
六个开源项目都在做多路召回,但每一路的分数尺度完全不一样,直接相加没有意义。我的做法是:
- 向量召回拿余弦相似度,天然在零到一之间。
- 关键词召回拿原始打分,归一化成带对数的软分数,让分数分布不要过于极端。
- 加权相加,默认是按经验调的六比四权重。
这个权重不是一成不变的。如果发现知识库里专业术语特别多,就把关键词召回的权重调高到五比五。对话口语化的问题多,向量召回权重就调高。但无论如何,先归一化再融合这一条不能破。
6.3 重排后的落库逻辑
重排模型会重新打散分数分布,所以设计上重排后不能丢原始召回分数。我给每条候选保留原始分数字段,重排后分别记录排序前的分数和排序后的分数。这个细节在调优的时候价值很大,能帮你分析到底是召回环节漏了该召回的内容,还是重排环节把好结果压下去了。
6.4 增量更新的一致性设计
文档更新在业务系统里非常高频。我用“版本号加软删除”的方式处理:
同一个文档重复入库,不直接删旧块,因为正在进行的对话可能还在引用旧块。新块写入并索引完成之后,才递增文档版本号,旧块标记为过期,不再参与新查询。这样用户不会看到检索结果突然从有变成没有的中间状态。
这个设计是从开源项目更新任务的失败重试机制里延伸出来的,但它们很多没有把查询一致性考虑进去,我在自研时把这块补上了。
7. 拆了那么多开源代码,我总结的避坑原则
7.1 别陷进抽象层泥潭
拆带框架性质的代码,比如LlamaIndex和LangChain,最容易被抽象方法绕进去。一个功能三行能实现的逻辑,追着装饰器和接口跑能追几十层。我的纠偏原则是:用端到端最小可运行版本做锚点,先确定整条链路能跑,再决定要不要深入某层抽象内部。不为研究抽象而研究抽象。
7.2 不要为了模型无关,把系统边界搞得很虚
很多开源项目强调了模型无关,但实际部署后会发现各模型行为差异很大。自研时如果死守纯可插拔模型,换来的是大量兼容代码和测试成本。我最后采用了折中方案:定一个模型接口规范,同时维护一份推荐模型清单。这样既留了变化空间,又不会为所有模型做过度的适配设计。
7.3 任务队列是绝对省不掉的组件
文档解析、向量化、索引写入这条链路,一定会遇到格式错误、模型超时、向量库偶发故障。没有任务队列,这些故障没办法重试,上游的错误会直接打进业务链路。这是拆完六款开源产品之后,我唯一绝对不敢省掉的组件。任务队列、重试机制、死信队列,在自研蓝图里都是标配。
7.4 光读代码不够,要用自己的数据验证
逆向工程最大的坑就是边读代码边觉得它设计得对。开源项目的代码放到它们自己的数据环境里,当然合理。但你的数据分布可能完全不同。所以每读一个项目的某个模块,我都会先用它的测试数据集跑通,再用自己的业务文档跑一遍。如果面对自己的文档效果不行,这个设计约束再漂亮也要做取舍。
我个人在实际操作中体会最深的一点是:逆向工程的价值不在复制某个项目的表面功能,而在理解它做每一个技术选择时的前提条件。把那些前提条件和自己的业务场景对齐,剩下的架构设计就能做得既扎实又轻量。这套从六款开源RAG产品里抽出来的蓝图,目前在我这边已经平稳跑了半年多,后续如果再做权限之外的功能扩展,我也会继续沿着这个思路,去更细地看开源项目是怎么处理多租户隔离和知识库路由的。