假设你问企业知识助手:我们去年与 A 公司签订的框架协议,赔偿上限是多少?在一个 Demo 里,这只是一次向量检索;在一家真实的企业里,它首先是一次权限判断。同一个知识空间里,可能同时存在公开制度、部门文档、项目材料、客户合同和管理层报告。一个人能进入空间,不代表他能看所有文件;能看某个文件夹,也不代表他能穿透被隐藏的子目录。权限还可能来自用户、部门、用户组、资源所有者、文件夹继承和针对单个文件的特例授权。这就是企业级 RAG 和普通文档问答的分水岭:企业级RAG 不能先把答案找出来,再问用户有没有权限。权限必须成为检索本身的一部分。以下内容,是北京数据项素 CTO姚劲,在开发企业场景的开源大模型应用开发平台BISHENG(毕昇)时,关于如何做企业级RAG权限管理的完整分享。01企业级RAG,不能把权限写到 Prompt
做权限管理,一个看似简单的做法,是先从向量库召回 TopK,再在 Prompt 里告诉模型:「请不要回答用户无权访问的内容。」问题是,当这句话出现时,泄露就已经发生了。无权 chunk 已经被向量库返回,进入应用内存,甚至进入 LLM 上下文。即使模型最终没有在正文里输出,它仍可能从引用、文件名、摘要、日志或调试字段中泄露。另一种做法是全量召回后过滤。它在安全上比 Prompt 约束更可靠,却会伤害召回率:假设 Top10 里的前 9 条都来自无权文件,过滤后只剩 1 条。系统虽然没有越权,但可能给出没有找到的错误答案。针对以上问题,我们设计了一条前置过滤‑召回复核‑展示兜底的三层安全防链,全程保障用户无法访问越权的知识库内容。图 1:权限不是 RAG 外围的一次 API 校验,而是贯穿召回与引用的数据面。第一步:检索请求会携带用户与租户身份发起查询,先完成知识库空间准入校验,再反向查询得到用户可读的文件集合第二步:由权限编译器,将权限规则转化为 Milvus 可执行的 IN/NOT IN 标量过滤条件;第三步:向量数据库会先基于该条件完成无权数据的前置过滤,再在合规数据集范围内执行 ANN 向量召回;第四步:召回结果经过业务层文件权限的二次精筛后,会仅将授权范围内的文档切片送入大模型生成回答第五步:最终在前端回填引用内容展示时执行最后一轮权限校验。02如何把人能看什么编译成向量库应该搜什么
权限系统擅长回答:「用户 1001 能读哪些 knowledge_file?」Milvus 擅长回答:「在给定候选集内,哪些向量与问题最相似?」在毕昇的入库链路中,每个 chunk 都携带结构化元数据,其中 document_id 把向量世界里的 chunk 与业务世界里的文件连了起来:chunk = { vector: [...], document_id: 4821, knowledge_id: 73, chunk_index: 16, page: 8, ...}检索时,系统先通过业务权限系统 PermissionService.list_accessible_ids() 获取当前用户可 can_read 的文件 ID,再与当前空间、文件夹、标签和主版本等业务范围求交集,最后生成 Milvus 可执行的布尔表达式:expr = "document_id in [101, 205, 309]"
这一点与 Milvus 的 filtered search 模型正好契合:先用标量条件缩小候选集,再在匹配实体上执行 ANN 检索。它的价值不仅是更快,更是让无权数据尽量不进入向量相似度竞争。03为什么不能永远使用 IN
如果用户只能看 20 个文件,document_id in [...] 很理想。但如果一个空间有 10 万个文件,某位管理者可以看其中 99980 个,把这 99980 个 ID 全部塞进 IN 表达式,并不划算。毕昇的 KnowledgeFileVisibilityService 会根据可见文件数 K、空间主版本文件总数 N 和默认阈值 5000,自适应选择策略:图 2:权限集的大小和密度,决定了它应该以白名单、黑名单还是后过滤形式进入检索。第一,文件夹和标签是业务边界,不是仅用于加速的权限边界。即使列表很大,也不能随意丢掉;当无法使用短 NOT IN 时,实现会优先保留 IN 条件,因为正确性高于表达式长度。第二,主版本过滤与权限过滤是叠加的。用户有权访问某个文件,不代表该文件的旧版本仍应参与回答。04
双层过滤:粗粒度为性能,细粒度为正确性
如果 Milvus 已经做了预过滤,为什么还要对召回结果再过滤一次?因为企业权限不是一个简单的 can_read=true。在毕昇的权限模型中,权限系统的 can_read 适合快速批量回答大致能读哪些文件;但列表 UI 里的最终可见性,还要经过 view_file 精细权限解析,综合文件到父文件夹、再到空间的关系链,以及「最近绑定优先」的特例规则。- 索引层粗过滤:用 can_read 生成 Milvus expr,尽量缩小 ANN 的搜索空间。
- 结果层精过滤:只对召回结果中去重后的 document_id 解析 view_file,与前端文件列表使用同一套有效权限语义。
这里需要注意两次检查的职责不同:第一层决定去哪里找,第二层决定什么最终可以被看见。05权限过滤后 TopK 不够,怎么办?
在一个权限稀疏的空间里,一次召回的候选中可能有大量文件在精过滤阶段被删除。如果只召回用户需要的 TopK,答案很容易出现证据不足的问题;如果不断扩大召回直到凑齐,又可能制造无上限的延迟。- 如果精过滤后没有可用结果,最多再扩张一次到 6 倍;
- 总召回次数最多为 2,仍不足就返回现有结果,不做无限循环。
毕昇的 Milvus 适配层还会确保 HNSW 的 ef 覆盖实际 k,避免上层放大候选数时仍使用过小的搜索宽度。Milvus 官方将 ef 定义为 HNSW 搜索时评估的候选邻居数;它越大,通常召回越高,但延迟也会增加。06不要把每个人的 ACL 写进每个 chunk
当权限问题变复杂时,一个自然反应是:在每个 chunk 上写入 allowed_user_ids。这对小规模系统可能有效,对企业系统却很快会变成权限数据复制灾难:一个部门的人员变更,可能需要改写数百万个 chunk;一次权限撤回,会变成长时间的索引更新窗口。- 权限系统维护「用户—部门—用户组—文件夹—文件」的关系与继承;
- Milvus 维护稳定的业务键,例如 document_id 和 knowledge_id;
这样,权限关系变化不会迫使向量全量重写。当前权限列表使用租户隔离的 Redis 缓存,默认 TTL 为 10 秒;授权变更走统一失效逻辑,正常情况下下一轮检索即可收敛,边缘情况由 TTL 兜底。07检索安全不等于答案安全
即使本轮检索是安全的,历史会话里的引用仍可能成为另一条越权通道。例如,用户昨天有权限看合同 A,模型的回答中留下了 citation;今天管理员撤回权限后,用户再打开历史会话,如果 citation 解析接口仍返回文件名、摘要、下载链接或坐标信息,权限撤回就只做对了一半。因此,毕昇的 citation resolve 链路会在回显时按当前权限重新校验:- 有 view_file 的 RAG 引用正常返回;
- 已失去权限的引用整条剔除,不返回 placeholder,也不泄露文件名和 snippet;
- 单条无权引用以不存在语义处理,避免通过错误差异探测资源。
检索时按当前权限取数,回显时仍按当前权限解析。历史答案不应成为权限快照。
08企业级 RAG 还必须确定以谁的身份检索
在单人聊天场景中,这个问题很简单:用当前登录用户。但一旦进入工作流、助手和对外 RPC,身份语义会立即变复杂:- 工作流应该使用流程创建者的权限,还是当前运行者的权限?
- 助手被共享后,召回应继承配置作者,还是每个调用者?
- RPC 如果由系统 operator 发起,谁是真正的 on_behalf_of_user?
毕昇当前的流程/助手检索已显式选择权限身份:「用户知识库权限校验」开启时使用运行用户,关闭时使用配置作者,并将 access_scope 传递到后续引用链。这比调一个权限 API更重要:如果身份本身没有定义清楚,任何权限判断都可能在精确执行错误的语义。09几个有意的失败策略
企业级检索最难的地方,往往不在正常路径,而在系统不完整时如何失败。1. 权限系统暂时不可达
批量可访问 ID 查询失败时,检索链路不应为了可用性放行全量数据。当前语义是回退到可确定的 scope,否则视为空集,让该次问答少答而不是越权。2. 用户有空间权限,但没有任何可见文件
这不是 403,而是合法的空检索。系统返回空文档集,由 Prompt 产生「未在你有权访问的内容中找到相关信息」之类的友好提示。3. 多知识库检索中某个库失败
不让一个库的权限或向量存储故障污染整个批次。无空间权限的库静默跳过,单库初始化/检索失败按 KB 隔离,其他库仍可返回结果。
立即咨询 JOTO
JOTO 提供覆盖企业智能体规划与搭建、AI 平台私有化部署、RAG 知识工程、AI 安全治理、FDE 驻场共创及持续运营优化的全周期 AI 落地服务,帮助企业把验证中的 AI 能力转化为安全、可控、可持续迭代的生产力。 联系 JOTO 获取 AI 落地咨询