首页 / 资讯中心 / 文章详情

28 切片的权限隔离:企业知识库不让 AI 说漏嘴,得从检索层下手

28 切片的权限隔离:企业知识库不让 AI 说漏嘴,得从检索层下手 ★ FEATURED ARTICLE
面试现场面试官端起杯子问了一个他大概率自己踩过坑的问题公司要上 AI 知识库。数据是内部的产品文档、客服工单还有人力资源那几份薪酬方案。我最担心的不是它答得对不对——是客服随口问一句AI 会不会把高管的薪酬方案念出来你打算怎么保证不出这种事这题看着像安全题其实考的是你对整条 RAG 检索链路的理解深度。权限这东西做浅了会翻车做错了方向比不做还危险——因为你以为你防住了。先把三档人的回答摆出来-小白搜出来之后在代码里判断一下没权限的过滤掉。——应用层事后过滤最典型、也最危险的答法。-中档文档入库的时候打个权限标签检索的时候带上条件。——方向对了但停在文档级没往深里想。-高手权限要沉到切片这一级当成元数据过滤要下推到向量库和相似度一起算还得能扛得住权限变更和删文。——这才是面试官想听的。这篇文章就把这三句话一句句拆开。一、为什么搜完再过滤是错的上一节里应用层过滤这个答法很多人的直觉都是对的搜都搜出来了我循环一遍把没权限的去掉不就行了代码大概长这样// ❌ 反例典型的事后过滤post-filter public ListDocument searchWithPermission(String query, String userId) { // 第一步先按相似度捞一批 ListDocument docs vectorStore.similaritySearch( SearchRequest.query(query).withTopK(10)); // 第二步在应用层把没权限的过滤掉 return docs.stream() .filter(doc - permissionService.hasAccess(userId, doc.getMetadata(docId))) .collect(Collectors.toList()); }这段代码能跑面试官却会立刻皱眉。它有三个问题一个比一个隐蔽。第一个问题召回被白白削掉。你向向量库要 Top-10它老实地返回了 10 条最相似的。然后你在应用层一过滤可能只剩 2 条——剩下 8 个名额全浪费给了你看不到的文档。更糟的情况是库里明明有 5 条你有权限、也确实相关的切片但它们相似度排在第 11 到第 15 位压根没进这 10 条于是模型回答没找到相关资料。权限过滤把答案挤出了候选集这是召回率的直接损失。第二个问题顺序里藏着泄密风险。如果哪个兄弟图省事先把这 10 条拼进 Prompt 发给模型了等模型答完再去过滤——那已经晚了敏感内容既进了 model 的上下文也可能已经出现在回答里。事后过滤能兜住的只有进没进最终回答兜不住进没进模型大脑。第三个问题规模一大就崩。数据量小的时候随手捞 100 条再过滤没什么感觉。等知识库到了千万级切片你为了过滤后还能剩 10 条只能把 TopK 一路调到几百甚至上千检索和网络传输成本直线上升延迟也扛不住。所以正确的姿势只有一句话把权限条件交给向量库让它先筛出你有权限的候选集再在这个子集里算相似度挑 TopK。这就是 pre-filter前置过滤。要说明的是pre-filter 也不是所有向量库都天生擅长。Milvus 这类原生向量库支持标量字段过滤 向量检索联合执行pgvector 早期版本是先取相似度再过滤过滤后不够还得靠人工重试直到 0.8.0 才引入了迭代扫描iterative scan来缓解——细节第三节讲。二、权限要沉到切片这一级很多人脑子里权限是文档这一级的事这份文件谁能看。但在 RAG 里真正进模型的是切片不是文档。一个 80 页的手册被切成 200 块其中只有 3 块是薪酬相关的敏感内容。你如果只能控制到文档这一级要么整篇手册都别让人搜到要么整篇都能搜到——两头都不对。正确做法是文档的权限在切片的时候继承下来作为元数据存进每一块。切片本身通常不单独定义权限它继承来源文档的属性只有在同一份文档里不同章节密级不同这种场景才需要切片级的独立密级。那元数据里到底要存哪些字段这是一张值得背下来的表字段示例用途tenant_idt_acme多租户隔离永远排第一个条件doc_iddoc_8891回链原文删文时联动清理deptcustomer-service按部门可见level2密级比如 0 公开 / 3 机密acl_tags[cs_public,l2]白名单标签灵活授权updated_at2026-10-01时效过滤、增量重建的锚点在这之上企业里常见的权限模型其实就三种面试时能说清楚区别就是加分项-RBAC按角色给用户分角色角色映射权限。实现最简单但角色一多、授权一细就爆炸且给某个人单独开个口子很别扭。-ACL白名单标签每条切片挂一组能看到它的标签/部门/人检索时拿当前用户的标签集合和切片标签集合求交集非空才可见。灵活适合文档按分组授权。-密级 / ABAC按属性给用户和文档各定一个层级规则是用户级别 ≥ 文档级别才可见level user.level。政府和国企的文档分级常用这套判断简单、性能好但授权粒度偏粗。多数企业从ACL 标签 部门起步就能覆盖九成场景密级作为补充。三、把过滤下推到检索层真实代码模型设计好了接下来是落地。这时候你要区分两种后端。用 Milvus 这类原生向量库过滤就是查询语句的一部分。Spring AI 的SearchRequest支持filterExpression// 服务端从登录态拿到当前用户的租户、部门、密级 AuthCtx ctx SecurityUtils.current(); // tenantId / dept / level String filter tenant_id \ ctx.tenantId() \ level ctx.level() (dept \ ctx.dept() \ || acl_tags in [\public\]); SearchRequest request SearchRequest.builder() .query(年假怎么算) .topK(5) .filterExpression(filter) // 关键过滤条件和向量检索一起下推 .build(); ListDocument docs vectorStore.similaritySearch(request);逐行看tenantid ...是租户硬隔离level user.level是密级比较dept和acltags用||兜底——用户所属部门能看或者内容是公开标签。过滤表达式和向量相似度在同一次查询里执行向量库先在满足标量条件的子集里做近邻搜索不相关的切片根本不参与计算也就不会占 TopK 名额。用 pgvectorPostgreSQL同理过滤就是WHERESELECT chunk_id, doc_id, content FROM kb_chunk WHERE tenant_id t_acme -- ① 租户隔离永远第一个 AND level 2 -- ② 密级用户级别 文档级别 AND acl_tags ARRAY[cs_public,dept_cs] -- ③ ACL数组重叠即命中 ORDER BY embedding :query_vec -- ④ 余弦距离排序 LIMIT 5;几个点值得说是 PostgreSQL 的数组重叠运算符acltags ARRAY[...]表示两组标签有交集是 pgvector 的余弦距离算子#内积、-L2。为了配合WHERE里的tenantid、level记得在这些标量列上建普通 B-tree 索引否则过滤本身就成了全表扫描。还有那个老问题——过滤会不会把召回削没了pgvector 0.8.0 给了出路打开迭代扫描。SET hnsw.iterative_scan relaxed_order; -- 或 strict_order它的意思是HNSW 索引第一轮找到的候选里被WHERE过滤掉一些、不够LIMIT时索引会继续往下扫直到凑够数量或扫完为止而不是一轮捞完就认命。这样既有前置过滤的正确性又不至于像纯 post-filter 那样白丢召回。还有一条也是最容易被忽略的一条过滤条件必须由服务端强制注入绝不能交给调用方。// ✅ 正确从登录态JWT / SSO session里取业务代码无权省略 AuthCtx ctx SecurityUtils.current(); // ❌ 错误让上层/前端传 filterExpression // search(query, userSuppliedFilter)前一种写法租户、部门、密级全部来自认证上下文任何业务代码都改不掉后一种写法只要有个接口忘了传、或者传错了整套校验就形同虚设。安全设计上有个原则默认拒绝而不是默认放行更是默认必须显式带上条件。另外拼 SQL 时该用参数化就用参数化?/$1表达式拼接只在 Milvus 这种拿不到参数位置的地方且值必须来自可信的服务端变量。四、多租户最容易在公司级产品上翻车如果你做的知识库是卖给多家公司的 SaaS那租户隔离就是一条红线比文档权限更硬。三种常见做法隔离强度和成本各不相同方案做法隔离强度成本每租户一个 collection物理隔离各存各的最高资源占用最多分区 partition / partition_key同一 collection 内逻辑分区中高中等单 collection tenant_id过滤纯逻辑隔离取决于过滤永不失手最低结论很直接多租户场景至少用 partition 起步条件允许就 collection per tenant。只靠tenant_id过滤的单 collection 方案一旦有哪次查询漏了租户条件——比如某个新写的内部调试接口、某个统计脚本——吐出来的就是跨公司的数据。这种事故的代价远高于多占的那点存储。五、三个最容易被问倒的边角前面的都是静态问题权限明确、数据不动。而生产里最难的是数据在动、权限在变。三个高频追问追问一员工调岗了、文档密级改了检索会跟着变吗这里有个关键区分文档属性部门、密级、ACL 标签存在切片里用户权限每次检索时实时算。只要守住这条线员工调岗这件事就不用碰向量库——他的新权限下次检索自动生效。真正需要重刷切片的是文档自己的属性变了比如一份产品文档从内部降级为公开。这种要么走 CDC/增量管道重新打标签要么靠updated_at定期重建。千万别把谁能看直接写死进切片不然改一次权限就得重刷全库。追问二文档删了切片还在怎么办删文档只删了源文件向量库里的切片不会自己消失用户照样可能搜到已删内容。做法是软删 异步清理源文档标记删除后检索时用status ! deleted把它排除立即不可见后台再慢慢把对应doc_id的切片物理清掉。顺序很重要——先保证立刻搜不到再去管彻底删干净。追问三检索结果缓存会不会造成越权这个最隐蔽。为了省调用、降延迟很多系统会缓存query → 检索结果。但如果缓存 key 只有 query 字符串那A 用户搜薪酬方案得到的他有权限的结果会被 B 用户命中——B 就看到了不该看的。修法是把缓存 key 带上租户 ID 权限指纹用户级别、标签集合的哈希不同权限的人各存各的。用户权限缓存在 Redis 里时也务必给短 TTL别缓存一整天否则调岗、离职后的权限收回要等到缓存过期才生效。 面试官视角的标准回答如果面试官问企业 AI 知识库你怎么做权限隔离防止越权泄露我分三层来讲。brbr第一层是权限建模。我把权限做成切片的元数据而不是事后再判。切片从文档继承属性——租户、部门、密级、ACL 标签、文档 ID、更新时间。用户的权限能看哪些标签、什么密级不写进数据每次检索时从登录态JWT 或 SSO实时取。这样人员变动不用碰向量库只有文档属性变更才需要重建。brbr第二层是检索时把过滤下推到向量库。用 Milvus 就写filterExpression用 pgvector 就写WHERE让标量过滤和向量相似度在一次查询里完成而不是先搜完 TopK 再在应用层过滤——那样既损失召回又有把敏感内容送进模型的风险。pgvector 可以开iterativescan解决过滤后召回不够的问题。过滤条件由服务端从认证上下文强制注入调用方无权省略。brbr第三层是边角。多租户用 partition 或 collection 做隔离绝不只靠一个tenantid过滤删文档做软删加异步清理检索先排除 deleted检索缓存和权限缓存的 key 要带权限指纹并设短 TTL防止跨权限命中。brbr再补一句权限过滤是必要条件不是充分条件。它是检索层的一道护栏还得配合 Prompt 加固、工具权限控制、输出脱敏整套下来才算完整。这篇把前面第 09 篇那句文档级权限控制和第 19 篇里acl 只是元数据两个半成品接上了讲的是原理和取舍。如果你想要一套能直接跑起来的版本——Milvus 建集合、挂字段、灌数据、带权限过滤的完整检索链路——下一篇可以接着出实战篇把它落地成可复现的代码。
阅读完成 · 觉得有帮助?
咨询建站