ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从开源逆向到自研:RAG系统架构蓝图与实战指南

从开源逆向到自研:RAG系统架构蓝图与实战指南 直接在开头切入主题说说我在看过六个开源RAG项目的源码、文档和issue之后整理出的自研蓝图。这活儿我干了快一个月从LangChain到RAGFlow从Haystack到QAnything翻完一遍最大的感受是开源方案解决的是“通用问题”而自研解决的是“你的问题”。但如果你直接照搬开源架构往往会被其抽象层级拖死完全不看开源又会重复踩他们踩过的坑。这篇文章就是把我逆向工程之后的收获拆给你看希望能让你的自研RAG少走三个月弯路。RAGRetrieval-Augmented Generation检索增强生成现在已经不是概念验证阶段了越来越多团队开始把它落地到内部知识库、客服问答、行业报告生成等场景。但真正自己动手做的时候你会发现开源项目之间差异巨大有的重编排、有的重检索、有的重文档解析。如果能在动手前看清它们的共同骨架自研时就有了底。适合谁看准备自研RAG系统的后端工程师、算法工程师、技术负责人以及正在选型“到底是改开源还是自己写”的团队。下面内容不涉及具体代码逐行解析但会给出模块划分、数据流、参数策略和坑点属于可以直接抄作业的蓝图。1. 为什么我要逆向六款开源RAG选型背后的真实考量1.1 当前开源RAG产品的生态格局我选定的六款产品分别是LangChain生态最全的编排框架、LlamaIndex数据框架侧重文档处理、Haystack生产级管线、RAGFlow深度文档解析、QAnything网易有道开源自带重排和双路召回、FastRAG英特尔开源偏高效检索。这六款风格各异恰好覆盖了RAG系统的所有关键环节。选它们的理由也很简单它们在GitHub上的star数、社区活跃度、issue反馈量都属于第一梯队。更重要的是这六款代表了六种不同的设计哲学。LangChain的Agent式编排、LlamaIndex的文档索引抽象、Haystack的Pipeline严格分层、RAGFlow的DeepDoc解析、QAnything的粗排精排双阶段、FastRAG的高性能检索优化放在一起正好拼出一张完整的RAG技术地图。通过逐个分析它们的架构、源码目录、配置文件和测试用例我总结出一个规律无论外在API怎么变底层都在解决同样几个问题——文档进得来、切得开、存得下、找得准、答得好。这就为逆向工程提供了基础。1.2 从源码和issue中提取架构脉络的方法逆向工程不是看README也不是跑一遍demo。我的方法是第一步先把整个项目的目录结构拉出来看模块划分比如data_loader、splitter、embedding、retriever、reranker、generator、evaluation这些文件夹的粒度基本能反映作者的架构思维。第二步盯着一条最核心的调用链看从你输入一段文本开始到最终返回一个回答中间经过哪些类、哪些方法、哪些外部服务。第三步去GitHub issues里搜“bug”、“failed”、“incorrect”看用户反馈最多的问题集中在哪一层往往那里就是系统最脆弱的地方。我特别关注了它们的“抽象层”。LangChain的Chain和Tool抽象LlamaIndex的Node和Index抽象Haystack的Pipeline和Component抽象这些抽象决定了扩展性。自研RAG最怕的是写成一坨面条代码所以我在逆向时重点记录了每个产品如何划分“数据层”、“索引层”、“检索层”、“生成层”。这就是蓝图的原始素材。1.3 自研RAG的常见瓶颈为什么不能直接套开源很多团队一开始图省事直接调用开源框架的API跑通demo后才发现问题。我总结了一下常见的三个瓶颈第一个瓶颈是黑盒化和定制困难。开源框架为了兼容各种场景做了大量抽象和动态分发。你想改一个切分逻辑可能需要重写好几个类你想加一个自定义元数据过滤可能得深入框架内部修改Pipeline。每升一次版本你的定制代码就会和上游冲突一次。我见过不少项目最后被LangChain版本锁死升级困难。第二个瓶颈是性能和成本失控。开源RAG默认配置往往不是最优的比如嵌入模型使用的维度、检索的top K、重排的candidate数量。如果照着默认值跑召回率可能不错但延迟和token消耗会很大。生产环境需要精细化控制这就必须自研管线。第三个瓶颈是数据安全与私有化部署。很多企业内部知识库不能上传到第三方嵌入API需要私有化部署嵌入模型、重排模型甚至LLM。开源框架往往设计为“自带默认配置”对接外部API私有化改造起来非常费劲。自研蓝图其实就是为了解决这三个瓶颈。2. 六款产品逆向后的共性架构一张“全家福”蓝图2.1 从数据输入到答案输出RAG管线的七个必经环节我把六款项目的核心流程摆在一起比对发现无论它们的API怎么隐藏最终都跑不出下面这条链路文档接入支持PDF、Word、Markdown、HTML、数据库、API等来源。文档解析从非结构化数据中提取文本、表格、图片、布局信息。文本切分把长文档切成适合嵌入和检索的chunk。向量化嵌入用Embedding模型将chunk变成向量。索引存储把向量和原文metadata写入向量数据库同时往往建立倒排索引。检索召回用户query经过嵌入后从索引中召回top N候选。重排与生成对候选进行精排拼接到Prompt中交给LLM生成答案。这七个环节不是可选项而是必选项。开源产品不同点在于有的把第2步和第5步做得重有的把第6步和第7步做得花。比如RAGFlow把绝大部分精力花在文档解析上QAnything则在检索后加了cross-encoder重排。自研时你不需要一开始就全做但设计时必须为每个环节留出接口。2.2 共性模块拆解每一款产品都在这些地方做了隐藏的工作逆向工程最大的收获是发现很多“隐性模块”。文档里往往不显眼但源码中占据了大量比例文件类型检测与转换模块判断是PDF还是DOCX决定走哪条解析管线。表格处理模块表格不能简单按行切分需要保留结构信息。RAGFlow内部专门做了表格识别。元数据管理模块每个chunk关联的来源文档、页码、标题层级、更新时间等。这个模块决定了引用溯源是否可行。向量库抽象层为了支持FAISS、Milvus、Elasticsearch、Chroma等不同后端开源项目都封装了一层统一的VectorStore接口。重排序模块很多开源项目默认包含一个reranker有的用bge-reranker有的用cross-encoder。缓存模块对相同或相似query的结果缓存避免重复调用LLM节省成本。评估模块好的开源项目会在examples或tests里提供评估脚本比如计算RecallK、F1、答案正确率。这些模块看起来琐碎但实际上是RAG系统能否上生产的分水岭。我见过很多自研RAG只做了嵌入检索生成结果在文档解析和元数据上栽了跟头导致回答无法引用出处、表格内容乱码等。2.3 关键选型对比向量库、嵌入模型、重排模型与LLM接口逆向完六款产品后我做了一张对比表基本覆盖了它们的默认选型逻辑功能项主流选择典型默认配置自研时建议向量库FAISS / Milvus / Qdrant / Chroma多数开源demo用Chroma或FAISS按数据量和QPS选十万级以下FAISS够用百万级上Milvus或Qdrant嵌入模型OpenAI text-embedding-3 / BGE / M3E英文默认OpenAI中文默认BGE中文场景优先BGE-large-zh或M3E私有化部署用ONNX量化切分器RecursiveCharacterTextSplitter / 语义切分固定chunk_size500, overlap50根据文档类型调整不要全局一个参数倒排索引BM25 / Elasticsearch部分项目内嵌ES必须和向量检索做混合提升关键词精确匹配重排器bge-reranker / CrossEncoderQAnything内置LangChain可选自研阶段重排可以后加但接口要预留LLM接口OpenAI格式 / vLLM / Ollama统一OpenAI兼容格式私有化用vLLM兼容OpenAI格式方便切换这张表的价值在于开源项目等于帮我们验证过了这些选型的组合效果我们自研时不必从头做试验直接拿这些组合当基线即可。比如中文RAG场景向量模型用BGE、重排用bge-reranker-v2-m3这两个组合在多个项目的issue中反响都不错。2.4 一个容易被忽略的模块配置与可观测性设计六款产品里只有一到两个把配置和监控做得比较完整。大多数开源项目把配置写在.env或yaml里简单粗暴。但自研系统要想长期维护必须把配置分层基础配置模型名、维度、数据库地址、文档处理配置切分大小、overlap、解析策略、检索配置召回数量、分数阈值、重排开关、生成配置Prompt模板、温度、最大长度。可观测性同样重要。我建议在管线每个关键节点都输出结构化日志解析了多少文档、切出多少chunk、嵌入耗时多少、检索召回多少、重排后保留哪些、生成用了多少token。这些数据平时看着没用一旦线上效果变差排查效率能提升数倍。开源项目则因为面向通用场景日志被抽象层拦截很难获得细粒度信息这也是自研的一大优势。3. 可复用的自研RAG蓝图从模块接口到参数策略3.1 模块化设计先定义好内部数据流转的Schema自研蓝图的第一步不是写代码而是先定义统一的数据结构。我推荐参考LlamaIndex的做法把一切中间产物抽象成统一的文档对象和块对象Document原始文档包含doc_id、source、content、metadata、file_type。Chunk切分后的片段包含chunk_id、doc_id、text、embedding、metadata、order。RetrievalResult检索结果包含chunk_id、score、text、metadata、rerank_score。GenerationInput最终拼给LLM的上下文包含query、context_list、instruction。定义好这些基础数据类之后每个模块只依赖这些接口不依赖上游具体实现。比如切分模块输出Chunk至于Chunk来自哪个解析器切分模块不关心检索模块输入Query和索引信息输出RetrievalResult。这样你就可以像拼积木一样替换模块。我建议把文档解析、切分、嵌入、检索、重排、生成分别做成独立的Python包或微服务。对于中小团队用Python包就够了通过函数调用串联对于大团队可以拆成Worker服务通过消息队列解耦。但不管哪种方式数据结构定义必须在开工前统一。3.2 文档解析实操RAGFlow的核心启发文档解析是RAG系统里最容易翻车的环节。我测过直接用pdfminer或pypdf提取PDF文本遇到扫描版PDF就是乱码。RAGFlow在解析方面做的很重它的DeepDoc模块能够识别版面、表格、图片再通过OCR补充。逆向它的源码后我总结出一套自研时也可以用的解析策略对于电子版PDF用PyMuPDF或pdfplumber提取文本块和坐标信息保留标题层级。对于扫描版PDF先做OCR。中文场景推荐PaddleOCR英文场景可以用tesseract或AWS Textract。OCR后需要保留文本块坐标方便后续表格重建。对于Word文档直接使用python-docx解析段落和表格注意读取样式层级。对于HTML用BeautifulSoup或html2text去除script、style、导航栏。对于Markdown直接按标题结构切分即可。这里有个关键经验解析阶段不要只输出纯文本一定要输出结构化的块比如“这是一个一级标题”“这是一个两行三列的表格”。有了这些结构化信息后续切分时可以按标题边界切表格可以单独作为一个chunk而不是被强行切断。我在实际项目里遇到最多的问题是表格被切分器拦腰截断导致检索到的内容残缺。后来把表格单独拎出来处理情况好了很多。3.3 切分策略为什么固定500字不是万能的六款开源产品里默认切分参数几乎都是chunk_size500、chunk_overlap50。但这个参数在中文文档和英文文档上的表现差很多。英文单词平均长度长500个字符大约是80-100个词中文500个字可能是一整段完整论述。所以我自己的经验是中文章节式文档chunk_size可以放宽到800-1000字英文技术文档500-600字更合适代码类文档按函数或类切分不能按字符切。切分还需要考虑“语义完整性”。如果文档有明确的章节标题优先按标题层级切分让一个chunk尽量对应一个完整的论述单元。如果文档是连续的论文或报告没有明显标题可以引入语义切分先做句子嵌入然后计算相邻句子的余弦相似度相似度低于阈值的点就作为切分点。这个技术在LlamaIndex里有实现叫SemanticSplitterNodeParser。自研时可以借鉴但注意计算成本。另外overlap也不是越大越好。overlap是为了保证跨chunk的语义衔接但如果overlap太大会导致重复内容过多浪费向量库存储和LLM的上下文窗口。我建议overlap设为chunk_size的10%-20%并且只在相邻chunk之间共享少量句子不要出现整段重复。3.4 索引设计向量倒排混合还要带上元数据纯向量检索有一个经典问题它擅长语义匹配但不擅长精确匹配。比如用户问“图1中的曲线代表什么”向量检索很难把它和图1附近的内容关联起来而BM25可以通过“图1”这个关键词精确命中。所以六款产品里有好几款实际上采用了混合检索向量召回BM25召回再用重排器融合。自研时的做法是先用向量检索召回top 50再用BM25召回top 50合并去重后得到候选集。然后给每个候选计算综合分数比如final_score 0.5 * vector_score 0.5 * bm25_score。注意向量分数一般是余弦相似度越大越好BM25分数是词频权重也是越大越好但量纲不同需要先归一化。最简单的方式是把分数转成排序序号的倒数比如rank_penalty 1/(rank1)再加权。元数据过滤也极其重要。比如你的知识库里有多个文档需要按“部门法务”“年份2024”这样的条件过滤。向量数据库基本都支持metadata过滤但如果你设计时没有把metadata存进索引后面根本没法过滤。所以我在文档解析时就会提取作者、日期、分类、标题等字段存入metadata然后建立二级索引。这一步做得早后面检索效率高很多。3.5 检索与重排从粗糙到精细的效果分层自研RAG的检索不能只依赖一个向量检索。我的实践是把检索分为三个阶段第一阶段是粗召回目标是不要漏掉正确内容。用向量检索BM25各召回50条合并得到最多100条候选。这个阶段宁可多召回因为后面还有重排。第二阶段是精排。用cross-encoder重排模型对query和每个候选chunk做打分。常用的bge-reranker-base或reranker-large输入是query和chunk的文本拼接输出相关性分数。这比向量检索的双塔模型更准因为它能充分交互query和doc的token。但速度慢所以只对粗召回结果重排。我一般取重排后top 5。第三阶段是阈值过滤。重排分数需要设定一个阈值低于阈值的不给LLM。阈值可以通过测试集上的分布来确定我常用的一个基线是bge-reranker-base的分数在0到1之间通常0.35以上才可接受但具体要看你的领域最好收集一批“bad case”来调阈值。需要注意的是检索结果必须有“可解释性”。每次检索要记录哪些chunk被命中命中的依据是什么。这样如果回答错了可以通过回溯看到是召回问题还是生成问题。QAnything在这一点做得比较好前端直接展示召回来源这个设计值得学习。3.6 生成与引用Prompt模板和上下文组装的艺术生成环节是RAG的最后一步也是最容易出“幻觉”的一步。即便检索到了正确答案如果Prompt构造不合理LLM依然可能自由发挥。我总结了一个稳定高效的Prompt模板核心是三段式第一段是系统指令明确告诉LLM“你是知识库问答助手只能依据提供的参考片段回答不要编造。若片段中没有答案请直接说不知道。” 第二段是参考片段按序号列出检索到的chunk每个chunk前标注来源文档和页码。 第三段是用户问题输入query。关键一点是参考片段的顺序很重要。不要把最相关的放在最后LLM对中间位置的内容记忆较弱。我通常把重排分数最高的放在最前面也可以按原文顺序排但更适合按相关性降序。然后要求LLM在回答末尾引用来源编号比如[1][2]这样才能追溯到chunk。引用溯源做起来也非常简单在生成Prompt时每个chunk前加上[source:文件名-页码]LLM会倾向于引用这些标记。但为了确保引用准确最好额外在后处理阶段校验从答案中提取[1]之类的标记确认它对应的chunk是否真的存在于输入上下文里。如果不匹配就删掉这个引用标记避免伪造。这个后处理逻辑虽然简单但能有效提升可信度。生成参数上温度建议设为0.1到0.3之间太高容易发散。max_tokens要根据答案类型调如果是“是/否”类问题50个token就够如果是报告生成可能需要1000。另外如果预算充足可以开启“流式输出”改善用户体验。不过流式输出会增加实现复杂度自研MVP阶段可以先不考虑。3.7 评估与调优没有评估的RAG就是盲人摸象开源项目里最常见的毛病就是没有配套评估。自研时如果不把评估体系建起来你根本不知道改动是变好了还是变坏了。我的建议是至少建立三层评估第一层是检索质量评估。准备一批query-answer对每个query标注它对应的标准chunk ID来自哪篇文档的哪个段。然后运行检索计算RecallK即在检索结果top K中是否包含标准chunk。这个指标能直接反映检索器好坏。我一般用Recall5作为核心指标目标至少在0.8以上。第二层是生成质量评估。把检索结果喂给LLM生成回答然后让人工或GPT-4打分评估答案是否准确、是否回答了问题、是否有引用。对于初期团队可以用“准确率”和“幻觉率”两个粗指标。幻觉率指答案中出现了参考片段中不存在的事实。这需要人工抽查。第三层是端到端评估。在用户真实query上做线上AB测试观察点赞率、点踩率、人工接管率。这个指标最真实但周期长。调优时我习惯用“控制变量法”。比如先固定检索器只调生成Prompt等生成稳定后再动检索参数。不要同时调多个变量否则出了问题都不知道是哪一步引起的。另外要建立失败case库。每个bad case记录当时的query、召回内容、最终回答然后归类是文档解析错了还是切分导致语义不完整还是检索没召回还是LLM幻觉归类后就能针对性地优化对应模块。4. 实际踩过的坑逆向工程之外的实战教训4.1 嵌入模型不一致导致的版本“灵异事件”我第一版自研时开发机用的嵌入模型是BAAI/bge-large-zh-v1.5后来在测试服务器上换了量化版结果向量维度一样但生成的向量空间对不上用开发机存的向量去跟测试服务器的向量做相似度计算分数全乱。后来才意识到嵌入模型只要版本不同、量化方式不同产生的向量就不能混用。所以自研RAG一定要把“嵌入模型版本”作为索引元数据保存下来。每次升级嵌入模型必须重建全量索引不能只增量更新。更细的坑是同一版本的模型在不同框架比如PyTorch和ONNX下输出可能会有微小差异如果线上用ONNX做推理离线建索引也必须用ONNX否则线上召回会变差。这个坑我踩了两次痛彻心扉。4.2 表格解析错位一个让RAG效果大跌的问题有段时间我的知识库里有很多带表格的政策文件。解析出来的表格如果按行切分比如三列数据切分后变成“名称: A数值: 120备注: 200”语义完全割裂用户问“A对应的数值是多少”检索根本召不回。后来我参考RAGFlow的思路把表格单独识别出来每一行转成“键值对”文本并保留表头信息。比如一个“产品价格表”转成产品名笔记本价格5999库存20这样语义就完整了。对于复杂表格多级表头、合并单元格我建议先做表格结构识别再用LLM生成表格语义描述。虽然增加了成本但效果提升明显。如果表格不重要可以直接纯文本化但要确保检索到的内容上下文完整。4.3 检索分数失效为什么相似度分数不能跨query比较经常有人在检索代码里写死了“相似度0.7才返回”。但在实际场景里不同query的相似度分布完全不同。比如问“什么是合同”所有关于合同的chunk相似度可能都在0.8以上而问“违约金的计算方式”可能最相关的chunk相似度只有0.6。所以固定阈值会导致一部分query没结果另一部分query返回一堆垃圾。正确做法是不设相似度阈值而是用“相对排名重排分数”来决定。重排器是query-aware的分数相对可比。或者可以为每个query动态选择top K然后根据重排分数的分布选择一个相对低谷作为阈值。这个细节直接决定了系统的鲁棒性。4.4 文档更新后索引不同步知识库变成了“僵尸库”自研RAG很容易忽略增量更新。比如你上传了一个新版本的PDF旧的chunk还在索引里检索时新旧版本内容混在一起回答经常自相矛盾。开源方案大多提供了“delete by doc_id”的接口但需要你自己在更新流程里调用。我给出的蓝图里必须包含一个索引同步模块当文档变更时先按doc_id删除旧chunk再重新解析、切分、嵌入、插入。同时记录每次更新的时间戳检索时可以通过metadata的“最新版本”过滤。这个模块看起来简单但在并发写入、异步任务失败重试时很容易出问题。建议用消息队列协调保证更新操作幂等。4.5 成本失控嵌入和重排的API费用比LLM还高很多人以为RAG最贵的是LLM生成实际算一下发现如果每个query都要把100条候选送给重排模型重排成本会高得吓人。我当时用bge-reranker-large处理100条候选每个候选约500字批量重排单GPU都扛不住。后来优化为向量BM25各召回50合并后粗排到20再交给重排。粗排阶段直接用向量分数排序只保留两类召回中得分前20的。这样重排输入从100降到20速度和费用都降了几倍。另外嵌入模型尽量本地化部署用text2vec或BGE的ONNX量化版单CPU也能跑性价比远超调用外部API。如果必须用外部API要加缓存对query的嵌入结果缓存起来避免同一query重复调用。5. 从蓝图到落地我建议的自研实施路线5.1 阶段一MVP验证1-2周第一周不要追求完美。用最简单的架构跑通端到端流程PDF上传 - PyMuPDF提取文本 - 固定长度切分 - BGE嵌入 - FAISS索引 - 向量检索top5 - 直接拼Prompt - 调用LLM回答。这个阶段的目标是让系统“能用”同时把数据Schema和模块接口固定下来。在这个阶段你需要完成的交付物包括一份接口文档Document、Chunk、RetrievalResult的定义、一个脚本式运行入口、20条测试query和人工标注答案。不要急着上重排和混合检索那些之后加。5.2 阶段二效果调优3-4周MVP跑通后开始逐个模块升级。我建议的顺序是先优化文档解析解决表格和扫描件问题再引入语义切分解决乱切问题然后加BM25混合检索提升精确匹配最后加cross-encoder重排提升排序质量。每一步改动后都用第一阶段的20条测试query跑一遍记录Recall5和人工评分。改动至少让3条以上query变好且没有query变差才认为有效。否则就研究原因可能是其他模块拖后腿。5.3 阶段三生产化2-4周效果稳定后开始做工程化用vLLM部署私有化LLM、把FAISS迁移到Milvus或Qdrant、增加日志和监控、设计文档更新流程、建立评估数据集。这个阶段还要做压测看系统能支撑多少并发query。生产化阶段最容易踩的坑是“并发更新索引导致检索中断”。我的做法是采用双索引切换新索引在后台构建构建完成后原子切换。这样在线服务永远只读可用索引更新无感。这个技巧在数据量不大时也可以用但到了百万级chunk就非常必要。5.4 团队技能要求和避坑建议如果团队里有人用过LangChain或LlamaIndex上手很快但提醒一点不要被他们的Chain思维洗脑。自研RAG的核心是数据流清晰你不需要复杂的Agent循环只需要一条单向管线。团队成员应该掌握Python基础、向量数据库基本操作、一种嵌入模型的使用、以及基础的Prompt工程。对于是否用开源框架我的建议是如果只是快速原型用LangChain/LlamaIndex没问题如果目标是生产级定制系统最好只参考它们的模块设计自己实现核心管线。这样你能完全掌控升级、扩展和排错。你可以把开源项目当做“参考实现”而不是“依赖库”。6. 写在最后我的一些真实感触逆向这六款开源产品花了我不少时间但回过头看非常值。最深的体会是RAG系统没有一个模块是能“一次写对的”。你以为简单的文本切分在真实业务数据上会遇到各种边界情况你以为检索就是算相似度实际上混合检索和重排才是拉开效果差距的关键。所以蓝图不是一蹴而就的你需要在实践中不断修正每个模块的细节。如果只给你一个建议那就是先把数据Schema定死把每个模块的输入输出定义清楚。只要接口稳定后续任何一个模块的重写都不会牵连全局。这种模块化带来的底气是我从开源项目里逆向到的最宝贵经验。另外自研RAG不要盲目追求“大而全”也不要一开始就上Agent、多轮对话、复杂工作流。先把检索质量做扎实再把生成的可信度提上来。等你手里的bad case越来越少自然就知道下一步该往哪里扩展了。希望这篇蓝图能帮你少走一些弯路也欢迎你把踩坑经历分享出来大家一起把RAG做得更踏实。
返回列表