1. 为什么还要自研RAG:开源项目解不了的那道题
1.1 开源RAG的两张面孔
最近两年我几乎每周都会被同一个问题砸中——“团队想上RAG,是直接用开源框架,还是自己写一套?”这个问题之所以难回答,是因为问法本身就把RAG当成了一个成品。但在实际工程里,RAG更像是一条需要你亲手维护的数据管道:文档要解析、知识要切分、索引要更新、检索要对齐、生成还要防幻觉。开源项目能帮你把其中最通用的部分做好,却很难替你回答“你的业务知识到底以什么形态存在、以什么粒度被检索”这种只有你自己知道的问题。
在我拆解过的开源RAG系统里,它们大致分两类。一类是通用编排型,特点是组件化程度高,把加载、拆分、向量化、检索、生成都抽象成可插拔模块。这种产品的好处是灵活,你的业务越奇怪,越能通过自定义模块满足;坏处是你得自己组装,等于一边用别人的零件,一边还得自己画图纸。
另一类是开箱即用型,更像一个装好的知识库问答系统。上传文档、自动切分、对话检索,半小时内就能跑出一个像模像样的Demo。但问题也出在“装好”上:当你要调整检索策略、嵌入公司权限体系、把多模态文档拆成结构化知识时,你会发现每一处定制都在跟产品预设的边界较劲。
我的态度很明确:如果你只有几百篇文档、对问答体验要求不极端,直接用开源方案是性价比最高的选择。但如果你身处数据敏感行业、需要毫秒级检索响应、或者知识形态非常规整——比如要精确引用某个章节号——那自研就不是一个选择,而是唯一出路。这篇文章分析的,就是看清开源产品帮我们趟过的路之后,再决定哪些值得自己重做。
1.2 判断“该不该自研”的三道题
我把自研的判断标准收敛成三道题,每道题过一遍,答案基本就清楚了。
第一道题:数据链路能不能全程内闭环。这里说的不只是数据不出内网,还包括权限模型是否可以在检索层直接生效。很多开源方案把权限放在应用层,意味着文档进了索引之后,要在查询时临时过滤,这一步做不好就会造成越权检索。如果你的场景里“谁能看到哪份文档”是不可妥协的硬需求,自研检索层几乎是必须的。
第二道题:延迟指标是否有硬约束。通用RAG框架为了适配更多场景,往往会在检索链路里多出几层抽象调用。一次查询在框架内部的流转时间,可能比向量数据库本身的检索时间还要高。对内部知识助手来说这没什么,但对高并发场景,每一毫秒都值得从自己的链路里挤出来。
第三道题:知识形态是否足够规整。如果你的知识库是清一色的Markdown文档,开源切分器完全够用;如果里面有扫描件、表格、PPT、甚至一段视频字幕,每一种形态都对应着不同的解析和索引策略,这时开源方案的通用处理就会变成累赘。
这三道题只要有两道命中,我建议就别犹豫,直接进入自研。但注意,自研不意味着重新发明轮子。下面的内容就是我从六款开源产品里“抄”出来的作业,把它们的共性收敛成一套可以直接复用的架构蓝图。
2. 逆向工程:六款开源产品的共性底座
2.1 六款产品的技术画像
我先说明一点:下文提到的是我在分析中归纳出的产品画像,不针对特定项目,只谈技术取向上的共性问题。这六款产品覆盖了不同定位——通用编排、轻量工具、企业搜索、学术问答、云原生网关、多模态知识库。每款产品我重点看四件事:链路如何编排、检索如何设计、上下文如何管理、失败如何兜底。
| 产品代号 | 定位 | 让我印象最深的特性 | 对自研的最大启发 |
|---|---|---|---|
| 产品A | 通用编排型 | 组件间用统一数据模型解耦 | 链路的抽象方式决定了系统上限 |
| 产品B | 轻量工具型 | 下载即跑,内存占用极小 | MVP阶段够用,深入定制会碰到天花板 |
| 产品C | 企业搜索型 | 连接器与权限语义内置 | 权限要进入索引设计,不能事后过滤 |
| 产品D | 学术问答型 | 长文档层级索引与引用溯源 | 答案能否溯源,直接决定可信度 |
| 产品E | 云原生网关型 | 全链路可观测与API化 | 服务化必须配套链路追踪与限流 |
| 产品F | 多模态型 | 表格与图片解析的前置处理 | 知识抽取的粒度决定了检索的天花板 |
这六款产品放在一起,表面看差异很大,但当我真的去比对各层的设计取舍后,发现它们的架构内核惊人地一致。顺着一致性继续挖,就能挖出一套可以复用的蓝图。
2.2 从差异中提炼出的四层架构
所有成熟的开源RAG,无论外表多花哨,最终都收敛到了四层结构。
第一层是接入与解析层。文档从哪里来、以什么格式进入系统、如何被清洗成标准文本。很多团队觉得这层简单,实际上RAG效果差,有相当比例是解析脏数据导致的。比如PDF表格被拆成乱序文本,章节标题和正文被当成平级内容。开源产品在这一层做得好的,普遍采用“先识别结构、再抽取内容”的思路,而不是直接按页面暴力切分。
第二层是索引与检索层。包括文本切分、向量化、索引构建、召回、重排。这是整个RAG系统的发动机,也是最值得自研的地方。
第三层是编排与会话层。处理用户输入该如何被理解、是否需要多轮上下文、是否需要路由到不同的检索策略。开源框架通常把这一层做成流程编排,自研时则要考虑引入更精细的状态管理。
第四层是生成与评估层。负责把检索到的片段组装成Prompt、交给大模型生成答案,并记录答案质量、反馈等信号。
为什么所有产品都收敛到这套分层?因为每一层对应一个独立变化的问题域。解析方式会随文档形态变,检索策略会随业务语义变,会话逻辑会随交互场景变。如果把这些耦合在一起,任何一处的调整都会牵动整条链路。分层不是架构洁癖,是让人能在不碰其他环节的前提下,单独优化某一环。
我把这套分层当成自研的骨架,接下来的内容,就是针对每一层给出具体的设计和参数建议。
3. 自研蓝图:一套可直接落地的模块化架构
3.1 检索层设计是成败关键,混合检索必须一开始就做
自研RAG最容易犯的错误,是把“向量检索”当成“RAG检索”的全部。真实业务里,用户问题往往包含精确的业务编号、型号、人名,这类词在向量空间里做语义匹配经常失效。我要查找“编号XX的订单退换规则”,如果知识库里只有“订单号为XX的售后政策”,纯向量检索可能因为语义偏移而召回不到。所以混合检索不是优化项,而是基础项。
我建议的默认组合是BM25关键词召回加向量语义召回,两条路各自取回候选集,合并后再交给重排模型。关键词召回负责精确命中,向量召回负责“意思相近但说法不同”的情况。两者结果用权重合并,权重不必一开始就想得很细,不必刻意追求比例,先按50比50跑,然后在评测集上调。
切分策略上,我强烈建议基于结构切分,而不是固定字符数。想象一份操作手册,它天然分章节、小节、步骤,如果你的切分点落在“步骤2”和“步骤3”中间,一组完整动作就被拆成了两半,答案引用时就只能说半截话。更稳妥的做法是先用标题层级把文档切成若干语义块,再对超长块做二次切分。这样可以保证每个检索片段内部是有完整语义的。
关于参数,直接给出我实测过比较稳的默认值:chunk_size取256到512之间,overlap取20%到25%,top_k初排时取30到50,重排后保留5到8个片段。这里的思路是先给足候选量,再靠重排把质量提上去。如果你一上来就取top_k=3,向量召回稍有偏差,整个答案就是错的,而且很难排查是哪个环节出了问题。
3.2 重排层与索引更新:两个最容易拖后腿的位置
重排是整个链路里最容易被省略、也最容易出效果的环节。初排的候选片段是从BM25和向量两条路合并来的,顺序并不代表真实相关性,直接用这个顺序去拼Prompt,大模型会优先关注排在前面的内容,偶尔就会被不相关的片段带偏。加一个轻量重排模型之后,相关性靠模型精排,答案质量会有可感知的提升。注意重排毕竟是有代价的环节,qps一高延迟就上来了,所以需要掌握节奏:初排只负责“别漏”,重排才负责“选准”。
索引更新是另一个坑。知识库不会是一成不变的,文档更新之后,旧片段如果还残留在索引里,会检索出过期内容。我踩过的做法是把索引做成双buffer结构:新索引在后台构建,build完以后整体切换,查询永远只读当前版本。这样可以避免边写边读的冲突,也不用担心构建过程中出现半新半旧的状态。
3.3 编排层:从线性流程到会话状态管理
很多自研RAG第一版都是线性流程:用户提问、检索、拼接Prompt、生成、返回。这种写法在单轮问答里没问题,一旦进入多轮对话就露馅了。用户说“那第二个方案呢”,这句话单独看没有任何检索价值,真正的检索条件是上一轮提到的内容。所以编排层必须有会话状态管理,至少要能区分两类输入:独立问题还是对历史的追问。
实现上不需要一上来就上复杂框架,可以从一个简单的消息历史队列开始:保存最近N轮的用户问题和系统答案,当检测到“代词开头”或“指代性表达”时,把最近一轮的上下文拼进检索条件。更进一步的方案是接入意图识别路由,根据问题类型决定走全文检索、走FAQ精确匹配还是走表格问答。这套路由的价值在知识库变复杂后会越来越明显,值得在蓝图阶段就预留扩展位。
3.4 生成层:把“有依据”变成Prompt里的硬约束
最后是生成层的设计。我见过很多自研系统跑通以后,检索质量上去了,但答案依然胡说,问题多半出在Prompt没有把引用约束讲死。需要明确要求模型“只能基于上下文片段作答,如果片段没有覆盖问题内容,直接说不知道”,同时把检索到的片段按顺序编号注入Prompt,并要求输出中必须携带对应的编号引用。这一步能大幅缓解幻觉,也让前端可以给用户展示“答案来自文档哪一段”。
此外,生成层还要做好兜底。当召回的平均得分低于某个阈值时,说明知识库里可能根本没有这个答案,此时与其硬答,不如返回预设话术,引导用户换一种问法或者联系人工。这块逻辑和检索层的相关度分是联动的,应该在蓝图阶段就确定下来,而不是等上线被用户问了怪问题再补救。
4. 落地过程中踩过的坑与排查实录
4.1 检索质量问题的排查路径
自研过程中,遇到最多的不是系统跑不起来,而是“跑起来了但答案不对”。这类问题我总结了一条排查路径,遇到答非所问,先按这个顺序找原因。
第一步查分段。把检索到的命中片段完整打印出来,看片段内部是否是完整语义。我遇到过一份合同被按字符数固定切分,导致赔偿条款被从中间切开,模型只能依据半句话作答。改成按条款编号切分后问题立刻消失。
第二步查召回。用原始问题直接查BM25单独召回和向量单独召回的结果,如果只有其中一路召回正确,说明合并策略或者其中一路的参数有问题。这时候我会分别加大关键词权重和向量权重做对照实验。
第三步查重排。初排的正确片段如果被重排压到很后面,而错误片段排到了前面,问题就出在重排模型和业务领域不匹配。换一个在相近领域数据上训练过的重排模型往往能解决。
排查工具的细节也很关键。从第一版开始就要给每次查询记录结构化日志:原始问题、会话上下文、各路的召回片段与得分、重排后的顺序、最终的Prompt、模型输出。这套日志一旦到位,绝大多数质量问题都能在一次复现中定位到具体环节,而不是靠猜。
4.2 性能与稳定性问题速查
性能问题在自研RAG里比功能问题更难缠,因为延迟往往不是某个环节单独造成的,而是链路叠加出来的。我把踩过的几个典型问题整理成了速查表。
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 首Token延迟明显偏高 | 重排与生成串行执行 | 重排结果提前流式返回,生成阶段并行预热 |
| 文档更新后仍检索到旧内容 | 索引写入和查询未隔离 | 采用双buffer索引,构建完成后整体切换 |
| 大量长文档导致内存膨胀 | 切分前全量加载解析 | 按页/按块懒加载,解析完立即释放 |
| 多路召回结果互相冲突 | 合并权重与阈值没有匹配 | 在评测集上做权重网格搜索,而非手动拍脑袋 |
| 高并发下向量检索超时 | 未对embedding和检索引擎做连接复用 | 增加连接池,禁用每次查询新建连接的方式 |
另外有一个看起来不起眼但很影响体验的点:知识库更新频率如果很高,频繁触发全量索引重建会吃掉大量CPU和向量数据库写入带宽。建议为增量更新单独设计一条轻量管道,新文档在进入队列后先做切分和向量化,再按批次合并进在线索引。全量重建只保留给首次上线或索引结构变更。
4.3 一个真实排查案例的复盘
有一次我把自研系统切到生产小流量后,用户反馈“对某业务问题的回答从完整变成了残缺”。我打开结构化日志,发现召回阶段的得分都在正常区间,但拼进Prompt时只保留了前三个片段,而正确答案所在的片段排在了第四位。往上一查,原来是重排阈值被我顺手调高了,导致真正相关的片段被当成低置信度结果过滤掉。这个问题的教训是两句话:任何阈值改动都要在评测集上留一个对照结果;重排输出不要静默丢弃,而是保留到调试日志里。排查RAG问题,信息永远比猜测管用。
5. 从蓝图到工程:上线前的自检清单
5.1 评测集先行,而不是感觉先行
很多团队的自研RAG死在“好不好用”这四个字上,因为没有一个客观标准。我强烈建议在写第一行业务代码之前,先花时间从真实业务数据里整理出一份评测集。数量不用贪多,质量一定要高,以100到200条为起点,覆盖三类问题:精确命中类,比如型号、编号、名称;多跳推理类,需要综合多个片段才能回答;无关拒答类,知识库里本来就没有答案,系统应明确说不知道。
评测指标不要只看一个点。核心指标是这三项:召回命中率,即正确片段是否进入最终候选集;答案忠实度,即答案内容是否都能在给定片段中找到依据;端到端相关性,即最终回答是否解决了用户的问题。每次改动检索参数、切分策略或Prompt,都要在这一套评测集上重新跑一遍,结果记录成表格。否则你很难判断这次优化是改好了还是改坏了,只能靠“感觉好像变好了”,这在大规模迭代时会非常痛苦。
5.2 可观测性设计,从第一行代码开始
自研RAG是一个典型的多组件系统,前面是网关,中间是编排服务,后面是向量库和大模型服务。任何一个环节出问题,表象都是“回答不对”或“响应变慢”。如果没有全链路追踪,排查起来就像在一个黑盒子里猜。我的做法是从第一版就引入链路ID,从用户请求进入系统的那一刻生成,贯穿会话状态、检索日志、重排记录、Prompt组装、模型响应,每一条日志都带上这个ID,再配合按时间线聚合的视图,线上问题基本可以在分钟内定位。
灰度策略同样不能省。新索引、新Prompt、新重排模型都不应该全量上线。稳妥的方式是以用户维度切5%小流量,观察检索命中率与用户负面反馈两个指标。注意负面反馈要主动收集,比如在回答下方放点赞和点踩按钮,并在点踩时把答案和对应上下文一起回流到日志,这组数据会成为你下一轮优化的最大养料。
5.3 容易被忽略的知识库治理
我在最后想专门提一个点:RAG的效果上限不是由模型决定的,而是由知识库质量决定的。文档过时、重复、多种版本并存,这些问题不管检索做得多好,都会侵蚀答案质量。所以上线前最好能建立一份知识库清单,记录每个知识来源的更新时间、负责人、解析状态、最近被检索到的次数。隔一段时间看这份清单,你会发现有些文档进入索引后从未被触发过,有些文档反复被高亮引用但内容已经过时。这种治理动作看着不性感,却往往是决定系统长期口碑的关键。
自研RAG走到这里,从架构蓝图到工程细节已经基本完整了。最后说一点我个人的体会:自研最大的坑不在于写代码,而在于把开源产品里的取舍当成自己的需求。真正有价值的拆解,不是看它用了哪个组件,而是理解它为什么在那个位置做了那样的决策。先跑通一个完整的闭环,再逐层优化,是我认为最靠谱的自研路径。希望这篇蓝图能帮你少走一段弯路。