1. 为什么我决定拆开源项目,而不是直接抄一个
先说个背景。去年我所在的小组要自研企业知识库问答系统,大家都清楚现在RAG方案看着热闹,但落到自己的业务数据上,经常是"一问就答非所问、一查就召回一堆无关片段"。当时我们面临一个决策:是直接套用某个开源框架,还是花钱买商业产品,又或者硬着头皮从零写。最终我提了个方案——把市面上口碑不错的几款开源RAG项目一个个拆开,看它们的代码结构、看它们的文档设计、看它们在GitHub issue里被抱怨最多的地方,然后把这些观察沉淀成一张自研蓝图。这篇文章就是那段时间工作的整理。
为什么选这条路线?因为RAG的瓶颈从来不是"调用一个大模型API",而是工程系统中每个环节的误差叠加问题。文档解析漏了表格、切分把一个完整段落拦腰截断、向量召回时Top-K里塞了太多语义相似但无关的片段、重排模型又把时间信息搞乱了——这些问题在你写出一版demo之前几乎感知不到,只有当你站在一个"已经跑起来"的系统上回头看,才知道每一层的坑长什么样。开源项目恰恰是最好的教材,它把决策过程和权衡结果都摆在那里,代码就是答案,issue区就是避坑手册。
我给自己定的拆解范围是六款:RAGFlow、QAnything、Kotaemon、Dify、LlamaIndex和LangChain。选它们的逻辑是:RAGFlow把重心压在文档解析上,QAnything在检索优化上做得很重,Kotaemon注重交互与引用溯源,Dify把知识库封装进应用编排,LlamaIndex是开发者向的索引抽象,LangChain则提供了完整的生态集成样板。六款产品覆盖了解析、索引、检索、重排、生成、交互这条完整链路,拆完它们,相当于把RAG各个派别的典型解法都见了一遍。
逆向工程的方法上,我有一条核心原则:不要去逐行读源码,而是去读每个项目的"决策痕迹"。所谓决策痕迹包括三类:架构文档和README里反复强调的定位、代码目录结构反映的分层逻辑、以及issues里用户反复吐槽的点。这三样东西拼起来,你看到的不是一个项目的代码,而是它背后团队对"什么重要、什么可以放弃"的一系列判断。写自研系统最缺的就是这种判断,因为任何模块都有十种实现方式,你要知道的是在什么约束下选哪一种,开源项目恰好把答案展示在你面前了。
2. 六款产品逆向拆解:它们各自的护城河与放弃
接下来我按产品逐个说,不面面俱到,只挑对自研有直接借鉴价值的部分讲。
2.1 RAGFlow:文档解析才是RAG的第一道坎
很多人做RAG第一个想到的是向量数据库和embedding模型,RAGFlow却把重心砸在了文档解析上,这个定位本身就很有启发。它用DeepDoc做版面分析,把PDF、Word、PPT先还原成"版面结构"——标题、段落、表格、图片、页眉页脚——再做后续处理。默认的切分逻辑也是配合版面来的,而不是简单按字符数吃,这和我见过的很多自研方案有本质区别。
从逆向角度,RAGFlow让我看到三件事。第一,解析层如果做得糙,后面所有环节都会被污染。一个PDF里面带三栏排版、页眉里有公司名、表格跨页,这些不做版面语义还原的话,解析出来的文本就是乱的,切分出来的chunk自然也是乱的。第二,RAGFlow引入了模板化配置,不同文档类型可以用不同解析模板,它没指望一套解析器通吃所有文档,这本质上是一种务实的工程妥协。第三,它对"文档还原"的优先级设置得很高,宁可检索慢一点,也要让喂给模型的内容是结构完整的。
自研借鉴点:在建设自己的RAG系统时,最先要确认的不是选哪个embedding模型,而是你的文档集里到底有多少种版式,每种版式需要什么级别的解析能力。如果文档以扫描件为主,OCR流程就要前置;如果表格密集,就必须考虑表格结构还原,否则检索到了也读不懂。
2.2 QAnything:两阶段检索的工程模板
QAnything是网易有道开源的,它的slogan很直接:"Anything to Answer,任何你能问的都能回答",但真正让我感兴趣的是它把检索做成了清晰的流水线。它默认使用BCE的embedding模型做第一轮召回,再接一个cross-encoder做精排,这种"粗召回加精排"的两阶段结构在搜索领域是老套路,但在RAG项目里真正做到工程可复用、并且把default配置给好的,QAnything算一个。
拆它的时候我在意的是几个细节。第一,每个知识库是隔离的,文件入库时自动做向量化和倒排索引,这避免了全局混合检索时互相污染的问题。第二,QAnything对query的处理不是直接丢给检索,而是有一套query改写逻辑,它会把用户口语化的问题转成更适合检索的表达。这个点在自研时很容易被忽略,但实际上用户问"今年Q3的销售额怎么样"和"2024年7月到9月的销售数据"在向量空间里的表达差异很大,不做改写就会召回不一致。
第三是它的排序策略。QAnything没有把Top-K直接交给LLM,而是先靠BM25和向量召回各取一批候选,合并后由cross-encoder精排,再截断到最终K条。这个思路我在自研时完整抄了,原因是:单路向量检索在长尾query上掉得厉害,加上BM25做互补,就相当于用关键词保证覆盖率、用向量保证语义相关性,再靠精排模型干粗活。这么一套下来,召回质量稳定很多,也特别好调试。
2.3 Kotaemon:交互层面决定了用户信不信你的RAG
Kotaemon是CLeanUI组织下的开源项目,做的是一个带界面、带文件管理、带对话历史的RAG问答系统。它的算法不算激进,真正做得讲究的是"让用户能追查答案来源"这件事。每个回答都会带上引用来源,用户点开引用就能看到原始文档中的对应片段,来源还能在文档里高亮。这个功能看似轻描淡写,实则是RAG系统能不能在真实业务里落地的关键——因为LLM的答案一旦没有出处,用户就无法判断它是胡编的还是依据文档说的,信任感直接崩塌。
拆Kotaemon还有个收获是它的对话组织方式。它把单轮问答升级成多轮对话,每一轮都会继承之前的检索历史,而且它没有把"历史记录"全部塞给模型,而是只保留和当前问题最相关的历史片段。这个决策值得细品,因为很多自研系统图省事,把最近几轮对话直接拼接进prompt,很快就把上下文窗口吃光了,反而导致当前问题的召回被历史噪声干扰。
Kotaemon的启发不在于代码,而在于产品意识:RAG系统的输出不只是"一个答案",还应该包含依据、过程、可验证性。自研时哪怕界面再朴素,引用溯源这个能力无论如何都要做进去,因为它不仅解决用户体验问题,还能当评测工具用——当回答出错时,你翻引用就知道是检索错了还是生成错了。
2.4 Dify:知识库在AI应用里只是中间件
Dify其实是做AI应用编排的,知识库只是它的功能模块之一,但正是这个定位让我对"知识库在整个应用中的位置"有了重新认识。Dify里的知识库是作为"工具"和"上下文"被工作流引用的,用户可以定义在什么条件下检索知识库、检索后如何拼装prompt、最终交给哪个模型生成。它把RAG从"文档进、答案出"的单一问答,变成了"一个可编排的数据处理中间件"。
拆Dify最大的收获是它的分段模式设计。它支持自定义分段标识符、最大分段长度、重叠长度,这些参数全都可以在界面上调,甚至允许按自定义规则做结构化分段。这个灵活度意味着同一个知识库可以根据不同业务场景调整切分策略,不用每次改代码重新跑入库。Dify还提供了召回模式的选择——向量召回、全文召回、混合召回——让用户根据内容类型决定用哪种。这些能力本身不稀奇,但它把"调参"这件事变成了产品能力,这给自研系统的提示是:RAG的配置化程度决定了它能不能被业务人员自己调优,而不是每次都要开发介入。
2.5 LlamaIndex:索引抽象是面向开发者的核心
LlamaIndex给我的感觉是"没替你决定任何事,但帮你把一切整理好了"。它不是一个开箱即用的产品,而是一套构建RAG的组件库。它最大的贡献是把"索引"这个概念抽象得很清晰——VectorStoreIndex、SummaryIndex、KeywordTableIndex、KnowledgeGraphIndex,每种索引对应一种数据组织和检索方式,你可以按需组合。而且它把数据连接器(LlamaHub)做成了插拔生态,各种格式的数据源都可以通过连接器灌进来。
从逆向角度看,LlamaIndex让我把注意力放回到"数据接入"这一层。它专门设计了文档节点(Node)这个中间结构,原始文档进来先被解析成节点,节点上可以挂元数据,再决定用哪种索引和检索方式。这个结构对自研的意义在于:你的系统里其实应该有一个统一的数据中间表达,不能让"文档格式"直接影响"索引结构"。先做解析、再转成标准节点、再决定索引方式,这个数据流一旦固定,后面加新文档格式就只是写一个新的解析器,而不是动检索链路。
2.6 LangChain:生态集成力胜过算法创新
LangChain我不需要多介绍,它已经成了AI应用开发的事实标准之一。但拆它的时候我想明白了一件事:LangChain的护城河不在算法,而在集成生态。它把几十种向量库、几十种模型、几十种工具全部抽象成统一接口,让开发者可以像搭积木一样组合方案。自研RAG不太可能复刻这种生态,但可以借鉴它的抽象思路——你的Retriever接口、Embedding接口、LLM接口都应该做成可替换的,这样以后升级模型、换向量库,才不需要重写整个系统。
LangChain还有一个值得抄的设计是它的Chain结构。它不把"检索加生成"写死成一个函数,而是拆成RetrievalQAChain、StuffDocumentsChain等一系列可组合的Chain。这个设计的好处是方便做中间结果观测——每一环的输入输出都有明确的类型和记录点,出了问题可以单独测试某一环。自研时哪怕不引入这个框架,也应该把手写逻辑拆成同样清晰的模块边界,而不是几百行代码揉在一起。
3. 从六款产品里抽象出的自研RAG架构蓝图
拆完六款产品,我把它们的共性部分抽出来,合并成一张自研可复用的蓝图。这张蓝图不绑定具体技术栈,而是描述RAG系统应该有的分层结构、每层的核心职责、以及层与层之间的数据接口。在动手写代码之前,把这张图画清楚,后面会少走很多弯路。
3.1 预处理与解析层:决定上限的一层
这一层是所有产品里最容易被低估的,但六款项目里做得用心的一款产品,几乎都把解析当成头等大事。蓝图上这一层的内容拆成四段流水线:格式识别(判断是PDF还是Word还是扫描件)、内容解析(版面分析、表格还原、OCR)、清洗规整(去页眉页脚、去重复空行、合并断行)、结构标注(识别标题层级、段落边界、列表项)。四段完成之后,原始文档才被改造成"干净的半结构化文本"。
这一层输出的不是纯文本,而是带元数据的节点列表。每个节点至少包含:文本内容、文档来源、页码或位置信息、文档标题、章节路径。这些元数据在后续检索和引用溯源时都是必需品。切分策略同样属于这层,我推荐的默认策略是"结构优先、长度兜底":优先按文档本身的标题和段落边界切,如果段落太长再按最大长度截断,同时设置少量重叠来避免上下文断裂。
3.2 索引与存储层:多路索引并行
把切好的节点入库时,蓝图建议不要只做一种索引,而是构建三类并存的结构:向量索引(对应语义检索)、倒排索引(对应关键词检索)、元数据索引(对应结构化过滤)。向量索引负责语义相似度,倒排索引负责精确匹配术语,元数据索引负责按时间、作者、文档类型做预筛选。这三类索引共享同一个节点存储,也就是同一个节点ID可以同时存在于三种索引中,只是索引键不同。
选择embedding模型时我的建议是优先看中文场景的通用表现,BGE系列和M3E系列在中文上默认效果都还可以,但最终要以自己的数据集评测为准。向量数据库现在选择很多,Milvus适合大规模、Elasticsearch适合已有ES技术栈、Qdrant和Weaviate部署更轻。蓝图里我建议先用一套最简单的方案跑通链路,因为瓶颈往往不在向量库本身,而在数据质量,等数据规模上来再迁移也不迟。
3.3 检索与排序层:混合召回加精排
这层的设计我直接参照了QAnything的两阶段方案。第一阶段是召回,并行执行三路:向量召回Top-K(默认K可以设大一点,比如50)、BM25召回Top-K、元数据过滤后的范围内召回。合并后做去重和分数归一化,进入第二阶段。第二阶段是精排,用cross-encoder模型对候选集逐一打分,输出最终Top-K。
我在实际使用中会把第一阶段的K值设得偏大,宁可多召回一些"可能相关"的内容让精排去判断,也不要在一开始就把正确答案漏掉。精排模型的选择上,bge-reranker-base在速度和效果之间比较均衡。另外提醒一句:精排之后不要直接把Top-K一股脑塞给LLM,建议做相关性阈值检查,低于阈值的宁可少给,避免噪声段落干扰生成;如果召回结果整体分数都很低,可以让系统明确告诉用户"知识库中没有找到足够相关的资料",而不是硬凑答案。
3.4 生成与交互层:prompt、引用和流式输出
生成层的核心不只是"把Top-K拼给LLM",而是模板化地组织上下文。我的prompt模板大致分四块:系统指令(说明你是基于知识库回答的助手,禁止编造)、知识库上下文(按相关性排序拼接,每条前面标记引用ID)、用户问题、输出要求(如果知识库没有相关内容,直接说明不确定)。引用溯源这一环节,把精排结果中每条文本对应的节点ID映射成一个可点击的引用标记,回答中每出现一个引用标记,界面上就能展示对应的原始片段。
流式输出我建议必做。RAG场景下用户等待时间本来就比普通对话长,流式输出能显著改善体感。另外对话历史这块不要简单拼接,最好对历史做轻量压缩或只取与当前问题相关的部分,Kotaemon的做法值得抄——它把历史对话也做了相关性判断,只保留有用部分。
4. 蓝图落地时的取舍:你的场景决定你该砍掉什么
蓝图是理想态,真上手时不可能全都要,取舍依据是你的数据规模、实时性要求、和团队维护能力。我根据拆解经验把常见场景分了三档,你可以对号入座。
4.1 轻量版:文档千篇以内,团队两人以内
这档适合个人知识库、小团队内部文档问答。技术栈可以极简:一个开源向量库(比如Chroma或Qdrant的本地模式)、一个embedding模型、一个rerank模型、一个LLM API。解析层不用做太复杂的版面分析,用成熟的文本抽取库先顶着,切分策略直接按结构优先的默认方式。蓝图中的元数据索引可以简化,只保留来源和页码。
轻量版的精排我仍然建议留,但可以用更轻量的模型。遇到的问题是recall不够还是precision不够,再决定是否升级解析或加更多路召回。最重要的是评估先行——先准备50到100条真实业务问题当评测集,每次改动前后都跑一遍,不然你根本不知道改完是变好了还是变坏了。
4.2 均衡版:面向企业知识库,需要权限控制和多租户
这档对应大多数做企业知识库的场景。解析层要上版面分析能力,因为企业文档里PDF和Word比例高,表格和扫描件都不少。索引层建议向量、倒排、元数据三路全上,元数据里要记录部门、权限级别、文档类型等字段,检索前先做权限过滤,这一步不是在提示词里加一句"只返回有权限的内容"——元数据过滤是硬规则,在进入检索之前就完成,不能依赖模型自觉。
均衡版还建议引入工作流编排能力,把知识库问答嵌入到具体的业务流程里。比如客服场景需要先检索FAQ再查产品文档,先做意图判断再决定检索策略,这些用Dify式的工作流设计可以灵活配置。模型层面,embedding和rerank都要选效果好一些的版本,因为企业场景对准确率的容忍度比个人场景低。
4.3 重型版:多源数据、大规模知识库、高并发
重型版的关注点开始转向系统性能和数据治理。解析层要支持异步流水线,因为文档量大时入库是持续性的任务,不能全塞在同步流程里。索引层要考虑分片和扩容,Milvus这类分布式向量库是合理选择,同时要注意embedding模型的批处理吞吐。检索层为了扛高并发,粗召回可以预先缓存热门query的结果,精排模型要支持GPU部署,必要时还要加一层规则缓存来降低重复问句的算力消耗。
重型版还有一个容易忽略的点是数据更新的时效性。文档改了之后,索引要不要立即可见?如果允许延迟,可以走定时增量更新;如果要求实时,就需要监听数据源变更事件,触发对应文档的重新解析和索引更新。这块复杂度不低,我建议先明确业务到底需要多实时,再决定投入多少成本。
5. 真正动工前,先想清楚这三件事
蓝图有了,取舍也定了,但如果这三件事没想清楚,代码写一半大概率会返工。
5.1 评测集:没有评测集,一切调优都是玄学
这是最重要的一条。动手第一天,就要从真实业务场景里攒一批有代表性的问题,最好包含简单事实问答、复杂推理问答、跨文档组合问答、知识库外问题四类,每类问题预先标注期望答案和它应该在哪些文档里。评测集不用大,50到100条足够启动,但要持续扩充。每次改动解析规则、切分参数、检索策略、prompt模板,都跑一遍评测集,用召回率和答案准确率两个指标看变化。没有评测集,你只会在"感觉变好了"和"感觉变差了"之间反复横跳,最后浪费大量时间。
5.2 渐进式落地:先用最小闭环跑通,再逐步加厚
不要一开始就把蓝图里所有模块都实现完。第一版系统可以用最简方案跑通——一个文档解析器、一个embedding、一个向量库、一个Top-K直接拼给LLM。跑通之后,拿评测集跑一遍,记录基线效果,然后逐个加模块:先加BM25双路召回看提升,再加rerank看提升,再加元数据过滤看提升。每一步都用量化效果验证这个模块到底值不值得加,这样你能清楚知道每个环节的边际收益,而不是凭感觉堆技术。
5.3 可观测性:每一个中间结果都要能看见
RAG系统调试最大的痛点是不透明——用户问一个问题,你很难知道是解析错了、召回错了还是生成错了。所以从第一版开始,就要把每个环节的中间结果记录成结构化日志:原始query、改写后的query、每路召回的候选及其分数、精排后的Top-K列表、最终拼进prompt的上下文、LLM的原始输出。这些日志既是排查问题的抓手,也是后续评估数据的重要来源。我见过太多团队把这一步拖到上线之后,结果一出问题就只能靠猜。别省这个功夫,它会让你在调优路上快很多。
几款开源项目拆下来,我最大的体感是:RAG不是一个"装上就能用"的技术,而是一个每个环节都要亲自校准的系统工程。开源项目的价值不在于让你少写代码,而在于给你提供了经过验证的判断框架——什么重要、什么可以放弃、哪里是坑。把这张蓝图变成适合你自己的版本,然后拿着你自己的文档和问题集,一段一段地调,一趟一趟地验证,最终那个系统才会真正长在你的业务上。