这两年老听到的一句话是“RAG是伪需求”但真把业务数据接进大模型后你会发现检索质量直接决定AI回复是“一本正经的胡说八道”还是“精准命中”。我用过不少开源RAG框架也零散写过一些内部工具但真正让我把整个体系想清楚的是一次“逆向工程”——我把六款开源RAG产品的架构、数据流和处理链路全部拆开看了一遍对着源码和实际日志做了一次横向对表。最后沉淀下来的不是某一款产品的复刻而是一套可以复用的自研RAG蓝图。这篇文章就是那次拆解的完整记录适合正在选型、准备自研或者已经在做RAG但总觉得差口气的团队参考。过去很多时候我们都在“用”而不是“造”。用到一定程度你会发现开源框架的默认行为就是你的业务边界它怎么切分你就得怎么切分它支持什么格式你就得转换什么格式它检索不到你甚至不知道是解析丢了、分块碎了还是embedding模型对这段文本不敏感。逆向工程的意义在于把“黑盒”变成“白盒”把别人的设计决策翻译成自己的技术方案。这篇文章希望能帮你省掉大量的测试时间也帮你避免那些我已踩过的坑。1. 先说清楚为什么要折腾这次逆向工程1.1 我为什么从“调包”转向“造轮子”早期做RAG基本就是LangChain里接一个向量库文档一传问答一跑demo就出来了。但真实业务场景很快会戳破这层窗户纸。举个实际例子我在做一套设备运维知识库里面大量内容来自PDF手册、Excel备件清单和老的Word维修记录。用现成框架的默认管线跑效果非常不稳定——有的PDF能搜到有的PDF完全被淹没Excel表格里的备件号明明存在但问“哪个型号适配A2电机”就是查不出来。后来我做了个小测试同一份文档分别用LangChain的默认TextSplitter、LlamaIndex的SentenceSplitter、RAGFlow的版面解析去处理看一眼落库的数据块和检索召回结果。这一下问题就暴露了——根本不是模型不行是数据管道太粗糙文档结构信息在分块时被丢弃了。也就是从那时起我决定把这些框架脱掉外衣逐个拆内部机制。1.2 选定六款产品与选型逻辑市面上的开源RAG项目非常多我没有全拆只选了三类六款产品通用编排型LangChain、LlamaIndex。它们是RAG的“集大成者”什么都有但什么都牵扯到抽象层设计。端到端产品型Dify、FastGPT、RAGFlow。三者都有一套完整的可视化编排和界面但它们的内部实现思路截然不同Dify偏工作流、FastGPT偏对话应用、RAGFlow偏深度文档理解。研究/轻量型FlashRAG。它没有花哨的界面胜在代码量小、结构干净适合做底层逻辑的参照物。选这些不是为了比较谁好谁坏而是因为它们覆盖了RAG系统里几乎所有的关键分歧点文档解析怎么做、分块如何切、元数据怎么用、检索怎么召回、重排要不要做、上下文窗口怎么组织、评估闭环有没有。1.3 我的拆解方法按链路切分而不是通读源码很多人看开源项目喜欢一行行通读我的做法不同。我先跑通流程然后盯着日志和数据库中间结果做“链路切片”把一条完整的RAG流程在多个界面处切一刀看每层的输入输出长什么样。具体切分节点是数据导入→解析→分块→向量化→入库→查询改写→召回→融合→重排→组装Prompt→生成回复。每个节点我都记录三个东西输入长什么样、输出长什么样、这层有哪些可调参数。六款产品都按同一套节点切最后横向一比共性就浮出来了。这个方法非常推荐给正在做自研的团队。你不需要复制任何一家的源码你只需要理解它们在各节点上的设计决策和取舍然后结合自己的业务选一套组合拳。2. 逐款拆解六款开源RAG的架构与设计取舍2.1 LangChain抽象层太多但它把上下文组装想得很透LangChain给我的最大感觉是“个大、门多”。它的问题众所周知抽象接口太多升级频繁文档经常滞后。但在RAG层面上它有一个点比很多产品都强——Prompt模板和上下文组装。它会非常显式地区分“系统提示词”“历史对话”“检索到的上下文块”“当前问题”并且对不同模型做了适配。这在自研的时候是个很好的参考上下文组装一定要做成独立的模板引擎而不是在业务代码里拼字符串。再有一点LangChain早期的文档加载器loader、拆分器splitter体系虽然一直被诟病但它们的接口设计是对的。所有loader统一输出Document对象包含page_content和metadata后续splitter只认这两个字段。这样的好处是导入源的替换不会污染下游逻辑。我自研时也采用了同样的模式一切非结构化数据入库前先转成统一的Document对象。2.2 LlamaIndex数据接入层的“最佳范本”如果说LangChain的强项是组装那LlamaIndex的强项就是数据接入。它的整个设计都围绕“Index”展开每个数据源都有对应的Reader每个Reader都能解析出带元数据节点的文档树。我最欣赏的是它对“节点”的处理节点不仅包含文本内容还保留父文档引用、位置信息、章节层级。这意味着你可以实现“检索到叶子节点→回溯到父节点→拿到更大范围的上下文”这种高级玩法。我在自研时把LlamaIndex的这个设计直接借用为蓝图分块不是切完就完每一块必须牢记自己的父ID、文档ID、标题路径和页码。这不是为了好看而是为了后面做多级召回和上下文扩展时有据可查。很多自研RAG的召回质量一直上不去不是因为embedding不好而是因为块与块之间没有层级关系产生了大量孤立数据。2.3 RAGFlow文档解析才是RAG的地基RAGFlow跟我之前用的所有框架都不一样它把重心放在了“深度文档理解”上专门针对PDF、扫描件、复杂版面的文档做解析。它的几个设计细节非常值得参考。一个是版面识别。RAGFlow会把页面上的标题、段落、表格、图片分别识别出来表格不会被当成流水文本切碎。这种处理解决了我之前提到的“Excel表格查不到”问题。另一个是它把OCR和布局分析放到了整个流程的最前端而不是等到embedding阶段再补救。这给了我一个明确的信号解析层不是RAG流程的“前菜”而是决定天花板的主菜。如果文档进去的时候结构已经丢了后面做再多检索优化都白搭。我在拆解RAGFlow时特意测试了它的Chunk切分策略——它并不是固定按字数切而是先做版面分析找出语义上的完整块比如一个标题下的若干段落再做二次微调。这种“结构感知分块”相比“固定长度滑动窗口”在召回质量上有代差。后面自研蓝图中我把“结构感知分块”列为首选策略固定长度分块只作为兜底。2.4 Dify把RAG嵌进可编排的工作流Dify很多人把它当成低代码LLM应用平台不觉得它对RAG有什么独门绝技。但拆完以后我改变了这个看法。Dify的RAG关键词是“节点化”。它的知识库检索不是一个孤立功能而是编排工作流里的一个节点前后可以接问题理解、意图分类、条件分支、多个知识库并行检索再接LLM生成。这种设计直接影响了我的自研架构。以前我总觉得RAG就是“查一下再加到Prompt里”但Dify让我意识到RAG应该是流程中的一环先判断问题要不要检索、检索哪个知识库、召回的置信度够不够、不够要不要让用户澄清。这些逻辑都通过节点编排串起来比写死在代码里灵活得多。我还注意到Dify在检索策略上提供了“向量检索”“全文检索”“混合检索”三种模式而在混合检索模式下它默认做了RRFReciprocal Rank Fusion倒数排名融合把向量检索和全文检索的排名结果合并。这个细节非常重要很多团队自己写混合检索时直接拼接两个结果集导致重复和排序混乱RRF是解决这个问题的成熟方案。2.5 FastGPT中文场景下的工程化样本FastGPT的定位比Dify更垂直它的核心目标是对话场景下的知识库问答所以在中文优化和“开箱即用”上做得很足。它的知识库支持多种向量模型选择、多种检索方式、内置重排模型并且在应用编排上采用了类似“对话流”的设计把AI对话和知识库检索揉在一起。FastGPT给我最大的启发是它对“引用来源”的呈现方式。用户在问答界面上可以看到每句话对应的知识块来源、匹配度分数和原文链接。这个设计虽然看起来只是UI层面的功夫但它实际上把“可解释性”做成了RAG产品的一项核心能力。没有来源引用的RAG用户在业务上根本不敢信任AI的回答。自研时我第一时间把“引用溯源”加入系统需求列表检索结果不仅要返回文本还要携带章节路径、元数据和命中分数。另外FastGPT对免费模型和本地部署做了专门的适配这对我这种既想控制成本、又要保证私域数据不出网的场景特别有参考价值。它的底数配置和模型管理逻辑基本可以直接抄成一套多provider接入规范。2.6 FlashRAG最干净的可复现代码骨架FlashRAG是学术界推出的一个轻量RAG框架没有界面、没有工作流甚至连文档都不算多。但它的代码结构是所有候选里最清晰的数据准备、检索器、生成器、评估器四个模块严格分离整个框架可以用配置文件驱动。我建议每个准备自研RAG的团队都读一遍FlashRAG的代码不是因为它功能全而是因为它把RAG研究中最基本的单元抽象出来了。尤其它的检索器抽象做得很好统一支持稠密检索Dense Retrieval、稀疏检索Sparse Retrieval如BM25、混合检索而且所有检索器返回的结果都是统一的格式一列候选块ID加上分数。这样的抽象让上层重排和融合策略变得非常简单。我在自研蓝图里几乎照搬了这个接口设计而不是像很多自研系统那样把各种召回逻辑散落在业务代码里。3. 抽丝剥茧拆掉包装后的RAG通用架构3.1 从六款产品提炼的共性管道把六款产品放在同一张表里对拍RAG的通用架构其实就四层数据管道层、索引存储层、检索编排层、生成与反馈层。不管你用的是什么产品最终都能映射到这四层。而且这四层的接口边界非常一致——数据管道输出的是一批带元数据的内容块索引存储层把内容块变成可检索的向量和全文索引检索编排层负责把“问题”变成“一组候选中签块”生成与反馈层负责把候选中签块组装成Prompt并产出可解释的回答。这一点对自研的意义很大。它意味着你不需要从零开始设计架构只需按这四层去划分模块、定义接口。模块之间用明确的数据结构通信比如数据管道输出统一的Block对象检索编排层接受统一的Query对象并返回统一的Hit对象哪个模块想替换都可以独立进行。3.2 检索链路从单路TopK到多路融合检索是RAG里差异最大的一层也是最能拉开体验的一层。拆解之后我发现成熟产品没有一个走“单路TopK”的简单路子它们至少都在做“多路召回融合重排”。多路召回的组合通常是这样一路稠密向量检索从向量库召回语义相近的块一路BM25全文检索从倒排索引召回关键词命中的块有些场景还会加第三路结构化过滤比如限定最近一个月、限定某个产品线。三路结果拿回来之后不能直接拼在一起而是要做融合去重。Dify和FastGPT都默认采用了RRF公式每个块在每路结果里的名次取倒数然后累加按总分重新排序。公式很简单score Σ 1/(krank)k通常取60。这样做的好处是避免某一路分数分布不均影响排序天然做了归一化和去重。重排是检索链路的最后一关。六款产品中凡是支持重排的都采用cross-encoder方式也就是把“问题候选块”拼在一起送进一个专门的排序模型输出一个相关性分数。这一步的计算量比向量检索大得多所以候选集必须先经过前面的召回压缩到几十条重排只对Top20到Top50做精细打分最终取Top5左右进Prompt。这套“粗召回→精重排”的结构和我之前做搜索时的“召回排序”思路完全一致只不过语义模型替代了传统特征工程。3.3 Agent编排RAG之上的决策逻辑拆完Dify和FastGPT我越来越确定一件事RAG要想在业务里真正可用必须嵌在Agent编排里面。纯粹“查了就用”的RAG只适合回答事实型、单跳问题一旦涉及“先判断该不该查”“查完不够要不要追问”“多知识库要不要并行”这类逻辑就必须有一个编排层来调度。自研时我不再单独设计“RAG模块”而是把“知识检索工具”作为Agent可调用工具之一。Agent拿到用户问题之后先经过一个意图识别如果判断是知识类问题就调用检索工具并获得一个结构化检索结果如果不是知识类问题就走普通对话链路。这个决策本身就是可编排、可替换的不会把RAG变成一套僵硬的中间件。3.4 评估闭环最容易被忽略的第四层六款产品里FlashRAG对评估的支持最到位其余几款更多是面向使用的产品化封装。但逆向工程后我发现了一个残酷事实没有评估闭环的RAG系统根本没办法做迭代因为你不知道改一个参数是变好还是变坏。评估通常分两个层面。一个是离线评估用一组标准的问答对包含标准答案和命中块ID跑完后计算命中率、召回率、忠实度。RAGAS是这类评估框架里的常用工具。另一个是在线追踪记录线上用户的问题、检索到的候选块、重排后的最终结果和用户反馈形成一条可回放的日志链路。我自研时把两者都做了离线评估用于发布前测试在线追踪用于发布后的bad case收集。没有这一步后面所有优化都只是感觉。4. 可复用的自研蓝图架构设计与关键实现4.1 总体架构双存储、四引擎我最终落地的自研架构可以概括为“双存储、四引擎、一工作台”。双存储是指一个向量数据库负责稠密向量检索一个Elasticsearch负责全文索引和元数据过滤。为什么不只用一个因为RRF融合需要两路不同召回来源。虽然很多向量库也支持稀疏索引但Elasticsearch在元数据过滤、复杂的布尔查询和运维生态上明显更成熟。事实证明这种双存储的成本很低收益却很大。四引擎分别是文档解析引擎、分块引擎、检索融合引擎、重排生成引擎。文档解析引擎统一对接Tika、OCR服务和版面分析模型分块引擎实现“结构感知分块为主、滑动窗口兜底”的策略检索融合引擎负责多路召回和RRF重排生成引擎负责任务串接和Prompt组装。每个引擎都是独立服务通过内部API通信。4.2 核心数据模型设计在动手写代码之前先把数据模型定好比什么都重要。我的核心模型是一个带层级关系的Block对象class Block: block_id: str # 全局唯一 doc_id: str # 归属文档 parent_block_id: str # 父块ID用于上下文回溯 title_path: list # 章节路径如 [第3章, 3.2 故障排查] content: str # 块内容 content_type: str # text/table/image page_no: int # 页码尽量保留 source_url: str # 原文来源 metadata: dict # 扩展属性如作者、日期、产品线 embedding: list # 向量可选存储这样的设计是从LlamaIndex的节点体系中抽象出来的。它带来的直接好处是检索返回一个叶子块时系统能通过parent_block_id回溯到父块把父块内容一起作为上下文塞给大模型解决“单块信息不足”的问题。同时title_path可以在返回引用时直接生成“来源位置”展示用户一眼看到答案来自哪个章节可解释性大大提升。4.3 分块策略结构优先参数兜底分块是决定RAG质量的第一道闸门。我的策略分三步走。第一步解析引擎输出结构化文档对象包括标题层级、段落、表格。第二步分块引擎按“结构感知”原则切分一个二级标题下的若干段落默认组成一个块表格单独成块不与其他段落混切代码块和列表也独立成块。第三步对于确实没有结构信息的纯文本采用滑动窗口兜底。参数如下chunk_size512个字符overlap64个字符最小块长度不低于150个字符。这个参数不是拍脑袋定的而是在实测对比中发现的平衡点——太短语义不完整太长又超模型窗口或者稀释注意力。overlap的作用很多人理解不到位多说一句。它不是为了“多召回几块”而是为了避免语义在切分边界被割裂。比如一个句子正好横跨两个块如果没有overlap检索“这句话”时两边的块都搜不全有了overlap至少有一侧能保持完整语义。4.4 检索与重排参数与实现细节我的检索链路设计为五步调试时所有参数都用配置项管理不写死在代码里。第一步查询改写。用户原始问题先经过一个轻量级的改写流程补全代词指代、扩展同义词。这个步骤在对话场景特别重要因为用户经常问“那它呢”这种省略句直接拿去向量检索基本必挂。第二步双路召回。向量检索召回Top30Elasticsearch的BM25召回Top30两路都要求最小分数阈值避免垃圾候选过多。第三步RRF融合。按公式score Σ 1/(60rank)计算融合分取前20。第四步重排。用bge-reranker-base对大模型进行cross-encoder细排生成相关性分数取Top5。第五步组装。Top5块连同各自的title_path、page_no一起注入Prompt并附上“引用来源”清单。这里特别提醒一个常见误区重排之后不是越多越好。我实测过Top5和Top10的效果差异在长文档场景下Top5反而更好因为Top10会塞入更多噪声稀释模型的注意力同时增加token成本。核心原则是“少而精”重排器的作用是把最相关的那几条挑出来而不是把所有可能沾边的都塞进去。4.5 评估系统的落地离线评估我实现了一个轻量的脚本化管道准备100条业务问答对每条包含标准问题和期望命中的块ID。运行检索链路后计算三个指标命中率期望块是否在Top5内、召回率期望块排名位置、生成质量人工或大模型打分。回归测试时每次调整参数都跑一遍这100条对比分数变化。这套东西让“优化”不再是玄学而是能看到数字的变化。在线追踪则依赖日志采集把每次检索的query、多路召回分数、RRF结果、重排结果全部结构化落库。这样出现bad case时可以回放整条链路看到底是哪一层出了问题。没有这套追踪你连“问题出在解析还是检索”都分不清。5. 实战避坑与高频问题实录5.1 高频翻车现场与排查思路拆解和自研过程中我踩过不少坑挑最有代表性的四个说一下。第一个坑是PDF解析“看着成功、实则丢数据”。很多PDF看起来排版正常但用文本抽取库直接读会丢失表格结构和多栏文本。排查方法是对比“原始页面的可见内容”和“落库后的块内容”只要发现表格数据在落库后变少基本就是版面解析没做对。解决方案是引入版面分析模型把表格区域单独识别再走OCR或表格还原管道而不是整页粗暴抽取。第二个坑是元数据在分块过程中丢失。很多框架的splitter只保留文本内容元数据被丢弃或覆盖导致后续按产品线、时间筛选时无数据可用。我的经验是分块引擎永远不要自己创建元数据元数据必须由解析引擎统一维护分块时只负责继承。这样保证了全链路元数据的一致性。第三个坑是RRF融合后的重复块。如果两路召回都用同一个embedding服务容易在向量库和全文索引中同时命中同一块的变体。融合后一定要加“相同block_id按一次计”的去重逻辑否则重复块会白白占掉Prompt窗口。第四个坑是重排模型和业务领域的适配。bge系列的通用重排模型在通用语料上表现不错但在专业术语非常密集的场景比如法律文书、医疗记录会出现误判。有条件的话收集一批业务内的正负样本对重排模型做微调效果提升非常明显。5.2 RAG知识库能存图片吗这个问题是很多做知识库的人都会问到的直接回答传统RAG链路默认是“图不能直接进检索库的”。因为向量库索引的是文本嵌入向量而图片本身没有可直接语义化的文本。常见的解决办法有三条路径。第一条最省事给图片配文字说明。把图片放到对象存储里在文档解析阶段用视觉模型生成一段图片描述再把“图片URL描述文本”作为一个块落库。用户检索时命中描述文本前端展示图片。这种方案实现成本低但依赖视觉模型的描述质量。第二条稍微正规一点用图文双塔模型。图片和文本分别过两个编码器映射到同一个向量空间这样可以直接对图片算向量检索。代价是模型和训练数据不好找部署成本也高一般团队不推荐。第三条是最实际的多模态路线遇到图片多的文档不强行向量化图片本身而是把“图片周围文字说明”作为一个复合块保存。检索时主要靠文字说明命中命中后把整块连同图片URL一起返回。这个方案我目前应用最多效果好、实现简单适合大多数企业知识库场景。简单总结就是图片能不能进RAG取决于你是否愿意为每一张图做“文字翻译层”。5.3 零基础本地部署的快速参考如果团队暂时不打算自研只是想先快速落地一套本地知识库可以用Ollama配合向量库搭一个最小可用的RAGOllama负责跑embedding模型和对话模型向量库选轻量级方案然后配合一款开源RAG工具完成文档上传、分块和检索。这条路的好处是隐私数据不出内网、成本很低适合验证业务价值。但要注意本地小模型的召回质量和生成质量都有局限正式上线前还是要做好评测。5.4 从开源走向自研的迁移节奏最容易犯的错误是一上来就推翻所有开源组件全部自研。更稳妥的路径是先基于开源框架跑通业务闭环同时埋好日志和评估点当业务量起来、问题定位到具体模块后再逐个替换自研模块。我的替换顺序是从外到内先换文档解析引擎解决数据质量问题再换分块策略解决召回质量问题然后引入重排和融合解决排序质量问题最后再考虑是否替换底层存储。这样做的好处是每一环都有明确的前后对比不会出现“全部换了但不知道哪里变好”的糊涂账。另外自研不代表什么都要写。向量库、全文索引、重排模型这些成熟的基础设施直接用商用或开源组件没问题真正需要自研的是“业务逻辑相关”的部分数据管道、分块策略、元数据体系、编排链路和评估体系。把精力和预算花在刀口上才能控制成本、加速落地。6. 我这段时间最深的一点体会拆完六款产品、写完自研蓝图之后我对RAG最大的感受是它不是一个模型问题而是一个系统工程问题。很多人觉得换个更好的embedding或者换个大模型就能解决一切但大多数bad case根本不是模型不够强而是文档进去的时候结构已经丢了、分块的时候语义被切碎了、检索的时候只有一路向量、排序的时候没有重排、回复的时候没有引用。每一环都不起眼但累积起来的差距就是“demo能用”和“生产可用”之间的距离。最后分享一个值得养成的习惯凡是开源项目拿到手先别急着调参数先把它跑通再把中间结果导出到本地用自己业务里的真实数据去检查每一层输出。这个过程会让你对RAG的理解上一个台阶也能让你在自研时少走大量弯路。这套蓝图不是一个终点它只是一个起点后续无论是接入更多模态数据、补齐重排模型微调还是深度整合Agent编排都可以在这个骨架上继续长出来。