在构建企业级 RAG 系统时,多租户隔离 (Multi-tenancy Isolation) 是一道永远绕不过去的“鬼门关”。

我曾在一次架构演进讨论中,向 AI 助手提出过一个极其尖锐的场景:“如果你在写 RAG 的多表联查 SQL 时,漏写了一个 tenant_id 过滤条件,A 公司的核心商业机密会不会出现在 B 公司的 AI 对话框里?”

那次讨论的沉默让我意识到:这种由于人为疏忽导致的“数据越权”,在 RAG 这种高度依赖语义模糊匹配、且经常涉及复杂 JOIN 查询的系统里,几乎是毁灭性的灾难。为了守住这条红线,在主导 Knowledge Hub (KH) 的研发时,我确立了一个不可撼动的设计准则:多租户隔离绝对不能靠开发者的“自觉”,必须下沉到数据库的最底层,变成一种自动化的、物理级的硬约束。

一、架构的洁癖:为什么应用层隔离是“漏洞之源”?

很多团队习惯于在应用层(Service 层)写各种判定:if user.Tid != doc.Tid { return err }。或者依赖开发者的“好习惯”,在每个 SQL 后面手动挂一个 .Where("tenant_id = ?", tid)

但在我看来,这种模式在真实复杂的工程环境下就是“极高风险”。
随着 KH 的业务日益庞大,我们引入了 VFS 2.0 的物化路径、多表联查的混合检索、以及各种复杂的异步后台任务。只要有一个开发者(或者是我自己在深夜疲惫时)漏写了一行过滤,整个系统的安全堤坝就会溃于蚁穴。

我选择了一套更“冷酷”的方案:利用 GORM 的 Scope 机制,在 internal/database/scope/acl.go 中构建了一套“查询注入引擎”。

二、Vibe 指挥:攻克 SQL 别名的“歧义陷阱”

我的 Vibe 指令非常直接:“我要让租户隔离像空气一样无孔不入,且对业务层代码完全透明。”

在实现 applyACLScope 时,AI 很快撞到了墙。由于企业级的 RAG 检索经常需要 JOIN 多张表,如果系统只是机械地注入 WHERE tenant_id = 'xxx',数据库会立即报 column reference is ambiguous(列名歧义)错误。

我指挥 AI 深入到 GORM 的运行时状态中,开发了一套“别名探针”逻辑:

1
2
3
4
5
6
7
// 核心实现片段解析 (internal/database/scope/acl.go)
prefix := ""
if db.Statement.Table != "" {
prefix = `"` + db.Statement.Table + `".` // 动态捕获当前 SQL 执行的主表名
} else if db.Statement.Schema != nil {
prefix = `"` + db.Statement.Schema.Table + `".`
}

这种精细到微米级的 SQL 构造能力,确保了系统能自动为 tenant_id 补上前缀(如 "document".tenant_id)。开发者在写业务代码时,根本不需要(也不被允许)去操心租户过滤,系统会在执行前的最后一毫秒,自动帮你把这道防线筑牢。

三、Fail-Safe:那个让我晚上睡得踏实的“1 = 0”

在安全性设计上,我一直推崇极致的 Fail-Safe(安全失败) 原则。

ACLScopeFromSpace 函数里,有一段代码曾引起过争议,但我顶住压力坚持了下来:

1
2
3
4
if sp == nil || sp.TenantID == "" {
// 极其严厉的熔断逻辑:如果拿不到有效的身份上下文,直接拉死闸
return func(db *gorm.DB) *gorm.DB { return db.Where("1 = 0") }
}

为什么要写这种看起来很“蠢”的 1 = 0
因为在分布式系统里,Context 丢失(比如鉴权拦截器失效、缓存穿透没取到 Token)是一个极其隐蔽的系统风险。一旦发生丢失,传统的逻辑可能会因为“没有过滤条件”而导致数据库执行“全量扫描”,后果不堪设想。

1 = 0 相当于在数据库层拉了一道死闸。只要你的租户标识有任何瑕疵,这个查询返回的结果集永远是零。这种“宁可错杀、绝不放过”的硬核逻辑,才敢拿出来承载真正敏感的企业资产。

四、RAG 向量检索的“联防联动”

向量数据库(pgvector)由于其查询语法的特殊性,往往是隔离的盲区。为了万无一失,我设计了“双重保险”:

  1. 入口强校验:在进入检索 Repo 前,强制通过 module.GetSpace(ctx) 校验。
  2. 强制 JOIN 校验:这是最关键的一步。在计算向量相似度的 SQL 中,我强行要求系统执行一个 JOIN document 的动作。

由于 document 表已经挂载了上述的 GORM Scope,这本质上形成了一次“联防联动”:哪怕向量切片本身出现了隔离漏洞,只要其所属的父级文档在当前租户下不可见,整个结果集就会在物理层面上被瞬间屏蔽。

五、结语:看不见的安全,才是真底气

回看 KH 的安全重构历程,我深感多租户隔离不应该是一个“选配功能”,它必须是系统的“基础设施”

通过把隔离逻辑下沉到数据库 Scope 层,我们实现了对业务代码的零侵入。这种看不见的安全屏障,不仅减少了代码的冗余,更重要的是,它将整个系统的安全边界从“人的好习惯”转移到了“程序的确定性”上。

在 AI 狂奔的时代,架构师的价值就在于这种“冷酷的坚持”。守住租户的边界,就是守住用户的信任。 这才是通向工业级 RAG 产品的必由之路。