2023年我刚开始碰RAG的时候,跟大多数人一样纠结:到底是直接用开源框架,还是自己从零写一套?后来我换了个思路——把当时能拿到手的六款开源RAG产品全部拉下来,一行一行地读源码、跑流程、对比设计决策,相当于对它们做了一次"逆向工程"。这六款产品分别是 LangChain、LlamaIndex、Dify、FastGPT、RAGFlow、QAnything。折腾了大概两个月之后,我发现它们的差异远没有想象中那么大,反而提炼出了一套非常稳定的共性骨架,最终支撑我搭出了一套自研RAG蓝图。这篇博文就把整个推导过程、拆解方法、共性结论和落地蓝图完整记录下来,给正在纠结"自研还是套框架"的团队和个人开发者一个可复用的参考。
1. 为什么要把六款开源RAG产品拆开看
1.1 自研RAG的两条路,其实都不好走
做自研RAG的人通常卡在两条路上。第一条是从零开始,从文档解析、分块、向量化、索引、检索、重排到生成全部自己写。这条路最大的问题是"不知道哪些环节是坑":你以为重点在向量检索,结果实际做下来发现PDF里一张表格就能让你的召回率崩掉一半。没有成熟产品的经验打底,你会在无数个低水平问题上反复试错,一个月过去了还在调分块参数。
第二条路是套LangChain这一类的框架。这条路上手快,但问题在于框架把关键决策都抽象掉了:默认的分块策略、默认的向量库、默认的提示词模板,全是封装好的黑盒。你调了很久参数,其实不知道自己在调什么,出了问题也不知道是该换分块器还是该换检索策略。更麻烦的是,框架为了通用性做了大量抽象层,排查链路深的时候,一行代码要跳五六个中间层才能看到真正的逻辑。
我后来走的是第三条路:把六款开源产品当作"会说话的设计文档"来读,看看成熟团队在这些关键决策点上到底怎么选的,然后把这些决策点搬回自研项目里。这比从头摸索快得多,也比直接套框架理解得深得多。
1.2 我对"逆向工程"的理解:不是抄代码,是对齐决策点
先声明一点:我说的"逆向工程"不是反编译、不是破解,而是对开源项目做系统性的源码阅读和架构还原。开源项目的License本来就允许学习和研究,只要不违反协议,把人家公开的设计思路吃透是完全正当的。
我更想强调的是"决策点"这个概念。读开源代码的时候,真正值钱的不是那几千行实现代码,而是代码背后回答过的一系列问题:
- 文档进来之后,第一件事是整体解析还是先切块再解析?
- 分块大小取多少?要不要重叠?重叠比例怎么定?
- 用纯向量检索还是混合检索?什么时候该上重排模型?
- 上下文窗口怎么塞?塞满了是截断还是压缩?
- 用户看到的是"答案"还是"答案+引用来源"?
每一个问题背后都是一次取舍。六款产品在这些决策点上的答案高度相似,说明这就是RAG系统的"标准答案";几款产品出现明显分歧的地方,说明那就是"可以按场景自定义"的部分。这套"共识+分歧"的分布,就是我要找的自研蓝图。
2. 六款产品的选型与第一印象
2.1 选型标准:不是挑最好的,是挑覆盖最全的
市面上RAG相关开源项目极多,选这六款不是随手抓的,我当时的筛选标准有三条。第一,产品定位必须差异化:我要看到框架型、数据框架型、平台型、流程编排型、深度文档解析型、检索增强型这六种不同的设计思路。第二,项目的RAG实现必须是自研的,不是套壳LangChain的那种,否则拆来拆去还是同一个设计。第三,社区活跃度必须够高,issue和PR里藏着大量真实场景的坑,这比官方文档值钱得多。
按这个标准筛下来,最终锁定的是这六款:LangChain代表"链式抽象派",LlamaIndex代表"数据结构派",Dify代表"平台集成派",FastGPT代表"流程编排派",RAGFlow代表"文档理解派",QAnything代表"检索增强派"。
2.2 六款产品定位速览
| 产品 | 定位 | 核心语言与存储 | RAG实现中最值得看的点 |
|---|---|---|---|
| LangChain | 通用LLM应用框架 | Python,适配各种向量库 | 链式抽象、文档加载器体系、检索QA链 |
| LlamaIndex | 数据到LLM的连接框架 | Python,自带多种索引 | Document/Node数据模型、摄取管线、查询引擎 |
| Dify | LLM应用开发平台 | Python后端,PostgreSQL+向量库 | 知识库分块、混合检索、工作流编排 |
| FastGPT | 可视化流程编排的问答应用 | Node.js后端,MongoDB+pgvector | 流程图编排、知识库管理、ReRank接入 |
| RAGFlow | 深度文档理解的RAG引擎 | Python后端,ES+MySQL | DeepDoc解析、版面识别、引用溯源 |
| QAnything | 面向企业知识的问答引擎 | Python后端,Milvus/ES | 两阶段检索(BM25+向量)、BCEmbedding重排 |
这张表做出来之后,我已经能感觉到一些共性了。比如六个项目的定位差异很大,但内部都必然有"解析-分块-向量化-检索-生成"这条主线。再看深一层,六款里至少有四款做了混合检索,三款做了独立重排阶段。这些信号比任何技术博客都可靠。
2.3 从仓库结构看设计哲学
看开源项目,我习惯先不看代码,先看目录结构。目录结构是一个团队架构决策的最直接体现。LangChain的目录按功能模块切分(loaders、splitters、embeddings、retrievers、chains),说明它是"能力库"思路,你要自己拼装。LlamaIndex的目录里有core、storage、indices这样的分层,看得出它特别在意数据索引的生命周期管理。FastGPT的目录明显分成页面、服务端和数据集,说明产品化程度很高。
这一层看完,基本就能判断:这套代码是给谁用的?如果它的抽象让你在很多地方做选择,它是框架;如果它把选择都替你做了,它是产品;如果它在解析环节投入了超出常规的代码量,那说明作者踩过解析的坑。RAGFlow就是这样——它的deepdoc目录占比极大,Unstructured、表格识别、版面分析全都有,这直接印证了"文档解析是所有RAG产品都绕不开的硬骨头"。
3. 逆向工程的核心方法论:三层拆解法
3.1 第一层:从文档与目录反推数据流
读完目录之后,第二步是找官方架构文档和博客,把每个项目的"数据流主线"画出来。这里我用的是最朴素的办法:把文档里提到的所有模块名词抄下来,然后按"输入文档到最后回答"的顺序排一遍。六个项目排下来之后,主线高度一致:文件上传 → 格式识别 → 内容抽取 → 清洗 → 分块 → 嵌入模型向量化 → 写入向量库 → 用户提问 → 查询向量化 → 检索 → 融合/重排 → 组装上下文 → 大模型生成 → 返回答案与引用。
这条主线是六个月就翻来覆去的"骨架",所有项目都长这样。区别只在于:有的产品把解析做得很重(RAGFlow),有的产品把重排做成了独立服务(QAnything),有的产品把整条线封装成了可视化工作流(FastGPT、Dify)。但骨架本身没有任何本质区别。
这个发现的意义非常大:它意味着你在设计自研RAG的时候,不需要发明新架构,只需要确认自己在每个节点选择什么方案。
3.2 第二层:追踪一条查询的执行路径
光看文档不够,还要看真实的执行路径。我的做法是每个项目跑起来一个最小实例,然后从一个API入口开始追代码。比如在LangChain里,我就追RetrievalQA链:用户问题进来之后先走什么、检索器返回什么格式、大模型接收到的是什么样的提示词、最终怎么拼出答案。在FastGPT里,我追一次对话的API调用链路,看它是怎么把用户输入变成向量查询、怎么从pgvector里把Top-K文档捞出来、又是怎么塞进大模型上下文里的。
追代码的过程中有个小技巧:优先读测试文件,再读源码。测试文件里包含了作者对每个函数的预期行为,是"设计意图"的最直接体现。比如LlamaIndex里很多测试会告诉你,某个检索器在空索引时应该返回什么、重复文档时怎么去重——这些边界行为才是你自研时最容易漏掉的东西。
3.3 第三层:把决策点整理成对比矩阵
三层拆解法到最后一步,是把每个项目的具体选择填进一张大表里,形成"决策点对比矩阵"。我当时建的表包含这些维度:分块策略(固定大小/语义分块/父子分块)、嵌入模型(BGE系列、OpenAI、商用国产模型等)、向量存储(Milvus、Qdrant、pgvector、ES、FAISS)、检索方式(纯向量/关键词/混合)、重排策略(有独立重排/无重排/两阶段)、上下文组装(直接拼接/压缩/多轮摘要)、引用机制(有引用/无引用)、可观测性(日志/链路追踪/评估模块)。
这张表做完,你会看到非常有意思的两个规律。第一,几乎所有产品在"文档解析要重投入"上达成共识;第二,在"要不要上重排"和"引用要不要做"这两个点上,产品化程度越高的项目越倾向于做。这两个规律直接奠定了我的自研蓝图。
4. 共性骨架:六款产品反复出现的模块
4.1 文档解析:所有产品都绕不开的硬骨头
读源码时有一个让我非常意外的发现:六个项目里,代码量最大、Issue最多、版本迭代最频繁的模块,不是检索也不是向量化,而是文档解析。LangChain用了一整套Loader体系对接各种格式;RAGFlow干脆自己写了一套DeepDoc来做版面识别、表格抽取和阅读顺序还原;QAnything把Office、PDF、图片全部接进来,还专门处理扫描件。Dify和FastGPT虽然没有自研解析引擎,但也投入了大量精力做Markdown转文本、表格结构保持这些细活。
这说明一个问题:RAG效果的瓶颈,很大程度上在"输入侧质量",不在检索算法。市面上流传的"RAG瓶颈"讨论,大多集中在检索召回率,但我在实际拆解中发现,很多召回率问题的根源是分块之前文档就已经被解析坏了。比如PDF里的表格被解析成断裂的文本行,语义完整性在源头就被破坏了,后面再怎么调检索也没用。自研RAG的第一条铁律就是:解析层值得砸最多的资源。
4.2 分块策略:没有银弹,但有参考区间
分块是新手最喜欢纠结的环节,我一开始也特别想找到一个"最优分块大小"。拆完六个项目之后得出的结论是:没有最优,但有参考区间和通用原则。
六款产品默认分块大小大多落在256到512个token之间,重叠率通常在10%到20%。这个区间的背后逻辑很朴素:太小的块(比如64token)检索时语义不完整,太大的块(比如2000token)会把不相关内容混进来,导致误召回。真正拉开差距的不是块大小,而是分块策略:LlamaIndex有NodeParser体系,支持按段落、句子、语义边界分块;RAGFlow支持按版面块分块,表格一个块、段落一个块;FastGPT在界面里直接让用户配置分块方式和重叠数。
这六款产品里,有不止一款实现了"父子分块":父块尺寸大保证语义完整,子块尺寸小保证定位精准,检索时命中子块,返回给大模型的是父块。这个设计在RAGFlow和LlamaIndex里都能看到,我后来在自研蓝图里直接采纳了它。
4.3 向量化与索引:存储才是隐形重心
嵌入模型的选择上,六款产品的倾向几乎一致:小规模部署用开源的BGE系列,追求效果用商用或者更大的模型,QAnything甚至专门训练了BCEmbedding来做中英文双语的嵌入和重排。嵌入模型的更新换代很快,所以架构上必须做到"模型可替换",不能把嵌入逻辑硬编码在业务代码里。
向量存储的选择比我预想的分散:Milvus、Qdrant、pgvector、ES都有人用,但仔细观察会发现,选型逻辑大多是"复用已有基础设施"。如果你的团队已经有PostgreSQL,就用pgvector起步;如果已经重度使用ES,就用ES的向量检索能力;海量数据、高并发才值得引入Milvus这类专用向量库。这是典型的"与其选最好的,不如选你已经有的"。
还有一点容易忽略:除了向量索引,几乎所有产品都保留了原文存储和关键词索引。QAnything的两阶段检索、Dify的"向量+全文"混合检索,本质上都在做"向量召回 + 关键词兜底"。纯向量方案在专有名词、精确匹配场景下会明显吃亏,比如产品型号"ABC-123"这种词,向量检索匹配精度反而不如关键词。
4.4 检索与重排:召回率不是唯一指标
拆到检索环节,我越来越确定一个判断:检索的评判标准不只是召回率,而是"送到大模型面前的内容质量"。召回率再高,塞进上下文的10个片段里有8个是无关的,大模型照样给你编一个答案。所以六款产品里凡是效果口碑好的,都做了两件事:混合检索和重排。
混合检索的做法一般是BM25关键词检索和向量检索并行,再把两路结果合并。合并算法常见的是RRF(Reciprocal Rank Fusion)或者加权和。QAnything的做法最典型:第一路BM25侧重精确匹配,第二路向量侧重语义匹配,两路结果进重排模型重新打分,最后取Top-K送大模型。FastGPT和Dify也都在知识库设置里内置了重排模型入口。
这里我学到的一个重要细节是:重排模型的作用不是"锦上添花",而是"纠正向量检索的排序错误"。向量检索的Top-K可能把真正相关的片段排在后面,重排模型用交叉编码器对"查询-片段"逐对打分,排序精度比双塔式的向量检索高一大截。代价是延迟,但通常可以接受。这个认知直接改变了我的蓝图设计——把重排从"可选优化项"提升为"标准组件"。
4.5 上下文管理与引用溯源
六款产品都有一个共同点:在把检索片段交给大模型之前,会做上下文的组装,而不是把所有片段一股脑全塞进去。组装方式有直接拼接、按相关度排序后拼接、截断、还有更高级的压缩/摘要。LlamaIndex有专门的PostProcessor体系,FastGPT在流程节点里可以设置引用条数和拼接模板,Dify把上下文编排做成了可视化节点。
引用溯源这个功能特别值得注意。RAGFlow的回答后面会带引用标注,点击能跳到原文位置;FastGPT会展示引用的文档片段。这个设计在技术上是小事,在体验上是大事——用户看到答案能直接判断"这个回答是来自我的文档还是大模型在编",信任感完全不一样。我后来在自研蓝图中把引用溯源做成必选项而不是可选项,就是从这六款产品里学到的。
4.6 可观测性:最容易被自研方案忽略的一环
说实话,刚开始做自研RAG的时候,我根本没想过可观测性问题。但拆完Dify和FastGPT之后,我意识到日志和链路追踪是RAG系统的"第三只眼"。FastGPT每次对话都会记录检索到了哪些片段、命中得分是多少、大模型用了多少token;Dify的日志页能回放每一次查询的完整链路。这些数据对于调优极其重要——没有"哪个环节产出低质量结果"的记录,你只能瞎猜。
自研方案如果连最基础的日志都没有,出了问题真的无从下手。比如用户反馈"答案不对",你需要知道问题出在解析、分块、召回还是生成。可观测性就是用来回答这个问题的。这块内容我后来单独拎出来做了设计,后面实操部分会展开讲。
5. 自研RAG蓝图:模块划分与关键取舍
5.1 蓝图总览:分层与主干
基于六款产品逆向出来的共性骨架,我最终确定的自研蓝图是五层结构:接入解析层、切分向量层、存储索引层、检索重排层、生成问答层,再加一条贯穿全链路的观测带。这里不用画花哨的架构图,我用文字描述主干流程:文件上传后先进接入解析层,按格式分流到不同解析器,输出带结构的清洗文本;然后切分向量层按可配置策略做分块,生成块级元数据,再交给嵌入模型向量化;存储索引层负责把向量和原文写入存储,同时维护关键词索引;用户提问时走检索重排层,混合召回、重排、过滤,拿到Top-K;最后生成问答层组装提示词、调用大模型、附加引用来源,流式返回给用户。
这五层每一层的设计取舍,我都从六款产品里找到了依据。
5.2 接入解析层:宁可过度设计,不可偷工减料
这个层的设计原则来自RAGFlow的启示:解析必须结构感知。我把它拆成四个环节:格式识别、内容抽取、结构还原、清洗归一。格式识别就是判断文件是PDF还是Word还是扫描图片,决定走进哪条解析器。内容抽取用不同的库来做,文本PDF直接用解析库取文本,扫描PDF走OCR,表格单独做表格抽取。结构还原是把标题层级、段落顺序、表格行列关系保留下来,这一步直接决定后续分块的质量。清洗归一做的是去掉页眉页脚、统一编码、压缩多余空白。
这里要特别说一个坑:千万不要把解析做成"solve all problems的单体函数"。我看到很多自研方案写一个parse_file(path)就完事,后面想升级某一种格式的解析能力就得大改。正确做法是每种格式一个独立解析器,统一实现一个Parser接口,输出统一的结构化文档对象。
5.3 切分向量层:可配置是底线,父子分块是标配
我最终采用的切分方案是"可配置的分块策略 + 父子分块"。分块策略参数暴露配置项:按字符数、按token数、还是按语义边界切;块大小默认512 token,重叠率默认15%。不同知识库可以设置不同参数,因为技术文档和聊天记录的最佳分块方式明显不一样。
父子分块的做法是:先把文档切成大块(父块,比如800到1000 token),再把每个父块内部按句子或小段落切成小块(子块,比如128到256 token)。向量索引只对子块做,检索命中的是子块,但送进大模型上下文的是子块对应的父块。这样既保证了检索的定位精度,又保证了语义上下文的完整性。这个设计在RAGFlow和LlamaIndex里都有对应实现,非常成熟。
有一点必须提醒:嵌入模型的维度会直接影响存储和检索成本。如果用BGE-large这种1024维的模型,单条向量的存储开销明显大于768维的模型。选模型之前先想清楚是追求效果上限还是控制资源消耗。自研RAG的好处就在这里——你可以随时换,不像在用框架时被默认配置绑住手脚。
5.4 检索重排层:混合检索是基线,重排是标配
这一层的设计是六款产品共识度最高的地方。我的做法是:BM25关键词检索和向量检索并行,两路结果做RRF融合,然后过一个重排模型重新打分,按重排分数取Top-K。检索这个环节里,有一个参数特别容易调过头:Top-K。我见过有人把Top-K设成20,结果上下文里塞满了低相关的片段,大模型被干扰得胡说八道。我的经验是Top-K取4到6比较稳,重排分数垫底的宁可不放进去。
过滤逻辑也值得设计:要支持按元数据过滤(比如只检索某个日期之后、某个来源、某个分类下的文档)。这个需求在实际业务里几乎必然出现,六款产品里全部内置了这个能力。
重排模型的选择上,开源首选BGE-reranker系列,效果和延迟都相对平衡。如果团队有条件用更大的商用模型,效果会更好,但要注意重排延迟对用户体感的影响。
5.5 生成问答层:提示词模板与引用机制
生成层是最后一道工序,也是翻车重灾区。提示词模板的设计原则是"明确角色、明确任务、明确约束"。我给大模型的指令一般包含四个要素:你是基于知识库回答问题的助手;只能使用给定上下文内容回答,上下文不足时明确说不知道;答案开头标注引用来源;禁止编造不在上下文里的内容。这里的关键是"禁止编造"这条约束,它能大幅降低幻觉率,但有代价——模型会更倾向于说"不知道",需要自己在业务上权衡。
引用机制的实现也不复杂:检索阶段保留每个片段的文档ID和原文位置,拼提示词时给每个片段编号,然后要求模型在回答中引用编号。最后把编号映射回原文链接或者可跳转的锚点。RAGFlow的引用跳转体验就是这么做的,看起来高级,其实实现成本并不高。
生成层的流式输出必须做。现在用户对大模型响应的等待容忍度极低,流式输出能把首字延迟从"等全部生成完"降低到"几百毫秒",体感是质的区别。这在自研方案里不算难,但容易忽略。
6. 落地过程中的坑与排查实录
6.1 常见问题速查表
| 现象 | 大概率原因 | 排查方向与解法 |
|---|---|---|
| 答案和文档内容对不上 | 分块破坏了语义完整性 | 检查分块大小和切分边界,考虑父子分块 |
| 检索结果全是无关片段 | 嵌入模型与领域不匹配 | 换领域微调的嵌入模型,或加混合检索兜底 |
| 精确型号/编号查不到 | 纯向量检索匹配精度不足 | 启用BM25关键词检索,做混合融合 |
| PDF表格内容乱掉 | 解析层丢结构 | 用表格抽取专用解析器,勿用纯文本提取 |
| 回答总是在编造 | 提示词缺少强约束 | 加"只能使用上下文、不得编造"硬指令,并做引用标注 |
| 接口延迟飙升 | 检索链路串行、无缓存 | 并行跑混合检索,重复查询走缓存 |
| 嵌入式部署显存不够 | 嵌入模型/重排模型过大 | 换小模型如BGE-small,或用CPU推理(延迟换资源) |
6.2 三个典型坑的详细复盘
第一个坑是PDF表格。我最初用通用文本提取库处理PDF,结果一张调价表格被解析成几十行断裂的文本,检索之后大模型把"原价"和"现价"彻底搞混。后来我学RAGFlow的做法,对含表格的页面单独走表格结构化解析,按行和列重组表格内容,再决定是整体作为一个块还是拆成单元格文本。这个改动让表格相关问答的准确率提升非常明显。
第二个坑是"大块语义撞车"。我一度把分块调大追求语义完整,结果一个800token的块里可能包含三段不同主题的内容,检索时只要命中其中一段,整个大块都会被塞进上下文,其他内容就成了干扰噪音。这个问题的标准解法就是父子分块,检索粒度细、上下文粒度粗,两者分离。
第三个坑是本地部署的资源失控。很多团队想全本地化部署,嵌入、重排、大模型全跑在本地,结果一张16G显存的卡根本顶不住并发。我的建议是拆分资源策略:嵌入模型用小的(BGE-small甚至更小),重排模型按需启用,大模型用量化版本或者干脆走本地推理框架。别把所有模型都塞进一个服务里,各自独立部署才能独立扩缩容。
6.3 性能评估怎么做:离线一套、在线一套
离线评估的核心是建设评测集:拿业务真实文档提炼一批"问题-正确答案-相关文档片段"三元组,至少准备一两百条覆盖不同文档类型。指标我常用命中率Hit@k、MRR和答案忠实度。Hit@k看检索能不能把相关片段捞回来,MRR看相关片段是不是排在前面,忠实度靠人工抽查或者用大模型当裁判对比答案和原文的一致性。
在线评估做的是监控:查询延迟、检索命中情况、用户是否点击了引用、多轮对话里用户是否追问同一问题。这些数据能告诉我检索质量是不是在下降、有没有新增文档类型把解析层搞挂。工具箱里,Langfuse这类开源方案可以直接借鉴,链路追踪的埋点设计其实也是从Dify和FastGPT的日志模块逆向出来的。
7. 验证蓝图:一个最小可复现的实现
7.1 技术栈选择与理由
为了验证这套蓝图,我搭了一个最小可复现的项目,技术栈刻意保持朴素,方便大家直接抄作业:Python + FastAPI做服务层,文档解析用Pdfplumber和python-docx,分块自己实现(不依赖框架的默认分块器),嵌入用sentence-transformers加载BGE-small模型,向量库先用Chroma起步(数据量大了再换Milvus),混合检索里关键词部分用SQLite的FTS5,重排用BGE-reranker-base,大模型先用OpenAI兼容接口或者Ollama本地模型都行。
选这套组合的原因很简单:每一层都是独立可替换的。哪天要换Milvus,只改存储索引层;哪天要换更强的嵌入模型,只改向量化函数;哪天要接更大的重排模型,只改一个接口。这就是从六款产品里拆出来的"模块可替换"原则。
7.2 核心实现代码:一条线跑通
先看摄取端的核心逻辑,这里展示了从文本到索引的完整过程:
from sentence_transformers import SentenceTransformer import chromadb import json embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5") client = chromadb.PersistentClient(path="./rag_blueprint_db") collection = client.get_or_create_collection( name="knowledge_base", metadata={"hnsw:space": "cosine"}, ) def chunk_document(text: str, chunk_size: int = 512, overlap: int = 64): """按token数切块,带重叠,这里是简易实现,生产环境建议按结构切。""" tokens = text.split() chunks = [] start = 0 while start < len(tokens): end = start + chunk_size chunks.append(" ".join(tokens[start:end])) start = end - overlap return chunks def ingest_document(doc_id: str, title: str, chunks: list[str], metadata: dict): embeddings = embedder.encode(chunks, normalize_embeddings=True).tolist() collection.add( ids=[f"{doc_id}_chunk_{i}" for i in range(len(chunks))], documents=chunks, embeddings=embeddings, metadatas=[{**metadata, "title": title, "doc_id": doc_id} for _ in chunks], )检索端的核心逻辑是混合检索加重排:
def hybrid_search(query: str, top_k: int = 8): # 向量路 query_vec = embedder.encode([query], normalize_embeddings=True).tolist() vector_hits = collection.query( query_embeddings=query_vec, n_results=top_k, include=["documents", "metadatas", "distances"], ) # 关键词路(FTS5,略) keyword_hits = fts_search(query, top_k) # RRF融合后取候选,再进重排模型 candidates = rrf_fusion(vector_hits, keyword_hits, top_k=12) reranked = reranker_rerank(query, [c["document"] for c in candidates]) return [candidates[i] for i in reranked[:4]], reranked生成端的提示词模板是这条线的收尾:
SYSTEM_PROMPT = """你是企业的知识库助手。请严格遵守: 1. 只能使用下面的上下文回答问题。 2. 回答末尾用[来源编号]标注引用来源。 3. 若上下文不足以回答,直接回复"知识库中未找到相关信息"。 4. 不得编造任何上下文之外的内容。""" def build_prompt(query: str, retrieved_chunks) -> str: context = "\n\n".join( f"[{i + 1}](来源:{c['metadata']['title']})\n{c['document']}" for i, c in enumerate(retrieved_chunks) ) return f"用户问题:{query}\n\n可用上下文:\n{context}\n\n请基于以上上下文回答。"这段代码极其简洁,但完整覆盖了蓝图里的核心链路:切分、向量化、入库、混合检索、重排、提示词组装。生产化的时候把它包进FastAPI接口,加上流式输出和日志埋点,就成了一版五脏俱全的自研RAG。
7.3 评估结果与调优方向
这套最小实现跑通之后,我用一份包含120条问题的评测集做了验证,覆盖了技术文档的问答、表格数据查询和长文档的跨段落推理。检索命中率Hit@5在八成左右,加上重排之后送入大模型的Top4片段里包含正确答案的比例明显提升。纯向量检索在处理产品型号查询时命中率偏低,开启关键词混合检索之后补上了一截。
调优方向上,下一步的三个重点是:把解析层升级成结构感知方案,PDF表格和扫描件单独处理;把存储层从Chroma迁移到Milvus,测试十万级文档的索引构建和查询延迟;把重排结果纳入日志观测,持续监控重排分数分布,及时发现检索质量退化。
这套蓝图最实在的价值是:每个模块的取舍都有据可依。踩过几次坑之后,我的体会是自研RAG最怕的不是技术不够深,而是架构上没有把关键决策点暴露出来。只要照蓝图把每一层拆清楚,遇到问题就知道该去哪个层排查,这比什么花哨的算法都管用。最后再分享一个小技巧:所有分块和检索的参数都不要写死在代码里,做成配置项放到界面上,让业务方自己去调——你会发现他们比你更懂自己的文档该怎么切。