
所谓自研RAG很多团队一上来就写代码结果一半时间花在处理PDF解析、切分粒度、向量库选型这些已经被别人反复验证过的问题上。我这一年多陆陆续续研究了六款开源RAG产品从LangChain这种大而全的框架到QAnything这种为检索而生的垂直方案逐个拆过它们的源码和设计文档。这篇文章就是想把逆向出来的共同设计规律整理成一套可复用的自研蓝图给正在做RAG或者准备做RAG的团队一个参考。如果你正卡在“召回不准”“上下文塞不下”“检索链路怎么写才优雅”这些问题上这篇应该能让你少走很多弯路。我不打算介绍它们怎么用因为这些项目文档都很全。我更关注的是它们作为一套系统是怎么拆分层、怎么定义数据流、怎么处理“文档进入索引”和“问题检索答案”这两条主链路的。把我们自己的需求映射到这套蓝图上自研从零到一就变成填格子的事。1. 为什么我要从开源产品逆向RAG设计1.1 自研之前先看看别人怎么踩坑很多人以为RAG难在模型其实不是。模型你直接调API或者部署一个开源模型就行真正的工程量全部集中在“文档怎么进、碎片怎么存、命中怎么找、上下文怎么拼”这几件事上。这几件事每一个都是一堆隐藏分支图片要不要解析、表格要不要还原、多级标题要不要保留、召回结果要不要过滤、重复内容怎么去重。如果你没有踩过这些坑你很难一次性做对。开源项目最大的价值就是把这些踩坑的痕迹留在代码里。你看RAGFlow花了那么大力气做深度文档解析你就知道PDF解析不是简单抽文本你看Dify把知识库和应用编排拆成两个独立模块你就知道该让知识库做知识库的事应用做应用的事。所谓逆向工程并不需要你去反编译二进制你把架构清楚了、核心代码读明白了设计意图自然就浮现出来。1.2 六款产品的选型逻辑和观察维度我选这六款产品不是随便凑数而是它们分别代表了一类设计流派产品流派我的观察重点LangChain框架流抽象边界怎么划定组件怎么定义接口LlamaIndex文档流索引结构与文档节点关系Haystack管线流Pipeline化检索可观测性设计Dify工程流知识库与应用解耦配置化思维RAGFlow解析流文档版面解析高质量索引的关键QAnything推理流基于排序的二次召回问与答分离观察维度主要是三条数据从进入到可检索之间的处理链路检索时做了几次筛选、每次用什么信号生成时上下文如何拼接裁剪。这三条线走通一个RAG系统的骨架就出来了。2. 六款开源RAG各自藏着哪些“宝藏设计”2.1 LangChain框架层的抽象边界与可插拔接口LangChain对我最大的启发不是它那几千个集成组件而是它定义了一套非常清晰的抽象。每个Retriever都接一个统一的接口不管底层是向量库、图数据库还是搜索引擎每个DocumentLoader都返回统一的Document对象不管来源是PDF、网页还是API。这就让“组件可替换”从口号变成了工程现实。自研的时候最容易犯的错就是把业务逻辑和具体实现焊死。你的代码里到处都是elasticsearch.query()将来想加一个向量检索就很难受。学LangChain的做法Retriever只做一件事接收一个问题返回一组文档。至于文档从哪个存储里来用不用混合检索是不是走了重排序全部是内部实现。这样你的业务逻辑永远只依赖一个抽象的Retrieve接口。另一个值得学的是它对“链”的处理。LangChain里的Chain可以嵌套你可以把“检索”和“生成”视为两个环节也可以把“改写问题”作为一个前置环节插入。这个思想放在自研里就是你的RAG流程应该是一条可组合的管道而不是一坨顺序执行的函数调用。2.2 LlamaIndex以文档为中心的索引解构LlamaIndex虽然也带各种高级功能但它的设计核心其实特别朴素你把文档给我我把它变成索引供你查询。它定义了一套Node边界的理念文档会被切成节点每个节点带上结构信息、元数据节点之间有关联关系。这一点比LangChain那边更贴近“知识管理”的本质。自研RAG时很多人直接把整篇文章切成一堆重叠的500字片段然后一股脑扔进向量库。这个做法不能说错但会产生很多问题一个跨章节的上下文被切断了标题信息丢了引用的出处对不上了。LlamaIndex的做法提醒我们切分不仅仅是按字数切还要理解文档结构。它能保留层级标题、把表格被当成独立节点、甚至维护节点之间的索引关系。它的另一个宝藏设计是“索引不是只有向量”。你可以对同一份文档构建多种索引向量索引用来语义召回摘要索引用来快速定位关系索引用来跳转到兄弟节点。查询时你可以组合这些索引结果。这比单纯依赖向量相似度要可靠得多因为向量只理解“语义像不像”不理解“结构上哪里关联”。2.3 HaystackPipeline化检索与可观测性Haystack是那种把“流程”做到极致的框架。你怎么定义检索管道就怎么在代码里搭积木。它的Pipeline允许你把DocumentStore、Retriever、Ranker等节点串起来并且每个节点都有输入输出名称。这个设计的妙处在于管道里的每一步都被显式化了你可以给同一个管道换掉某个节点而整个数据流不变。我曾经按照Haystack的思路把自研检索模块改造成了一个Pipelinequery先过关键词召回再过向量召回两个结果分别加权合并送进重排序最后截断TopK。每一步都是独立的类分别实现了run()方法。排查问题的时候非常方便每一步的中间结果都能单独打印出来看。对比之前那种一坨代码写下来可观测性提升了不止一个档次。Haystack还带了完整的评估工具它内置了一套评测集的处理逻辑能算出retrieval的RecallK和生成答案的SAS等指标。这对我们自研时的持续优化特别关键。很多人上线之后只能说“感觉还行”一旦涉及调参就开始瞎试。有了快照化的中间结果和评测指标优化就变成了科学实验。2.4 Dify应用编排与知识库分离的工程智慧Dify严格来说不是一个RAG引擎而是一个LLM应用开发平台。但它的知识库模块设计很值得拆。Dify把“知识库管理”和“对话应用”分成两个独立的功能域你先在知识库里上传文档、分段、建立索引然后在应用里引用这个知识库通过“上下文策略”限制检索行为。这个分离看起来简单做起来很影响后期维护。自研RAG时我们很容易把知识库和聊天逻辑写在一个服务里最后用户上传一个文档触发全链路重灌代价非常高。Dify告诉我知识库是一个独立资源有自己生命周期它可以被多个应用复用。而且知识库的索引方式是可选的高质量模式用Embedding经济模式用关键词还有一种混合的“上下文检索”模式专门为RAG场景设计。它的检索参数设置也很有意思不是让你选“用什么检索器”而是直接让你调“召回数量”“相似度阈值”“得分权重”把实现细节隐藏起来。这说明成熟产品考虑的是“用户能理解什么”而不是“技术能提供什么”。自研时我们也应该把面向业务的参数收拢成这几个核心旋钮。2.5 RAGFlow深度文档解析与版面还原RAGFlow在开源圈子里火得很真实因为它解决的是一个被低估的痛点RAG效果差很多时候不是LLM不行是文档解析太粗糙。RAGFlow花了很大力气做布局识别、表格抽取、标题层级还原把传统PDF、扫描件转成了有结构的Markdown然后再切片。这一步做对了后面的检索和生成才会通。我拿一份扫描版合同试过RAGFlow它不仅能识别出正文还能保留标题缩进甚至把表格完整还原成Markdown表格。后来检索“违约金比例”这种埋藏在表格里的信息也能准确命中了。这对我影响很大我自研时就把文档解析单独做成一个模块接OCR和版面分析模型而不是简单地用pdftotext抽一下。它另一个亮点是“基于模板的分段策略”你可以为不同文档类型定义分段规则比如一个合同按条款分一个产品手册按章节分。这个设计很聪明因为通用切分永远无法适配所有场景只有规则模板才能真正贴近业务知识结构。在自研蓝图中我把它拆成了“解析器”和“分段器”两层解析器负责把文件变成结构化中间格式分段器负责根据模板切分。2.6 QAnything基于推理的召回为什么更好QAnything是网易有道开源的RAG方案它有一个明显不同query和document做了分开的两阶段——第一阶段用embedding做粗召回第二阶段用一个排序模型把答案从对应段落里再选一遍。表面上看它是个重排序实际上它把“找段落”和“找答案”拆开了。这种做法对答案出处的准确度提升很大。QAnything的设计对我最大的启发是它引入了“得分阈值”和“文档相关性”两个维度的校验。自研时我们经常遇到向量得分明明不低但内容压根不相关。其原因在于embedding模型对细粒度语义不敏感它擅长找“话题相近”不擅长找“答案正确”。所以必须加一个更强大的排序模型做二次判断。QAnything用开源的bge-reranker做这事我照搬到了自己的系统里效果立竿见影。它还内置了“无法回答则直接拒绝”的机制。当检索结果都低于置信度阈值时模型不会硬编答案而是会提示“暂无相关知识”。这个看似简单的设计其实守住了一条质量底线至少比瞎编强得多。我在自研时也把这一条作为硬性要求。3. 逆向归纳出的自研RAG蓝图3.1 从六款产品提炼的核心架构分层综合这六款产品的源码和设计思路一套可复用的自研RAG蓝图本质上就是五层结构数据接入层负责格式适配、解析、清洗、分段。知识索引层负责生成向量、构建倒排索引、维护门面索引。检索召回层负责关键词召回、向量召回、多路合并。精排与上下文组装层负责重排序、去重、裁剪、拼接。生成与输出层负责提示词构建、LLM调用、输出校验。每一层之间用明确的接口隔离层与层之间只传标准化的数据结构。谁要换实现谁就实现对应接口不影响上下游。这个分层看起来很普通但很多自研RAG项目没做到。常见的问题是解析完的数据直接向量化检索完的结果直接塞给LLM中间完全缺少独立处理的环节。你需要一个贯穿所有层的“文档对象标准”。我强烈建议为文档定义统一的数据类比如KnowledgeChunk里面至少包含原始内容、清洗后内容、元数据、标题路径、block类型、hash值这六个字段。六款产品各不相同但最后都能归到这个数据结构上。3.2 数据接入层文件解析与切分的通用方案数据接入层是整个RAG效果的地基。这个层不要自己做复杂业务而是定义解析器的接口输入一个文件流输出一个标准化的Document列表。每个Document里面包含正文、元数据和结构信息。解析器要按文件类型分开写PDF、Word、Markdown、HTML各用各的工具。解析完成后先做清洗这个环节很多人跳过。清洗包括去掉页眉页脚、过滤脚本标记、修正OCR错别字、统一换行符。如果不做你的向量库里会存很多垃圾片段检索时它们还会干扰相似度。亲身踩过的一个坑是HTML转文本后留下了无数导航链接文本检索“价格”时经常把页面上的其他产品链接搜出来。分段策略不能一刀切。我建议至少提供“固定长度”和“结构分段”两种模式。结构分段就是根据标题、段落、表格等结构切分借助版面分析结果或Markdown的heading层级。六款开源产品中RAGFlow的结构分段做得最深QAnything和Dify也提供了多种分段模式。你自研时可以起步做“固定长度重叠窗口”但一定要预留结构化分段的位置因为真实知识库的文档结构差异太大早晚要上。3.3 检索层混合检索重排序别吝啬粗排检索层的设计直接决定RAG的上限。很多团队的第一个版本就是“向量召回TopK”这是底线方案不是完整方案。我的建议是上混合检索关键词检索BM25和向量检索并行然后在合并结果里做重排序。为什么需要混合因为向量检索擅长语义泛化但不擅长精确匹配。你说“甲公司2023年报里的净利润”向量模型很可能把“2023年度报告”和“净利润”两个概念在不同向量空间里匹配结果召回一堆包含“收入”或“2022年”的片段。关键词检索能精确卡住“2023”“净利润”这些字面信号互补性很强。合并策略有三种常见选择互斥加权、RRFReciprocal Rank Fusion、得分叠加。我在实践中更推荐RRF它不依赖各路的绝对分数而是按排名位置加权避免向量得分与BM25得分量纲不一致导致某一方被淹没。示例公式很简单对每个候选文档计算Σ(1/(krank_i))k一般取60。谁在两个列表里都排前面谁的综合排名就高。重排序是精排层的核心。粗召回数量可以设为30~50精排后只保留3~5个片段。重排序模型普遍用交叉编码器比如bge-reranker-base一次算一个query-文档对的相关性分数比双塔embedding精度高很多。自研时可以直接在蓝图中预留这个位置初期甚至可以用一个简单的规则替代但接口必须留在那里。3.4 生成层上下文组装与提示词模板化生成层要解决的不是“调用LLM”而是“怎么把检索到的片段变成LLM能理解的高质量上下文”。首先做去重和裁剪。多路召回经常带回来几乎重复的内容可以通过hash去重也可以用语义相似度聚类去重。裁剪是为了避免超过模型的上下文窗口同时减少噪音。上下文组装的模板我建议用三段式系统角色、参考知识和用户问题。参考知识不是把片段堆上去就完了要给它加编号和来源标记比如【引用1】然后在提示词里要求模型回答时带上引用编号。这一步能让模型形成“引用必须来源于给定内容”的强约束大幅减少幻觉。保留引用的另一个好处是可以通过后处理校验答案。我习惯在输出环节加一个校验器检查模型回答中的引用编号是否都存在于输入片段列表中如果引用了不存在的编号就自动剔除或重新生成。这个做法来自我对QAnything“无答案拒绝回答”思路的延伸。提示词模板化也很重要你不太可能对每个query都手写提示词但可以用Jinja2或F-string做一个模板系统把检索片段、用户问题、对话历史填入。模板要支持切换比如“简洁回答”“详细分析”“仅用引用回答”三种预设方便调试。3.5 可观测性与评估没有测评就没有迭代如果没有评测体系你的RAG系统就像一辆没有仪表盘的汽车。开源产品中Haystack和LlamaIndex都内置了评测工具Dify控制台也有日志但这些对自研来说还只是参考。我建立了一套最小可观测体系线上记录每次请求的完整链路日志包含切分片段数、每路召回的TopK明细、重排序后的得分、最终组装的字数。然后建一个离线评测集至少要有100条真实业务问题每条标注了标准答案和它在源文档中的位置。每次改动索引策略、切分参数或模型后跑一遍评测集看RecallK、MRR、忠实度这三个指标。不需要搞得很复杂但必须坚持跑。你还可以做A/B测试把不同切分粒度或不同重排序模型的流量各分一半用户反馈“这个问题回答得好不好”作为质量信号。这看起来笨但长期积累下来你会发现它比任何理论推导都可靠。4. 关键实现细节与代码级参考4.1 embedding模型选型与分块参数配置embedding模型的选择直接影响召回质量。中文场景下我目前用下来效果最稳的是BAAI/bge-m3它支持8000长度输入生成1024维向量混合检索下表现不错。如果你的算力比较紧张用bge-small-zh-v1.5也够用但语义细粒度会差一点。注意一定不要直接用通用的OpenAI text-embedding来处理中文知识库成本高且分块长度受限。分块参数我最常用的配置参考如下固定长度500字符、重叠80字符这个配置在“内容完整度”和“检索颗粒度”之间比较平衡。如果你们的文档是长条款型分段可以放大到800字符重叠加到120如果是短问答型250字符更合适。没有万能参数蓝图里应该把这两个参数暴露出来方便针对数据集做网格搜索。关于分块是否要带上标题前缀我强烈建议要。把文档的标题层级路径拼到每个块的开头比如合同/甲方义务/3.1 付款条件这样向量检索时上下文信息更充足。LlamaIndex和Dify都有类似的字段设计这也是我从它们那边学来的。4.2 向量库全文索引的融合检索实现同时用向量库和全文索引最简单的方式是用Elasticsearch的knnmatch查询或者用Milvus的混合检索接口。但我更推荐在代码层面做“双路召回”因为这样能自控合并和调参逻辑。下面是一个最小实现骨架def hybrid_search(query, top_k30): # 向量召回 query_vector embed(query) vector_ids, vector_scores vector_store.search(query_vector, top_k) # 关键词召回 bm25_hits es_client.search( indexknowledge, query{match: {content: query}}, sizetop_k ) # 合并候选集 merged merge_rrf(vector_ids, bm25_ids) return merged注意向量库里存的ID要和ES里的文档ID保持一致这样合并才方便。你还需要存一份chunk_id - chunk_content的内存映射方便后续取原文。如果你不想维护两个存储也可以只用Milvus或Qdrant它们现在自带稀疏向量功能本质上你只用一个数据库就能实现混合检索。我建议自研初期用方案二减少运维复杂度但数据规模大了以后ES的全文检索成熟度还是更高。4.3 重排序模型的接入与阈值设置重排序这里我用FlagEmbedding库配合bge-reranker-base成本不高效果提升却很显著。接入流程很简单加载模型对每一条(question, doc) pair打分按得分倒序截断。实际使用中我一般在重排序后设置一个相对阈值比如取最高得分乘以0.6作为最低门槛。低于这个门槛的片段直接丢弃。这个阈值不是拍脑袋定的而是我跑了200条评测数据看了“错误召回片段”的得分分布发现它们基本都低于最高分的55%。不同数据分布阈值需要自己标定所以蓝图里要把“阈值可配置”作为设计目标。问一句重排序模型要不要自己部署可以用CPU推理因为检索量不大业务QPS通常几十几百跑一个base大小的重排序模型在4核CPU上也能扛得住如果你并发要求高换GPU推理更稳。用模型做精排不求快求准。4.4 组件化工程把蓝图落成代码结构最后还是要回到代码。蓝图再好代码里如果没有对应的模块边界就只是PPT。我推荐一个最小目录结构app/ parser/ # 文件解析器 splitter/ # 分段器 indexer/ # 索引写入 retriever/ # 检索器 reranker/ # 重排序 generator/ # 生成器 eval/ # 评测脚本每个模块都留一个抽象基类和至少一个默认实现。比如retriever下面可以有VectorRetriever、BM25Retriever、HybridRetriever。生成器里要有PromptTemplateLoader和ResponseValidator。接口定义时要注意一点每个模块的输入输出尽量用自定义的数据类而不是裸的dict。裸dict虽然写起来快但一旦字段名被改所有下游全部静默出错。我吃过这个亏很喜欢在网上看到的“类型即文档”的说法自定义数据类本身就是在写文档。5. 自研RAG过程中的常见问题与避坑笔记5.1 召回不准时别急着换模型先看切分我踩过的最深的坑是召回结果老是不准我怀疑是embedding模型不行准备全换。但后来把召回结果打印出来才看到命中的片段只截断在一个句子里问的是“方案对比”命中的片段只剩下“方案对比表”这几个字上下文全丢在上一段了。这就是典型的切分粒度太细破坏了原文的论述结构。切分调整优先于模型替换。先看看你的召回片段是不是“半句话”如果片段内容明显没头没尾就调大块长度或者减少重叠窗口之间的缝隙。如果片段精度够但相关性差再考虑换embedding。顺序搞反会浪费大量时间。5.2 上下文窗口限制下如何取舍知识块即使模型支持128K也不可能把30个召回片段全塞进去因为塞多了反而会干扰答案聚焦。我的取舍逻辑是在重排序后的Top10里用“多样性”来进一步筛选。先保留得分最高的1个然后依次检查下一个片段如果与已选片段的语义相似度超过0.85就跳过它直到凑满5个最后按文档顺序排好。这个“按文档顺序”是很多人忽略的细节。从不同文档召回来的片段强行拼在一起LLM可能以为是连续的长文导致逻辑混乱。理想做法是把同一文档的多个片段合并并插入元信息“该片段来自某文档第几节”。你们的业务如果经常涉及跨文档回答上下文里一定要拍清楚来源不然模型很容易把两个不同合同的内容混在一起。5.3 多路召回结果合并如何避免排序“打架”当你同时用向量和BM25时最难受的是两路分数不等价。向量得分是余弦相似度0~1之间BM25得分通常是个位数到几十如果直接相加BM25会被向量秒杀。很多人会用归一化但归一化如果按每路独立进行也会带来问题单路得分普遍偏高时容易把低质量的召回顶上。我建议别直接比绝对分数用RRF或者“排名加成”。排名是相对位置抗尺度干扰能力强。RRF虽然简单却是我经过多轮对比后仍然选择保留的方案。如果你用ES的multimatch它会内部做归一化也比较省事但你就失去了对每路权重的控制。这个自由度对后续调优很重要别一开始就为了省事把它丢掉。5.4 开源协议与商用合规给我的提醒这件事必须放在优先级最高的位置。你逆向工程开源项目的设计思想不等于你可以直接抄代码然后闭源商用版权边界必须自己把关。Apache 2.0协议比较友好MIT更宽松但GPL系项目的代码复制进自研项目时就要非常谨慎。每个开源项目的源码仓库里都会有LICENSE文件动手之前一定先看一遍。我做了一个简单动作为每个模块顶部注释里写清设计参考来源同时申请范围内只借鉴API设计和架构分层不直接引用受传染性许可证约束的代码。如果你们公司有法务团队尽早让法务参与评估没有的话就尽量只读不抄把“启发”留在文档里。5.5 一个小技巧把检索链路做成可视化测试台这个技巧是我后来才补上的但作用非常大。我把所有检索中间结果同步到一个简单的FastAPI调试页面上页面展示了“输入问题–分段数量–向量Top5–BM25Top5–重排序Top3–最终上下文”每次调完参数直接刷新页面看效果直观得可怕。这个方法比看日志快很多也让团队成员更容易参与讨论和提意见。调试页面不需要很精致接口返回JSON即可前端用简单的HTML表格渲染。不过是个很小的投入却能解决“检索链路黑盒”的问题。只要你的自研蓝图把每一层的结果数据都暴露出来了这一步几乎是免费的。我个人在实际操作中的体会是开源RAG项目从来不是让你白嫖代码的而是给你看别人怎么想问题的。六款产品逆向下来最大的收获不是某个具体算法而是终于知道一个RAG系统应该从哪里切一刀——切分是在为检索定边界检索是在为生成收数据而评估是为整个系统兜底。如果你准备自研RAG我的建议是别急着从第一行代码写起先拿我这份蓝图对照一下找到你自己的“第0层”然后把每个模块做成能独立替换的最小单元。这么做下来你会发现RAG没有想象中那么玄。