2361 字
约 7 分钟
15
S20-A22 · 检索的权限过滤:为什么"答案里出现了别人的数据"

一个内部知识库的检索接了三类用户:全员、部门、项目组。上线后一次抽查发现:项目组的用户搜到了"别的项目"的会议纪要——因为检索时只按"关键词"召回,而"权限"是在"生成答案之后"才检查的(检查的是"用户有没有权限看这个知识库",而不是"这段内容")。

RAG 的权限问题与"传统系统"不同:它不是"能不能访问一个页面",而是"能不能看到一段文本"——这个"分块"的单位决定了难度:一个分块可能有 500 字,而其中 50 字是敏感的——于是"整块放行"会泄漏,"整块拦截"会丢信息。
而"一段文本"来自"分块",分块可能混合了不同权限的内容。


1. 三个"过滤位置"

位置 效果
检索前过滤(query 里带权限条件) 最安全
检索后过滤(召回后丢弃无权限的) 安全但有代价
生成后过滤(答案产出后检查) 已泄漏

第三项是"开头那个事故"的形态:**内容已经进了模型上下文,模型已经"看过"**第三项的"判据"很硬:只要"内容进入上下文",就必须假设"它可能被用上"——于是"输出侧拦截"只能降低"明显泄漏",不能作为"权限控制"。
——即使最终答案被拦,也无法保证"没有间接泄漏"(如"根据资料,某项目存在风险")。

2. 权限模型的三层

层 说明
文档级 整篇文档的可见范围
分块级 分块继承文档权限(最常见)
字段级 段落/表格里的敏感字段(最细)

第二项是"实践中的默认":分块时把"权限标签"复制到每个分块,于是"检索时能按标签过滤"。

第三项的"成本"在"识别":规则(如"含'薪酬'的段落")会漏,"模型识别"会有误判——所以它通常只在"极敏感"的语料上做,且要"人工复核"。
第三项在"同一文档里有敏感段落"时必要:分块时"识别敏感段"并降级它的可见范围——而它需要"能识别敏感内容"(规则或模型)。

// 分块的权限元数据(检索时作为过滤条件)
chunk:
  id: c-00123
  doc_id: d-0042
  text: "..."
  acl:                       # 谁能看这一块
    visibility: project      # public | dept | project | private
    project_id: P-77
    owner_dept: D-12
  sensitivity: internal      # 用于"字段级"降级

3. "检索前过滤"的三种实现

实现 说明
元数据过滤(ES filter) 最高效(先过滤再打分)
多索引/多租户(每权限一组索引) 最隔离(成本高)
查询改写(把权限拼进 query) 灵活但易漏

第一项是"主流":在 query 里加 filter 子句,于是"无权限的分块根本不参与打分"——它同时解决"安全"与"性能"。

第三项的风险在"拼接":权限条件与用户输入混在一起,容易"被注入或漏括号"——而"漏一个括号"就等于"不过滤"。

// 检索前过滤(filter 与 query 分离,权限不由用户输入决定)
{"query": {
  "bool": {
    "must":   [{"match": {"text": "<用户查询>"}}],
    "filter": [{"term": {"acl.visibility": "project"}},
               {"term": {"acl.project_id": "<当前用户的 projects>"}}]
}}}
// 关键:filter 里的值来自"服务端的用户上下文",不来自请求参数

4. "检索后过滤"的代价

代价 机制
召回数下降 过滤掉一部分,可能不够
必须多召回 于是"取 20 条再过滤剩 5 条"
上下文浪费 被过滤的内容已占 token

第三项在"过滤比例高"时很实际:取 20 条、过滤剩 3 条第三项的"量化"是"过滤比例":过滤掉 50% 以上时,"检索后过滤"基本不可用(因为要"取 100 条才能剩 50 条",成本与延迟都翻倍)。
,于是"模型的上下文里只有 3 条,质量下降"——而"调大 top_k"又会带来成本。

所以"检索后过滤"只适合"权限粒度粗、过滤比例低"的场景;否则该做"检索前过滤"。

"缓存"是权限问题最容易被忽略的一处:按 query 缓存的结果,会被"权限更低的人"命中。

// 两种缓存的差别
错:cache["如何申请报销"] = [chunk1, chunk2]     ← 不分权限
对:cache["如何申请报销|dept=D12|proj=P77"] = [...]  ← 键里含权限维度
// 判据:缓存键是否包含"影响结果集的全部维度"

5. "生成后过滤"为什么不够

风险 说明
间接泄漏 模型"看过"就能"转述"
无法审计 "看过什么"不留痕
拦截不完整 只拦"明显引用"

第一项的"例子"很典型:答案里没有"某项目"这个词,却说了"有个项目存在交付风险"——它没引用,但它"用上了"。
第一项是"最根本"的:模型的输出不是"引用",而是"综合"——它可以"不引用原文"但"用上原文的信息"。

所以"权限必须在"内容进入上下文之前"解决",而不是"在输出之后"这个"之前"也包括"日志与缓存":检索结果的缓存如果"按 query 存"而不按"用户权限"分组,就会把 A 的结果给 B——而它比"输出泄漏"更隐蔽。
。

"权限是否生效"可以用一个"双账号对照"的自检跑,成本两分钟:

// 三个对照组(用两个权限不同的测试账号)
1 全员词(如"考勤制度"):两个账号结果应相同
2 部门词(如"本部门预算"):应只有该部门账号能召回
3 项目词(如"P-77 纪要"):应只有项目成员能召回
// 判据:第 2、3 组若"两个账号结果相同" → 权限过滤没生效

6. 排错:五种"权限相关问题"

现象 成因 处理方向
答案含他人数据 生成后才过滤 改检索前过滤
召回数太少 过滤比例高 提前过滤 + 扩召回
某些用户搜不到自己的 权限标签错 校验标签
分块继承错 入库时未复制 ACL 入库校验
换权限后旧索引没更新 标签未重建 重建索引

第五项的"识别"是"用两个权限不同的账号搜同一个词,比对结果"——若结果集相同,说明"权限没生效"(或"标签全是 public")。
第五项最容易被漏:用户权限变了(调岗),而分块的 ACL 是"入库时算的"——于是"他还能搜到原部门的内容"。

修法有两种:"实时算权限"(查询时按用户当前权限过滤) 或 "定期重建 ACL"。前者更准,后者更便宜。

最后一个"分块"视角:分块粒度越细,"整块敏感"的概率越低——但"分块越小",上下文越碎。

// 分块粒度与权限的取舍
粗分块(1000 字):上下文完整,但"一块里有敏感段"时只能整块拦
细分块(200 字):敏感段可单独拦,但"上下文不足"(模型看不全)
// 折中:粗分块 + "敏感段标记"(在块内标出"这段不可引用")

7. 边界:什么情况不必做细粒度权限

场景 为什么 做法
全员可见的知识库 无权限差异 不做
单一租户(自建自用) 无跨租户 不做
内容本身不敏感 泄漏无后果 不做
已用"物理隔离"(每租户独立实例) 天然隔离 不做

判断口径一句话:"同一批分块是否会被"不同权限的人"检索到"。最后一条与"审计"有关的边界:"谁看过什么"要留痕:检索日志里要记录"用户 + 命中的 doc_id 列表"——否则"事后追查泄漏"没有依据。
会(多部门、多项目、多租户),就必须在"检索前"过滤;不会(全员可见),它只是"多余的条件"。


8. 自测

  1. 三个"过滤位置"里,哪一个"已经泄漏"?为什么"拦下最终答案"不够?
  2. 权限模型三层里,哪一层是"实践中的默认"?"字段级"什么时候必要?
  3. "检索前过滤"的三种实现里,哪一种同时解决"安全"与"性能"?
  4. "查询改写"拼权限的风险是什么?为什么"漏一个括号"就等于不过滤?
  5. "用户调岗后还能搜到原部门内容"是怎么发生的?两种修法各有什么取舍?
  6. 判断"是否需要细粒度权限"的那个问题是什么?
S20-A22 · 检索的权限过滤:为什么"答案里出现了别人的数据"
https://www.clxhxhhr.top/posts/6616/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。