我一直在想一个挺反直觉的问题:为什么很多做RAG的人,明明代码写得也还行,最后做出来的系统却总感觉差口气?不是检索模型不够好,不是大模型不够聪明,而是整体设计根本没有把“检索增强”当成一个系统工程来做。为了弄明白这个问题,我在准备自研RAG方案时,没有急着选型或写代码,而是挑了一批有代表性的开源RAG项目,逐个跑通、拆代码、看架构,最后从里面逆向出一套可以直接拿来自研的蓝图。这篇文章就是把那次拆解和落地的过程做个复盘。
先说清楚我的目的:不是让你去抄某个开源项目的代码,而是搞清楚这些项目为什么这样设计,哪些模块必须自己写,哪些可以借现成的,以及自研RAG时真正值得花钱花精力的地方在哪里。如果你是刚开始搭RAG、正在纠结架构的工程师,或者已经在用开源框架但想收回来自己做更可控的团队,这篇内容应该能帮上忙。
1. 为什么非要把开源项目逐个拆开看
我见过太多团队做RAG的方式了:先选一个框架,连上向量数据库,丢几篇文档进去,demo跑通了就觉得完事了。结果等到面对真实业务数据时,问题一个接一个冒出来:表格内容被切得乱七八糟、多轮对话总是翻旧账、员工问了一个语料里没有的问题导致幻觉、某些敏感文档居然被别的部门的人检索到了。这些问题都不是单点技术能解决的,而是架构层面的问题。
所以我采用了一种比较笨但有效的办法:找六个有代表性的开源RAG项目,把它们当成“参考实现”来逆向工程,看每个项目在数据链路的不同环节是怎么做决策的,然后把这些决策综合成一张可用蓝图。
1.1 逆向工程不是在逆向代码,而是在逆向决策
我对每个项目只问四组问题,不追求把每一行代码都读懂。
第一,数据是怎么进来的(接入层)?支持哪些文档格式,有没有权限模型,图片表格怎么处理?第二,数据是怎么被索引的(索引层)?切分策略是什么,嵌入了哪些元数据,用了几个索引?第三,检索和生成是怎么衔接的(编排层)?是简单把检索结果拼进Prompt,还是做了查询改写、多轮记忆、上下文压缩?第四,项目怎么证明自己有效(评测层)?有没有内置评测集,有没有可观测指标,还是纯靠人工看效果?
这四个问题,恰恰是大多数自研RAG忽略掉的部分。大多数人只关心“嵌入模型选哪个”“向量库用什么”,但开放源代码里真正拉开差距的,往往是文档解析、元数据管理、检索后的重排、以及一套能反复跑的评测集合。
1.2 六款样本到底怎么选出来的
我选样本的标准有三个:一是能独立跑起来,不依赖某个特定平台;二是代码量跨度要大,既要有适合学习的小型项目,也要有接近企业级的完整平台;三是功能定位要有差异,最好能覆盖RAG的不同侧重点。
最终我选了六款,匿名称作A到F,避免广告嫌疑也避免先入为主。
- 产品A:一个偏轻量的检索增强方案,核心是“向量检索 + 大模型生成”,代码量很小,适合理解RAG最简链路。
- 产品B:一个通用编排框架,把文档解析、检索、记忆、生成拆成可插拔组件,扩展性很强。
- 产品C:一个面向企业级场景的知识库平台,开箱即用,自带权限、多用户和完整的问答界面。
- 产品D:一个以评测和回归测试为卖点的项目,重点关注答案质量、命中率和回归对比。
- 产品E:一个把知识图谱和向量检索结合的方案,强调多跳推理和实体关系。
- 产品F:一个偏全栈多模态的解决方案,文档里包含复杂表格、图片、版面信息,强调结构化抽取。
这六个放在一起,基本覆盖了RAG的各个流派:单路检索、多路混合、图谱增强、企业级平台、评测优先、多模态复杂文档。把它们放在一起拆,比单独研究某一个成熟框架能看到更完整的全局。
2. 横向解剖后发现的三层关键差异
看完六个项目的目录结构和核心代码后,我可以明确说一句:RAG的难点根本不在大模型调用,而在数据侧和评测侧。大多数项目的核心代码里,LLM调用只是薄薄的一层,真正厚重的部分是文档处理、索引管理、检索路由和评测工具。
2.1 文档接入与解析层占了四成以上的代码量
产品C和F在这块投入最重。产品C把PDF、Word、Markdown、HTML、扫描件都做了适配器,每个类型有独立的解析流程。产品F干脆把版面分析、表格识别、阅读顺序还原做成了独立模块,对扫描件还会先走一遍OCR再加布局识别。
最让我意外的是,哪怕是产品A这种极简项目,也没敢把PDF直接塞进向量库,而是先转成Markdown再切块。这个细节特别关键,因为很多自研RAG翻车就翻在这里:直接把PDF文本抽出来一切,表格被拆得四分五裂,标题层级全丢了,检索出来的内容虽然看起来相关,但句子断得前言不搭后语。
真正好用的做法是保留文档结构,把标题、段落、表格、列表的层级关系作为元数据存下来,这样切块的时候可以让一个块尽量落在同一个语义单元里。这不是什么高深技术,就是老老实实把解析层做厚。我在自研方案里直接照搬了这个思路,先用解析器把文档转成带结构标记的中间格式,再做切块。
2.2 索引与检索层:单路到多路的进化路线
产品A代表的是最朴素的单路方案:文档切块,向量化,存入向量库,检索时做相似度搜索,取Top-K。跑demo没问题,但一到真实数据就会暴露一个尴尬的事实——有些高频词和精确匹配的查询,向量召回的效果反而不如传统的关键词检索。
产品E走的是另一条路。它在向量索引之外又维护了一个知识图谱,文档里的实体和实体之间的关系会被抽取出来存成三元组。检索时同时发出去两路查询,一路走向量,一路走图谱,再合并结果排序。
产品B做法更具启发性:它不限制你只能用一个检索器,而是把检索做成一个可插拔的“路由层”,你可以同时挂上向量检索、全文检索和外部API检索,由上层逻辑决定各自的结果怎么合并。从这几个项目身上可以总结出,自研RAG只要数据量超过几千篇文档,就不能只押注向量索引一种路线,至少要考虑“向量 + 关键词”的双通道方案。
2.3 生成与状态管理:谁在做多轮对话的脏活累活
如果你只用RAG做单轮问答,生成部分很简单:把检索到的文本块拼接到Prompt里,丢给大模型就算完事。但一旦涉及多轮对话,问题就来了:用户上一轮提到的指代词(“它的价格是多少?”“那个方案可行吗?”),如果没有上下文记忆,模型根本不知道“它”和“那个”指代什么。
产品B内部做了一套对话状态管理,把历史对话压缩成摘要,再结合当前问题做一个“查询改写”,把包含指代关系的原始问题改写成一个完整的、可单独检索的句子,然后再去索引里查询。产品C更彻底,直接为用户会话维护了一个短期记忆库,最近几轮的关键信息会被存下来,回答时先做记忆检索,再走RAG主链路。
这个把“查询改写”放在检索之前的设计,是自研RAG最容易漏掉但收益很大的环节。我在自己的方案里也复刻了这个机制:多轮场景下,先用模型把当前问题结合历史改写成一个无指代的问题,再进入检索环节。
3. 从六款产品倒推出来的七层自研蓝图
拆完这些项目之后,我脑子里逐渐形成了一张自研RAG的蓝图。它不是某一个产品的复制,而是把六款产品里各自做得好的模块挑出来,重新组合成一个适合自己业务的分层结构。
3.1 蓝图长什么样
整个自研RAG系统可以拆成七层,每层只关心一件事,层与层之间通过清晰的接口对接。这样最大的好处是,每一层都能独立替换、独立测试,也方便定位问题。
| 层 | 职责 | 关键输入 | 关键输出 | 灵感来源 |
|---|---|---|---|---|
| 接入层 | 对接各类数据源 | 文件、网页、数据库、API | 标准化文档对象 | 产品C |
| 解析层 | 解析格式并保留结构 | PDF、Word、扫描件等 | 带结构标记的中间格式 | 产品F |
| 切分与增强层 | 切块并补充元数据 | 中间格式 | 文档块 + 元数据 | 产品A、E |
| 索引与检索层 | 建立多路索引并提供统一检索接口 | 文档块 | 多路召回结果 | 产品B、E |
| 精排与过滤层 | 重排、去重、权限过滤 | 多路召回结果 | 最终进入Prompt的文本 | 产品D |
| 生成与记忆层 | 组装上下文、多轮对话 | 精排结果 + 历史对话 | 答案与引用来源 | 产品B、C |
| 评测与迭代层 | 离线评测与线上反馈收集 | 黄金问答集 | 指标报表、回归结果 | 产品D |
这个七层结构看着有点重,但对自研项目来说,每一层都可以先用最朴素的实现,再逐步迭代,不需要一开始就全部做厚。
3.2 为什么越不起眼的层,越能决定成败
大多数团队自研RAG时,把注意力放在第二层到第四层之间,觉得有了向量索引、接上大模型,系统就建成了一半。但我在拆完这六款产品后反而觉得,最关键的其实是第一层、第五层和第七层。
第一层接入层决定了你能不能把数据“无损”地送进来,如果接入层没做好,后面所有步骤都是在垃圾上加工;第五层精排和过滤决定了进入大模型的信息质量,很多幻觉其实不是模型产生的,而是检索出的坏文本块带进去的;第七层评测决定了你改完一个模块后,怎么知道自己改好了还是改坏了。没有评测层,RAG系统就无法持续演进。
产品D给了一个很好的参考:它内置了一套黄金问答集,每次跑完都会输出命中率、正确率和无效回答率三个指标。我在自研时也模仿了这个做法,用业务部门提供的典型问题建了一套两百条的评测集,每次改动都会跑一遍,看分数变化。
4. 四个关键决策:从拆解到复用的核心细节
光有分层架构还不够,实际写代码的时候,有几个决策点会反复出现。这四个决策的认知,基本决定了一个RAG项目是“能用”还是“好用”。
4.1 分块策略:固定窗口是最偷懒也最危险的选择
把文档切块的方式,直接决定检索质量。很多人图省事,直接按固定token数量切,比如每512个token一块。这种做法对付纯文本短文勉强能行,但一碰到带结构的文档就很容易切出“语义残废块”:表格被从中间截断,标题和正文被拆到两个块的末尾和开头,代码块中间被切断,甚至一句话被分成两半,导致检索时文本块的语义不完整。
我综合六个项目后总结出的最优策略是“结构优先,语义兜底”:先把文档解析成结构化的中间格式,优先按标题层级切块,切不下去的地方再按段落切,段落还太长就按句边界切并带上少量重叠。实测下来,这种切法比固定窗口的检索命中率明显要好,尤其是在企业知识库这种标题层级比较清晰的场景下。
重叠窗口也有讲究。过度重叠会让索引体积膨胀、检索结果重复;完全不重叠又会把跨段落的语义切断。我的经验是重叠一两句话就够了,目的是保持上下文的连贯性,而不是让同一个信息出现在几十个块里。
4.2 混合检索:向量不是唯一答案,BM25依然能打
向量检索擅长语义相似,但它有一个明显的软肋:对精确匹配不敏感。比如产品编号“P-1002-X”,用户输入“P1002X”,向量检索可能把它当成完全不同的东西,但传统的BM25检索可以基于词面重合轻松匹配到正确文档。代码片段、型号、缩写、人名这些场景,字面检索往往更可靠。
产品E率先做了混合检索的示范,产品B则进一步把混合逻辑抽象成了可配置策略。我自研时参考了它们的做法,采用双通道方案:向量索引负责找“意思相近”的块,全文索引负责找“字面匹配”的块,然后用一个合并函数把两边的分数归一化后再排序。
这里有个容易踩的坑:两种检索方式的分数区间不同,不能直接相加。必须各自做归一化处理,比如先转换成排名分或百分位分,再做加权融合。权重怎么调呢?我的经验是先跑一批评测集,用网格搜索试几组权重,取评测指标最优的那组。
4.3 重排到底是不是必需环节
在拆解时我注意到一个很有意思的现象:产品A和产品E把检索结果直接拼给大模型,产品B和产品D则留了重排接口,其中产品D内置了轻量重排逻辑。
重排的价值在于,第一轮粗召回往往带回来几十个块,如果全部放进Prompt,一是超出上下文窗口,二是数量太多会让模型注意力分散。先粗召回再精排,可以把最相关的三到五个块选出来,信息密度会高很多。
但重排不是免费的。用大模型做重排成本太高,不适合高频调用;用专门的排序模型需要额外部署一个模型服务。我的建议是:前期先用“向量分数 + 关键词分数加权”做轻量排序,等评测数据积累到一定程度,发现真的存在“信息相关但排序靠后”的问题时,再引入重排模型。一上来就上重排,常常是杀鸡用牛刀。
4.4 知识组织:元数据比图谱更早该到位
产品E的图谱方案很炫,但我在真正落地时发现,图谱不是每个场景都需要。对企业内部文档来说,更紧迫的是做好基础元数据管理:文件来源、部门、作者、更新时间、阅读权限、文档类型、章节路径。这些元数据被存在每个文本块上,检索时可以做结构化过滤,比如“只查某个部门的文档”“只看最近三个月的资料”“排除敏感级别过高的内容”。
图谱有价值的地方在于处理“多跳问题”,比如“哪个项目经理负责我们采购的那套系统?”,这需要把项目、采购记录、负责人三条信息关联起来。如果你的业务场景经常出现这类跨文档串联式提问,那可以在元数据稳定之后,再渐进式引入实体抽取和图谱存储。
我的建议是:基础元数据永远先做。因为有了文档级别的过滤,至少能解决权限问题和范围收敛问题,这是图谱无法替代的。
5. 真正落到生产环境后才暴露的坑
分层结构和检索策略都是纸面设计,真正让我把这些原则内化成本能的,是跑数据时撞上的那些坑。下面四个问题,基本是每个自研RAG都会遇到的。
5.1 权限隔离不做好,检索就是一场事故
很多时候,我们搭的RAG是面向内部多人使用的。不同部门、不同职级能看到的数据范围不同。如果不做权限隔离,就会出现很尴尬的情况:一个普通员工问“我们公司今年核心项目的预算是多少”,系统从只允许高管访问的文档里检索出了答案,然后光明正大地回答了出来。
处理办法其实不复杂:在索引阶段,就给每个文本块打上权限标签;在检索阶段,先根据当前用户的权限集合做过滤,再进入向量检索。如果权限集合很多,还可以用位图或组ID的方式,避免在千万级向量的集合上做逐条权限判断。权限字段的过滤必须在向量相似度计算之前完成,否则效率会非常差。
5.2 增量更新和删除:开源项目里最少被写清楚的部分
这些项目里,做一次性索引的很多,但把增量更新做扎实的很少。真实企业场景里,文档每天都在变:有新增的,有被修改的,还有被删除的。如果不处理增量,就会出现系统回答里引用了已经无效的旧版本内容,这是非常影响信任感的。
我参考某几个项目的文档管理模块后,做了一个文件状态记录表:记录每个源文件的哈希值、文件路径、解析后的文本块ID列表。每次更新任务跑起来,先扫描源目录,对比哈希值,发现文件变化就删除旧的文本块记录,重新解析、重新索引。删除文件时则直接级联删除关联的文本块。这套机制不复杂,但增量更新能力是RAG从演示走向生产的硬门槛。
5.3 评测集是唯一能救你于水火的东西
没有评测集,RAG就是一台没有仪表盘的飞机。你改了一个切分参数、换了一个检索模型,靠人工试问几个问题根本看不出优劣。产品D启发我最深的一点,就是把“跑测试”做进代码流程里。
我也建了一套评测集,大概两百条带标准答案的问题,分成了四类:可以精确回答的、需要多文档综合的、语料里没有的(用来测幻觉)、以及多轮对话的。每次改动,就跑一遍评测脚本,对比前后两轮的指标变化,而不是靠感觉。没有评测集,你根本不知道自己是在改进还是在开倒车。
5.4 成本控制:嵌入计算不是一次性消费
很多人忽略掉一个隐性成本:嵌入计算。一个五十万字的文档库,切成几万块,每块都要调一次嵌入服务,量一上来费用和等待时间都不小。自研RAG必须考虑增量嵌入调度:新文档来的时候,直接为新增的块算嵌入;老文档没变,就不重复算。还可以在离线低峰期批量跑嵌入任务,避免挤占在线资源。
另外一个常见的浪费是,文档没有任何改动,却因为程序重启导致所有文本块重新嵌入了一遍。加上文件哈希对比机制,这种浪费就能避免。成本控制的核心思路就八个字:能复用就复用,能离线就离线。
6. 一套可落地的自研最小实现路线
最后,我把拆解得到的东西收敛成一条实际可走的路线,给已经被各种概念搞得眼花缭乱的人一个具体参考。
6.1 技术选型的最小组合
自研RAG并不需要一开始就上一套重型分布式系统。我的建议是:
- 文档解析和切分:用Python写一个管道,中间格式用Markdown或JSON,方便调试。
- 向量索引:用一个支持元数据过滤并有Python客户端的开源向量库就够了。
- 全文索引:直接用带BM25能力的全文检索引擎,专门承接字面匹配场景。
- 元数据存储:一个关系型数据库就行,记录文件状态、文本块归属、权限标签。
- 大模型服务:调用通用模型API,初期不必自己在GPU上调模型。
- 评测和流水线:用一套评测脚本加定时任务的调度方式,把离线评测跑起来。
这套组合的好处是每一层都可以独立替换。今天用的向量库不合适,换一个不需要动其他代码;今天想换嵌入模型,只需要重新跑一遍离线嵌入。
6.2 分阶段推进的时间表
我倾向用四个阶段来推进,每个阶段都有明确的验收标准,否则很容易在“不断换新模型”这个坑里出不来。
第一阶段,跑通最简链路。用文档解析、结构切分、向量索引、Top-K检索加生成回答,能对二三十篇文档做单轮问答就算成功。这个阶段的目的不是优化,而是让整条链路完整可见。
第二阶段,建立评测集和增量更新。写两百条评测问题,跑出第一版指标;加入文件状态哈希对比,让新增和修改的文档能自动同步。这个阶段结束后,系统才具备了持续演进的地基。
第三阶段,做检索引擎升级。加入全文检索引擎做混合检索,加上分数归一化和权限过滤,这会让系统在真实业务数据面前变得可用。
第四阶段,做多轮对话和重排。引入查询改写逻辑,再根据此前积累的评测数据决定是否加一个重排层。这一刻,你会发现最初从开源项目里逆向出来的那张蓝图,已经被你自己走完了一遍。
6.3 我最后想分享的一点体会
做了这么多拆解和落地后,我最大的感受是:自研RAG最关键的能力不是写代码,而是判断哪一层需要自己写、哪一层可以借现成的。没有哪个开源项目能直接满足所有业务,但它们的架构思路和模块边界,才是真正值得逆向的东西。如果你的瓶颈是数据权限和格式,那就重点借鉴企业级平台的设计;如果你被多轮对话的指代问题困扰,那就优先把查询改写和记忆层补上。
照着这张蓝图做一遍之后,你会发现,RAG真正考验的是把每一个细节都打磨到位:解析得更结构化、切分得更语义完整、检索得更精准、权限过滤得更严格。这些细节,才是自研RAG和demo级RAG之间的分水岭。