Files
zhuangang/topics/ai/rag/scenarios/permission-aware-kb.md
T

5.9 KiB
Raw Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
id title type domain stack tags level prerequisites est_hours related week
ai.rag.scenario.permission-aware-kb 带权限的企业知识库问答:不同部门看到不同内容 scenario ai
rag
vector-db
elasticsearch
fastapi
企业知识库
权限过滤
文档解析
混合检索
重排
拒答
增量更新
advanced
ai.rag.generation-eval
3
ai.rag.interview
ai.vector-db.scenario.multi-tenant-index
backend.elasticsearch.index
5
6

带权限的企业知识库问答:不同部门看到不同内容

业务背景与问题

公司把员工手册、IT 指南、财务制度、各部门内部文档都导入知识库,做一个“制度问答助手”。试运行时发现:

  1. 研发同事问“A 级调薪幅度多少”,助手根据 HR 内部的薪酬文档给出了答案——越权;
  2. PDF 里的表格(差旅标准)被解析成一串数字,问“一线城市住宿标准”答错;
  3. 问“报错 0x80070005 怎么办”,检索不到包含这个错误码的文档;
  4. 制度更新后,新旧两个版本都被检索到,答案自相矛盾;
  5. 知识库里没有的问题(股票期权),助手也一本正经地回答。

约束

  • 权限以现有的组织架构(部门、角色)为准,文档有“全员 / 指定部门 / 指定角色”三种可见范围。
  • 文档格式:PDF(含表格、扫描件)、Word、Confluence 类网页。
  • 答案必须给出引用来源,方便员工核对原文。
  • 回答延迟目标:首字 2 秒以内(流式)。

方案设计

flowchart LR
  subgraph 入库(异步任务)
    S[文档源] --> P[解析:PDF/Word/HTML → Markdown<br/>表格转 Markdown 表格,扫描件 OCR] --> C[按标题切分 + 标题路径前缀]
    C --> E[嵌入] --> V[(向量库<br/>块 + acl + doc_id + version)]
    C --> K[(Elasticsearch<br/>中文分词 BM25)]
  end
  subgraph 问答
    U[用户] --> A[鉴权:取部门 / 角色] --> R1[向量检索 + acl 过滤]
    A --> R2[ES 检索 + acl 过滤]
    R1 & R2 --> F[RRF 融合 → 重排 top 5] --> G{最高分 ≥ 阈值?}
    G -->|否| N[拒答 + 转人工入口]
    G -->|是| L[带编号资料的提示词 → 流式生成] --> O[答案 + 引用]
  end
  1. 权限在检索阶段过滤:每个块带 acl(可见的部门 / 角色列表),检索时作为过滤条件,无权限的内容根本不进入提示词。不要“先检索再让模型别说”。
  2. 文档解析:PDF 先转结构化文本,表格转成 Markdown 表格整体作为一个块;扫描件走 OCR;去掉页眉页脚。解析结果抽样人工检查。
  3. 混合检索:向量检索负责语义,Elasticsearch(IK 等中文分词)负责错误码、编号等精确词;RRF 融合后用重排模型取前 5。
  4. 版本和增量更新:块带 doc_id 和 version;文档更新时按 doc_id 删除旧块再写入新块(或写入新版本后切换),避免新旧共存。
  5. 拒答:重排后最高分低于阈值直接拒答,给出“提交工单 / 联系 HR”入口。
  6. 评测和回归:评测集包含有权限 / 无权限 / 无答案 / 精确词 / 表格类问题,每次改动跑一遍。

关键代码

权限过滤和拒答的最小实现(完整代码 examples/mini-rag/mini_rag.py):

def allowed(self, user_depts: set[str]) -> np.ndarray:
    return np.array([("all" in c.acl) or bool(c.acl & user_depts) for c in self.chunks])


def vector_search(self, q: str, mask: np.ndarray, k: int):
    scores = self.matrix @ embed(q)
    scores[~mask] = -np.inf          # 无权限的块直接排除,不会进入提示词
    ...

示例里验证了:HR 部门问调薪,命中薪酬制度;研发部门问同一个问题,薪酬制度不会出现在候选中(结果为空 → 拒答);问知识库中没有的“股票期权”→ 拒答。

真实向量库的过滤写法(以 Chroma 为例,where 条件;其他向量库语法不同,以官方文档为准):

results = collection.query(
    query_embeddings=[q_vec],
    n_results=20,
    where={"$or": [{"acl_all": True}, {"dept": {"$in": list(user_depts)}}]},
)

取舍

选择 优点 缺点
检索时按 acl 过滤(本方案) 安全,无权限内容不会进入模型 权限变化时要更新块的 acl
按部门分别建索引 隔离彻底 跨部门文档要重复存储;部门多时管理困难
检索后再过滤 实现简单 top-k 被无权限内容占满后,有权限的内容反而召回不到
只靠提示词让模型不说 —— 不可接受,模型可能泄露
向量库 + ES 两套存储 混合检索效果好 两边数据同步、一致性要处理
只用支持混合检索的单一引擎 运维简单 依赖具体产品的能力(ES、部分向量库已支持向量 + 关键词),以官方文档为准

易错点

  • 权限只在前端或网关判断,检索层不过滤。
  • 先取 top-5 再过滤权限:结果可能被过滤成空,实际上还有相关的有权限内容。先过滤再取 top-k。
  • 用户换部门后缓存的答案仍可见:缓存 key 要包含权限维度,或不缓存敏感问答。
  • 表格被拆成多块:一行数据和表头分离,数字失去含义。
  • 文档删除 / 下线后向量库里还有残留。
  • 日志里记录了完整的提示词(包含敏感文档内容),日志本身成为泄露渠道。

面试怎么讲

“企业知识库最大的问题是权限。我在每个块上带了 acl 元数据,检索阶段就过滤,无权限的内容不会进入提示词,而不是靠提示词让模型别说;检索用向量加 ES 的混合检索再重排,解决错误码这类精确查询;文档解析时把表格转成 Markdown 整体入库;文档按 doc_id 和版本增量更新;最高分低于阈值就拒答。评测集专门包含无权限和无答案的问题,每次改动都跑回归。”