在构建企业级 RAG(检索增强生成)系统时,你可以对界面的简陋保持克制,可以对偶尔的 AI 幻觉进行补救,但有一个绝对不能逾越的红线:数据安全

在一次内部架构审计中,我曾向 AI 搭档提出过一个极其极端的 Case:“如果一个没有任何核心权限的外包实习生,在 AI 的对话框里问‘本季度的核心高管薪酬优化方案是什么?’,底层的向量库会不会因为语义相似度过高,直接把机密文件的切片吐出来?”

AI 当时的回答显得非常“天真”:“我们可以在 System Prompt 里加上一句‘请严格检查用户的权限,不要回答未授权内容’。”

作为一个有着多年后端经验、对“漏洞”有着天然警觉的架构师,我看着这行建议,感到一阵不寒而栗。这种依赖大模型“自觉性”的防线,在稍微复杂一点的 Prompt 注入攻击(Prompt Injection)面前简直就是纸糊的。

为了掐灭这个隐患,在主导 Knowledge Hub (KH) 的研发时,我确立了一个不可辩驳的原则:权限校验必须是检索动作的物理前置条件,且必须发生在数据库最底层的 SQL 层面。

一、架构的清醒:为什么应用层过滤是“皇帝的新衣”?

很多开发团队在处理 RAG 权限时,为了追求上线速度,习惯于采用“后置过滤”方案:先从向量库里搜出相似度最高的 Top 10 结果,然后用 Go 或 Python 代码遍历这些结果,通过 if user.HasAccess(doc) { ... } 的逻辑把没权限的切片剔除掉。

这种做法在小规模 Demo 里可行,但在企业级环境下存在两个致命缺陷:

  1. 召回截断(Recall Truncation):如果底层召回的 Top 10 结果全是不具备权限的机密文档,被应用层剔除后,最终留给大模型的证据就是 0 个!哪怕排名第 11 的是一个高度相关且完全公开的合法文档,大模型也“看”不到了。这直接摧毁了 RAG 的召回质量。
  2. 不可控的维护黑洞:随着业务逻辑变复杂(比如引入了 VFS 目录穿透、跨租户搜索),一旦某位开发者在写新的检索接口时漏掉了那行 HasAccess 判断,整个系统的安全底座就彻底崩溃了。

安全,绝对不能依赖开发者的“好习惯”。

二、Vibe 驱动:基于 Scope 的 SQL 实时注入引擎

在明确了“权限下沉”的意图后,我开始指挥 AI 进行底层的硬核重构。我给出的 Vibe 指令非常直接:

“我要你利用 GORM 的 Scope 机制,实现一套‘查询注入 (Query Injection)’引擎。只要系统发起了任何数据库查询,不管是在哪个业务模块,底层必须自动拼接上严密的 ACL(访问控制列表)逻辑。这套逻辑必须像空气一样无孔不入,且对业务代码零侵入。”

在我的调度下,AI 在 internal/database/scope/acl.go 中构建了核心的 applyACLScope 函数。

2.1 解决 SQL 别名的“歧义陷阱”

在实操中,AI 很快遇到了阻碍。企业级的 RAG 检索绝不是单表查询,它往往需要对 document_segment(切片表)和 document(文档表)进行复杂的多表 JOIN。

如果只是生硬地在 SQL 后面追加 WHERE tenant_id = 'xxx',数据库会立即报 column ambiguity(列名歧义)错误。面对 AI 提出的“硬编码表名”这种丑陋的妥协方案,我再次进行了决策剪枝

我引导 AI 去钻研 GORM 的底层运行机制,利用 db.Statement.Table 动态捕获当前查询的主表别名,并将其作为前缀注入到所有的权限条件中。这套极其健壮的“别名探针”代码,确保了在多表联查下,权限过滤依然能精准指向正确的字段。

三、硬核实战:VFS 目录穿透与 EXISTS 子查询

在 KH 中,最变态的权限挑战来自于 VFS(虚拟文件系统)的层级展示。
我们有一个硬性的业务规则:“只要用户有权限看某个深层的文件(如 /docs/secret/api.md),哪怕他没有上级 /docs/secret/ 目录的显式权限,这个目录也必须对他可见,否则文件树就断链了。”

这在工程上被称为目录穿透 (Directory Penetration)

AI 曾提议维护一张“冗余权限宽表”,在后台用异步任务算好每个人的最终可见目录。
这种方案查起来快,但我一眼就识破了其“状态不一致”的巨大运维风险。如果权限变更频繁,异步任务的几秒钟延迟,就会导致严重的越权泄露。作为独自一人承担所有压力、甚至没有测试团队支持的开发者,我绝对不能给未来的自己留下这种隐蔽的“定时炸弹”。

我选择了最硬核的实时计算路线。我指挥 AI 在 ACLScopeForVFSNode 中,利用 EXISTS 子查询写出了堪称暴力美学的 SQL 逻辑:

1
2
3
4
5
6
7
-- 核心逻辑还原:
OR (type = 'directory' AND EXISTS (
SELECT 1 FROM project_node c
JOIN document d2 ON d2.id = c.document_id
WHERE c.materialized_path LIKE current_node.materialized_path || '%'
AND (d2 具备用户的访问权限)
))

这不仅考验了数据库的 B-Tree 索引性能(利用了物化路径的前缀匹配),更彻底根除了状态不同步的风险。在性能和绝对一致性之间,我作为架构师,坚定地选择了“实时物理隔离”,并通过底层的索引优化兜住了性能的底。

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

internal/database/scope/acl.go 的最顶层,有一段非常简单、甚至看起来有些突兀的代码:

1
2
3
4
if sp == nil || sp.TenantID == "" {
// 身份或租户上下文丢失,直接拉死闸
return func(db *gorm.DB) *gorm.DB { return db.Where("1 = 0") }
}

很多人第一次看这段代码时,不理解为什么要写这么笨的 1 = 0
因为在复杂的微服务流转中,身份上下文(Space)丢失是一个隐蔽的毒瘤。一旦发生,底层的 GORM 查询可能就会退化为“无条件的全表扫描”,全公司的机密会瞬间暴露。

1 = 0 相当于在数据库层拉了一道死闸。只要你的身份标识有任何瑕疵,这个查询在数据库端执行的结果集永远是零。这种“宁可杀错、绝不放过”的硬核熔断,才是我心中企业级系统该有的底气。

五、结语:看不见的安全屏障,才是真安全

在构建 Knowledge Hub 的过程中,AI 帮我扛下了海量的代码生成,但支撑起这套系统脊梁的,是对安全边界近乎偏执的执着。

多租户与权限隔离,从来都不应该是一个需要业务开发者在每一行代码里去小心翼翼维护的功能,它必须沉淀为系统的基础设施

通过 GORM Scope 的底层注入、动态别名的智能捕获以及 1=0 的硬核熔断,我们建立起了一道物理级的隔离墙。在 AI 狂奔的时代,我们虽然把键盘交给了机器,但作为“系统导演”,我们对数据安全的敬畏心不仅不能少,反而必须通过更严密的架构治理来被放大。这才是企业级知识系统真正能“站得住”的基石。