多租户 RAG:数据隔离到底怎么做才不漏

2026-07-28

数据截至 2026-07,各项目能力以官方文档当前版本为准。

多租户 RAG 的隔离,绝大多数事故不发生在向量检索这一步,而发生在它前后:嵌入缓存、原始文件直链、日志与追踪、离线评估集、以及”上一轮对话摘要”。向量库那一层的过滤条件反而是最容易写对、也最容易被验证的部分。所以做隔离的正确顺序是先定隔离粒度,再把租户身份变成整条链路上无法被绕过的东西,最后才去挑框架。

常见的误解是:既然向量库支持按元数据过滤,那给每条切片打上 tenant_id、查询时带上这个条件,隔离就算做完了。这个判断在检索那一刻是成立的,问题在于它假设了”每一次查询都一定带上了这个条件”,而这个假设在真实工程里非常脆弱——有人为了调试写了一次裸查询、有人加了个”全局知识”的开关、有人在批处理脚本里复用了另一个函数。隔离不是一个条件表达式,是一套让人写不出错的结构。

一、先决定隔离粒度:三档做法,代价各不相同

在动手之前先回答一个问题:你能接受不同租户的数据在物理上共处一处吗?答案不同,架构就完全不同。

第一档,每个租户一套独立实例。 向量库、文档存储、甚至模型调用出口都各自独立。隔离强度最高,出事故的面最小,合规审计时最好解释。代价也直白:运维成本随租户数线性增长,一百个租户就是一百套要升级、要备份、要监控的东西。适合租户数量少、单个租户体量大、且合同里写死了”数据物理隔离”的场景,比如给几家大企业各自私有化部署。

第二档,共用一套实例,每个租户一个独立集合(或独立库)。 这是多数中等规模项目的落点。集合之间天然不互通,误查的可能性比共享集合低得多;同时又共用一套基础设施,运维负担可控。它的痛点在于集合数量多了以后的管理成本——每个集合都有自己的索引、自己的内存占用,租户数上千时资源利用率会很难看,而且新增租户要走一遍建集合、建索引的流程。

第三档,共享集合,靠元数据字段区分。 资源利用率最好,新增租户几乎零成本,小租户不会各自占一块几乎空着的索引。风险也集中在这里:所有隔离都依赖查询条件写对。要走这条路,租户字段必须建成可过滤的索引字段而不是普通载荷——像 Qdrant 这类向量库支持过滤、payload 索引与量化,第三方评测称在 100 万级以上向量规模下可做到 50 毫秒以内的检索(这是第三方基准口径,不是官方承诺的指标),字段索引建了没建,对这个量级的过滤查询影响会很直接。

实践中还有一种混着来的做法:默认走第三档共享集合,遇到明确要求物理隔离的大客户就单独拆一套。这不是不专业,反而是把成本花在刀刃上,只是要注意别让两套路径在代码里长成两份逻辑,否则修一个 bug 要改两处。

二、让过滤条件成为”绕不过去”的东西

选了共享集合,接下来的全部工作就是一件事:让任何一次检索都不可能缺少租户条件。几个可操作的做法:

租户身份从请求上下文取,不从函数参数传。 如果 search(query, tenant_id) 这样的签名存在,就一定会有人在某处传错或传空。更稳的做法是在请求入口处解析出租户身份,放进一个请求作用域的上下文对象,检索层从上下文里读,不接受调用方覆盖。多传一个参数的自由度,在这里是负资产。

封装一层自己的检索客户端,禁止业务代码直接碰向量库 SDK。 对外只暴露一个方法,租户条件在这一层拼装,并且和业务传进来的其他过滤条件用”与”关系合并,而不是让业务传进来的条件有机会覆盖它。合并逻辑写反是真实发生过的错误:业务传了个 filter 字典,实现里做了 {**tenant_filter, **user_filter},键一撞,租户条件就被冲掉了。

默认拒绝,而不是默认放行。 上下文里取不到租户身份时,正确的行为是抛异常中断,不是查全库。很多泄漏就来自”取不到就当管理员”这种顺手的兜底。

离线任务同样走这一层。 定时重建索引、批量补数据、后台的问答质量抽检脚本,这些不走 HTTP 请求的入口最容易被忘掉。给它们准备一个显式设置租户上下文的入口,宁可写起来啰嗦。

写入侧和读取侧用同一套字段约定。 切片入库时如果租户字段名写成 tenantId,检索时过滤 tenant_id,过滤条件会静默匹配不到任何东西——这种错误在测试数据只有一个租户时完全暴露不出来,上线后才发现”检索什么都查不到”或者更糟的”过滤形同虚设”。字段名集中定义成常量,别各处手写字符串。

三、向量库之外,还有五个位置会漏

这一节是本文最想强调的部分。把检索那一层做扎实之后,剩下的风险按发生频率排,大致是这五处:

一是嵌入与检索结果缓存。 为了省钱省延迟给查询结果加缓存很常见,但缓存键如果只用了 query 文本的哈希,A 租户问过的问题,B 租户问同样一句话就会拿到 A 的检索结果。缓存键必须把租户标识拼进去,这条同样适用于问答结果缓存、重排结果缓存。

二是原始文件的访问链接。 检索结果里通常要给出处,出处指向原始 PDF 或文档。如果这个链接是对象存储的公开直链,或者是可以枚举的顺序 ID,隔离就在这里破了——向量检索没漏,文件下载漏了。原始文件的读取要走同一套鉴权,链接最好是短期有效的签名地址。

三是日志、追踪与调试面板。 把完整 prompt(里面拼着检索到的原文片段)打进日志、发到追踪平台、或者在内部调试页面上展示,等于给日志系统的所有可访问者开了一个绕过隔离的口子。要么脱敏,要么把这类日志的访问权限收到和业务数据同级。

四是离线评估集与数据分析。 做评估要抽真实问题和真实召回片段,这些抽出来的样本一旦落到一张不带租户字段的表里、进了给全公司看的分析看板,隔离就名存实亡。评估集也要带租户标识,并按同样的策略管权限。

五是对话历史与摘要。 多轮对话为了控制上下文长度,常把前几轮压缩成摘要再带进下一轮。摘要里含着检索到的租户内容,如果会话对象的归属做得比检索粗糙(比如按用户 ID 存但用户可切换租户),跨租户的内容就会被上一轮的摘要带过来。会话存储的隔离粒度要和检索一致。

这五处的共同点是:它们都不在”RAG 流程图”上,是工程实现里为了性能和可观测性加出来的东西,评审时容易被跳过。

四、隔离要从解析和分块阶段就带上

不少团队的入库流程是先把所有文档解析、切块、算好向量,再统一写进库,租户信息在最后一步才补。这个顺序会留下两个隐患:中间产物(解析出来的纯文本、切好的 chunk)落在临时目录或中间表里,本身没有归属;出错重试时容易把上一批的残留混进这一批。

更稳的做法是让租户标识跟着文档从第一步走到最后一步:解析任务、中间产物路径、切片记录,全都带上租户和文档 ID。这样即便某一步崩了,残留数据也是有归属、能定位、能清理的。

文档类型复杂的时候,这件事会和解析质量绑在一起。如果语料是扫描版 PDF、财报、版式复杂的法律文书,传统文本解析器往往搞不定,需要版面感知的解析能力——RAGFlow 这类开源 RAG 引擎的强项就在深度文档理解,分块会尊重文档结构,也内置了知识图谱构建;LlamaIndex 一侧则以数据为先,ingestion 连接器和索引策略更贴身。选哪条路取决于你的瓶颈在哪一段,这部分展开可以看RAG 框架怎么选分块策略怎么定。这里要说的是:无论用哪个解析方案,解析产物落地时就把归属信息写上,别等到入库。

五、框架能替你做多少,别抱太高期待

要有个清醒认识:LangChain 和 LlamaIndex 本质上是库,它们提供的是检索、编排、索引的原语,多租户隔离这套约束需要你自己在应用层建立。它们能帮你的是过滤条件的表达能力和向量库集成的覆盖面,帮不了你的是”确保没人绕过这层封装”。RAGFlow 这类完整引擎自带知识库这一层概念,天然更接近”每个租户一个知识库”的模型,但具体的权限边界、账号体系能做到什么程度,要以官方文档当前版本为准,不要凭截图推断。

顺带提一句选型时常被误用的信号:截至 2026 年 1 月的 GitHub star 数,LangChain 约 125,000、Dify 约 114,000、RAGFlow 约 70,000、LlamaIndex 约 46,500、Haystack 约 24,000。这组数字只反映关注度,跟你的隔离需求是否被满足没有对应关系。另有第三方评测指出,LangChain 的 LCEL 语法能做复杂的查询重写,但相比 LlamaIndex 的直接检索会多出约 15% 到 20% 的延迟开销——这同样是第三方基准口径而非官方数据,多租户系统里如果每次查询都要额外拼一层重写,这部分开销值得实测一遍再决定要不要用。

自托管形态也影响隔离方案的落地成本。RAGFlow 走 Docker 部署,提供 slim 与 full 两种镜像,体积分别约 2GB 和 9GB,区别在于是否内置嵌入模型;LangChain、LlamaIndex 的”自托管”则是你自己跑 Python 服务和向量库,隔离逻辑写在你自己的代码里。数据能不能出内网这类前置问题,可以先看自托管还是低代码平台

生产环境里这几个东西也不是互斥的,常见组合是 LlamaIndex 做 ingestion 与索引、LangChain 做编排、LangGraph 跑 agent 工作流。组合着用的时候要额外注意:租户上下文要能贯穿这几层,别在交接处丢掉。

六、删除、退租与残留

隔离做得再好,租户要求删数据时删不干净,一样是事故。这里列几个容易漏的地方:

向量库里的切片删了,原始文件还在对象存储;切片删了,缓存里还有它的检索结果;主库删了,只读副本和昨天的备份里还有。删除要设计成一个跨组件的流程,而不是一条 delete 语句,并且要能给出”什么时候删完、删了多少条”的凭据。

增量更新时的旧版本也是同一类问题。文档改了重新入库,如果只是新增切片没有清理旧切片,租户会检索到已经作废的内容,这在合规文档场景里后果不小。这块的具体做法可以看增量更新怎么做

备份策略要和删除承诺对齐。如果对客户承诺了删除时限,而备份保留周期比这个时限长,两者是矛盾的,要么调整保留周期,要么在合同措辞上把备份窗口讲清楚。这属于要和法务一起定的事,不是技术单方面能拍板的。

七、怎么证明隔离生效

写完不等于做对,需要可重复的验证手段:

跨租户探针测试。 准备至少两个测试租户,各自灌入一份带独特标记词的文档(比如一串不会自然出现的随机串)。测试用例做的事很简单:以 A 的身份问 B 的标记词,断言检索结果为空、回答里不出现该标记词。这个用例放进 CI,每次改检索链路都跑。

缺失租户上下文的用例。 构造一个没有设置租户上下文的调用,断言它抛异常而不是返回结果。这条专门用来守住”默认拒绝”。

审计日志。 记录每次检索的租户标识、查询哈希、命中的文档 ID 数量,不记录原文。出问题时能回答”谁在什么时候检索到了哪些文档”,这在事故复盘和合规审查里是硬需求。

定期抽查而不是只靠上线前测一次。 隔离的破口往往是后来加的功能带进来的,回归测试比一次性验收更有用。评估侧的方法论可以参考 RAGAS 评估怎么用,把隔离用例和质量用例放在同一套流水线里跑。

这套做法的局限

需要诚实说清楚几点。第一,本文讲的是应用层隔离,它防不住拿到数据库直连凭据的人,那属于基础设施和权限管理的范畴,可以顺带看看数据安全风险那篇。第二,如果调用的是外部大模型 API,检索到的租户原文会离开你的机房,这是隔离方案之外的另一个决策点,和你选自托管还是云服务直接相关。第三,共享集合下的隔离强度终究依赖代码正确性,对于合同里明确要求物理隔离的客户,再漂亮的过滤逻辑也替代不了单独部署。第四,各框架和向量库的多租户相关能力一直在变,本文提到的形态请以官方文档当前版本核对,别照抄旧教程里的参数。

小结

多租户 RAG 的隔离先是架构选择,后才是代码细节:租户少而重就物理拆,规模化就共享集合加强制过滤,中间地带用独立集合过渡。真正要花力气的地方在向量检索之外——缓存键、文件直链、日志追踪、评估集、会话摘要,这五处比过滤条件更容易出事。租户身份要从请求入口贯穿到最底层,取不到就报错,而不是兜底放行。删除要当成跨组件的流程来设计,备份周期和删除承诺得对得上。最后,用跨租户探针测试把这些约束固化进 CI,隔离才算有凭据,而不是靠人记得。

接下来看什么

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。