RAG 权限过滤:召回结果正确,也不能越权
一、RAG 最怕把不该看的资料说出来
RAG 系统经常把注意力放在召回率和答案质量上,但权限过滤同样关键。一个答案如果引用了用户无权访问的文档,即使技术上回答正确,也是严重事故。知识库越大、租户越多,这个风险越高。
权限过滤不能依赖模型自觉。模型不知道每个 chunk 的真实权限,也不能保证生成时不泄露上下文。权限必须在检索阶段和证据注入阶段完成硬过滤。
我曾经处理过一个险情:内部知识库上线两个月后,运营同事投诉说"能搜到竞争对手的合同信息"。排查发现是权限过滤的 SQL 条件有一个 bug——“space_id IS NULL”的文档被当成了公开文档返回给了所有用户。模型在回答时引用了这个文档,造成了数据泄露。这个 bug 不是模型造成的,而是权限过滤实现有漏洞。教训是:权限过滤要像门禁一样,"什么人都能过"和"什么人都不让过"一样危险,必须精确到每一个 chunk。
二、权限元数据要跟着 chunk 走
flowchart LR A[原始文档] --> B[权限解析] B --> C[切分 Chunk] C --> D[写入向量库] D --> E[检索过滤] E --> F[生成答案]每个 chunk 都要带上 tenant_id、space_id、owner、role_scope、classification 等元数据。检索时先过滤权限,再做相似度排序,或者使用向量库支持的混合过滤能力。不能先召回再让模型挑。
文档切分也要尊重权限边界。如果一个 chunk 混入了公开段落和私密段落,后续过滤会非常难做。切分前最好先识别权限域,保证最小证据单元本身就是权限一致的。这个原则和加密数据分区类似:一条记录只能属于一个安全域,跨域混合就意味着信息泄露风险。
权限元数据的管理还要考虑权限变更。用户被移出某个空间后,向量库里该空间的 chunk 应该对该用户不可见。如果权限变更后没有及时同步到向量库的元数据,就会出现"权限系统显示无权,但 RAG 仍然能查到"的不一致。建议在权限变更时发消息到 Outbox,由消费者批量更新向量库元数据。
三、过滤条件要可测试
type QueryScope struct { TenantID string UserID string Roles []string Spaces []string }权限过滤要有单元测试和集成测试。准备几类用户:普通成员、空间管理员、跨部门用户、外部访客。每类用户都要验证可见文档和不可见文档。只测管理员账号,结果通常过于乐观。
func BuildFilter(scope QueryScope) map[string]any { return map[string]any{ "tenant_id": scope.TenantID, "space_id": map[string]any{"$in": scope.Spaces}, "role": map[string]any{"$overlap": scope.Roles}, } }还要测试空权限。用户没有任何空间权限时,系统应该返回空证据,而不是退化成全库检索。很多越权问题就来自"过滤条件为空时忽略过滤"。这条规则要进代码规范:任何权限过滤条件都不能以空条件或无权限状态作为"放行"信号。
四、引用展示也要检查权限
retrieval_log: user_scope_hash: 8fe2 matched_chunks: 12 filtered_chunks: 38权限过滤不只发生在向量库。答案引用、展开原文、下载附件、追溯来源,都要再次校验权限。否则生成阶段安全,展示阶段仍可能泄露。这是一个纵深防御原则:任何输出用户可见内容的接口,都应该重新验证用户对内容的访问权限,不能依赖上游的过滤结果。
观测日志要记录过滤数量和 scope hash,但不要记录完整权限列表。这样既能排查"为什么搜不到",也能避免日志泄露权限结构。scope hash 是对权限组合的哈希值,两个用户 hash 相同说明权限一致,不同说明有差异——既能归类排查又不泄露具体权限。
还要处理缓存。很多 RAG 服务会缓存检索结果或最终答案,如果缓存 key 里没有权限范围,就可能把一个用户的证据返回给另一个用户。缓存 key 至少要包含租户、空间范围、角色摘要和知识库版本。权限变更后,要么主动失效相关缓存,要么把权限版本写入 key。否则权限系统做得再严,缓存层也会开后门。
权限过滤也需要压测。过滤条件复杂后,向量库可能从毫秒级变成百毫秒级。要分别测试公开库、小空间、多角色和大租户场景,确认过滤不会把检索延迟拖垮。安全和性能不是二选一,关键是提前知道成本。一个经验数值:当过滤条件涉及的 IN/NOT IN 子句超过 100 个时,部分向量库的查询性能会明显下降,此时需要考虑权限分组压缩或使用位图索引替代列表匹配。
五、总结
RAG 权限过滤要把权限元数据写到 chunk,检索阶段强过滤,切分时保持权限一致,并在引用展示时再次校验。空权限要拦截而不是放行,缓存要带权限标识。
召回结果正确只是第一步。对生产 RAG 来说,答案必须既正确,又属于当前用户可以知道的范围。权限是安全底线,不是功能后端。