RAG 这个东西,从 2023 年火到现在,市面上能叫得出名字的开源框架我基本都拉下来跑过一遍。LangChain、LlamaIndex、Haystack、RAGFlow、Dify、FastGPT,再加上一些更轻量的方案,前后折腾了不下十套。最开始我也觉得,开源框架开箱即用,何必自己造轮子。但真到了要往生产环境推的时候,问题就一个接一个冒出来:检索召回率上不去、chunk 切得稀碎、多路召回融合逻辑黑盒、想改一个排序策略得翻半天源码。后来我干脆换了个思路,把这六款产品当成六个"老师",逐个拆解它们的架构设计,逆向出一套自己能掌控的自研蓝图。这篇文章就是这次逆向工程的完整记录,从模块拆解到代码落地,从选型逻辑到踩坑经验,尽量讲透。
1. 为什么我最终放弃了"拿来即用"的开源RAG
1.1 开源框架的甜蜜期与阵痛期
刚开始接触 RAG 的时候,我和大多数人一样,直接 pip install 一个框架,照着官方 Quickstart 跑通一个"文档问答"的 Demo,感觉这东西真香。上传 PDF、自动切分、向量化、检索、拼 Prompt、调 LLM 出答案,整条链路半小时就能跑起来。这个阶段我称之为"甜蜜期",框架帮你屏蔽了几乎所有底层细节,你只需要关心业务数据就行。
但甜蜜期通常撑不过两周。一旦你开始往真实业务场景里塞数据,阵痛就来了。我印象最深的一次,是给一个内部知识库做问答,文档量大概三千多份,格式混杂着 PDF、Word、Markdown 和网页存档。用某框架默认的切分策略跑出来的效果惨不忍睹:一个完整的操作步骤被切成了三段,检索的时候只召回了中间那段,LLM 拿到残缺的上下文,回答自然驴唇不对马嘴。我想去调切分参数,结果发现框架把切分逻辑封装在好几层抽象后面,改一个参数要顺着继承链找半天。
更让人头疼的是检索环节。默认的向量检索用的是单一 Embedding 模型加余弦相似度,遇到"同义不同词"或者"需要多跳推理"的问题就歇菜。我想加一路 BM25 做混合检索,再叠一个 Rerank 模型,结果发现框架虽然支持,但配置项散落在四五个文件里,而且多路召回的打分融合逻辑是个黑盒,我根本不知道它内部是怎么加权合并的。这种"能跑但不可控"的状态,对于一个要长期维护的系统来说,是致命的。
1.2 自研RAG到底要解决哪些"卡脖子"问题
痛定思痛之后,我梳理了一下自研 RAG 真正要解决的核心问题,其实就四类。
第一类是切分策略的可控性。不同文档类型需要不同的切分逻辑,技术文档要按标题层级切,对话记录要按轮次切,代码文件要按函数切。开源框架往往只提供通用的递归字符切分,想按语义或结构切,得自己写。
第二类是检索链路的可解释性。向量检索、关键词检索、Rerank 这三路怎么组合,权重怎么定,召回多少条进 Rerank,这些参数直接决定最终效果。如果融合逻辑是黑盒,你调优就全靠玄学。
第三类是数据流转的可观测性。一份文档从入库到被检索出来,中间经过了哪些处理、生成了多少个 chunk、每个 chunk 的向量维度是多少、检索时的相似度分数分布如何,这些信息在排查问题时至关重要。开源框架大多只给你最终结果,中间过程不透明。
第四类是与业务系统的耦合度。真实项目里,RAG 往往不是孤立的,它要和权限系统、缓存系统、监控系统对接。开源框架的插件机制虽然灵活,但深度定制时反而成了束缚,你得像做外科手术一样去改它的源码。
想清楚这四点之后,我决定不再纠结于"用哪个框架",而是转向"从框架里学什么"。这就引出了后面的逆向工程思路。
2. 六款开源RAG的架构拆解:我到底从它们身上学到了什么
2.1 LangChain:链式抽象的得与失
LangChain 是我最早接触的框架,它的核心贡献是把 RAG 流程抽象成了一条"链"(Chain)。从 Document Loader 到 Text Splitter,再到 VectorStore、Retriever、LLM,每个环节都是一个可替换的组件,通过 LCEL 表达式串起来。这种设计的好处是模块化程度高,你想换一个 Embedding 模型,只需要替换对应的组件实例。
但 LangChain 的问题也很明显:抽象层数太多。一个简单的检索问答,背后可能涉及十几个类的继承和组合关系。我在调试一个检索结果为空的问题时,顺着调用栈追了七八层才找到真正执行向量查询的地方。而且它的版本迭代非常快,经常出现"昨天能跑的代码今天报错"的情况,因为某个组件的接口签名变了。
从 LangChain 身上,我学到的核心是组件化思想:把 RAG 拆成独立的、职责单一的模块,每个模块定义清晰的输入输出接口。但我也吸取了它的教训:抽象层数要克制,能两层解决的就不要三层,接口要稳定,不要频繁变动。
2.2 LlamaIndex:数据索引的精细化设计
LlamaIndex 最初叫 GPT Index,从名字就能看出它的重心在"索引"上。它对数据结构的抽象比 LangChain 更细致,提出了 Node、Index、Retriever 三层概念。Node 是最小的数据单元,可以携带丰富的元数据;Index 是 Node 的组织形式,除了常见的向量索引,还支持树形索引、关键词索引、列表索引等;Retriever 则负责从 Index 中取数据。
我特别喜欢它的Node 元数据设计。每个 Node 除了文本内容,还可以挂载来源文件、页码、章节标题、时间戳等信息。这些元数据在检索后可以用来做过滤和重排,比如"只检索最近三个月的文档"或者"优先返回来自官方手册的内容"。这个设计在我后来的自研方案里被完整保留了下来。
LlamaIndex 的另一个亮点是Response Synthesizer,也就是拿到检索结果后如何组织成最终答案。它提供了多种合成策略,比如"逐条精炼"、"树形归纳"、"简单拼接"等。这让我意识到,检索之后的上下文组织方式,对最终答案质量的影响不亚于检索本身。
2.3 Haystack:管道式编排的工程化思路
Haystack 是 deepset 团队的作品,它的核心概念是 Pipeline。和 LangChain 的链式抽象不同,Haystack 的 Pipeline 更像是一个有向无环图,每个节点是一个组件,节点之间通过输入输出连接。这种设计在工程化上更清晰,你可以很直观地看到数据从哪个节点流向哪个节点。
Haystack 让我印象最深的是它的组件协议设计。每个组件都必须声明自己的输入和输出,Pipeline 在运行前会做一次校验,确保上下游组件的接口能对上。这个机制在大型项目里非常有用,能提前发现配置错误,而不是等到运行时才报错。
另外,Haystack 对评估的支持比较完善,内置了召回率、精确率、MRR 等指标的计算工具。这提醒我,自研 RAG 不能只关注"能不能跑",还要有一套评估体系来衡量"跑得好不好"。
2.4 RAGFlow:文档解析的深度优化
RAGFlow 是近两年比较火的一个项目,它的最大特色是深度文档理解。普通的 RAG 框架对 PDF 的处理就是简单抽取文本,遇到表格、图片、复杂排版就抓瞎。RAGFlow 引入了 OCR 和版面分析,能识别文档中的表格结构、图片标题、页眉页脚,并把这些信息结构化地保留下来。
我拿一份带复杂表格的技术白皮书测试过,RAGFlow 能把表格转成 Markdown 格式,保留行列关系,而其他框架抽出来的就是一堆乱序的文字。这个能力在处理企业文档时太重要了,因为很多关键信息就藏在表格里。
从 RAGFlow 身上,我学到的核心是文档解析要前置且要深。切分不是简单的按字符数切,而是要先理解文档的结构,再按结构切。这个思路直接影响了我的自研方案:我在切分之前加了一个"文档结构分析"的步骤,先识别标题、段落、表格、列表,再决定怎么切。
2.5 Dify:可视化编排与工程化落地
Dify 严格来说不只是一个 RAG 框架,它更像是一个 LLM 应用开发平台。但它的 RAG 部分做得相当完整,而且提供了可视化的编排界面。你可以通过拖拽节点的方式,把文档处理、检索、LLM 调用串成一条工作流。
Dify 让我学到的不是某个具体技术点,而是工程化落地的思路。它把 RAG 的各个环节都做成了可配置的模块,并且提供了日志、监控、版本管理等功能。这让我意识到,自研 RAG 不能只做一个"能跑的脚本",而要做一个"能运维的系统"。日志要打全,关键指标要能监控,配置要能热更新,这些都是生产环境的基本要求。
2.6 FastGPT:轻量级与高性能的平衡
FastGPT 的定位是"开箱即用的知识库问答系统",它的架构比前面几个都轻。它没有那么多抽象层,代码结构相对直白,核心逻辑就是"向量检索 + 关键词检索 + Rerank"三路融合。
FastGPT 让我最欣赏的是它对性能的优化。它用了连接池管理数据库连接,对向量检索做了缓存,对高频查询做了预计算。这些优化在数据量大的时候效果非常明显。我实测过,在百万级向量库上,FastGPT 的检索延迟比某些"重框架"低不少。
从 FastGPT 身上,我学到的核心是不要过度设计。RAG 的核心链路其实就那么几步,把每一步做扎实,比堆砌花哨的功能更重要。
把这六款产品拆完,我脑子里已经有一张比较清晰的架构图了。接下来就是把这些学到的设计思想,融合成一套自己的方案。
3. 逆向出来的自研RAG蓝图:分层架构与核心模块
3.1 整体分层:从数据接入到答案生成
我把自研 RAG 分成了四层,从下到上依次是数据接入层、索引存储层、检索召回层、生成编排层。这个分层不是拍脑袋定的,而是综合了前面六款产品的设计后,找到的一个平衡点:既不过度抽象,又能保证各层职责清晰。
数据接入层负责把各种格式的原始文档,转成统一的内部表示。这一层要处理 PDF、Word、Markdown、HTML、纯文本等格式,还要做文档结构分析。我参考了 RAGFlow 的思路,在解析阶段就尽量保留结构信息,比如标题层级、表格、列表。
索引存储层负责把处理好的文档切分成 chunk,生成向量,存进向量库,同时建立关键词索引。这一层我借鉴了 LlamaIndex 的 Node 设计,每个 chunk 都携带丰富的元数据,包括来源、页码、章节路径、时间戳等。
检索召回层负责根据用户 query,从索引中召回相关 chunk。这一层是 RAG 的核心,我采用了三路召回:向量检索、关键词检索、元数据过滤。三路结果经过融合和 Rerank 后,输出最终的上下文。
生成编排层负责把检索到的上下文和用户 query 组织成 Prompt,调用 LLM 生成答案。这一层我参考了 LlamaIndex 的 Response Synthesizer,支持多种上下文组织策略,并且加入了引用标注和置信度评估。
这四层之间通过明确定义的接口通信,每层都可以独立替换和测试。比如你想换一个向量库,只需要实现索引存储层的接口就行,上层完全不用改。
3.2 文档解析与切分:结构感知的Chunk策略
切分是 RAG 里最容易被低估的环节。很多人觉得切分就是按固定字符数切,其实远不止如此。我的做法是先分析结构,再决定切分策略。
具体来说,文档进来之后,先经过一个结构分析器,识别出标题、段落、表格、列表、代码块等元素。然后根据元素类型选择不同的切分方式:
- 标题:作为 chunk 的元数据保留,同时作为切分的边界。一个二级标题下的内容,尽量切在一个或几个 chunk 里,不要跨标题切。
- 段落:按语义完整性切,如果一段太长,再按句子边界切。我设了一个阈值,单 chunk 超过 512 个 token 就考虑切分,但切分点要选在句子结束的地方。
- 表格:整个表格作为一个 chunk,转成 Markdown 格式保留结构。如果表格太大,按行切分,但每行都要带上表头。
- 列表:一个完整的列表作为一个 chunk,如果列表项太多,按项切分,但保留列表的上下文说明。
- 代码块:整个代码块作为一个 chunk,保留语言标识。
这样切出来的 chunk,语义完整性比简单的字符切分好很多。我做过对比测试,在技术文档问答场景下,结构感知切分的召回准确率比固定字符切分高了将近二十个百分点。
另外,我还会给每个 chunk 加上上下文前缀。比如一个 chunk 来自"第三章 部署指南 > 3.2 配置数据库",那这个 chunk 的文本前面会加上"[部署指南 - 配置数据库]",这样即使 chunk 被单独检索出来,LLM 也能知道它属于哪个章节。这个小技巧对提升答案质量很有帮助。
3.3 三路召回融合:向量、关键词与元数据的协同
检索是 RAG 的心脏。单一向量检索的问题在于,它对"精确匹配"不敏感。比如用户问"错误码 E5021 是什么意思",向量检索可能召回一堆语义相似但错误码不同的文档。这时候就需要关键词检索来兜底。
我的三路召回设计是这样的:
第一路:向量检索。用 Embedding 模型把 query 和 chunk 都转成向量,算余弦相似度,取 Top-K。这一路负责语义匹配,能处理"同义不同词"的情况。我用的 Embedding 模型是 BGE 系列的中文模型,实测在中文场景下效果比 OpenAI 的 text-embedding-ada-002 好。
第二路:关键词检索。用 BM25 算法做全文检索,取 Top-K。这一路负责精确匹配,对专有名词、错误码、型号等特别有效。我用的是 Elasticsearch 的 BM25 实现,因为它成熟稳定,而且支持中文分词。
第三路:元数据过滤。根据 query 中提取的实体(比如时间、产品名、文档类型),对 chunk 的元数据做过滤。这一路负责缩小范围,比如用户问"最新的部署文档",就可以过滤掉旧版本的 chunk。
三路召回的结果,需要做融合。融合策略我试过几种,最后选的是RRF(Reciprocal Rank Fusion)。RRF 的思路很简单:不看具体分数,只看排名。每个结果在各自召回列表里的排名,取倒数后相加,得到最终分数。这样做的好处是避免了不同召回路的分数尺度不一致的问题,而且实现简单,效果稳定。
融合之后,取 Top-N 条(我一般设 20 条)进入 Rerank 环节。Rerank 用的是 Cross-Encoder 模型,它会把 query 和每个 chunk 拼在一起做一次完整的注意力计算,打分更准,但速度慢。所以只对少量候选做 Rerank,兼顾效果和性能。
3.4 上下文组织与Prompt工程:让LLM用好检索结果
检索出来的 chunk 怎么组织成 Prompt,直接决定最终答案的质量。我见过很多 RAG 系统,检索结果明明是对的,但 LLM 就是答不好,问题往往出在 Prompt 上。
我的做法是结构化组织上下文。每个 chunk 在 Prompt 里不是简单拼接,而是带上来源标注和序号,格式如下:
[1] 来源:部署指南.pdf 第12页 内容:配置数据库连接时,需要设置以下参数... [2] 来源:常见问题.md 内容:如果连接失败,请检查...然后在 Prompt 里明确告诉 LLM:优先使用编号靠前的资料,如果资料中没有相关信息,就明确说"根据现有资料无法回答",不要编造。这个"不要编造"的指令非常重要,能大幅降低幻觉率。
另外,我还会根据 query 的类型,动态调整上下文组织策略。如果是"事实型"问题(比如"XX 参数默认值是多少"),就直接把最相关的几条 chunk 拼进去;如果是"总结型"问题(比如"总结一下这个模块的功能"),就用 Map-Reduce 的方式,先对每个 chunk 单独总结,再合并总结。
3.5 评估与迭代:没有度量就没有优化
自研 RAG 最大的优势,就是你可以建立一套自己的评估体系。我在系统里内置了一个评估模块,支持两种评估方式:离线评估和在线评估。
离线评估用标注好的问答对,计算召回率、精确率、MRR、答案准确率等指标。我一般会准备一百到两百个测试问题,覆盖不同类型和难度。每次调整切分策略、检索参数或 Prompt,都跑一遍离线评估,看指标有没有提升。
在线评估则是收集真实用户的反馈,比如"这个答案有帮助吗"的点赞点踩,以及用户是否点击了引用的来源。这些数据能反映系统在真实场景下的表现,比离线评估更有说服力。
评估体系建立起来之后,优化就有了方向。我踩过的一个坑是:一开始只看答案准确率,结果发现准确率上去了,但召回率下降了。后来才明白,是因为 Rerank 太激进,把一些相关但排名靠后的 chunk 过滤掉了。所以评估指标要综合看,不能只盯一个。
4. 落地过程中的真实踩坑与调优记录
4.1 切分粒度:太大和太小都是灾难
切分粒度是我踩的第一个大坑。最开始我按 1000 个字符切,结果 chunk 太大,检索出来的内容里有一半是无关信息,LLM 被干扰得厉害。后来改成 200 个字符,又发现 chunk 太小,一个完整的逻辑被切碎,检索出来的都是片段,LLM 拼不起来。
经过反复测试,我最终把 chunk 大小定在300 到 500 个 token之间,并且加了50 个 token 的重叠。重叠的作用是防止关键信息正好落在切分边界上被切断。这个参数不是绝对的,要根据文档类型调整。技术文档可以大一点,对话记录要小一点。
还有一个细节:不同层级的标题下,chunk 大小可以不同。比如一级标题下的内容,chunk 可以大一些,因为它是概括性的;三级标题下的内容,chunk 要小一些,因为它是具体的操作步骤。这种动态调整让检索效果又上了一个台阶。
4.2 向量模型的选型:别迷信排行榜
选 Embedding 模型的时候,我一开始迷信 MTEB 排行榜,选了一个排名很高的英文模型。结果在中文数据上效果一塌糊涂,因为那个模型的中文训练数据很少。后来换成 BGE 的中文模型,效果立刻好了很多。
这件事让我明白:排行榜只能参考,不能迷信。选模型一定要在自己的数据上实测。我一般会准备一批 query 和对应的正确 chunk,然后算不同模型的召回率,选召回率最高的那个。
另外,向量维度也不是越高越好。高维度确实能表达更多信息,但存储和计算成本也高。我实测过,768 维和 1024 维在大多数场景下效果差异不大,但 768 维的存储成本低了四分之一。所以除非你的数据特别复杂,否则 768 维够用了。
4.3 检索为空:那些让人抓狂的边界情况
检索为空是 RAG 系统里最让人抓狂的问题之一。用户问了一个问题,系统返回"根据现有资料无法回答",但你明明知道库里有相关资料。这种情况我遇到过好几次,排查下来原因各不相同。
有一次是因为分词问题。用户问的是"如何配置 Nginx",但文档里写的是"nginx 配置方法",大小写不一致,关键词检索没匹配上。后来我在入库时统一做了小写处理,问题解决。
还有一次是因为向量模型对长 query 不友好。用户问了一个很长的复合问题,向量模型把整个 query 编码成一个向量,结果语义被稀释了,检索不到相关 chunk。后来我加了一个 query 改写步骤,先用 LLM 把长 query 拆成几个短 query,分别检索后再合并,效果好了很多。
最常见的原因是文档本身就没有覆盖这个问题。这时候系统返回"无法回答"其实是正确的行为,但用户会觉得系统不好用。我的做法是在返回"无法回答"的同时,给出最相关的几条资料链接,让用户自己判断。这样即使没直接回答,也提供了有价值的信息。
4.4 幻觉抑制:让LLM老实说话
幻觉是 RAG 要解决的核心问题,但 RAG 本身也会产生幻觉。我遇到过好几次,检索结果里明明没有某个信息,但 LLM 就是"编"了一个答案出来。后来我总结了几条抑制幻觉的经验。
第一,Prompt 里要明确约束。我用的指令是:"请严格基于以下资料回答问题。如果资料中没有相关信息,请直接说'根据现有资料无法回答',不要根据常识或推测补充。"这条指令能挡掉大部分幻觉。
第二,降低 temperature。生成答案时把 temperature 设成 0 或 0.1,让 LLM 尽量做确定性输出,减少随机发挥。
第三,加引用标注。要求 LLM 在答案中标注每个信息的来源编号,比如"根据[1],配置步骤如下..."。这样即使出现幻觉,也能通过检查引用快速发现。
第四,做答案校验。生成答案后,再用一个轻量模型检查答案中的每个事实是否能在检索结果中找到依据。找不到依据的就标记出来,提示用户注意。这个步骤会增加一点延迟,但对准确性提升明显。
4.5 性能优化:从秒级到毫秒级的检索
数据量上来之后,检索延迟成了瓶颈。最开始百万级向量库,一次检索要一两秒,用户体验很差。我做了几轮优化,把延迟降到了两百毫秒以内。
第一轮优化是加缓存。对高频 query 的检索结果做缓存,下次同样的 query 直接返回缓存。缓存用 Redis,设置合理的过期时间。这一招把重复 query 的延迟降到了毫秒级。
第二轮优化是向量索引调参。我用的是 HNSW 索引,调整了 efSearch 和 M 参数,在召回率和速度之间找平衡。efSearch 调小能提速,但召回率会降;调大则相反。我实测下来,efSearch 设成 64 是个不错的平衡点。
第三轮优化是并行召回。三路召回本来是串行的,后来改成并行,用线程池同时发起,总延迟取决于最慢的那一路,而不是三路之和。
第四轮优化是Rerank 降级。Rerank 模型虽然准,但慢。我的策略是:如果候选数量少(比如少于 10 条),就不做 Rerank,直接按融合分数排序;只有候选多的时候才做 Rerank。这样在大多数情况下都能省掉 Rerank 的开销。
5. 这套蓝图还能怎么扩展
5.1 多模态RAG:图片和表格的处理
现在的 RAG 大多只处理文本,但真实文档里有大量图片和表格。我最近在尝试把多模态能力加进来。思路是:对图片用 OCR 提取文字,同时用视觉模型生成图片描述;对表格转成 Markdown 保留结构。这些多模态信息作为额外的 chunk 存入索引,检索时和文本 chunk 一起参与召回。
实测下来,加了图片描述之后,涉及图表的问题回答准确率有明显提升。比如用户问"那个架构图里有哪些组件",纯文本 RAG 完全答不了,但多模态 RAG 能根据图片描述给出答案。
5.2 智能体化:让RAG学会多步推理
传统 RAG 是"一次检索,一次生成",但很多问题需要多步推理。比如"对比 A 方案和 B 方案的优缺点",需要先分别检索 A 和 B 的资料,再对比。这种场景下,可以把 RAG 包装成一个智能体工具,让 LLM 自己决定什么时候检索、检索什么、检索几次。
我试过一个简单的实现:给 LLM 一个"检索"工具,让它自己规划检索步骤。对于复杂问题,LLM 会先检索 A 的资料,再检索 B 的资料,最后综合生成答案。这种方式比单次检索灵活得多,但延迟也更高,适合对准确性要求高、对延迟不敏感的场景。
5.3 知识图谱融合:结构化知识的补充
向量检索擅长处理非结构化文本,但对结构化关系无能为力。比如"张三的上级是谁"这种问题,如果文档里没有直接写,向量检索就找不到。这时候可以引入知识图谱,把实体和关系结构化存储,检索时同时查向量库和图谱,把结果融合。
我目前还在探索阶段,初步的想法是用 LLM 从文档中抽取实体和关系,构建一个轻量级的知识图谱。检索时,先用图谱做一轮关系推理,把推理结果作为额外的上下文补充给 LLM。这个方向潜力很大,但工程复杂度也高,需要权衡投入产出比。
5.4 持续学习:让系统越用越聪明
RAG 系统上线不是终点,而是起点。用户的每一次提问、每一次反馈,都是优化系统的素材。我在系统里加了一个反馈闭环:用户对答案的点赞点踩、对引用来源的点击,都会被记录下来。定期分析这些数据,找出检索效果差的问题类型,针对性地优化切分策略或检索参数。
另外,我还做了一个"热门问题"的挖掘。把高频 query 聚类,看看用户最关心什么,然后针对性地补充文档或优化索引。这个做法让系统的答案准确率在三个月内提升了十几个百分点。
这套自研蓝图从逆向六款开源产品开始,到现在已经跑了半年多,支撑了一个中等规模的企业知识库。过程中踩的坑不少,但每一步的取舍和优化都有据可循。如果你也在做 RAG,我的建议是:不要一上来就追求大而全,先把核心链路跑通,再逐步加能力。开源框架值得学,但不要被它绑死。真正理解每个环节的原理,才能做出适合自己业务的东西。