做了几年检索增强生成(RAG)的落地,我有个越来越强的感受:大部分自研RAG翻车,不是模型不够强,而是设计决策没想清楚。很多人一上来就写代码,把文档一切、向量一存、提示词一拼,一测效果稀烂,然后归咎于模型。其实不少问题在动手前就能避免。
去年我抽出三周,把代码托管平台上star数靠前的六款开源RAG项目逐个拆了一遍。拆解方式不是跑个演示就完事,而是从使用层一路拆到索引层,把每条链路走通,再反过来推敲每个模块为什么要这么设计。拆完最大的收获,不是抄到某段代码,而是得到一套可以复用的自研RAG蓝图:哪些模块必须有,哪些参数必须调,哪些坑可以提前绕开。
1. 我为什么选择拆开源:选品逻辑与逆向工程方法
1.1 自研RAG的核心困难不在代码,而在设计决策
先说说为什么自研RAG的核心是设计决策。RAG的本质是把外部知识先检索出来,再喂给生成模型去回答。它是一条流水线,而流水线的瓶颈往往不在最后一环,而在前几环。比如检索不到正确内容,生成模型再强大也只能编。所以真正要决策的,是数据进来怎么清洗、文档怎么切、向量怎么存、检索怎么召回、结果怎么交给模型。这些决策每一项都会直接影响效果,而且越早拍板影响越大。
我在实际项目里见过太多反面案例:团队花了两周把整个链路跑通,结果上线后发现用户问一个专有名词,系统返回一堆语义相近但完全不对的内容。这时候再回头调切片参数、换检索策略,成本比一开始设计时高出一个量级。所以拆开源项目,核心目的不是看别人写了什么代码,而是看别人在同样的岔路口做了什么选择,以及为什么那样选。
1.2 逆向工程的三个层次:我到底在拆什么
我习惯把逆向工程分成三个层次。第一层是使用层,只看产品对外表现出的形态:支持哪些文件格式、问答结果长什么样、有没有引用标注、能不能调参数。这一层回答的是“用户需求被如何满足”。第二层是流程层,看一条查询从进来开始,经过哪些中间步骤才输出回答,哪些环节是可插拔的组件。这一层回答的是“模块边界在哪里”。第三层是数据层,核心是索引结构、切片粒度、元数据字段设计。这一层最容易被忽略,但恰恰最值钱,因为数据设计决定了检索上限。
举个例子,只看使用层你会发现某款产品能很好地处理扫描版PDF,但如果拆到数据层,会发现它在索引前先做了版面分析,把标题、正文、表格分开,表格单独走结构识别。这个差异不是UI能体现的,但恰恰是效果差距的来源。逆向工程要的就是这层“看不见的设计”。
1.3 六款代表性产品的选品策略
选品不能随便挑六款star数高的就拆。我的筛选标准是:架构风格要有差异,同时每款产品在某一个环节上做到过极致。最终选出的六款,大致覆盖了四类架构路线。
| 产品类型 | 核心特征 | 最值得借用的设计 |
|---|---|---|
| 索引编排框架型 | 数据加载器和节点解析器抽象极深,索引结构灵活 | 统一的文档解析器与节点协议 |
| 链式编排框架型 | 把RAG拆成可组合步骤,生态组件丰富 | 提示词模板与链路抽象 |
| 企业搜索型 | 沉淀了多年的检索组件,生产可用性强 | 混合检索与排序组件 |
| 文档理解型 | 内置版面解析、OCR,面向复杂文档 | 先版面分析后语义切分 |
| 应用平台型 | 面向非工程师,可视化编排 | 配置化流程与文档级管理 |
| 中文场景型 | 针对中文文档和问答做了专项优化 | 中文分词与稀疏检索融合 |
差异足够大,共性才可信。如果只拆六个同类型项目,总结出的“规律”很可能只是某个圈子里的习惯,换个场景就不适用了。反过来,六种不同思路最后在某几个设计点上收敛到同一方向,那这个方向大概率就是绕不开的必经之路。
2. 共性拆解:四层链路里被反复验证的设计
2.1 文档接入:一切检索效果的天花板
六款产品中,五款把文档接入做成了独立模块,而不是简单读文件。典型的做法是先做格式识别,再转成中间表示,最后进入切分。文档理解型产品走得最远:PDF进来先做版面分析,区分标题、正文、表格、页眉页脚,表格区域再单独走结构识别。这个设计背后是很朴素的道理:切片质量取决于输入文本的干净程度。如果一页PDF里混着页眉和正文,切出来的块里就带着噪声,向量化之后检索精度一定受影响。
我在自己的项目里也复刻了这个思路,所有文档先统一转成Markdown,再进入后面的流程。这么做的好处是格式一旦统一,后续的切分规则、元数据抽取、检索展示都只需要面对一种结构,复杂度大幅下降。很多团队忽视这一步,直接从PDF里抽文本就切片,结果页眉页脚、页码、水印全部混进向量库,检索效果自然上不去。
2.2 切片与索引:几乎所有项目都在做同一件事
索引层拆完之后发现共性很强:几乎每个项目都使用节点或片段作为最小检索单元,节点上挂向量和元数据。切片参数主流区间是300到800个token,重叠区间80到150个token。有一款产品还额外支持父文档索引,也就是片段级检索、父级内容回答,用来解决单块信息不足的问题。
这给我的启发是:切片不是按字数把文章切完那么简单,它承担着“语义完整性”和“检索粒度”两个指标,而这两个指标是矛盾的。切得越小检索粒度越细,但单块上下文越容易缺失。所以重叠机制被反复使用,父级索引则是在粒度不够时的一种补救手段。如果没有特别强的理由,我建议跟随这个主流区间,而不是自己拍脑袋定一个很极端的值。
2.3 检索链路:单一向量召回只是及格线
更重要的共性是检索不再单靠向量。六款产品里至少四款提供了向量检索与稀疏检索的组合,有的内置BM25,有的外接全文搜索引擎。背后原因在实践中很常见:向量检索擅长语义相近,但遇到精确匹配、专有名词、商品型号就常常翻车。比如用户搜“AB-2000”,向量检索很可能召回一堆“类似型号”而不是原词。稀疏检索能精确命中token,但不懂语义。两者结合才是完整方案。
几款产品在召回后还会接一个重排器,对候选结果做精排。这个设计我在后面蓝图里会细化。这里想强调的是,单一向量召回在开源社区已经基本被视作“及格线”,而不是“完整方案”。如果自研RAG还停留在“只做向量检索”的阶段,那相当于还在用几年前的方案解决今天的问题。
2.4 生成增强:提示词模板与引用溯源的设计惯例
生成层的设计也有明显共识。所有产品都不是简单把检索结果拼进提示词,而是有固定的结构化方式:先是系统指令,规定角色与回答规范;然后是检索上下文,标注每一条来自哪个文档;最后是用户问题。引用标注几乎是标配。有些产品会在回答后用序号标注来源片段,并附上原文档链接。
从工程角度讲,引用不只是UI细节,它让用户能核验答案,也就让系统在“回答错误”时还有据可查,这在企业场景里近乎底线。我见过不少自研项目把引用标注当作后期优化项,结果上线后被业务方追着问“这个数据哪来的”,才回来补功能,补的时候发现提示词结构没预留位置,上下文格式也不支持,改起来非常被动。这个设计应当在一开始就进入提示词模板。
3. 自研蓝图:五层可复用架构与关键接口设计
3.1 接入与清洗层:统一为Markdown流
基于拆解出的共性,我整理出一套五层蓝图。第一层是接入与清洗层,目标是把各类文档统一成干净的结构化文本流。我的做法是:所有格式先转成Markdown,因为Markdown天然保留标题层级,还能表示表格和代码块,切分时可以借此感知结构边界。PDF在转换前先做版面识别,去掉页眉页脚,表格单独处理;DOCX要识别标题样式;网页则先抽取正文主干。
这一步不追求完美,但一定要把噪声滤掉。我在处理一份产品手册时,因为PDF转换后残留了大量页眉,导致检索时经常命中“公司内部资料”这种无意义片段。后来在清洗层加了一条规则:连续三行以上相同的内容直接剔除。这个小规则成本极低,却让检索命中率肉眼可见地提升。清洗层做得越扎实,后面每层越省力。
3.2 索引层:切片参数、向量化与元数据
第二层是索引层。核心参数我建议从chunk_size=512 tokens起步、overlap=96 tokens起步,后续根据业务调整。向量模型选择上,优先选在目标语言和领域上有优势的开源向量模型,固定之后不要轻易更换,否则全库向量都要重算。元数据里至少要存:来源文档ID、章节路径、片段序号、文档类型、更新时间。
别小看元数据,后续做时间过滤、权限过滤、来源过滤全靠它。如果一开始漏掉,后面补的成本极高,因为历史数据需要全量重跑一遍。我在一个知识库项目里就吃过这个亏:初期没存文档来源,后来要做按部门隔离,只能重新解析全部文档,耗时整整一个周末。这个字段设计应该在索引层一次性想清楚。
3.3 检索层:召回、融合与重排的服务化设计
第三层是检索层,我拆成三个子模块。召回模块同时执行向量检索和关键词检索,向量取top20,关键词取top20;融合模块把两路结果合并去重,排序方式推荐RRF而不是直接加权;重排模块用一个轻量级开源重排模型对候选池里的top20做精排,最后取top5。
三个子模块分开部署的好处是:每一层都能单独压测和替换。比如关键词检索换成更强大的全文检索引擎时,不需要动其他模块。我在实际项目中把这三个子模块做成了三个独立服务,之间通过内部接口通信。效果迭代时非常方便,重排模型升级只影响重排服务,完全不影响召回侧。
3.4 生成层:提示词结构与引用机制
第四层是生成层。提示词模板我建议固定成三段式。系统指令设定角色、回答规范、以及“无法回答时明说”;然后将检索到的片段按序号排列,每个片段前面注明来源;最后是用户问题。输出格式要求模型在关键结论后标注片段序号,例如“根据资料[2][3],该参数默认值为10”。
有了这个结构,后续的上屏高亮、文档跳转都顺理成章。关键是引用必须和片段序号严格对应,否则模型会编造序号。我在提示词里故意强调“只引用上下文中出现的序号,未引用内容以资料中未明确说明回应”,实测能显著降低乱标引用的情况。
3.5 评估层:让优化不再靠感觉
第五层是评估层,这个最容易被自研团队跳过。我的建议是最少维护一套30到50条的标准问答集,覆盖单跳事实查询、多跳推理、否定问题、数值比较、时效性判断五类。每次改动检索策略或提示词后,就批量跑一遍,统计三个指标:检索命中率、回答忠实度、答案有用性。
只要把评估跑起来,优化就从“感觉变好了”变成“指标变好了”。我在一个项目里通过评估发现,重排模型上线后检索命中率提升了14个百分点,但忠实度只提升了5个点,说明瓶颈已经转移到生成侧,于是调整提示词,忠实度又涨了8个点。没有评估,这些判断全靠猜,效率低得多。
4. 实操落地:从蓝图到工程的六个关键决策
4.1 向量存储选型:先搞清楚你的过滤需求
蓝图落地的第一步是存储选型。我的经验是先从过滤需求倒推:如果业务需要复杂的元数据过滤、多租户隔离、删除更新频繁,选一个支持结构化过滤的成熟向量数据库;如果数据量小、场景简单,直接用轻量的向量索引就行。
实际项目里我见过许多团队在初期选了功能简单的方案,后期因为过滤能力不足而迁移,迁移成本很高。另外要注意向量索引的参数,比如HNSW的M和efConstruction直接决定召回率与索引体积的平衡。这个参数不是默认值就完事,最好基于自己的数据做一次小规模扫描。我通常的做法是取1万条真实文档做候选集,把M从16调到64,看召回率变化曲线,找到拐点再固定。
4.2 切片参数怎么定:从token数反推文本长度
很多人问切片到底是512还是256。我的回答是:先定长度单位,再反推业务文本。长度单位用token数比字符数可靠,因为向量模型对token敏感。以主流开源向量模型为例,一个英文单词大约1到1.3个token,一个中文字大约1到2个token,所以chunk_size=512大约对应400个英文单词或者300到500个中文字。
如果业务文档段落普遍很长,可以适当调大,但超过800后检索粒度会变粗,小块信息容易被淹没。重叠部分建议取chunk的15%到20%,也就是80到100个token。太少起不到衔接作用,太多浪费存储。我在处理一份技术规范时发现,该公司文档每个章节特别长,512切出来的块经常把“参数表”和“说明文字”拆开,后来把chunk调到768,重叠调到120,问题才解决。参数一定要结合自己的文档结构来定,不能照搬。
4.3 混合检索怎么融合:为什么我推荐RRF而不是加权求和
混合检索的融合方式,我强烈建议用RRF而不是线性加权。线性加权的问题在于向量分数和关键词分数分布完全不同,直接相加等于先归一化,而归一化本身又是一层麻烦。RRF的思路简单粗暴:不比较原始分数,而是比较排名。它对两路结果分别按排名给分,公式是score=Σ 1/(k+rank),k取60是常见实践。
这个直觉是:如果一个片段在两路检索里都排名靠前,那它是真正相关的概率就很高。实测下来RRF调参成本低、效果稳。我自己的测试数据里,RRF融合后的top5命中率比线性加权高出约8个百分点,主要赢在关键词检索给出的高排名结果没有被向量分数压下去。这也是多款开源产品内置融合时的一致选择。
4.4 重排的引入时机与成本测算
重排模块什么时候引入?我的判断标准是:当top20里出现“确实相关但排得很靠后”的情况时,就该引入。重排模型的成本可以估算:假设每日查询1万次、每次重排20个片段、单个片段推理耗时50毫秒,不考虑并行优化的情况下,理论耗时就是1万乘20乘50毫秒,约2.8小时的计算量。所以工程上只对融合后的前20个片段重排,而不是对全部候选。
实测中交叉编码器重排后,top3的准确率通常能提升10到20个百分点。我在一个客服知识库项目里上线重排后,工单里“答非所问”的投诉从每周十几条降到两三条。这个收益在检索链路里是最明显的,强烈建议不要在重排上省钱。
4.5 上下文窗口利用:多给反而更差
很多团队拿到长上下文模型后,习惯把检索片段尽量多塞。这是个反模式。检索噪声是准确率的头号杀手,塞进去的每个不相关片段都在拉低回答质量。我的实践是把最终上下文控制在5个片段左右,每个片段300到500字,加上问题和指令,总共在2500到3000字上下。这样既留出模型推理的注意力预算,又避免无关信息干扰。
如果确实需要更长的参考资料,优先考虑让模型分步处理:先检索、再总结、最后回答,而不是一口气吞进去。我在一个法律文书项目里试过把上下文从5段扩到10段,结果的忠实度不但没涨,反而因为引入了大量弱相关条文而下降。控制上下文长度这件事,约等于控制回答质量。
4.6 评测集怎么搭:单条问答的构造范式
标准问答集的构造有个范式:每个评测对包含问题和答案,答案要标注应当引用的片段来源。比如问题“该系统的超时时间默认值是多少”,参考答案“默认值是10秒,依据文档3”。构造时故意混合反例,比如把相似但不相关的文档放进来,看系统会不会被带偏。
我习惯每次迭代先跑小集再跑全量。小集维持在10条左右,一两分钟出结果,方便快速验证;全量50条下班前跑,第二天看报告。这样既保证了迭代速度,又不会因为评测集太大导致每次改动都等半天。评估集本身也要定期更新,把线上用户实际问过但回答不好的问题加进去,评测集和真实分布偏离太大时,指标的参考价值会迅速下降。
5. 常见问题与排查技巧实录
5.1 检索为空:库里有数据,却什么都召不回
最常见的坑是:库里明明有数据,检索却返回空。排查顺序从数据链路看起。先确认文档是否真的入库,索引任务是不是因为解析失败而静默跳过;再看查询的embedding和文档的embedding是否来自同一模型,很多团队升级过向量模型但没重建索引,新旧向量混在一起,结果一团糟;最后检查元数据过滤,比如权限过滤把结果全滤掉了,日志里却看不到任何提示。
这三步能解决大部分“查不到”的问题。有一次我排查一个线上问题查了两小时,最后发现是索引任务里一个小写转换规则把文档ID全改写了,元数据里的ID对不上,过滤条件直接把所有结果滤掉了。这类问题日志里通常不会报错,只能靠逐段核对数据流。
5.2 召回不相关:精确匹配与语义匹配的失衡
检索结果与问题无关,原因通常是切片方式与查询粒度不匹配。比如整页切成一块,一个1000字的块里只有一句相关,向量化后语义被稀释,自然排不上去。解法是先压缩切片粒度,再引入关键词检索,最后加重排。还有一个隐藏因素:查询本身太长。用户粘贴一段完整需求进来,向量化后重点被冲淡,这种情况下可以对查询先做意图压缩或关键词抽取,再进入检索。
我在日志分析里发现,查询超过50个字的会话里,“答非所问”的比例比短查询高出一倍。后来加了一层查询改写:先让模型把长问题压缩成关键词列表,再检索,问题基本消失。这个细节很少被文档提到,但实战价值很高。
5.3 回答幻觉:引用不能只是装饰
回答看似通顺但内容没有依据,先别急着怪模型。检查两件事:第一,进入上下文的片段到底相不相关,如果检索回来的都是弱相关片段,模型只能“合理编造”;第二,提示词里有没有明确允许模型说“不知道”,如果没有,模型会被迫给答案。工程上还要靠引用机制约束,要求回答里的每个数据点都标注来源,标注不出来的部分要提示“资料中没有明确说明”。
配合忠实度评分,幻觉率是可测、可降的。我在一个医疗问答项目里把提示词改成“回答必须基于上下文,上下文不包含的信息需要明确说明”,幻觉率降了一半。这个改动成本几乎为零,但往往被忽略。
5.4 延迟与成本:三个开销大户
延迟开销通常集中在三个地方:向量索引查询、重排模型推理、大模型生成。向量查询一般是个位数到几十毫秒,不是瓶颈;重排模型单个片段几十毫秒,要控制候选数;真正的大头是生成阶段,输出token越多越慢。优化手段包括:对高频问题做缓存、把重排候选从20降到10、用更小更快的生成模型处理简单问题。
成本控制方面,建议按字段精简存储,不用的长文本不要全量存入向量库;日志单独立库,别跟业务数据混在一起。我在一个项目里通过缓存高频问题,把每日大模型调用量压低了三成,延迟P95从4秒降到2秒左右。先把缓存做起来,再谈其他优化,性价比最高。
拆完六款产品,我最大的体会是:自研RAG的价值不在于把开源功能重写一遍,而在于理解那些被反复验证的设计决策,再用自己的工程约束去实现它们。文档清洗、切片、检索、重排、评估,每一层都不难单独实现,难的是把它们串成一条可迭代的链路。如果你也想自研,我建议从评估层开始搭,哪怕先只维护二十条评测样本,也比跑几个demo看感觉强得多。这套蓝图本身也不是终点,后续还可以往多轮对话记忆、结构化知识抽取、增量更新这些方向扩展。先把基础链路做稳,再谈更多。