Files

109 lines
5.9 KiB
Markdown

---
id: ai.rag.scenario.permission-aware-kb
title: 带权限的企业知识库问答:不同部门看到不同内容
type: scenario
domain: ai
stack: [rag, vector-db, elasticsearch, fastapi]
tags: [企业知识库, 权限过滤, 文档解析, 混合检索, 重排, 拒答, 增量更新]
level: advanced
prerequisites: [ai.rag.generation-eval]
est_hours: 3
related: [ai.rag.interview, ai.vector-db.scenario.multi-tenant-index, backend.elasticsearch.index]
week: [5, 6]
---
# 带权限的企业知识库问答:不同部门看到不同内容
## 业务背景与问题
公司把员工手册、IT 指南、财务制度、各部门内部文档都导入知识库,做一个“制度问答助手”。试运行时发现:
1. 研发同事问“A 级调薪幅度多少”,助手根据 HR 内部的薪酬文档给出了答案——**越权**;
2. PDF 里的表格(差旅标准)被解析成一串数字,问“一线城市住宿标准”答错;
3. 问“报错 0x80070005 怎么办”,检索不到包含这个错误码的文档;
4. 制度更新后,新旧两个版本都被检索到,答案自相矛盾;
5. 知识库里没有的问题(股票期权),助手也一本正经地回答。
## 约束
- 权限以现有的组织架构(部门、角色)为准,文档有“全员 / 指定部门 / 指定角色”三种可见范围。
- 文档格式:PDF(含表格、扫描件)、Word、Confluence 类网页。
- 答案必须给出引用来源,方便员工核对原文。
- 回答延迟目标:首字 2 秒以内(流式)。
## 方案设计
```mermaid
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](../examples/mini-rag/mini_rag.py)):
```python
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` 条件;其他向量库语法不同,以官方文档为准):
```python
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 和版本增量更新;最高分低于阈值就拒答。评测集专门包含无权限和无答案的问题,每次改动都跑回归。”