ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAG 知识库权限隔离全链路实战:从入库到生成,避开泄露陷阱

RAG 知识库权限隔离全链路实战:从入库到生成,避开泄露陷阱 RAG 知识库最容易翻车的地方往往不是召回率也不是大模型答得不好而是权限。我见过太多团队检索链路调得漂漂亮亮评测集上准确率能到 90%结果一上线就出事故A 部门的员工问了一句今年的差旅报销标准是多少模型把 B 部门内部讨论稿里的数字原封不动吐了出来。这不是模型的问题是整条链路上根本没人管谁能看到什么。这篇内容我想把 RAG 知识库的权限隔离从头到尾拆一遍。所谓全链路指的是从文档入库、切片、向量化、索引、检索、重排一直到最终拼进 Prompt 送给模型每一个环节都得带上权限语义而不是只在最后一道关卡做个过滤。适合正在做企业级 RAG、准备把 Demo 推向生产、或者已经被权限问题坑过一次的同行参考。下面这些方案和踩坑记录都是我在实际项目里验证过的不是纸上谈兵。1. 为什么权限隔离在 RAG 里比传统搜索更难做1.1 传统权限模型和向量检索的根本冲突传统文档系统的权限很好做文件存在数据库里每条记录挂一个owner_id或者acl字段用户请求进来先查权限表没权限的直接不返回。逻辑是先鉴权再取数据边界清清楚楚。RAG 的麻烦在于它的核心是向量相似度检索。用户的问题被编码成一个向量然后去向量库里找最像的 Top-K 片段。这个最像是纯数学的它不知道也不关心这个片段属于谁。如果你在检索完之后才做权限过滤就会出现一个很尴尬的局面Top-K 里本来有 10 条过滤掉 7 条没权限的只剩 3 条召回质量直接崩了。更糟的是如果这 3 条里恰好没有答案模型就会开始编因为它拿到的上下文是残缺的。所以核心矛盾是向量检索追求的是语义最相关而权限要求的是可见性约束这两件事必须在检索发生的那一刻就同时满足而不是事后补救。1.2 事后过滤和事前过滤的代价差异我先把两种思路摆出来对比这是整个设计的分水岭。维度事后过滤Post-filter事前过滤Pre-filter实现难度低检索完加个 where 条件高要改索引结构召回质量差Top-K 被稀释好K 条都是可见的性能检索快过滤慢尤其大 ACL检索稍慢但结果稳定适用场景权限简单、数据量小企业级、多租户、权限复杂典型坑过滤后结果不足模型幻觉索引膨胀元数据管理复杂事后过滤看起来省事但它在数据量一大、权限一复杂的时候就彻底失效。举个例子一个 500 人的公司某份文档只对 3 个人可见你检索 Top-20命中这份文档的概率本来就低就算命中了过滤逻辑还得判断当前用户是否在那 3 人里。如果每个片段都要做这种判断延迟会肉眼可见地涨。事前过滤才是正解但它的前提是权限信息必须作为向量库的一等公民和向量一起存进去并且支持在检索时作为过滤条件参与计算。这就引出了后面要讲的元数据设计。1.3 一个真实事故的复盘说个我亲历的事。某项目做内部制度问答向量库用的是当时比较流行的方案权限靠检索后按部门过滤。测试阶段一切正常因为测试账号都是管理员能看到所有文档。上线第一天一个普通员工问年假怎么算模型给出的答案里混进了一段管理层讨论稿的内容里面提到拟将年假从 15 天调整为 12 天。这段内容当时还没定稿属于高度敏感。排查下来问题出在三个地方叠加一是切片时把讨论稿和正式制度切进了同一个 chunk二是检索时没带部门过滤因为开发觉得反正最后会过滤三是过滤逻辑写在了 Prompt 拼接之后等于模型已经看到了。三个环节任何一个做对了都不会出事但偏偏全错了。这件事之后我把权限必须贯穿全链路当成了铁律。2. 权限元数据的建模ACL 到底该怎么存2.1 从 RBAC 到文档级 ACL 的映射企业里最常见的权限模型是 RBAC基于角色的访问控制用户属于角色角色拥有权限。但 RAG 检索面对的是文档片段不是功能菜单所以你需要把 RBAC 映射到文档级。我的做法是给每个 chunk 打上一组标签至少包含这几个维度tenant_id租户隔离多租户场景必备单租户也要留字段方便以后扩展。dept_id部门维度最常用的粗粒度过滤。role_allow允许访问的角色列表比如[hr, finance]。user_allow白名单用户处理只给某几个人看的敏感文档。sensitivity密级比如public / internal / confidential。doc_id和chunk_id溯源用出问题能定位到原文。这里有个关键决策用允许列表还是拒绝列表我强烈建议用允许列表allow-list。拒绝列表deny-list在权限系统里是灾难因为没被拒绝不等于被允许一旦有新角色加入默认就是可见的容易漏。允许列表虽然写起来麻烦但语义明确不在列表里就是不可见。2.2 元数据字段的取舍与索引成本元数据不是越多越好。每加一个字段向量库的索引就大一圈过滤时的计算也重一点。我一般会区分高频过滤字段和低频溯源字段高频过滤字段必须建索引tenant_id、dept_id、sensitivity。这三个几乎每次检索都要用。低频溯源字段可以不建索引doc_id、chunk_id、create_time。这些主要用于事后审计和展示引用来源不参与过滤。role_allow和user_allow比较特殊它们是数组类型。不同向量库对数组过滤的支持差异很大有的支持contains语义有的需要你提前展开成多个布尔字段。选型时一定要确认这一点否则设计得再漂亮也落不了地。提示如果你的向量库不支持数组字段的高效过滤一个折中方案是把角色编码成位图bitmap比如用整数表示角色集合过滤时做位运算。这个方案在角色数量固定比如不超过 64 个时非常好用性能比数组过滤高一个数量级。2.3 权限变更时的同步难题权限不是静态的。员工调岗、离职、项目组解散权限随时在变。如果权限信息只存在向量库里那每次变更都要去更新海量 chunk 的元数据成本极高。我的经验是权限元数据不要写死在向量库里而是存一份权限映射表在关系库或缓存里向量库只存一个轻量的acl_key。检索时先用acl_key做粗过滤拿到候选后再去缓存里做精确校验。这样权限变更只需要改一处不用动向量库。具体流程是这样入库时给每个 chunk 算一个acl_key比如dept_hr或者proj_alpha。检索时根据当前用户的身份先算出他能访问的所有acl_key集合然后把这个集合作为过滤条件传给向量库。用户权限变了只需要重新计算他的acl_key集合向量库那边完全不用动。3. 入库链路切片阶段就要埋下权限的种子3.1 文档解析时如何继承权限权限的源头在文档不在 chunk。一份文档有什么权限它的所有 chunk 就继承什么权限这是基本原则。但现实里经常有例外一份公开的制度文档里夹了一段内部数据。这时候如果整篇继承就会泄露如果按段落拆权限又太细碎。我的处理方式是在解析阶段做一次权限标注允许对文档内的特定段落覆盖默认权限。具体做法是在解析器里加一个规则引擎识别敏感标记比如内部资料请勿外传这类字样或者文档里的特定样式命中就把这段的sensitivity提升一级。这个规则不用很复杂但能挡住大部分意外泄露。3.2 切片粒度对权限隔离的影响切片粒度是个老话题但在权限场景下它有了新含义。切片太大权限会连坐一段里只要有一句敏感内容整段都得提权导致大量本来可见的内容被误伤。切片太小语义会碎检索出来的片段缺乏上下文模型答不好。我一般用 300 到 500 字作为默认切片长度重叠 50 到 80 字。但在权限敏感的场景我会额外做一件事按语义边界切而不是按固定长度切。比如按标题、段落、列表项切保证每个 chunk 的权限属性是纯的。这样即使某段敏感也只影响那一段不会污染整篇。3.3 向量化之前的那道校验很多人忽略的一点向量化之前应该有一次权限校验。什么意思就是确认这个 chunk 的权限元数据已经正确填充了没有空值、没有默认值兜底。我见过因为解析器 bug导致部分 chunk 的dept_id是空的结果这些 chunk 对所有部门都可见——因为过滤条件dept_id in (...)对空值的行为取决于具体实现有的库会把它当成匹配所有。所以入库流水线里我会加一个断言任何 chunk 的tenant_id和sensitivity不能为空dept_id为空时必须显式标记为public。这个校验成本极低但能挡住一类非常隐蔽的泄露。4. 检索链路过滤条件怎么下推才不伤召回4.1 把权限过滤下推到向量检索层这是全链路设计里技术含量最高的一环。理想情况下权限过滤应该和向量相似度计算在同一个引擎里完成也就是带过滤的向量检索filtered vector search。但不同向量库的实现质量参差不齐这里有几个坑要提前知道。第一种实现是先过滤再检索pre-filtering先用元数据筛出可见的候选集再在这个子集里做向量检索。优点是结果一定合规缺点是如果可见集很小检索会退化成暴力扫描慢。第二种是先检索再过滤post-filtering先做向量检索拿 Top-N再过滤。优点是快缺点就是我前面说的召回稀释。第三种是过滤与检索融合filter-aware ANN在 HNSW 或 IVF 这类索引的遍历过程中就带上过滤条件跳过不可见的节点。这是最理想的但支持它的库不多而且过滤条件的选择性会影响索引效果。我的建议是优先选支持融合过滤的向量库如果只能选前两种宁可牺牲一点延迟也要用 pre-filtering因为召回质量是 RAG 的命根子。4.2 多租户场景下的物理隔离 vs 逻辑隔离多租户是个绕不开的话题。逻辑隔离就是所有租户的数据放一个索引靠tenant_id过滤。物理隔离是每个租户一个独立的 collection 或索引。方案成本隔离强度运维复杂度适用规模逻辑隔离低中依赖过滤正确性低中小租户、租户数多物理隔离高高天然隔离高大客户、合规要求高混合中高中大客户物理、小客户逻辑我一般用混合方案头部大客户走物理隔离长尾小客户走逻辑隔离。这样既满足了合规要求高的客户又控制了整体成本。切换的阈值可以按数据量或者合同金额来定。4.3 重排阶段的权限二次确认即使检索层做了过滤我仍然建议在重排rerank阶段做一次权限二次确认。原因有两个一是检索层的过滤可能因为缓存、索引延迟等原因出现脏读二是重排模型可能会引入一些检索层没覆盖的候选。二次确认的成本很低就是拿候选 chunk 的acl_key去缓存里查一下当前用户是否有权限。这一步相当于最后一道闸门能挡住绝大多数意外。我把它叫做纵深防御单点做对了不够要多个环节都做对。5. 生成链路Prompt 拼接前的最后一道闸门5.1 上下文拼装时的权限复核到了拼 Prompt 这一步其实已经晚了但还是要做。因为前面所有环节都可能因为 bug 而失效这一步是兜底。具体做法是在把每个 chunk 拼进上下文之前再校验一次权限不通过的直接丢弃并且记录一条告警日志。这里有个细节丢弃之后要不要补位我的做法是补位从重排结果里顺延取下一个候选直到凑够上下文长度或者候选耗尽。这样能尽量减少因为权限过滤导致上下文不足的情况。5.2 引用溯源与权限的联动RAG 通常会给出引用来源方便用户核对。但引用来源本身也可能泄露信息。比如用户看到参考文档《XX 项目内部讨论稿》即使内容没给光这个标题就泄露了信息。所以引用展示也要过权限只展示用户有权访问的文档标题无权访问的引用直接隐藏或者用内部资料这类模糊描述代替。这个细节很容易被忽略但在实际使用中用户对为什么这条引用看不到的追问往往比内容本身更麻烦。5.3 日志与审计谁在什么时候问了什么权限隔离不只是防泄露还要可追溯。每次检索和生成都应该记录用户 ID、查询内容、命中的 chunk ID 列表、过滤掉的 chunk ID 列表、最终进入上下文的 chunk ID 列表。这些日志在出事故时是救命的。我一般会把日志分成两层访问日志记录谁问了什么过滤日志记录哪些内容被权限挡掉了。过滤日志尤其重要它能帮你发现是不是过滤太严导致答不出来或者是不是过滤失效导致泄露。6. 几个我踩过的坑和对应的解法6.1 缓存导致的权限穿越为了提速很多团队会给检索结果加缓存。但缓存如果不带用户身份就会出大问题A 用户查过的结果被缓存B 用户来查直接命中缓存看到了 A 才能看的内容。解法很简单但必须做缓存 key 必须包含用户的权限指纹比如acl_key集合的哈希。权限不同的用户缓存 key 不同天然隔离。权限变更时对应的缓存要失效这个可以通过版本号或者 TTL 来控制。6.2 向量库过滤条件的空值陷阱前面提过dept_id为空时不同向量库的行为不一样。有的把空值当成不匹配任何值有的当成匹配所有值。这个差异在测试环境很难发现因为测试数据往往都是填满的。我的解法是入库时强制填充不允许空值同时在检索层加一层防御如果用户的权限集合为空直接返回空结果不要让它去匹配任何东西。这个空集合短路逻辑能挡住一类非常危险的边界情况。6.3 权限继承链断裂企业文档往往有层级项目属于部门部门属于公司。权限也应该是继承的能看公司文档的人应该能看部门文档。但如果你的acl_key设计成扁平的就会出现能看部门但看不了公司的怪事。解法是在计算用户权限集合时做向上展开用户能访问dept_hr就自动把company_all也加进去如果公司文档对所有人可见。这个展开逻辑放在权限服务里不要散落在检索代码里否则维护起来是噩梦。6.4 评测集里的权限盲区最后说个容易被忽略的你的 RAG 评测集有没有覆盖权限场景大多数团队的评测集只测答得准不准不测该不该答。我建议专门建一个权限评测集包含这些用例用户问一个他无权访问的问题模型应该拒答或者答没有相关信息。用户问一个跨部门的问题模型只能引用他有权访问的部分。权限变更后同样的查询结果应该随之变化。这个评测集不需要很大几十条就够但能帮你在上线前发现大部分权限问题。7. 一套可落地的最小实现方案说了这么多原理最后给一套能直接上手的最小方案适合中小团队快速落地。存储层向量库选支持元数据过滤的大部分主流库都支持每个 chunk 存tenant_id、dept_id、sensitivity、acl_key四个字段前三个建索引。权限服务单独一个服务输入用户 ID输出他能访问的acl_key集合和dept_id集合带缓存权限变更时主动失效。检索层查询进来先调权限服务拿权限集合然后带过滤条件做向量检索Top-K 取大一点比如 30给重排留余量。重排层对候选做重排同时二次校验权限过滤掉不合规的。生成层拼 Prompt 前最后校验一次记录过滤日志引用展示也过权限。审计层所有检索和生成打日志定期抽查过滤日志看有没有异常。这套方案的核心思想就一句话权限不是一道关卡而是一条贯穿始终的线。任何一个环节偷懒都可能成为泄露的缺口。我在实际项目里最大的体会是权限隔离的投入产出比在前期看起来很低——它不提升准确率不加快速度甚至会让检索变慢。但一旦出事它的价值就体现出来了。与其事后救火不如一开始就把这条线拉直。
返回列表