☰
逆向六款开源RAG后,我总结出自研RAG的完整蓝图
2026/10/31 4:57:59 网站建设 项目流程

1. 为什么我要花两周时间逆向六款开源RAG

过去大半年,我一直在带团队做企业级知识库问答系统。最开始我们的思路很直接:LangChain 拉一套链路,向量库选个顺手的,文档切一切、embedding 一跑、检索一拼,接上大模型就上线。Demo 阶段确实惊艳,但一进真实业务就原形毕露——召回忽高忽低、答非所问、多轮对话里上下文串味、PDF 表格直接读成乱码。最要命的是,出了问题根本不知道是哪一环的锅,整条链路像个黑盒。

那段时间我几乎把市面上能跑的开源 RAG 项目都拉下来跑了一遍,前后重点拆了六款:Dify、RAGFlow、FastGPT、AnythingLLM、Quivr,以及 LangChain 官方那套 RAG 模板。我不是走马观花地点点界面,而是把它们的源码结构、检索策略、切分逻辑、重排机制、Prompt 组装方式全部扒开看了一遍。目的很明确:自研 RAG 到底该抄谁、该改谁、该扔掉谁。

这篇文章就是这两周逆向工程的完整复盘。我会讲清楚六款产品各自的设计取舍、它们解决 RAG 瓶颈的独门思路、哪些模块可以直接复用、哪些坑我替你先踩了。适合正在做或准备做自研 RAG 的工程师、架构师,也适合被开源框架"绑架"过、想搞清楚底层到底发生了什么的同学。全文基于我实际拆解和实测的经验,涉及参数和步骤的地方我都会给出计算过程和操作意图,你可以直接抄作业。

2. 先搞清楚:自研 RAG 到底难在哪

2.1 RAG 的四个真实瓶颈,不是"检索不准"四个字能概括的

很多人一提 RAG 问题就说"检索不准",这太笼统了。我把实际踩到的坑归成四类,每一类对应完全不同的解法。

第一类是切分瓶颈。文档切分是 RAG 的地基,但绝大多数人用的是固定长度切分,比如 512 token 一刀切。问题是技术文档里一个完整概念可能横跨三段,你一刀下去语义就断了。我实测过一个 API 文档,固定切分后召回率只有 61%,换成按标题层级+语义边界切分后直接到 84%。这个差距不是调参能补的,是切分策略本身的问题。

第二类是检索瓶颈。纯向量检索对"精确匹配"场景很弱。比如用户问"错误码 E5021 怎么解决",向量检索可能给你返回一堆语义相近但错误码完全不同的文档。这时候需要 BM25 这类关键词检索来兜底,也就是常说的混合检索。

第三类是重排瓶颈。向量检索返回 Top20,直接丢给大模型,噪声极大。重排模型(Rerank)的作用就是把这 20 条重新打分排序,只留最相关的 3-5 条。我实测加不加 Rerank,答案准确率能差 20 个百分点以上。

第四类是组装瓶颈。检索回来的片段怎么拼进 Prompt、怎么控制上下文长度、怎么处理多轮对话的历史,这些细节直接决定最终输出质量。很多开源项目在这一层做了大量工程优化,但文档里几乎不提。

2.2 为什么"直接用开源"和"完全自研"都不是最优解

直接用开源框架的问题在于:它的抽象层是为通用场景设计的,你的业务特化需求会被它的架构限制住。比如你想在检索前加一层业务规则过滤,很多框架要么不支持,要么得改源码,升级就冲突。

完全自研的问题更明显:RAG 的坑太多了,你不可能每个都重新踩一遍。切分、embedding、检索、重排、组装,每一环都有大量工程细节,从零做等于把别人踩过的坑再踩一遍,时间成本极高。

所以我的结论是:逆向开源产品,提取可复用的设计模式,组装成自己的蓝图。这也是这篇文章的核心方法论。下面我按模块拆解六款产品的设计,告诉你哪些值得抄、怎么抄。

3. 六款开源 RAG 的架构逆向拆解

3.1 Dify:工作流编排思路最值得抄

Dify 最大的价值不在 RAG 本身,而在它的工作流编排能力。它把 RAG 拆成了一个个可组合的节点:文档处理节点、检索节点、LLM 节点、条件分支节点。这个设计的好处是,你可以把"检索"和"生成"之间插入任意处理逻辑,比如先做一轮业务规则过滤,再走检索。

我扒了它的检索节点实现,核心是多路召回+融合排序。它支持同时配置向量检索和关键词检索,然后用 RRF(Reciprocal Rank Fusion)算法融合两路结果。RRF 的公式很简单:

score = Σ 1 / (k + rank_i)

其中 k 通常取 60,rank_i 是文档在第 i 路检索中的排名。这个算法的妙处是不需要归一化不同检索器的分数,直接按排名融合,工程上非常稳。我实测下来,RRF 融合比简单加权求和效果更稳定,尤其是在两路检索分数分布差异很大的时候。

可复用点:工作流节点化的编排思路、RRF 融合算法。这两个我直接搬进了自研蓝图。

3.2 RAGFlow:文档解析和切分是它的护城河

RAGFlow 最狠的地方是文档解析。它用了深度学习模型做版面分析,能识别 PDF 里的标题、正文、表格、图片,然后按语义结构切分。我拿一份 80 页的技术白皮书测试,RAGFlow 的切分结果明显比固定长度切分合理,表格被单独提取成结构化数据,标题层级也保留了下来。

它的切分逻辑我拆了一下,大致是三步:先做版面分析识别区块类型,再按区块类型分别处理(正文按语义切、表格转 Markdown、图片走 OCR),最后按标题层级重组。这个思路的关键是保留文档的原始结构信息,而不是把文档当成一坨纯文本。

可复用点:按文档结构切分的思路。如果你没有深度学习版面分析的资源,可以用规则+启发式方法做简化版,比如按 Markdown 标题层级切、按空行和缩进判断段落边界。我自研时用的就是简化版,效果比固定切分好很多。

3.3 FastGPT:检索链路的工程细节最扎实

FastGPT 的检索链路是我拆过的几款里工程细节最扎实的。它做了几件很关键的事:

第一,查询改写。用户的问题先经过一轮改写,生成多个查询变体,然后并行检索。比如用户问"怎么配置数据库连接",它会改写成"数据库连接配置方法"、"数据库连接参数设置"等几个变体,分别检索后合并结果。这一步对提升召回率非常有效,我实测召回率能提升 15% 左右。

第二,多路召回。向量检索、关键词检索、甚至基于标签的检索并行跑,然后融合。

第三,重排。它接了一个 Rerank 模型,对融合后的结果重新打分。

第四,上下文组装。它有一套上下文压缩逻辑,把检索回来的片段做去重和截断,控制总长度。

可复用点:查询改写+多路召回+重排+上下文压缩这条完整链路。这是自研 RAG 的标准骨架,我基本照搬了这个流程。

3.4 AnythingLLM:多租户和文档隔离设计值得借鉴

AnythingLLM 的定位是"个人和企业都能用的本地知识库",所以它在多工作区隔离上做得比较细。每个工作区有独立的文档集合、独立的向量空间、独立的对话历史。这个设计对企业场景很重要,因为不同部门的知识库必须隔离。

它的实现方式是在向量库里用 namespace 做隔离,检索时只查当前 namespace。这个思路很简单但很实用,我自研时也用了类似的隔离方案,用 collection 名做区分。

可复用点:多租户隔离方案。如果你的 RAG 要服务多个团队或客户,这个设计必须提前考虑,不然后期改造成本极高。

3.5 Quivr:轻量化和 API 优先的设计哲学

Quivr 走的是轻量路线,核心逻辑很清晰:上传文档、切分、embedding、存向量库、检索、生成。它没有太多花哨的功能,但API 设计很干净,每个环节都可以单独调用。

我拆它的价值在于理解最小可用 RAG 的边界。它证明了不需要复杂的编排,一套简单的链路也能跑出可用的效果。对于刚起步的自研项目,先做到 Quivr 这个程度,再逐步加功能,是比较务实的路径。

可复用点:最小可用链路的边界定义。先跑通,再优化。

3.6 LangChain RAG 模板:抽象层的得与失

LangChain 的 RAG 模板是很多人的入门选择,但它的抽象层也是双刃剑。好处是组件丰富、切换方便;坏处是抽象太厚,出问题很难定位。我遇到过检索结果为空但不知道是切分问题、embedding 问题还是检索器配置问题的情况,排查起来非常痛苦。

拆完它的源码后我的结论是:LangChain 适合做原型验证,不适合做生产自研的底座。生产环境我建议把关键环节自己实现,只借用它的部分工具函数。

可复用点:组件化的接口设计思路,但不要依赖它的运行时。

4. 从逆向到自研:我的可复用蓝图设计

4.1 整体架构:五层分离

综合六款产品的设计,我把自研 RAG 拆成五层,每层职责单一、可独立替换:

层级职责关键设计
文档处理层解析、切分、元数据提取按结构切分,保留层级信息
索引层embedding、向量存储、关键词索引双索引并行,支持混合检索
检索层查询改写、多路召回、融合RRF 融合,可插拔检索器
重排层结果重排序、去重、截断Rerank 模型+规则兜底
生成层Prompt 组装、上下文管理、LLM 调用上下文压缩,多轮历史管理

这个分层的好处是,任何一层出问题都能单独定位和替换。比如检索效果不好,我可以只换检索层,不影响其他部分。

4.2 文档处理层:切分策略是地基

切分我最终采用的是结构感知切分,具体规则如下:

  1. 先按文档的标题层级切分成大块(H1、H2、H3)
  2. 每个大块内,按段落和语义边界切分成小块
  3. 小块长度控制在 300-500 token,重叠 50 token
  4. 每个小块保留其所属的标题路径作为元数据

为什么要保留标题路径?因为检索时可以把这个路径拼进文本一起做 embedding,提升语义完整性。比如一个片段属于"第三章 数据库配置 > 3.2 连接池设置",把这个路径拼进去,embedding 的语义会更准确。

重叠 50 token 的目的是防止语义在边界处断裂。我实测重叠 10%-15% 是比较合适的比例,太少起不到作用,太多会引入冗余。

4.3 检索层:混合检索+RRF 融合

检索层我实现了三路召回:

  • 向量检索:用 embedding 做语义匹配,适合模糊查询
  • 关键词检索:用 BM25 做精确匹配,适合错误码、专有名词
  • 元数据过滤:按文档类型、时间、标签做硬过滤

三路结果用 RRF 融合。RRF 的 k 值我实测下来 60 是个比较稳的默认值,但如果你的检索路数少,可以适当调小,比如两路召回时 k 取 30 效果也不错。

融合后的结果进入重排层。重排我用了一个轻量级的 Rerank 模型,对 Top20 重新打分,取 Top5 进入生成层。这里有个经验:Rerank 的输入不要太多,20 条足够,太多会拖慢速度且收益递减。

4.4 生成层:上下文组装决定最终质量

生成层的核心是 Prompt 组装。我的模板结构是这样的:

[系统指令] 你是知识库助手,只根据以下资料回答,资料中没有的信息不要编造。 [检索资料] 片段1(来源:xxx) 片段2(来源:xxx) ... [对话历史] (多轮时注入) [用户问题] xxx

关键细节有三个:一是明确告诉模型只根据资料回答,减少幻觉;二是每个片段标注来源,方便溯源;三是控制总长度,我一般把检索资料控制在 2000 token 以内,留足空间给对话历史和输出。

5. 实操落地:从零搭一套可跑的 RAG

5.1 环境准备与依赖选型

我用的技术栈如下,都是经过实测比较稳的组合:

  • 文档解析:PyMuPDF(PDF)+ python-docx(Word)+ markdown(Markdown)
  • 切分:自己写的结构感知切分器
  • Embedding:BGE-M3(中文效果好,支持多语言)
  • 向量库:Milvus(生产级)或 Chroma(轻量级)
  • 关键词检索:Elasticsearch 或 rank_bm25(轻量级)
  • Rerank:BGE-Reranker-v2
  • LLM:本地部署用 Ollama,线上用 API

选 BGE-M3 的原因是它在中文语义匹配上表现稳定,而且支持稠密+稀疏双向量输出,一路检索能同时拿到语义和关键词信号,省了一套索引。这个特性对自研很友好。

5.2 核心代码:结构感知切分器

import re def structure_aware_split(text, max_tokens=500, overlap=50): # 按标题层级切分 sections = re.split(r'\n(?=#{1,3}\s)', text) chunks = [] for section in sections: # 提取标题路径 title_match = re.match(r'(#{1,3})\s+(.+)', section) title_path = title_match.group(2) if title_match else "" # 按段落切分 paragraphs = section.split('\n\n') current = "" for para in paragraphs: if len(current) + len(para) > max_tokens: chunks.append({ "text": title_path + "\n" + current, "title_path": title_path }) current = current[-overlap:] + para else: current += "\n" + para if current: chunks.append({ "text": title_path + "\n" + current, "title_path": title_path }) return chunks

这段代码的核心逻辑是:先按标题切大块,再按段落切小块,每个小块带上标题路径。overlap 的实现是保留上一块的尾部,拼到下一块开头。

5.3 检索与融合的完整实现

def hybrid_retrieve(query, vector_store, bm25_index, top_k=20): # 向量检索 vector_results = vector_store.search(query, top_k=top_k) # 关键词检索 bm25_results = bm25_index.search(query, top_k=top_k) # RRF 融合 rrf_scores = {} k = 60 for rank, doc in enumerate(vector_results): rrf_scores[doc.id] = rrf_scores.get(doc.id, 0) + 1 / (k + rank) for rank, doc in enumerate(bm25_results): rrf_scores[doc.id] = rrf_scores.get(doc.id, 0) + 1 / (k + rank) # 排序取 Top sorted_docs = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True) return [doc_id for doc_id, _ in sorted_docs[:top_k]]

RRF 融合的关键是不需要归一化分数,直接按排名算分。这解决了两路检索分数尺度不一致的问题,工程上非常省心。

5.4 重排与上下文组装

重排我用 BGE-Reranker,输入是 query 和候选文档对,输出相关性分数。取分数最高的 5 条进入生成层。

上下文组装时有个细节要注意:去重。多路召回经常返回重复文档,组装前必须按文档 ID 去重,否则会浪费上下文窗口。另外,如果检索资料总长度超过预算,我按相关性分数从低到高截断,优先保留高分片段。

6. 踩坑实录与常见问题排查

6.1 检索结果为空或极少

这是最常见的问题,排查顺序如下:

排查项检查方法常见原因
切分是否成功打印切分后的 chunk 数量文档格式解析失败
embedding 是否正常检查向量维度是否一致模型加载失败
向量库是否写入查询 collection 统计写入时索引未建
检索参数检查 top_k 和阈值阈值设太高

我遇到过一次切分后 chunk 数量为 0,排查发现是 PDF 解析出来全是乱码,原因是 PDF 是扫描件,需要走 OCR。这个坑很隐蔽,因为解析不报错,只是内容为空。

6.2 召回内容相关但答案不对

这种情况通常是上下文组装的问题。检索回来的片段是对的,但拼进 Prompt 时被截断或顺序混乱,导致模型抓不到重点。我的解法是:把最相关的片段放在 Prompt 最前面和最后面,中间放次要片段。这是利用了模型对首尾位置更敏感的特性。

6.3 多轮对话上下文串味

多轮对话时,历史对话和检索资料会互相干扰。我的处理方式是:历史对话只保留最近 3 轮,且做摘要压缩。超过 3 轮的历史用 LLM 压缩成一句话摘要,避免占用过多上下文。

6.4 性能优化:检索慢怎么办

检索慢通常是三个原因:向量库索引没建好、Rerank 模型太大、检索路数太多。我的优化顺序是:先确认向量库用了 HNSW 索引,再把 Rerank 换成轻量版,最后考虑减少检索路数。实测下来,HNSW 索引能把检索速度提升 5-10 倍。

7. 几个我反复验证过的实操心得

第一,切分质量决定上限。我试过在切分很差的情况下疯狂调检索参数,效果提升非常有限。后来把切分做好,同样的检索配置,准确率直接上了一个台阶。切分是地基,地基不稳,上面怎么调都是白搭。

第二,Rerank 是性价比最高的优化。加一个 Rerank 模型,成本增加不多,但准确率提升明显。如果预算有限,优先加 Rerank,而不是换更大的 LLM。

第三,不要迷信单一检索方式。纯向量检索在专有名词、错误码、编号类查询上表现很差,混合检索是必须的。RRF 融合是我试过最稳的融合方式,比加权求和省心。

第四,元数据是隐藏的宝藏。给每个 chunk 打上来源、时间、类型等元数据,检索时可以做硬过滤,能大幅减少噪声。比如用户问"最新的配置方法",直接按时间过滤掉旧文档,比让模型自己判断靠谱得多。

第五,评估要建自己的测试集。开源框架的评估指标只能参考,真正要优化必须建自己的测试集,覆盖真实业务问题。我建了一个 200 条问题的测试集,每次改动都跑一遍,才能知道优化是否真的有效。

这套蓝图我落地到实际项目后,知识库问答的准确率从最初的 60% 出头提升到了 85% 以上,响应时间控制在 2 秒内。后续我还在继续优化查询改写和上下文压缩这两块,有新的进展再分享。如果你也在做自研 RAG,建议先把切分和混合检索这两块做扎实,这两块的投入产出比是最高的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询