ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RAG系统存储架构指南:四层分工让知识库从能跑到能扛

2026/10/8 13:50:10 拓冰建站 浏览量
RAG系统存储架构指南:四层分工让知识库从能跑到能扛 1. RAGFlow 的四层存储架构到底在解决什么问题如果你和我一样过去半年一直在折腾 RAG 落地大概会有一个共同感受很多检索增强生成项目不是死在模型效果上而是死在存储设计上。文档传上去解析完了向量也灌进去了结果一查询要么慢得离谱要么文件更新后旧的回答还赖在缓存里不走。RAGFlow 把这摊事拆成了元数据、对象、检索、缓存四层乍一看像是把简单问题复杂化实际跑过之后我才明白这四层分工恰恰是 RAG 系统从能跑走向能扛的关键。先说个最直观的例子。一套企业级知识库系统可能要管理几万份 PDF、Word、PPT每份几十上百兆。如果你把所有内容一股脑塞进一个存储里会产生两个致命问题第一大文件的读写会污染高频的检索 IO导致问答响应变得极不稳定第二文件本身、文件的解析结果、文件的索引结构、高频查询的临时结果它们的访问频次和生命周期完全不同硬捆在一起没法做针对性的容量规划和故障隔离。RAGFlow 的四层存储本质上就是按数据的不同特征把它们分到不同工位上各干各的活再通过上层调度把结果串起来。这四层分别是元数据存储负责文档、知识库、用户、任务状态这类结构化信息对象存储负责原始文件和解析产物的二进制内容检索存储负责可供语义检索和关键词匹配的向量与文本索引缓存存储负责会话状态、热门查询结果、临时计算成果。我在第一次部署 RAGFlow 时曾以为只要把 MySQL、对象存储、Elasticsearch、Redis 都装起来就算完事后来才发现理解这四者之间如何协作比会装服务重要得多。1.1 如果只有一个存储会怎样我们不妨先做个反事实推演假设不用四层架构只用一张 MySQL 表存所有东西。文件二进制用 BLOB 字段存向量用 JSON 字段存查询结果也存临时表。这样做的后果有几个层次第一IO 模型冲突。元数据访问是典型的小文件随机读通常几十毫秒就要返回对象内容访问是顺序大文件流动辄几十兆上百兆的读向量检索是计算密集型的相似度扫描。三种流量混在一起任何一种突增都会拖垮另外两种。尤其是大批量导入文档时对象存储写入会长时间占用磁盘带宽热门问答查询的延迟会肉眼可见地劣化。第二生命周期无法区分。原始文件可能要在审计场景下保留三年解析出的分块内容在文档更新后立刻作废缓存更是几十分钟就得过期。如果它们躺在同一个存储桶里你就得为最长的保留周期买单还得自己写一套脆弱的定时清理逻辑。第三恢复和扩展不灵活。某一层出故障时需要快速恢复或扩容混在一起的话只能整体迁移。RAGFlow 的四层存储把数据特征和存储介质解耦允许你在不同层级独立选择 MySQL 或 PostgreSQL、S3 或 MinIO、Elasticsearch 或 Infinity、Redis 或其他 KV 缓存——每一层都能独立伸缩这才是云原生时代该有的思路。1.2 四层存储各自扮演的角色我用一个生活化类比来理解这四层一家图书馆。元数据存储是图书管理系统里的书目卡片记录书名、作者、分类、借阅状态对象存储是仓库里的实体书架存放整本整本的书检索存储是目录索引和摘要卡帮你靠大概意思快速找到相关段落缓存存储则是前台柜台上常备的热门书复印本大家都在问的那几页随取随用不用每次都跑仓库。对应到 RAGFlow 里元数据层用户信息、知识库定义、文档列表、解析状态、分块配置、权限信息。这些数据量不大但要求强一致、支持事务适合关系型数据库。对象层用户上传的原始文件、解析后的版面 JSON、抽取出的图片、附件。这些对象只要求按 Key 读写不要求复杂查询。检索层文档解析后切出的文本块Chunk及其向量表示、全文倒排索引。承担查询时的召回职责是 RAG 效果的物理基础。缓存层会话上下文、Embedding 结果、重排结果、高频问题的拼接上下文甚至大模型返回的 token 用量统计。要的就是快丢了也能重算。1.3 RAG 场景的特殊性决定了分层为什么通用 Web 应用不需要这么麻烦的存储分层因为通用应用的数据访问路径相对固定订单、用户、商品各管各的很少出现从几万份文档里语义查找最相关五段话再拼给模型这种横跨多类数据的操作。RAG 的核心链路是文档入库 → 解析 → 切块 → 向量化 → 存储索引 → 查询时向量化 → 召回 → 重排 → 拼装上下文 → 交给 LLM。这条链路上每一环都消费不同类型的数据入库消费二进制文件切块产生结构文本向量化产生浮点数组拼装需要原文本片段。四层存储正是对齐了这条流水线的每个阶段对象层管原始输入元数据层管处理状态检索层管可召回的知识单元缓存层管重复计算。所以我一直觉得看 RAGFlow 的存储架构不能只看部署清单里有几个中间件而要把它当成一条数据的生产线。生产线上哪个环节的仓储出问题整条流水线都得停。2. 元数据存储数据库里保存的文档户口本2.1 元数据层到底存了哪些东西我最初看 RAGFlow 的数据库结构时以为无非就是几张用户表和文档表。实际翻下来发现元数据层的责任比想象中大得多。它至少承担了四类信息实体信息用户、团队、知识库、API Token、权限关系。这部分和普通业务系统差不多。文档档案文档在对象存储里的路径、文件名、大小、格式类型、上传时间、上传者、解析状态、解析所用的 Chunk 方法、字符数预估。这里最关键的是status字段它记录了文档从 uploaded 到 parsing、done、failed 的全过程异步任务全靠它来驱动状态机。索引映射文档、分块、向量索引之间的关联关系。一个文档被切成了哪些 Chunk每个 Chunk 的向量写入了哪个索引、存储在哪个 doc_id 下这些映射必须可靠记录否则文档更新或删除时无法精准清理旧的索引数据。配置与任务知识库的检索参数召回条数、相似度阈值、重排开关、Embedding 模型的配置、解析任务的进度快照。这些数据有一个共同特征单条体积小、增长平稳、需要事务和约束。比如同一份文档不能同时被两个解析任务处理这种互斥逻辑放在对象存储里几乎没法做放到 MySQL/PostgreSQL 里就是一行UPDATE ... WHERE statusuploaded的事。2.2 为什么用关系型库而不是全塞进对象存储可能有朋友问既然都有对象存储了把元数据写成 JSON 文件丢进 MinIO 不是更省事短期 Demo 可以这么玩但到了多用户、多文档、并发上传的场景关系型数据库的几个特性就变得不可替代一是原子性。上传文档时需要同时写入文档记录、创建初始解析任务、更新知识库文档计数。三步操作要么全成功要么全失败否则会出现文档看不到但对象存储里多了个大文件这类幽灵数据。二是条件更新。多个解析 worker 同时拉取任务时靠状态为 running 的记录只有自己能改这种乐观锁机制来避免重复处理。对象存储的 PUT 操作不提供这种带条件的更新语义。三是关联查询。比如查某个知识库里所有解析失败的文档附带其上传者名字关系型库一条 JOIN 搞定。如果元数据散落在 JSON 文件里你得遍历所有文件再在内存里做关联数据量上来后完全不可维护。RAGFlow 在这层默认使用 MySQL 8.x也支持 PostgreSQL。我在生产环境更倾向用 PostgreSQL一方面是 JSONB 字段在处理动态解析结果时更灵活另一方面是在批量更新大量文档状态时性能更好一些。当然如果团队只有 MySQL 运维经验用它也完全没问题毕竟这一层的数据量通常不会成为瓶颈。2.3 元数据与对象存储的引用关系这里有个容易想歪的点元数据存储里不保存文件内容本身只保存指向对象存储的引用。可以理解成数据库里有一列叫storage_key值类似knowledge_base/2025/06/18/uuid.pdf。读取流程是先查数据库拿到这个 Key再去对象存储里 GET 对应对象。这种引用关系还要处理两件麻烦事。一件是孤儿对象清理如果文档上传后解析失败被删除数据库记录没了但对象存储里的文件可能还在。RAGFlow 的做法是靠文档状态流转在删除记录时同步删除对象但在异常崩溃场景下仍可能留下残留。我在实际运维中会写一个定时脚本扫描对象存储里最近 N 天没有被任何元数据引用的 Key定期清理。另一件是版本一致性当用户覆盖上传同名文档时元数据层要更新版本号并生成新的storage_key不能复用旧 Key。旧 Key 的清理可以延后但新查询必须落到新对象上否则会出现文档已更新回答仍是旧内容的尴尬情况。3. 对象存储大文件该待的地方3.1 对象存储与文件系统的本质区别很多从单体应用转过来的开发者对对象存储的第一反应是这不就是个网盘吗。实际区别在于文件系统有目录树、有文件锁、支持就地修改文件的一部分对象存储则是扁平的 Key-Value 空间对象一写就完整落盘修改只能整体覆盖。RAGFlow 选择对象存储承载原始文件正是看中了它的几个特性容量可以做到 PB 级横向扩展、访问通过 HTTP API 无需挂载磁盘、天然支持多副本和版本管理。更重要的是对象存储很适合 RAG 的写多读少、写大读小模式。原始 PDF 一旦上传除了 document parser 需要读取之外普通查询根本不会碰它。这种冷数据用对象存储比放在热目录划算得多成本低、运维省心。3.2 RAGFlow 怎么把文件放进去、取出来以我部署的版本为例流程大概是这样的用户上传文件后后端服务调用对象存储 SDK 生成一个预签名 URL前端直接通过该 URL 上传到 bucket避免大文件经过应用服务器中转。上传完成后后端在元数据层登记文档记录并封装一条解析任务进入队列供 Worker 消费。解析 Worker 从对象存储下载文件到本地临时目录调用 DeepDoc 做版面分析、OCR、表格识别产出结构化的文本块后再把这些解析产物比如版面 JSON、提取出的图表图片上传回对象存储的另一组 Key 下。也就是说对象存储里既有原始文件也有解析中间产物两类对象生命周期管理要区别对待原始文件在文档被删除前一直保留中间产物在检索索引构建完成后就可以压缩归档或清理。3.3 选型云 OSS vs MinIO vs 本地目录RAGFlow 支持 S3 兼容协议所以对象存储的选型非常灵活。我三种方式都试过讲点真实感受云厂商 OSS/S3最省心吞吐能力由云平台兜底适合多节点部署。成本这块要注意流量费RAGFlow 的解析 Worker 和向量库构建都要反复读文件跨区域取回费用会悄悄涨。建议把 RAGFlow 的部署区域和对象存储区域放在同一个云服务商的同地域内网访问通常不收费。自建 MinIO团队有运维能力、数据要留在内网时的首选。MinIO 的纠删码模式能扛磁盘故障部署也简单一个 Docker Compose 就能起来。我踩过的坑是单机模式性能上限有限并发解析时容易把磁盘 IO 打满生产环境至少用四节点甚至更多。本地目录默认假模式RAGFlow 支持把存储 backend 配置成本地目录适合单机开发和功能验证。这个模式的坑在于一旦换成多副本部署每台机器的本地目录不一致文件会散落各处。我见过有人开发环境一直用本地目录上线时忘了改配置结果查询时定时换一台机器就报文件不存在。切记要在部署早期就决定对象存储 backend不要拖到上线前才切换。对象存储层的运维重点其实是容量监控和生命周期策略。RAGFlow 的文档解析会产生大量中间对象大小往往是原始文件的数倍。建议为中间产物单独设置一个前缀并配置自动过期规则比如 7 天未引用即删除避免存储容量被历史解析垃圾拖垮。4. 检索存储向量库和全文索引的分工4.1 混合检索语义向量与关键词召回RAG 系统里最核心的存储是检索层。RAGFlow 的默认检索策略是混合检索即同时跑两路召回一路是向量检索把用户问题 Embedding 后和库里所有 Chunk 向量做相似度计算擅长理解语义另一路是全文检索利用 BM25 之类的算法做关键词匹配擅长精准命中专有名词和代码片段。两路结果合并后进入重排模型Reranker把真正相关的段落提到最前面。这套机制直接决定了检索存储必须具备两个能力高维向量的近似最近邻ANN搜索以及倒排索引全文搜索。RAGFlow 在自己的文档引擎里同时实现了这两种能力早期版本以 Elasticsearch 为主新版支持 Infinity 引擎并把文档的文本块、向量以及必要的元数据统一存放在检索存储中。4.2 检索层的索引三元组理解检索存储的协作方式可以抓住一个三元组question、content、vector。content是文档切出的原始文本块例如RAGFlow 支持从 PDF、Word、Excel 中提取表格数据并构建知识库。vector是对该文本块 Embedding 后得到的浮点数组维度取决于模型常见 768 或 1024 维。question是系统为每个 Chunk 自动生成的一个示例问题用来提升召回率。RAGFlow 在构建索引时会为每个 Chunk 生成content和vector同时通过 LLM 生成若干条可能的question一并写入检索存储。查询时用户的 query 会和vector做语义检索也会和question、content做全文匹配。这好比一本书的每个章节不仅有正文还预写了几道可能的考试题检索时既能按意思找也能按原话找命中率自然高很多。这里有个容易被忽略的细节更新知识库时不是更新某个 Chunk而是先删后插。因为向量索引的更新通常不是原地修改而是删除旧向量再加新向量。如果删除旧向量时元数据映射没跟上会出现文档明明删了检索还能召回的脏读。RAGFlow 依赖元数据层维护的文档与 Chunk 映射关系来执行批量删除所以你在做二次开发时一定不要绕过元数据层直接往检索存储里写数据否则后续清理流程会彻底失灵。4.3 分块Chunking对检索质量的影响检索存储的效果不只看引擎选型更看分块策略。同样的文档切成 128 字的小块和切成 1024 字的大块召回的准确度天差地别。RAGFlow 的解析器会按版面结构分块标题、段落、表格各自保留语义边界。比如一个大表格如果被硬切到两个 Chunk 里查询某产品的销量趋势时任何一个 Chunk 都信息不全Reranker 再厉害也无力回天。我常用的经验是业务文档信息密度高如财报、技术手册Chunk 控制在 256 到 512 字之间重叠率 10% 左右如果是长对话记录或叙事类文本可以放宽到 512 到 1024 字。还需要注意 Embedding 模型的最大输入长度。有些模型最长支持 512 token你切出来的 Chunk 超过这个长度向量化时会被截断语义信息直接丢失。建议在知识库配置里显式设置 Chunk 上限并抽样检查向量化日志里是否有大量截断告警。5. 缓存存储Redis 是怎么让整个链路变快的5.1 缓存到底缓存了什么四层里最容易被低估的是缓存层。早期我甚至想过既然检索存储已经很快了缓存是不是可有可无直到生产环境出现并发问题才明白缓存不是用来锦上添花的而是用来兜住重复计算的。RAGFlow 里 Redis 承担了几类职责。第一类是会话状态多轮对话的上下文、用户会话的 Token 用量、会话列表这些属于高频读写、允许丢失丢了就从元数据层恢复的热数据。第二类是异步任务状态文档解析任务执行进度、队列状态Worker 和 API Server 之间靠 Redis 做轻量级协调避免频繁查数据库。第三类是计算结果的短时存储比如一段文本的 Embedding 结果、一次检索出的 Top-K 候选、重排后的最终片段特别是在重排模型推理较慢的场景缓存命中一次能省下一两秒。如果你在做 RAGFlow 二次开发想加相似问题答案缓存也可以直接复用它现有的 Redis 实例。我实践过的方式是用户的问题经过归一化后计算一个哈希 Key把最终的答案和引用的文档列表缓存 10 到 15 分钟命中后直接返回极大降低 LLM API 的调用成本。注意缓存 Key 必须包含知识库 ID 和用户的权限上下文否则跨知识库会串答案。5.2 缓存一致性问题文件更新后怎么让旧缓存失效缓存层最大的坑不是性能而是一致性。设想一个场景知识库里有一份《员工手册》用户早上问了年假几天系统回答了 15 天这个问答结果被缓存了。下午管理员更新了手册把年假改成 10 天。如果缓存不懂失效第二个用户再问同样的问题拿到的还是 15 天。RAGFlow 处理这一问题的思路是分层失效文档更新事件会触发生成一条消息监听者会把与该文档相关的检索缓存、Embedding 缓存标记为失效。这里最容易踩的坑是只更新了向量索引没有清问答缓存。我建议在知识库的文档更新回调里主动按kb_id doc_id维度批量删除相关缓存 Key宁可误删多一点也不能留着旧答案害人。对于自己加的 LLM 回答缓存失效策略我推荐版本号 时间过期双保险每个知识库维护一个版本号知识库内容变更时版本号加一缓存 Key 里带上版本号同时设置最大过期时间。这样既能在变更后立刻失效也能防止版本号机制漏掉某条路径时缓存永远不死。5.3 缓存命中与失效策略缓存不是存得越久越好。RAG 场景的缓存有几种典型策略需要根据数据类型组合使用只读配置类如 Embedding 模型的调用参数基本不变可以设 24 小时甚至更长。会话类数据随着对话轮次持续推进上一次的回答结果会逐渐失去意义建议 30 分钟到 1 小时过期。可重算类数据如重排结果、检索候选即使丢了重新计算也就多花几百毫秒设 5 到 10 分钟即可不必浪费内存空间。还有一个和缓存穿透有关的经验当多个用户几乎同时问同一个问题而缓存里恰好没有时所有请求会一起穿透到检索层和 LLM。为避免 LLM API 被刷爆需要加一个简单的分布式锁命中缓存直接返回未命中时先尝试抢锁抢到的人才真正调 LLM其他人等待几秒后读缓存。这个逻辑用 Redis 的SETNX配合过期时间就能实现。RAGFlow 原本没有这层默认逻辑对高并发问答场景很有必要自己补上。6. 四层协作的完整链路从上传文档到给出回答6.1 文档上传与解析入库链路把四层拆开讲完再串起来看一次完整的数据流才能真正理解协作方式。第一步用户上传 PDF前端把文件流送到后端后端在元数据层创建一条文档记录状态为uploaded同时生成storage_key然后通过预签名 URL 把文件写入对象存储的原始文件区。第二步后端把解析任务 ID 推入任务队列Redis解析 Worker 消费任务后从对象存储下载文件执行版面分析和内容抽取产出结构文本与表格图片。第三步Worker 把解析结果传回对象存储中间产物区同时在元数据层更新文档状态为parsing_done。第四步构建索引的进程读取解析后的文本执行切块、Embedding、生成 question把三元组写入检索存储。全部完成后把元数据层文档状态更新为done。这条链路里每一步的产物都成了下一步的输入对象存储为解析供料元数据层登记进展检索层沉淀知识Redis 协调任务调度。任何一个环节阻塞后续环节都会原地等待。6.2 用户提问时的检索与回答链路提问链路和入库链路方向相反但同样横跨四层。用户输入 query 后系统先把 query 送去 Embedding 模型转成向量。这里的 Embedding 结果可以先查缓存如果同一个 query 最近算过直接用缓存的向量省一次模型调用。然后拿着向量去检索存储做语义召回同时用 query 原文做全文召回。两路结果合并后用 Reranker 重排取 Top-K。接下来从检索存储取出对应的content原文拼装 prompt交给 LLM 生成回答。最后问答结果和引用的文档列表可以选择写入缓存层供后续相同问题复用。整个链路中元数据层负责提供知识库配置和权限检查对象存储基本不参与问答路径只在需要展示查看原文 PDF这类功能时才会被读取。这也是分层的好处高频查询路径上的每一步都调用轻量、快速的存储大文件读取被隔离在低频路径之外。6.3 一张表看懂数据流转我用一张表把四层存储在整个 RAG 生命周期里的参与方式汇总一下方便你对照排查阶段元数据层MySQL对象存储层S3/MinIO检索存储层ES/Infinity缓存层Redis文档上传插入文档记录状态 uploaded保存原始文件无写入解析任务 ID文档解析更新状态为 parsing读取原始文件写回中间产物无更新任务进度构建索引更新状态为 done读取中间产物写入 Chunk 三元组清理相关旧缓存用户查询读取知识库配置一般不参与向量 全文召回缓存 query 向量、候选结果生成回答记录会话日志需要时取原文取 content 拼 context缓存最终问答对文档删除删除记录删除对象删除索引清空相关缓存这张表对排查问题很有用。比如文档能上传但无法被检索到可以按行检查元数据状态是否done检索存储里是否有对应 KeyCache 是否残留了旧的空结果。按图索骥比在日志里瞎翻高效得多。7. 落地部署时的踩坑记录与调优建议7.1 存储配置最容易错的地方我前后部署过三次 RAGFlow每次都会遇到配置相关的坑最典型的几个一是对象存储的访问地址配置不当。RAGFlow 部署在容器里时容器内部的 MinIO 地址可能是minio:9000而外部浏览器下载文件却需要localhost:9000或内网 IP。如果只配置了一个地址会出现后台日志一切正常但前端下载附件永远失败的诡异问题。解决办法是同时配置内网访问端点供后端服务用和公网/外网访问端点供前端签名 URL 用。二是检索存储的磁盘规划问题。向量索引的写放大非常严重一个 10GB 的文档库经过解析和向量化后检索存储可能膨胀到 30GB 以上。很多人部署时给 Elasticsearch 容器只分了 20GB 磁盘结果运行两周后索引直接变只读。建议在规划阶段给检索层至少预留文档源文件 5 倍的存储空间并单独挂载数据卷。三是Redis 内存没有设上限。默认配置下 Redis 会吃掉所有可用内存一旦触发 OOM 会拖垮整个容器。务必设置maxmemory和合适的淘汰策略。RAGFlow 的缓存数据大多是允许重算的临时数据用allkeys-lru通常比noeviction合适。7.2 性能调优的几个方向如果问答延迟偏高我建议按先缓存、再检索、后解析的顺序做优化。先看缓存命中观察 Redis 命中率如果大量查询是重复问题调高问答缓存过期时间是最便宜的优化如果命中率很低也不要盲目延长过期时间可能你的问题是长尾化的加缓存意义不大。再看检索层向量检索的召回条数Top-K不是越大越好。K 太大重排环节的输入会暴涨延迟主要耗在 Reranker 上但回答质量的提升却很有限。常规配置是召回 30 到 50 条重排后取 5 到 8 条送入 LLM。如果业务场景简单甚至可以跳过 Reranker直接用向量检索的前 5 条。最后看解析层大批量入库时的并发控制很关键。RAGFlow 支持多个解析 Worker并行工作但每个 Worker 都会同时打对象存储和检索存储。如果对象存储在云上且带宽有限并发太高反而会把带宽占满拖慢整个集群。经验值是从 2 个 Worker 起步观察 CPU、IO、对象存储请求延迟后再逐步加。7.3 对存储层做监控和容量规划四层存储的监控指标完全不同不能拿一套模板应付所有组件。我给几个建议元数据层监控慢查询和连接数。这个层最容易出问题的不是容量而是连接池打满。大批量导入时多个异步任务同时访问 MySQL很容易把默认连接数耗尽。提前调大连接池上限并给高频表加上索引。对象存储层监控桶内对象数量、总容量、按前缀的增长率。中间产物前缀增长过快要警惕解析过程是否陷入死循环或重复任务。过期清理任务要记录每次清理的对象数量和空间释放量。检索存储层监控索引分片状态、磁盘容量、查询耗时分位数。倒排索引和向量索引在构建时都会消耗大量内存确保 JVM 或引擎进程的堆配置合理。缓存层监控内存用量、命中率、过期淘汰数。命中率长期低于 10% 说明缓存设计可能有问题要么 Key 设置不合理要么过期太短要么缓存的值根本很少被重复读取。容量规划方面我给一个保守的估算公式检索存储容量 ≈ 源文件大小 × 5解析膨胀 向量化对象存储容量 ≈ 源文件大小 × 2一份原始 一份解析中间产物中间产物定期清理可降为 1.5元数据存储容量通常不超过几十 GBRedis 控制在 1 到 2GB 以内即可。按照这个框架规划一般不会出现上线后被迫停机扩容的窘境。说实话把 RAGFlow 的四层存储全部捋顺之后再去看其他 RAG 框架的存储设计都会有种一览众山小的感觉。存储层是 RAG 系统最容易出隐藏问题的地方但也是收益最实在的优化切入点。希望这篇拆解能帮你少走我走过的弯路。如果你正在部署或者已经跑起来了建议从元数据的文档状态入手对照我给你那张数据流转表把你自己的系统日志从头到尾模拟一遍所有断层问题都会自己浮出水面。