
1. 先承认现实RAG在真实场景里的瓶颈比开源Demo里难看得多我之所以决定把团队内部自研的RAG推倒重来是因为生产环境连续三个月被同一个问题折磨知识库检索命中率看着有80%但用户真正满意的回答不到一半。这个数字说出去非常尴尬可它恰恰是很多RAG项目的真实状态。开源仓库里的演示项目都太干净了数据是精选过的问句是模板化的评估是看感觉的——而真实业务里一份PDF可能混着表格、扫描图片、修订水印用户提问里全是口语、错别字和省略上下文。先说清楚我观察到的RAG瓶颈到底长什么样不然后面所有拆解都找不到落点。我把它总结成五个失效模式。第一个是召回不全。公司内部的制度文件经常会有版本更替一份《差旅报销管理规定》可能叫这个名字也可能在某次修订后被叫做《差旅管理规范2024修订版》。用户问报销标准是什么向量检索按语义去找往往只召回其中一个版本另一个版本里完全不同的额度标准就被漏掉了。这不是模型问题是索引侧对同义文档、版本关系完全没有处理。第二个是分块边界破坏语义。很多开源方案默认按固定token数切分但这会把一条完整的报销条款从中间切开。结果就是检索到的片段里只有住宿标准为几个字后面跟着的半句话在上一块。你让大模型怎么回答它只能瞎猜。就算你做了overlap也只解决了上下文断了的表层问题没解决块本身就是完整语义单元这个根本要求。第三个是数据更新滞后。业务方上午在后台改了一条价格下午用户提问检索出来的还是旧值。向量库的删除、更新和重建链路如果做得不精细脏数据会一直挂在索引里。我见过好几个项目在知识库更新后直接全量重建耗时几十分钟到几小时期间问答服务用的还是旧索引。这在内部工具上忍忍也就过去了放到客户侧产品上就是事故。第四个是引用冲突没有裁决。几个文档对同一件事说法不一致时RAG会把互斥的证据同时塞进上下文。开源框架大多不做矛盾检测模型只能自己挑一个信或者更糟把两种说法折中成一句两头各踩一半的话。回答质量下降还是小事关键是用户会开始不信任整个系统。第五个是评估基本靠肉眼。少了标准评估集和回归测试每次调整切分参数、换embedding模型、改prompt都不知道是在变好还是变坏。感觉好像变聪明了是团队里出现频率最高也最危险的一句话。这些失效模式共同指向一个结论RAG的瓶颈不在大模型而在检索链路和数据治理。这也是为什么我不赞成先换个更强的向量模型试试这种思路。向量模型能改善相似度匹配的精度但解决不了分块语义、版本冲突、元数据过滤、更新时效这类工程问题。想清楚这一点才有后面的动作去开源项目里找已经被验证过的解法而不是重新发明轮子。2. 逆向工程到底拆什么六款开源产品的选型与拆解顺序传统意义上逆向工程多指拿二进制还原逻辑但用在开源RAG项目上我觉得更准确的理解是不把开源项目当黑盒用而是从代码和数据结构层面反推它为什么这样设计然后把可复用的抽象拷贝到自己的蓝图里。这是向成熟产品学习架构的最快方式。2.1 我选了哪六款每一款盯着什么学市面上能做RAG的开源项目太多了我只挑六款不是因为其他不好而是它们恰好覆盖了从底层索引到上层应用的全部剖面。表格如下这是我自己选型时整理的口径产品类型我盯着它学什么DifyLLM应用平台数据集管理、知识库与应用之间的配置关系、召回测试工具FastGPT知识库问答应用多知识库路由、文件分段与搜索测试的交互设计RAGFlow文档智能解析RAG复杂PDF版面解析、表格与图片的抽取策略LlamaIndex数据框架文档节点、索引结构、检索器抽象层的划分方式Haystack管道框架Retriever/Reranker/Reader的标准接口设计Qdrant向量数据库向量索引结构、payload过滤、混合检索的服务端实现选这六款还有个私心它们不是同一层的东西。Dify和FastGPT是完整应用你能看到用户层面的操作流转LlamaIndex和Haystack是框架你能看到更高一层的抽象Qdrant是存储底座你能看到索引到底怎么落盘。同一类问题在不同层次上的解法往往不一样而这些差异恰恰是自研时需要取舍的地方。2.2 拆解的固定动作跑通Demo、抓请求、落到模块图我拆一个开源项目基本上固定四步走。第一步是先把项目跑起来。别急着读源码先启动服务、导入几份测试文档、发起几条问答把系统当黑盒玩一遍。这个过程中我会特别留意两个东西导入数据和创建知识库时的耗时、用户界面里暴露了哪些配置项。配置项就是设计者认为值得让用户调整的参数这往往是理解系统关键的捷径。第二步是抓接口。开着浏览器的开发者工具和抓包工具把一次完整问答的请求全部记录下来包括上传文档、发起索引、查询、获取流式回答这几个阶段。抓到请求就能看到数据结构长什么样比如一个知识库的配置对象里有哪些字段一次搜索请求传了哪些过滤条件。这比直接裸读源码快得多。第三步是顺着请求链路进源码。入口通常很好找就是Web框架的route定义。从路由到service再到具体的数据访问层一比一对照抓到的请求参数很快就能画清楚数据流。我会刻意先不看业务逻辑细节只关心一个请求经过哪些模块、在哪个节点做了向量检索、在哪个节点做了重排。第四步是回到模块级视角画一张架构图。不需要精确到类级别只要标清楚加载、切分、向量化、存储、检索、重排、生成、评估这几个环节分别由哪些组件负责以及组件之间的接口契约是什么。这张图就是我这篇文章后面要讲的自研蓝图的最初底稿。2.3 在Mac上快速搭本地环境的小经验很多人在Mac上搭RAG知识库会卡在环境依赖上。我的建议是尽量用Docker Compose把向量库、中间件和应用服务一次性拉起来同时本地起一个轻量级embedding模型做验证。别一上来就接云端的Embedding API你在本地反复调整切分参数时光是请求延迟和限流就能把你逼疯。我自己的组合很朴素Docker里放Qdrant和PostgreSQL本地用sentence-transformers跑一个300维左右的embeddings模型Python侧用FastAPI包一个最小的服务数据文件用Markdown和PDF混合测试。这套环境在Mac上搭完也就半小时但它足够支撑起一整轮的逆向分析和原型验证。等确认模块设计没问题了再换生产级的embedding模型和更大规模的向量库这是我把验证成本和试错成本分开的惯用做法。3. 拆完后的最大发现开源RAG的共性骨架其实是同一套拆完六款产品之后最让我意外的是它们虽然外观差异巨大但核心模块高度趋同。不管你用的是偏应用的Dify、偏框架的LlamaIndex还是偏底层的Qdrant只要做的是RAG就逃不出下面这套骨架。3.1 共性骨架的八个模块我把反复看到的模块整理成一张表模块职责六款产品里的具体表现Document Loader读取原始文件支持PDF、Word、Markdown、网页抓取Parser识别文档结构读取段落、表格、图片位置、标题层级Chunker切分语义片段按标题、段落、固定token数切分带overlapEmbedder文本向量化替换OpenAI、本地sentence-transformer模型Store存储向量和元数据多数用向量库可以附加payload过滤Retriever召回候选片段向量召回、关键词召回、混合召回Reranker精排候选片段交叉编码器、LLM作为rerankerGenerator组合上下文生成回答构造prompt、注入引用来源、流式输出这套骨架在不同项目里的命名不一样比如LlamaIndex叫NodeDify叫SegmentFastGPT叫Paragraph但本质都是切分后的检索单元。Qdrant里没有Chunker因为它只管存储和检索可你只要接上任何一套上层应用它还是会回到这条链路上来。3.2 为什么模块边界会收敛到相似我一开始怀疑这是大家互相抄后来仔细想想这个收敛背后有更扎实的原因。第一职责的生命周期不同。清洗入库是异步的批处理查询是低延迟的在线链路。解析文档可能要花几十秒但用户检索不能等这么久。所以它们天然不能混在一个模块里边界是性能需求逼出来的。第二输入输出必须标准化。解析器输出的都是片段元数据检索器返回的都是候选块得分这个契约是一切可替换性的基础。六款产品不管内部多复杂对外接口几乎都长这样原因很简单只有接口统一才能自由替换Embedding模型和向量库。第三失败域隔离。解析出错不应该影响检索检索返回空也不应该导致生成报错。模块独立之后每个环节的异常可以被单独监控、单独重试。这是工程上很朴素但极其重要的原则。3.3 骨架之外的差异点才是自研的竞争空间共性骨架告诉我们哪些部分必须做但真正拉开体验差距的是骨架之外的细节。我这次逆向拆解最大的收获就在这些差异点上。Dify比FastGPT强在数据集管理上的精细化它给每个知识库提供了单独的召回测试入口能让你直接对比不同分段和检索参数的效果。RAGFlow的差异则集中在前端的Parser环节它对表格、页眉页脚、扫描件做了大量预判实测对复杂PDF的解析准确率明显高于很多拿文本抽取硬做的方案。Qdrant的差异化能力在Store层它的payload索引和过滤条件可以做到非常复杂的组合这在多租户场景下价值极大。这些差异点给自研的提示是不要在人人都会做的链路标准化上堆差异化那只是及格线。真正值得投入的地方是那些开源项目为了普适而妥协、但你的业务场景里不得不做死的地方。4. 可复用自研蓝图把共性骨架翻译成模块接口与数据流看完共性骨架接下来要把抽象翻译成能落地的东西。我画蓝图的时候一直遵守一个原则每个模块只定义接口不绑定实现。因为自研RAG本质上是给自己留后路——今天用这个embedding模型不代表三个月后不能换另一个今天用Qdrant不代表以后不能换别的向量库。一旦接口设计好了换实现就是替换一个类的事。4.1 索引侧蓝图文档解析、切分、索引索引侧解决的是知识从原始文件变成可检索数据的问题整体流程是文档加载、结构解析、切分、向量化、存储。文档加载和解析我建议分开。加载只负责拿到文件字节流解析负责从字节流里提取结构化内容。这个拆分在纯文本文件上显得多余但遇到PDF就会知道值不值——同一个PDF用PyPDF2硬抽文本和用专门的版面解析工具抽取结果差距非常大。RAGFlow给的最大启发就是这里值得下重注尤其是表格、图片、复杂版面。切分模块的输出必须是统一的Chunk对象我一般用dataclass定义from dataclasses import dataclass, field dataclass class Chunk: chunk_id: str doc_id: str content: str metadata: dict embedding: list[float] field(default_factorylist)metadata这块一定要从第一天就开始认真维护包括文档类型、所属部门、版本号、生效时间、权限范围。很多人做RAG只把它当附带字段等到要做过滤、做权限隔离、做版本控制的时候才发现metadata缺了一堆只能回炉重造。这是我踩出来的教训。向量化模块别写死在某个模型上。定义一个单独的接口封装成可替换的组件生产环境换模型的时候只需要改配置。存储模块同理优先选择支持元数据过滤的向量库而不是只能存向量的玩具方案。4.2 查询侧蓝图召回、融合、重排、生成查询侧解决的是用户提一个问题系统给出可靠回答的问题核心链路是查询理解、召回、融合、重排、生成。这里面最容易被人漏掉的是查询理解。用户问题里经常带着噪音请问一下咱们公司的年假制度是什么样的谢谢——你要先把主干抽取出来甚至要把用户提到的别名映射到知识库里的标准实体名这一步不做好后面的向量检索就是拿一个满带口语的query去找一个满带书面语的index效果自然拉胯。召回阶段不要只用一种方案。我在生产里长期保留两种召回并行向量召回负责语义相似BM25这种关键词召回负责精确匹配。两者在开源项目里几乎都是标配但很多自研项目起步时只做了向量召回等遇到专有名词、缩写、编号查不到时才想起来补。补召回容易难的是把两种来源的得分放进同一个尺度里比较。这里推荐用RRFReciprocal Rank Fusion它不看绝对得分只看排名合并起来非常稳定代码也就十几行def rrf_fuse(rank_lists, k60): scores {} for rank_list in rank_lists: for rank, doc_id in enumerate(rank_list): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)融合之后再交给Reranker精排生产环境我建议用交叉编码器模型把query和每个候选片段拼在一起做打分。这个模块很贵但能显著提升头部结果的准确率。最后才是生成模块它负责把重排后的片段、引用来源和系统提示词组装成最终给大模型的输入。生成模块里务必做引用映射回答里哪些句子来自哪个片段都要对得上。4.3 一份可直接落地的Python代码骨架按上面的蓝图我给你一个最小可运行的接口骨架直接用抽象类定义后面按需实现from abc import ABC, abstractmethod class Chunker(ABC): abstractmethod def split(self, doc_text: str, metadata: dict) - list[Chunk]: ... class Embedder(ABC): abstractmethod def embed(self, texts: list[str]) - list[list[float]]: ... class VectorStore(ABC): abstractmethod def upsert(self, chunks: list[Chunk]) - None: ... abstractmethod def search(self, query_vector: list[float], filters: dict | None None, top_k: int 5) - list[Chunk]: ... class Retriever(ABC): abstractmethod def retrieve(self, query: str, filters: dict | None None) - list[Chunk]: ... class Reranker(ABC): abstractmethod def rerank(self, query: str, chunks: list[Chunk]) - list[Chunk]: ... class Generator(ABC): abstractmethod def generate(self, query: str, contexts: list[Chunk]) - str: ...这几个接口就是你能拿回公司直接当讨论底稿的东西。每个团队都可以按自己的技术栈实现但只要接口不变换分块策略也好、换向量库也好、换大模型也好对上下游的影响都被限制在一个类内部。我把这当成自研RAG的第一条设计纪律。5. 真正决定自研成败的隐性设计切分、元数据、评估和可观测性把核心链路跑通只是万里长征第一步真正决定RAG在真实项目里能不能活下去的往往是那些不写在架构图角落里的隐性设计。这一节我把最影响我判断的四块摊开讲。5.1 切分策略按语义边界而不是固定字数先回答一个我经常被问到的问题RAG知识库能存储图片吗能但要分两种情况。一种图片是纸面信息载体比如PDF里的扫描截图、发票照片、带文字的流程图这类图片里的信息其实是文字正确做法是先做OCR再走常规的文本切分和向量化而不是把图片本身硬塞给向量模型。另一种图片是真正的多模态内容比如产品外观图、设计稿这才需要多模态Embedding模型或者先把图片转成文字描述再入库。普通业务场景里90%的图片需求都落第一种。切分策略上很多项目的默认方案是按固定token数硬切我要泼一盆冷水这只能作为兜底不能作为主力。我试过按标题层级切、按段落切、按语义完整度切效果普遍比固定token数好因为法律条款、产品FAQ、制度规范这类文本的天然语义边界就是章节和段落强行切碎只会把答案拦腰截断。比较好的做法是混合策略先按文档结构切一层保留标题层级作为metadata如果某个段落特别长再按句子或者固定窗口子切同时给每个子块带上父级标题的上下文。RAGFlow在PDF解析上的经验在这里同样适用它做的不是简单抽取文本而是先把版面结构还原出来再基于结构做切分。这套思路我吸收之后切分质量直接上了一个台阶。5.2 元数据过滤没有过滤的向量检索等于盲人摸象很多RAG自研项目的检索接口长这样query进去向量出来topK。这种裸奔设计在数据量小、内容单一的时候能跑一旦数据量上去、业务复杂起来没有元数据过滤的向量检索就是灾难。举个例子。你的知识库里同时装着《销售激励政策2023版》和《销售激励政策2024版》用户问今年提成比例是多少。向量检索很可能把2023版和2024版都召回来因为语义太像了。如果你不做版本过滤模型就只能靠猜或者更糟取一个模糊的中间值。如果从一开始就维护好版本号生效时间这两个metadata字段查询侧翻译用户问题里的今年为版本号2024检索时直接加过滤条件这个问题就彻底消失了。元数据过滤还可以解决权限隔离。不同角色能看的知识范围不同这不该放在生成阶段让模型判断而应该在检索阶段就用过滤条件把无权访问的内容挡在外面。模型判断会有失误检索过滤不会。5.3 结构化知识库与RAG知识库的区别和应用场景拆开源产品时我也格外留意了另一条线有些团队认为RAG能包打一切把本属于结构化存储的数据也塞进文档里。这是概念层面的混淆。RAG知识库适合的是非结构化文本问答看重语义相似。你问公司年假制度是什么答案在文档里但原文和提问措辞完全不同这时候RAG的向量检索非常合适。结构化知识库适合的是精确查询和聚合计算。你问上个月华东区销售额是多少如果数据存在SQL库里一条GROUP BY就能回答硬塞进RAG反而需要模型去做数学计算误差和幻觉都会放大。工程上更推荐做混合。自研RAG蓝图里Retriever模块一开始就可以设计成多源召回文本召回走向量库精确事实走SQL或键值库关系型知识走知识图谱。把RAG和结构化检索放在一起由查询理解模块决定各走多少权重。这样系统才不会因为只会做向量检索而被迫用向量去解决所有问题。5.4 评估集和回归测试自研翻车的重灾区开源项目很少能教你评估怎么做因为它们的demo场景太简单跑通即可。但自研RAG一定要把评估当成一等公民。我从上线第一周就开始积累黄金评估集来源很简单用户反馈里被标记为答得不对的真实问答、业务方手工挑出的高频问题、每次发版前人工抽测的50条样本。评估集不需要一开始很大但必须坚持积累。每调整一次切分参数、换一次Embedding模型、改一次Prompt我都跑一遍这组数据对比新旧版本的召回率和答案正确率。没有这个过程你根本不知道上一次改动是变好了还是变坏了。可观测性也一样重要。每个查询都要记录召回了哪些片段、重排后谁进了上下文、模型最终用了哪些片段、用户有没有点赞。这套日志不光是排查问题的手段更是评估集的自动挖掘机——把大量低满意度的查询连同当时的检索日志存下来人工复核后丢进评估集循环就跑起来了。6. 从开源产品里偷师的高级组件语义缓存、多路召回和图谱路由把基础链路做稳之后我复盘六款产品里真正让我觉得值回票价的设计高级不等于复杂很多组件就是一个小技巧却能实打实地省钱和提效。6.1 语义缓存是怎么让自研RAG成本下降三成的我先说一个数字加了语义缓存之后线上RAG的日均大模型调用量下降了三成左右。原理很简单把用户问题向量化去缓存库查最近邻居如果距离小于阈值说明这是一个已经被回答过的相似问题直接返回当时的结果不再走完整检索和生成链路。这个设计在开源产品里非常常见尤其是很多基于LLM的应用框架都内置了缓存抽象。我吸收进自研蓝图后发现它不仅省了账单上的费用还顺手解决了两个体验问题一是热点问题的响应时间从两三秒降到几百毫秒二是同一个问题不会因为模型随机性而每次给出不同的答案用户观感变得更稳定。语义缓存的坑在失效策略上。业务数据更新之后相关问题的缓存必须能主动或被动失效。我的做法是给缓存项打上索引版本号每次知识库更新导致索引版本变化时按影响范围批量清理相关缓存。没有这层设计缓存会变成另一份脏数据源。6.2 多路召回与Score归一化前面提到混合召回这里具体展开。我在生产里同时跑三路召回向量召回、BM25关键词召回、以及基于标题和metadata的精确召回。向量负责语义BM25负责专有名字和编号metadata精确召回负责那些我知道它一定存在但语义上很难匹配的内容。多路召回的难点不在召回在合并。三路得分尺度不同向量距离和BM25相关度根本不能直接相加。我前面给了RRF的方案这里再补充一个细节RRF对每路召回的规模比较敏感一般每路最多取前50到100名进融合太多会引入大量尾部噪音。此外融合之后上重排才是关键重排模型一次只处理几十个候选成本可控效果显著。在自研蓝图里Retriever接口从设计上就得支持返回多个召回源的结果而不是单路召回的一维列表。这样后续扩展语义召回、关键词召回、图谱召回都只是往Retriever里面加实现不影响上层。6.3 知识图谱与Ontology RAG什么时候值得引入这是我拆解产品时格外关注的一个方向因为很多团队在问到底要不要上知识图谱。我的判断是当你的查询涉及多跳关系、实体关联、细粒度分类时纯文本RAG确实有天花板。比如查出所有在A部门工作过、现在负责B项目的员工名单这类问题文本里根本没有直接答案它分散在多份文档的多个表格里靠向量检索拼不出来。知识图谱的引入方式有两条路。一条是完整引入图谱存储和查询把实体、关系建好查询时先做实体识别再通过图查询拿到关联证据最后把证据喂给大模型生成答案。另一条是轻量级的Ontology RAG只维护概念层级和别名映射不建完整图单纯用来做查询改写和实体归一。比如用户说出差规定系统先映射到差旅管理再拿这个标准实体去检索效果就已经提升一大截了。我的经验是不要一上来就建全量知识图谱。先把Ontology层做出来成本低见效快等确实出现大量多跳关系查询了再逐步引入图存储。这也是我从那几个成熟开源项目里观察到的演进路径——它们很少在起步阶段就把图谱绑定进核心链路。7. 逆向工程六款产品之后我留给自己的几条经验拆了六款开源产品、画了一套自研蓝图之后真正沉淀下来的不是代码而是几条有点反常识的经验。把这些写下来既是对这次逆向工程的交代也是给后来者的一份省时间指南。第一开源是教材不是依赖。我见过太多团队把LangChain或LlamaIndex直接嵌进生产系统被框架的抽象层级绑架升级一个版本都可能要改一堆代码。我这次逆向拆解的核心目的就是从这些成熟项目里提炼出如果让我自己写我会怎么抽象而不是我该怎么调用它的API。教材看完要合上书自己动手写骨架这才是把别人的架构能力变成自己的。第二先做窄再做宽。拆完六款产品后我差点把语义缓存、多路召回、知识图谱、Reranker全都规划进第一版。后来冷静下来第一版只做了三件事文档解析与切分、向量关键词混合检索、带引用来源的生成。其余组件全部通过接口预留位置后续按需接入。事实证明这个决策是对的过早堆复杂组件只会让问题排查无从下手。第三评估集才是自研的护城河。开源产品之间的RAG能力差距远没有Demo里看起来那么大因为你拿到套壳部署都一样跑得通。真正拉开差距的是你手上有多少反映真实业务分布的评估数据以及你能不能快速通过回归测试验证每一次改动。我在Mac上起本地实验环境时第一件事不是搭向量库而是把第一条黄金评估数据录进去。第四数据治理比模型选择更值得花时间。不管是版本冲突、元数据缺失还是脏数据无法清理这些都是数据治理问题不是你换一个更大的模型能解决的。自研RAG如果不在数据侧下功夫底层模型再强喂进去的也是残缺的知识。最后说一个实操层面的建议。如果你也开始做类似的逆向工程不要雨露均沾地把每个开源项目都读一遍而是挑一到两个和你业务最接近的按我上面说的步骤拆透。拆透一个比浏览十个都管用。拆完之后马上用你自己的数据跑一遍蓝图的垂直切片哪怕只是最简单的Markdown文件呢也要让流程先转起来再谈优化。一切纸上蓝图都不如一个能跑通的真实链路来得踏实。