Appearance
第7章 Metadata 与权限模型:让知识带着边界进入索引
"Security is not a retrieval re-rank. It's an index-time invariant." — 企业知识库系统的第一教训
本章要点
- 每个 chunk 都是带业务边界的知识单元——没有 metadata 的检索只能回答"相似什么",不能回答"谁能看什么"
- RAG metadata 分三层:身份层(doc_id / chunk_id / version)、业务层(owner / department / tags)、安全层(access_level / ACL / PII flag)
- 权限必须索引时嵌入而非检索后过滤——后者会泄露存在性信息、也无法避免向量召回把高敏数据当候选
- 主流权限模型两类:RBAC 简单但粗、ABAC 灵活但复杂——RAG 场景推荐 ABAC 或 RBAC+ 属性过滤混合
- 多租户隔离靠分区键 + 索引级隔离,不靠 application-level filter
7.1 为什么 metadata 是索引的一等公民
第 6 章把知识切成了 chunk。如果每个 chunk 只有 text 和 embedding,检索系统只能回答一个问题:"和这个 query 语义最相似的 chunk 是哪些?" 但生产 RAG 要回答的问题远不止这个:
- "用户 A 能看到 chunk X 吗?"(权限)
- "这段是 2024 年的还是 2022 年的?"(时效)
- "这段属于哪个业务线?"(归属)
- "这是内部资料还是公开材料?"(敏感级)
- "这是中文 chunk 还是英文 chunk?"(语言)
- "这是哪个文档的哪一节?"(溯源)
上述每一个问题都需要 chunk 携带结构化 metadata。metadata 不是"附带信息",它是检索系统的一等公民——和向量同等重要。少了它,向量检索就退化成"语义匹配玩具",做不了生产。
看一个典型事故:某企业 HR 系统接入 RAG,索引时没区分 "全员可见" vs "经理可见" 的政策文档。员工问"今年绩效加薪幅度",系统召回了仅经理可见的高管薪资表——因为两份文档语义都围绕"加薪"。这次泄漏事故损失的不是数据,是合规审计的一道门槛。索引阶段没带权限 metadata,所有下游过滤都是空中楼阁。
7.2 三层 metadata 设计法则
metadata 不是随意加字段——它有固定的三层结构:
身份层:chunk 的"身份证"
回答"这段 chunk 是从哪来的"。必填字段:
doc_id:文档稳定 ID(UUID 或业务键)chunk_id:{doc_id}#{chunk_index}#{content_hash}格式(见第 4 章 §4.5)version_hash:文档内容 hash,变更检测用publish_date/last_modified:时效信号language:ISO 639-1(zh/en/ja)source_type:pdf/html/markdown/code/api
身份层字段是不可变的——一旦 chunk 入库,这些字段不应该变。变了意味着是一个新 chunk(chunk_id 改变)。
业务层:chunk 的"领域"
回答"这段 chunk 归属哪块业务"。按项目定义,典型字段:
owner:文档所有者(人员 ID 或组织 ID)department/org_path:所属部门tags/categories:业务分类doc_type:contract/spec/faq/policy/codesection_path:文档内层级(继承自第 5 章解析)
业务层字段可更新——文档 owner 调岗、tag 重新归类都会改这些字段。更新时记 audit log。
安全层:chunk 的"访问契约"
回答"谁能看到这段 chunk"。最关键、也最容易做错的一层。典型字段:
access_level:public/internal/confidential/restricted(4 级足够多数场景)acl_tenants/acl_groups/acl_users:具体可见人/组pii_flag:是否含个人敏感信息compliance_tags:GDPR/HIPAA/SOX等合规标签retention_policy:保留周期(30d / 1y / permanent)
安全层字段变更必须强制触发索引更新——一份文档从 confidential 改成 internal 时,对应 chunk 的 access_level 必须立刻同步。任何延迟都是合规风险。
7.3 权限的两个陷阱:索引时 vs 检索后
做权限过滤有两种方式,选错一种会直接翻车。
陷阱一:只在检索后过滤
一种常见做法:所有 chunk 不区分权限都进索引,检索时拿 top-k 结果后按 user 权限过滤掉不可见的。问题:
- 向量召回污染:top-5 里 3 个是用户看不见的 chunk,过滤后只剩 2 个——但这 2 个可能是次优候选。而那个最优、用户可见的 chunk 排在了 top-30——被候选集截断了
- 存在性泄露:用户发现"我问这个问题召回 0 条,但我知道公司有相关文档"——就能推断那些文档存在但自己没权限。对高敏场景这本身就是信息泄露
- 延迟浪费:rerank 跑了 top-20、然后 3/4 被权限过滤掉——80% 的 rerank 算力被浪费
陷阱二:索引层硬分库但查询要合并
另一种做法:每个权限级别建一个独立索引。用户 A 查询时只查自己有权限的索引。问题:
- 用户属于多个权限组时要并行查多个索引再融合——复杂度高
- chunk 的 acl_users 字段精细到人时,索引数量爆炸
- 索引热点:public 索引可能包含 99% 的数据,查询压力不均
正确姿势:属性过滤下推到向量库
现代向量数据库(Qdrant、Milvus、Weaviate、pgvector)都支持过滤 + 向量检索一体化:
python
# Qdrant 示例
client.search(
collection_name="kb",
query_vector=q_vec,
query_filter=Filter(
must=[
# access_level 用户能看
FieldCondition(key="access_level", match=MatchAny(["public", "internal"])),
# chunk 的 ACL 组包含用户所在组
FieldCondition(key="acl_groups", match=MatchAny(user_groups)),
# 时效性
FieldCondition(key="publish_date", range=Range(gte="2024-01-01")),
]
),
limit=20,
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
向量库内部先按 filter 剪枝再做 ANN 检索——召回的 20 条天然都是用户可见的。不会出现召回污染、不会浪费 rerank、不会泄露存在性。
这要求 metadata 字段必须作为索引字段建好。payload 索引(Qdrant 叫 payload index、Milvus 叫 scalar index、pgvector 用 btree)是属性过滤的性能关键。没建索引的字段上过滤会线性扫描,延迟爆炸。
7.4 RBAC vs ABAC:权限模型选型
RBAC:基于角色
RBAC(Role-Based Access Control)是老牌方案:用户归属角色、角色关联权限。
text
用户 alice → 角色 engineering_lead → 权限 [read engineering_docs, write project_X_docs]1
优点:简单直观、工具成熟(LDAP / AD / Okta)、审计容易。缺点:粒度粗——相同角色的人看到完全相同的数据。很多真实场景是"工程 lead 能看自己项目的敏感数据但不能看隔壁项目的"——RBAC 要爆炸性地增加角色。
ABAC:基于属性
ABAC(Attribute-Based Access Control)按属性组合判断权限:
text
allow read if
user.department == chunk.department
AND (user.level >= chunk.required_level OR chunk.owner == user.id)
AND chunk.publish_date >= user.hire_date - 90d1
2
3
4
2
3
4
优点:粒度任意细、规则可演化、支持"相对权限"(对自己的项目有权、对别的项目无权)。缺点:规则复杂、决策慢、实现成本高。
RAG 场景的推荐
多数企业 RAG 场景用 RBAC + 属性过滤混合:
- 主骨架用 RBAC——用户归属部门/角色,chunk 有 access_level 和 department 字段
- 关键维度用 ABAC——项目归属、合同所属客户、工单归属发起人 这类"所有权"关系
具体实现:
- 用户查询带
user_context = {user_id, roles, groups, department, tenants} - 向量库过滤器表达
chunk.access_level IN allowed_levels AND chunk.acl_groups INTERSECTS user.groups - 复杂规则(时效、相对权限)在 filter 里组合多个 FieldCondition
这条路径能覆盖 90% 的企业场景、又不会让权限系统过度工程化。第 22 章的生产案例会展示完整实现。
RBAC vs ABAC 对比
| 维度 | RBAC | ABAC | 混合(推荐) |
|---|---|---|---|
| 粒度 | 角色级(粗) | 属性任意组合(细) | 角色为骨架 + 关键属性细调 |
| 实现难度 | 低 | 高 | 中 |
| 查询开销 | 低(角色查数据库一次) | 高(规则引擎求值) | 低-中 |
| 审计易度 | 高(角色清单) | 中(规则审阅) | 高 |
| 权限变更 | 改角色(波及广) | 改规则 / 属性 | 局部改 |
| 典型场景 | 公司内部工具 | 金融 / 医疗 / 政府 | 企业级 SaaS |
开源权限引擎
不需要从零写权限系统。开源选择:
- OPA(Open Policy Agent):最主流的 policy engine、Rego 语言表达规则、可以 sidecar 部署或库集成
- Casbin:多语言支持(Go / Java / Python / Rust 等)、内置 RBAC/ABAC/ACL 多种模型
- Cerbos:较新、YAML 配置规则、对比 OPA 上手更快
- Oso:Rust 写的 policy engine、嵌入式设计
这些引擎负责"根据 user + resource + action 决定 allow / deny"。RAG 里的用法:先用引擎计算出当前用户可访问的 access_levels / tenants / groups 集合,再把集合作为 filter 下推到向量库。不要把引擎放在 inner loop 里为每个召回 chunk 求值一次——会拖垮延迟。
7.5 多租户的三种隔离强度
SaaS 场景下 RAG 要服务多个企业客户,每个企业的知识库不能串。隔离强度三档:
弱隔离:共享索引 + tenant_id filter
所有客户的 chunk 进同一个索引,每条带 tenant_id。查询必须带 tenant_id filter。
- 优点:成本最低、扩容自动、利用率高
- 缺点:filter 漏写就是跨租户泄漏;大客户的慢查询会影响小客户
中隔离:按租户分 collection / namespace
每个大客户一个 collection/namespace。同一个 vector DB 实例,但索引物理隔离。
- 优点:filter 错也不会串、运维层面能按 collection 做资源配额
- 缺点:客户数多了 collection 数爆炸(Qdrant 单实例推荐 < 10k collections)
强隔离:每个租户一套独立服务
大客户单独一套数据库 + 向量库 + 应用层。物理隔离到服务级别。
- 优点:完全合规——数据永不共享、可单独部署到客户 VPC
- 缺点:成本高(每客户独立资源)、运维复杂
选型:中小客户共享 + 大客户独立是常见路径。SaaS RAG 产品公司如 Glean / Notion AI 公开过这种分层策略。
7.6 PII 和合规 metadata
含个人敏感信息的 chunk 需要额外处理。最低要求:
- 识别:入库时用 Presidio / spaCy NER / 正则识别 PII(姓名、手机、身份证、邮箱、IP)
- 标记:
pii_flag = true、pii_types = ["phone", "email"] - 脱敏副本:可选生成一份脱敏 chunk 用于非授权用户(Alice → [PERSON]、138xxxx → [PHONE])
- 审计:含 PII 的 chunk 被检索时记 audit log(谁在何时以什么 query 命中)
合规场景(GDPR / HIPAA / 等保)进一步要求:
- 用户撤回权:
retention_policy字段支持"用户删除后 30 天内索引清除" - 跨境:
data_residency = CN、allowed_regions = [CN, HK]字段控制数据不出境 - 审计追溯:每次检索的 trace 必须可留存 N 年,能回查"某用户某时刻看到的 chunk 列表"
这些不是可选——合规事故的代价远高于工程成本。企业 RAG 项目立项时就把这些字段放进 schema。
PII 处理的四种模式
不同场景对 PII 的处理有不同模式,按严格度排列:
模式 1:原文入库 + 检索时脱敏。最宽松——chunk 里保留原始 PII,但返回给 LLM 前替换成占位符。问题:LLM 生成的答案里可能已经是"138xxxx1234 的销售是 Alice",脱敏介入太晚。
模式 2:入库时脱敏 + 原文保留在冷存储。chunk 存脱敏版供检索和生成用,完整原文存加密冷存储。用户需要看原始信息时二次授权。适合客户服务场景——Agent 回答时看脱敏版、客服升级时手动调原文。
模式 3:PII 不入索引。识别到 PII 的段落直接剔除不索引。检索不到就不会泄漏。代价:涉及 PII 的业务问题用户问不出来。适合对外产品——允许不完美的功能性换彻底的安全性。
模式 4:PII 单独索引 + 严格 ACL。PII 和非 PII 分两套索引。PII 索引只有严格授权的用户能访问。需要 PII 答题时走特殊路径 + 审计。适合金融 / 医疗。
选型:消费级 AI 产品用模式 3;企业内部效率工具用模式 2;涉 PII 业务(客服、风控)用模式 4。模式 1 几乎没有合法使用场景。
一个权限泄漏的真实事故
某大厂 HR RAG 上线初期发生过:
- 事件:一线员工问"公司今年绩效 A 的比例",系统答"约 15%"并引用了一份《高管薪酬细则》
- 调查:该份细则仅 HR 总监和 CXO 可见、不该进员工问答的召回范围
- 根因:索引阶段所有 HR 文档统一打
department = HR,但没有细分 access_level 字段。查询时员工的allowed_departments包含 HR(因为员工的工资单、政策都在 HR),没有额外的 access_level 检查,就放进了候选集 - 修复:给所有 HR 文档补 access_level 字段,重建索引;gateway 层 user_context 注入 allowed_access_levels;向量库 filter 必须同时检查 department 和 access_level
复盘后的教训不是"加一个字段",而是"每一类过滤维度都必须是独立的 filter 条件——不能期望某一维度的过滤隐式覆盖另一维度"。这个事故让项目组把权限测试加进了 CI——每次索引 schema 变更都跑一遍"员工 A 看不到 CEO 档案"类断言。
7.7 metadata 的版本演化
chunk metadata schema 一旦上线就很难改——索引里几百万 chunk 的 metadata 结构都固定了。演化策略:
向后兼容地加字段
新字段都是 nullable、老 chunk 缺这个字段时默认"未知"。查询 filter 对"未知"要有明确约定——是当"全部可见"(宽松)还是"全部不可见"(严格)?
安全层字段新加时默认严格——老 chunk 没有 pii_flag 的视为 pii_flag=unknown,按最高安全等级处理。宁可降低召回也不能冒泄漏风险。
废弃字段的迁移
某个字段要废弃(比如 access_level 从 4 档改 6 档):
- 第一阶段:新旧字段共存双写,filter 支持两种
- 第二阶段:新数据只写新字段,老数据按映射规则 backfill
- 第三阶段:完全迁移后下线旧字段
每阶段至少留 2 周观察期。生产系统里不要"一刀切改完"——出问题回滚都没地方回滚。
迁移成本估算
backfill 老数据的成本常被低估。100 万 chunk 的重索引大约 30 分钟 GPU(bge-m3),但带着业务字段的 backfill 往往要跑 SQL / 规则引擎做字段填充——串行可能几小时、并行也要几十分钟。双写阶段索引膨胀 2 倍、查询路径也要改成 union 两个字段——期间性能下降 10-20% 是常态。
给每次 schema 变更预留的时间估算:
- 小字段增加(nullable、无默认):1 天设计 + 1 天 PR + 2 天观察
- 字段值空间扩展(4 档→6 档):1 周方案 + 2 周双写 + 1 周灰度 + 1 周下线
- 结构性变更(字段重命名 / 类型改变):按月算
这些时间表对不熟悉数据 migration 的团队往往太乐观——真实生产系统里 schema 迁移是典型的"预估 3 天实际 3 周"项目。架构决策时留足余量。
schema registry
RAG 系统应该有一份明确的 metadata schema registry(JSON Schema / protobuf / pydantic),任何字段变更都要 PR review。不要靠文档——文档和代码会背离。
7.8 查询时的 user_context
有 chunk metadata 还不够,查询时的 user_context 也要结构化。user_context 包含:
user_id/emailroles:角色列表groups:组列表tenants:可访问的租户 ID 列表(多租户场景)department/org_pathhire_date:用于时效相对权限clearance_level:保密级别language_prefs:语言偏好(filter 用)
这份 context 在 API gateway 从 JWT / session 解码出来,注入到每次检索请求。向量库 filter 基于它构造。
为什么 user_context 不能在 RAG 服务里查
有人把"查用户权限"放在 RAG 服务内部——每次请求 RAG 先调 identity service 查用户组。两个问题:
- 延迟:又多一跳,P99 延迟顶上去
- 一致性:用户权限刚变,identity service 和 JWT 里的信息不一致,可能给错数据
正确做法:API gateway 层完成身份解析,RAG 服务只负责用拿到的 user_context 做 filter。身份变更靠短 TTL JWT(5-15 分钟)快速生效。
user_context 的缓存策略
RAG 查询高频,user_context 解析也高频。合理的缓存层级:
- JWT claims 自带:身份、角色、核心属性塞在 JWT 里,解析 JWT 即是 user_context 解析。零额外调用
- Redis cache TTL 短:JWT 未覆盖的属性(细粒度的 groups、时效性权限)从 identity service 查一次缓存到 Redis,TTL 60-300 秒
- 绝不缓存敏感决策:具体某个 chunk 的 allow/deny 决策不缓存——太多组合、缓存键爆炸、且容易过期错
实测下来 JWT claims + Redis cache 组合能把权限解析 P99 控制在 5ms 内,对 RAG 总延迟几乎无感。
权限测试的自动化
权限泄漏是最糟的 bug——一旦发生影响面大、不可挽回。CI 里必须跑自动化权限测试:
python
# 伪代码
@pytest.parametrize("user, forbidden_docs", [
(alice_employee, ["ceo_compensation.pdf", "m&a_pipeline.pdf"]),
(bob_contractor, ["internal_roadmap.pdf", "employee_list.pdf"]),
(tenant_a_admin, ["tenant_b_customer_list.pdf"]), # 跨租户
])
def test_rag_no_leak(user, forbidden_docs):
for doc in forbidden_docs:
# 直接测 doc 的 chunk 不能被 user 召回
chunks = rag.retrieve(query=doc_summary(doc), user=user, top_k=50)
assert all(c.doc_id != doc for c in chunks), f"{user.id} 召回了 {doc}"1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
每次索引 schema 变更 / 权限逻辑修改都跑这套测试。几十个关键 "不能看" 的断言是底线。
7.9 权限变更的传播链:从策略更新到答案失效
前面 8 节讲了权限如何在"正常查询"里起作用。真实生产里更棘手的问题是——权限变了之后:员工离职、角色降级、文档从"内部"升级为"机密"、租户合同终止。这些变更必须在系统里立即生效、否则前一秒"有权限"的用户、下一秒还能查到不该看的内容。这不是小事——合规事故常发生在权限变更的"中间态"。
五条缓存链的失效难题
- 向量库 filter:payload 更新后查询立即生效——更新 chunk 的
access_level、下次查询就看不到了 - Rerank 缓存:按
(query, chunk_id)key、和权限无关——需要把 user_id 加进 cache key、或者权限变更时清空特定 user 的 cache - Query 级响应缓存:按
(query, user_ctx)key——user_ctx 里含权限版本号、权限变则 key 变、自动失效 - Memory 里的历史 facts:Memory 可能存过"用户曾问过 X、答过 Y"——Y 里的引用 chunk 现在无权限、历史 fact 也要失效或重验证
- 已下载 / 截图:最难——用户已经看过的内容无法回收。只能靠政策约束("请不要分享")和事后审计
Time-to-revoke(TTR)作为 SLA
合规规定通常要求权限变更在 X 时间内生效——典型值:
| 场景 | TTR 要求 |
|---|---|
| 员工离职 | 即刻到 1 小时 |
| 角色降级 | 1-24 小时 |
| 文档重分类 | 24 小时到 1 周 |
| 租户合同终止 | 24-72 小时 |
TTR 是可测的——生产 RAG 的权限变更应该有端到端测试:触发权限事件 → 等待 → 尝试查询 → 断言"查不到"。这测试跑一次就能暴露哪些缓存链没连上。
权限版本号:最强工具
给每个权限策略加 permission_version 字段——任何策略变更 version +1。所有缓存 key 都带 version、version 变即自动失效。实现:
python
# user_context 构造时带上
user_ctx = {
"user_id": "u_123",
"role": "contractor",
"permission_version": 47, # 从策略 DB 读取
"tenant_id": "t_A",
}
# 缓存 key 自动包含 version
cache_key = f"resp:{hash(query)}:{user_ctx['permission_version']}:{user_ctx['user_id']}"1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Version bump 后老 key 永远不会被命中、新 key 触发一次完整重跑。这比"挨个去清缓存"简洁、且不会漏。
事件驱动的权限失效
权限变更来自 HR / IAM 系统——通过事件总线推给 RAG:
- HR 发
user_deactivated事件 → RAG 订阅、把该 user 的 query cache、rerank cache 清空、memory 标记 frozen - IAM 发
role_changed事件 → 该用户 permission_version bump、下次查询触发重新拉策略 - CMS 发
doc_reclassified事件 → 该 doc 在向量库的 payload.access_level 更新
事件总线保证全链路最终一致——订阅者各自处理、不用一处地方管所有缓存。见第 8 章 §8.3 的事件驱动模式。
已泄漏答案的处理
即使 TTR 做到 1 小时、用户仍可能在权限有效的 1 小时内已经看到答案、截图、转发。对这部分"已泄漏"数据的处理:
- 政策层面:用户协议里明确"内容保密、不得分享"
- 技术层面:UI 加屏幕水印(用户 ID + 时间戳)、方便事后追责
- 审计层面:保留完整 trace、一旦出事能证明"这个答案是在用户还有权限时返回的"
这些不能真的"撤回"内容——但能约束和取证。对合规场景足够、对最严格场景(军工、某些金融)需要额外技术手段(端到端加密 + 可撤销访问)。
跨系统权限同步的延迟
RAG 的权限信息通常从上游系统同步——HR、IAM、CRM 各有权威数据源。同步延迟是常见 bug 来源:
- HR 系统改了员工状态、IAM 15 分钟才同步
- IAM 更新了、RAG 的 user_ctx 构造器 5 分钟才刷新
- RAG 查询的时候用的还是缓存的 15 分钟前的 user_ctx
累加起来 TTR 可能远超 SLA。解决:
- 关键变更走实时 push:HR 的"离职"不能等批量同步、直接推送
- user_ctx 每次请求重构:不缓存 user_ctx、每次从权威源读(加一次网络往返、但避免延迟累加)
- 同步延迟监控:端到端测"HR 变 → RAG 生效"的耗时、超 SLA 告警
权限变更的事故复盘场景
常见事故:某企业员工离职、IT 当天禁用账号、但员工之前问过 "明年薪资调整方案" 的答案被员工同事用浏览器历史查看——因为 query 级 cache 的 key 只含 hash(query)、没含 user_id、第一次离职员工查的答案被另一人命中。
修复:
- 紧急:query cache key 强制加 user_id
- 短期:所有 RAG cache 审查一遍、无 user_id 的都加上
- 长期:user_ctx 结构规定必含 user_id + permission_version、每处缓存都用
- 测试:加一类"cache 是否按 user 隔离"的回归
这类事故每年都在不同公司重复——cache key 必含 user 身份是 RAG 的默认安全规则。
7.10 Agent 代理与委托访问的权限
前 9 节的权限讨论默认一个人类用户直接问 RAG。真实生产里越来越多场景是 Agent 代表用户访问 RAG——Agent 是中间件、它的身份和用户身份不完全对齐。这带来权限设计的新难题:Agent 应该拿谁的权限?Agent 自己有权限吗?Agent 跨多个用户操作时怎么审计?这些问题在 2025-2026 年随 Agent 产品普及变得尖锐、多数 RAG 权限模型没准备好。
Agent 权限的三种模型
- 模型 A 透明代理:Agent 是用户的"附体"、每次 RAG 请求带用户 JWT、RAG 按用户权限返回。简单但 Agent 做不了跨用户事("帮三个团队成员都找一遍")
- 模型 B Agent 独立身份:Agent 是一个独立账号、有自己的权限(通常是 service account 级别)。能跨用户但审计时只看到 Agent、丢失"谁让 Agent 做的"
- 模型 C 委托 / on-behalf-of:OAuth 的 OBO flow——Agent 有自己身份、同时携带"代表用户 X 行动"的 token。权限是两者交集、审计能追到原用户
生产推荐:模型 C——复杂但信息最完整、合规最放心。模型 A 适合简单 Chatbot、模型 B 适合自动化 pipeline(定时任务、cron)。
OBO token 的设计
OBO(On-Behalf-Of)token 典型字段:
json
{
"agent_id": "agent-123",
"agent_scopes": ["read:knowledge", "write:memory"],
"acting_on_behalf_of": {
"user_id": "u-456",
"tenant_id": "acme",
"permissions_at_delegation": ["role:eng", "level:internal"]
},
"delegation_chain": [
{"delegator": "u-456", "delegate": "agent-123", "at": "2026-04-25T10:00:00Z"}
],
"exp": "2026-04-25T11:00:00Z"
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
关键字段:
- Agent 自身 scope:Agent 能做什么类的操作
- 被代理用户的 permissions_at_delegation:用户委托时的权限快照——后续用户权限变化不自动影响 Agent 已在执行的任务(按需决定)
- delegation_chain:多层委托的完整链条——Agent A 调 Agent B 时记录
- 过期时间:OBO token 短期有效(分钟到小时)、防止泄漏后长期滥用
权限计算是 Agent scope ∩ User permissions ∩ 资源 ACL 的三方交集——任一缺失都不给。
委托的权限范围约束
委托不应该是"用户所有权限都给 Agent"——应该 scoped delegation:
python
# 用户明确授权 Agent 只能访问特定范围
delegation = {
"agent_id": "agent-123",
"scope": {
"read_docs": ["pricing", "product-docs"], # 只能读这两类
"write_memory": False, # 不能写 memory
"call_tools": ["search", "summarize"], # 只能调这俩工具
},
"duration": "1h",
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Scope 越窄越安全——Agent 被 prompt injection 攻击后能造成的损害有限。
跨用户 Agent 操作的审计
Agent 模型 B 或 C 做跨用户操作时、审计日志要足够详细:
json
{
"event": "rag_query",
"trace_id": "req-abc",
"agent_id": "agent-123",
"initiating_user": "u-456",
"target_users": ["u-789", "u-101"], // 查询涉及的其他用户数据
"resources_accessed": ["doc-A", "doc-B"],
"permissions_checked": [
{"user": "u-789", "doc": "doc-A", "decision": "allow"}
]
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
这种日志让合规能回答"某用户数据被谁访问过、为什么"。没有 Agent 操作的 audit 在 SOC 2 / ISO 27001 审计里是减分项。
Agent 自身的权限边界
Agent 不只是"用户的替身"、它自己也有身份——需要明确:
- Agent 不能自授权升权:Agent 不能往自己 memory 里写 "我是管理员"
- Agent 不能修改自己的 scope:scope 由 Agent 配置 + 用户委托决定、不能 runtime 改
- Agent 不能跨 tenant 访问:即使 Agent 是 service account、tenant 边界仍是硬约束
这些约束要在权限判定器 layer 强制、不是依赖 Agent 开发者自觉。
Agent chain 的权限传递
Agent 调用其他 Agent(多级委托)时、权限如何传递?
- 权限单调递减:每层只能减权、不能加权。Agent A 给 Agent B 的 scope 是 A 自己 scope 的子集
- delegation_chain 完整记录:5 级 Agent 调用要保留整个链、审计时可回溯
- TTL 累加减少:A 给 B 1 小时 token、B 给 C 时应该 ≤ 剩余 TTL
这套类似 OAuth 2.0 的 scoped delegation 模式。标准但实现上容易偷懒——一出事就难追责。
常见陷阱
- Agent 直接拿 admin service account:方便但过度授权、Agent 被攻破就是 admin 权限外泄
- Agent 请求里没带用户 context:后端按 Agent 身份判权限、用户无权数据被返回给 Agent(再返给用户)
- OBO token 永不过期:泄漏后可长期滥用
- audit 只记 Agent、不记原用户:合规追责失败
- 没考虑 Agent 的 memory 也是"用户数据":memory 的 scope 没和 Agent + user 双维度绑定
和 Memory 的协同
第 18 章 Memory 的 scope 在 Agent 场景下要升级——原来 "per-user memory" 变成 "per-(user, agent) memory" 或 "per-agent memory":
- 用户在 Agent X 里说的话、不能被 Agent Y 访问
- Agent 自身的 "使用习惯 memory"(用户偏好)和 "任务上下文 memory"(当前任务进展)要分开
这种双维度 memory scope 是 Agent 产品的标配、不做好就变成 "不同 Agent 互相读到用户的秘密"。
Agent 权限的设计 checklist
上线 Agent 前自检:
- [ ] 明确选了 A / B / C 哪种模型、全团队对齐
- [ ] OBO token 结构固化、包含 delegation chain
- [ ] Agent scope 在配置文件里、不是代码写死
- [ ] 审计日志包含 agent_id + initiating_user + target_users
- [ ] 跨用户 / 跨 tenant 的访问有专门 safety check
- [ ] Agent 自己的 memory 也受 scope 约束
- [ ] 有 agent 权限事故的 runbook
这几项齐全、Agent 上线才有资格说"权限没问题"。
7.11 权限的系统性测试方法
前 10 节讲了权限的设计和传播——但代码改了怎么验证权限没坏?RAG 的权限不是单元测试能全覆盖的——涉及多层(filter、检索、生成、UI)、多维度(用户、租户、文档敏感度、时间)。系统性的权限测试是很多团队的盲区——大多数权限漏洞都是回归测试没发现的。这节给出一套专门的权限测试框架。
权限测试为什么难
- 组合爆炸:user 100 个 × doc 1 万个 × 操作 5 种 = 500 万组合、不可能穷举
- 沉默失败:权限越权没有明显症状(用户拿到了不该有的答案、可能不知道是越权)
- 多层耦合:filter 层对了但应用层 bug、或应用对了但 cache key 漏——任一层失误就出事
- 边界模糊:"同部门但跨组"这种规则、边界案例多
- 跨系统依赖:权限定义在 IAM、HR 变动触发修改、业务 DB 记特殊例外——都要同步
这些让 "permission bugs" 成为 silent killers——必须系统化工程解决、不能靠手工测。
三层测试策略
- 单元测试:权限判定函数本身(
can_access(user, doc))、边界条件覆盖 - 集成测试:完整 RAG 查询链路、验证 user A 查不到 user B 的数据
- 对抗测试:专门构造"应该不能拿到"的场景、看系统是否坚守底线
三层必须都做、缺一层就有漏洞可钻。
单元测试:权限函数本身
权限判定函数是所有权限的"基础设施"——单元测试要覆盖:
python
def test_rbac_basic():
assert can_access(admin, doc_hr) == True
assert can_access(regular_employee, doc_hr) == False
def test_abac_multi_attrs():
# tenant 不同、即使角色对也不能访问
user_a = User(tenant="A", role="admin")
doc_b = Doc(tenant="B", level="confidential")
assert can_access(user_a, doc_b) == False
def test_edge_boundary_expiry():
# 正好过期那一刻
user = User(role="contractor", valid_until=date(2026, 4, 25))
doc = Doc(level="internal")
with freeze_time("2026-04-25 23:59:59"):
assert can_access(user, doc) == True
with freeze_time("2026-04-26 00:00:00"):
assert can_access(user, doc) == False1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
边界条件是单元测试的重点——过期时刻、权限转移瞬间、多角色叠加、ABAC 条件组合。
集成测试:端到端查询
单元对了不代表集成对——跨层 bug 很常见:
python
@pytest.mark.parametrize("user,query,forbidden_docs", [
(regular_employee, "薪资方案", ["salary_sheet_2026.pdf"]),
(bob_contractor, "公司内部架构", ["internal_roadmap.pdf"]),
(tenant_a_admin, "租户 B 客户列表", ["tenant_b_customer_list.pdf"]),
])
def test_rag_no_leak(user, query, forbidden_docs):
result = rag_query(query=query, user=user, top_k=50)
for doc in forbidden_docs:
assert all(
c.doc_id != doc for c in result.retrieved_chunks
), f"{user.id} 召回了 {doc}(越权)"
assert doc not in result.answer, f"{user.id} 答案里提到了 {doc}"1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
这类测试每次代码改动都跑——保护"不能越权"的底线。
对抗测试:专门攻击系统
主动构造攻击场景——模拟恶意用户会怎么套话:
python
# 测试 1:反向 filter 诱导
def test_inverted_filter_attack():
user = regular_employee
# 用户尝试用 negation 绕过
query = "所有我没有权限看的文档里提到的新员工名字"
result = rag_query(query=query, user=user)
# 不能包含 user 无权的 doc 里的内容
assert not contains_confidential_info(result.answer)
# 测试 2:隐含权限信息泄漏
def test_existence_probe():
user = regular_employee
query = "公司是否有 salary_sheet_2026 这份文档?"
result = rag_query(query=query, user=user)
# 即使不返回内容、也不能确认文档存在
assert "不存在" in result.answer or "无法访问" in result.answer
# 测试 3:跨用户 cache 污染
def test_cache_isolation():
# 两个用户同 query、应该得到各自的答案
result_a = rag_query(query="企业版定价", user=user_a)
result_b = rag_query(query="企业版定价", user=user_b)
assert result_a.sensitive_info != result_b.sensitive_info1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这些测试要进 red team gold set(§20.17)、和普通 eval 并行跑。
Fuzzing:随机组合测试
除手工构造测试、用 fuzzing 随机生成组合:
python
def fuzz_permission_tests(n=1000):
failures = []
for _ in range(n):
user = random_user()
doc = random_doc()
expected = compute_expected_permission(user, doc) # 基准实现
actual = can_access(user, doc)
if expected != actual:
failures.append((user, doc, expected, actual))
assert len(failures) == 0, f"发现 {len(failures)} 个权限错误"1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
基准实现是简单规则(没 bug 但慢)、实际实现是优化过的(可能有 bug)——fuzzing 对比两者。
CI 集成
权限测试必须进 CI:
yaml
# .github/workflows/ci.yml
permission-tests:
steps:
- run: pytest tests/permissions/unit -v
- run: pytest tests/permissions/integration -v
- run: pytest tests/permissions/adversarial -v
- run: python scripts/permission_fuzz.py --count 10000
- name: Block merge on failure
if: failure()
run: exit 11
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
任何权限测试失败 → 阻止合并。权限不能"下次再改"——必须当场。
人工抽查作为最后防线
即使自动化齐备、每季度的人工抽查也必不可少:
- 随机抽 100 条线上请求 + 100 个用户
- 人工验证"这个用户本来应该看到什么、实际看到什么"
- 发现 gap 进 gold set 作后续回归
自动化测 "已知规则"、人工抽查发现 "未知 bug"——两者都不可少。
性能 vs 安全的权衡
全部权限测试跑一次可能几分钟甚至几十分钟——怎么平衡 CI 速度和覆盖?
- 每次 commit:单元 + 小规模集成(< 1 分钟)
- 每次 PR:完整集成 + 对抗(5-10 分钟)
- 每日 nightly:fuzzing(几十分钟)
- 每周 red team gold set:全量(1 小时)
分层跑、关键路径不慢、长尾保覆盖。
权限 bug 的典型根因
实际事故里的权限 bug 集中在几类:
- cache key 漏 user_id(§7.9 已讲)
- filter 下推没对:应用层写了 filter、但向量库 API 没应用
- 新 feature 没加权限检查:产品加了新端点、没走权限中间件
- 权限变更未传播:HR 改了、RAG 的 user_ctx 还是旧的
- 边界条件:0/null/空集合等退化情况
这五类的测试必须进自动化——不是靠人记得。
权限测试的组织责任
这不只是工程的事:
- 工程:写测试、集成 CI
- 安全 / 合规:审查测试覆盖度、提出对抗用例
- 产品:提供真实业务场景的权限边界
- 法务:对高合规场景设定测试门槛
每季度"权限测试健康 review"——跨团队参与、看覆盖率和最新风险。
7.12 Onboarding / offboarding 的权限自动化
前 11 节讲的权限模型和测试——但实际运营里、权限变化每天都在发生:新员工入职、员工离职、角色调整、外包到期。如果这些变化需要人工一处处改 RAG 的权限、容易错 / 慢 / 累。Onboarding / offboarding 的自动化是成熟 RAG 团队的 baseline——和 HR / IAM 的集成不能省。
入职和离职对 RAG 的直接影响
每个事件都触发一系列 RAG 侧操作——手工做容易漏、自动化必须。
Onboarding 的自动化
新员工第一次用 RAG、需要几件事就绪:
text
入职 Day 0(HR 系统录入):
→ 触发 `user_created` 事件
→ IAM 分配 roles
→ RAG 侧:
- 创建 user_ctx 模板(tenant_id + roles + 权限版本)
- 初始化空 memory
- 发送欢迎 prompt(可选、解释 RAG 功能)
- 预加载默认偏好(based on job title)
Day 1(员工首次登录):
→ RAG gateway 验证 JWT
→ 加载 user_ctx
→ 可用1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
这套流程完全自动化——新员工入职几分钟就能用 RAG、无需手动配置。
角色变更的处理
员工从开发工程师升到 tech lead:
- 新角色可能有新权限(能看 roadmap 等)
- 旧角色的权限不能减(tech lead 包含开发权限)
- 某些特殊权限可能移除(比如原来的 junior 培训视频访问)
自动化实现:
python
@on_event("role_changed")
async def handle_role_change(event):
old_roles = event.old_roles
new_roles = event.new_roles
# 更新 user_ctx
user_ctx.roles = new_roles
user_ctx.permission_version += 1 # bump 版本触发 cache 失效
# 通知下游
await invalidate_cache(event.user_id)
await audit_log.record({
"event": "role_change",
"user": event.user_id,
"from": old_roles,
"to": new_roles,
"by": event.actor,
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
permission_version bump 是核心——§7.9 讲过、让所有 cache 失效。
Offboarding 的时效要求
离职是最严的——合规要求严格:
| 离职类型 | 访问撤销 SLA |
|---|---|
| 正常离职 | 当天(24h 内) |
| 主动辞职 | 最后工作日即刻 |
| 被解雇 | 通知前就禁止(IT 提前操作) |
| 违规解雇 | 立即(分钟级) |
python
@on_event("user_deactivated")
async def handle_offboarding(event):
# 1. 立即禁止访问
await user_ctx_store.set_active(event.user_id, False)
# 2. 清所有 cache
await cache.delete_by_user(event.user_id)
# 3. 归档 memory(不删、留审计)
await memory_store.archive(event.user_id)
# 4. 如果是敏感场景、freeze 该用户的 RAG 日志
if event.reason == "terminated_for_cause":
await legal_hold(event.user_id)
# 5. 审计
await audit_log.record({...})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
严格 offboarding 是合规审计的必查项——每个用户的 deactivation 必须有完整审计链。
特殊用户类型
除正式员工、还有几类特殊用户:
Contractor / 外包:
- 通常有时效(合同到期日)
- 自动过期:
user_ctx.valid_until字段、到期自动失效 - 权限通常窄于正式员工
Intern / 实习:
- 短期、可能限制某些敏感数据
- 实习结束 → 和 offboarding 一样处理
外部合作者 / partner:
- 只能访问特定文档 / tenant
- 需要定期 review 访问权限
- 多因子认证必须
Service account:
- 没人类关联、长期存在
- 密钥定期轮换
- 访问要限定到特定操作
每类用户的 onboarding / offboarding 流程独立、不能混用。
HR / IAM 集成的标准接口
和外部系统集成的通用 API:
python
# Webhook 端点(供 HR 系统调用)
@app.post("/webhooks/user_event")
async def user_event_webhook(event):
# 验证签名
verify_signature(event)
# 分派到对应 handler
if event.type == "user_created":
await handle_onboarding(event)
elif event.type == "user_deactivated":
await handle_offboarding(event)
elif event.type == "role_changed":
await handle_role_change(event)
# ...1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
HR 系统(Workday / BambooHR / 飞书 HR)都支持 webhook——直接推事件过来。
事件丢失的补救
Webhook 可能丢(网络、超时)——对账机制必备:
python
@scheduled(every="1h")
async def reconcile_users():
# 从 HR 拉全量 active user 列表
hr_users = await hr_api.list_active_users()
rag_users = await user_ctx_store.list_active()
# 找差异
should_be_active = set(hr_users) - set(rag_users)
should_be_inactive = set(rag_users) - set(hr_users)
for user in should_be_active:
await handle_onboarding({"user_id": user})
for user in should_be_inactive:
await handle_offboarding({"user_id": user})
alert(f"Found inactive user in RAG: {user}") # webhook 丢过1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
每小时一次对账——兜底 webhook 漏事件。
权限变更的审计
每次变更进 audit log、合规审计时能回答:
- 2026-04-20 10:00、员工 X 的权限是什么?
- 2026-04-21 14:00、员工 X 的权限变了、谁改的、为什么?
- 谁当前有 "查看合同" 权限?
审计查询 UI:
text
[ 时间 ] [ 用户 ] [ 事件 ] [ 执行者 ]
4-20 9:00 bob user_created hr-system
4-21 14:0 bob role_changed hr-system (manager approved)
from: eng → to: eng-lead
5-01 17:0 bob deactivated hr-system (resignation)1
2
3
4
5
2
3
4
5
这个审计日志合规场景(SOC 2 / ISO 27001)必查。
内部员工 vs 客户账号
B2B / B2C 产品里、RAG 服务的"用户"可能是客户账号——不是自己员工:
- Onboarding:客户注册时自动创建
- 权限:基于 subscription tier(免费版 / pro / enterprise)
- Offboarding:客户取消订阅或违反 ToS
处理逻辑类似、但数据源从 HR 变成业务 DB、SLA 可能更严(客户对服务期望高)。
测试 Onboarding / offboarding
这套自动化要测试:
python
def test_onboarding_full_flow():
# 模拟 HR 事件
simulate_hr_event({"type": "user_created", "user_id": "test-123"})
# 等异步处理
wait_for_handler(timeout=5)
# 验证 RAG 里 user 已就绪
assert user_ctx_store.exists("test-123")
assert user_ctx_store.is_active("test-123")
def test_offboarding_cache_clear():
# 预热 cache
cache.set("user-123:query:xxx", "cached_answer")
# 触发 offboarding
simulate_hr_event({"type": "user_deactivated", "user_id": "user-123"})
wait_for_handler(timeout=5)
# 验证 cache 已清
assert cache.get("user-123:query:xxx") is None1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
End-to-end 测试在 CI 里跑、每次改代码验证不破坏流程。
常见反模式
- 手工操作:HR 告知后工程手动加权限——慢、易错
- 没对账:只信 webhook、事件漏了不知道
- Offboarding 不彻底:账号禁用了、但 cache / memory 还在
- 审计日志缺失:合规审计失败
- 特殊用户走正式流程:contractor 按员工 onboard、权限过大
自动化的 ROI
手工处理 vs 自动化:
- 手工:每个事件 15-30 分钟、20 人 = 5-10 小时 / 周
- 自动化:初期建设 2-3 人月、后续运维几乎零成本
团队规模上了百人、自动化几乎必做——手工不可持续。
合规是硬需求
GDPR / SOC 2 / ISO 27001 都要求:
- 访问权限变更及时生效
- 每次变更可审计
- 离职后访问撤销在 SLA 内
- 定期审计权限合规性
没自动化 = 合规风险。中大型公司不做 = 审计过不了。
7.13 跨组织共享:B2B 和 partner 访问
前面章节的权限默认单一组织内部——员工、客户、合约工。真实企业有更复杂的场景:和合作伙伴共享部分知识(供应商 / 咨询顾问 / 审计方)、给 B2B 客户开放 RAG(大客户带自己的员工接入)、和子公司互相授权。这些跨组织场景的权限模型不是简单的 RBAC 就能覆盖——需要专门的设计。
B2B 访问的典型场景
几种典型:
- 供应商访问:A 让供应商 B 看某些产品规格(但不是全部)
- 咨询顾问:律所 / 会计师 / 顾问获得临时访问
- B2B SaaS:A 是 SaaS、B 是客户、B 员工用 A 的 RAG
- 合资 / 子公司:互相授权部分知识
- 客户成功:A 员工代 B 员工查询(权限继承)
每种场景权限模型不同——不能一刀切。
跨组织权限的挑战
比内部权限难在哪:
- 多 IAM 对接:A / B 各有 IAM、信任基础不同
- 临时性:很多跨组织访问有时限(合同 / 项目结束)
- 追责:B 员工看了不该看的——谁负责?
- 合规区域:A 在欧盟、B 在美国、数据主权要管
- 信任边界:A 不完全信任 B、给的权限要小
每项都需要专门的工程和法务设计。
跨组织权限模型
典型设计:两层授权:
json
{
"subject": {
"external_org_id": "partner-acme",
"external_user_id": "bob@acme.com",
"role_in_own_org": "admin",
},
"granted_by": {
"my_org_admin": "alice@mycompany.com",
"scope": "read:product-specs",
"valid_until": "2026-06-30",
},
"revocable_anytime": true,
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 外部身份:由合作方的 IAM 认证(SSO / SAML 联合登录)
- 本方授权:我方明确授予的 scope、有过期
- 随时撤销:合作结束立即关
这种模型清晰——谁给谁的权限、为什么、到什么时候一目了然。
临时访问 / 过期链接
某些场景不是"给用户长期权限"、是"给特定文档一次性访问":
text
律所 Alice 发邮件:我需要看这份合同
↓
我们生成临时链接(token 包含文档 ID + 过期时间 + 使用次数)
↓
Alice 点链接、不需要注册账号、直接看(或问 RAG)
↓
链接用完或过期自动失效1
2
3
4
5
6
7
2
3
4
5
6
7
类似云盘的"分享链接"——但用于 RAG 查询。好处:
- 不用给对方创建账号
- 细粒度控制(看哪份文档)
- 天然过期
风险:链接泄漏即失控——所以要短时间有效 + 次数限制 + 可远程撤销。
SSO 联合登录(SAML / OIDC)
正式合作伙伴关系用联合登录:
- A 公司员工用 A 公司的 SSO 登录 A 的 RAG(内部)
- B 公司员工用 B 公司的 SSO 登录 A 的 RAG(B 的身份 + A 给的权限)
技术:SAML / OIDC federation——两边 IAM 建立信任关系:
- B 的用户登录 → B 的 IdP 签 JWT → A 的服务验签
- A 根据 JWT 里的 org_id 和 email 查"给 B 哪些权限"
- 返回答案
这让B 员工用自己公司的密码登录 A 的系统——体验好、安全。
合作伙伴的 onboarding
给新合作伙伴开通 RAG 访问:
每一步都有明确 deliverable 和签字——不是"口头答应给访问"。
数据主权的边界
跨组织访问涉及数据驻留:
- A 的数据物理存于中国、B 是美国公司——中国法律可能禁止某些数据出境
- A 的数据含 EU 公民 PII、B 在美国——GDPR 限制
- 合规方不同步 → 冲突
解决:
- 数据分级:不同级别的数据允许不同范围共享
- 物理隔离:敏感数据只在本国的副本、跨境只给非敏感
- 法务审核:跨境访问走法务 review、不是工程单方决定
追责和审计
跨组织的审计比内部严:
- 完整 trace:每次 B 员工的访问都记 trace、存档备合规查
- 定期 report:每月给 A 管理层一份"B 访问了什么"报告
- 异常告警:B 在非工作时间大量访问——自动告警
- 销毁记录:合作结束后、定期清理 B 的 session / cache / trace
没这套审计、合作关系一旦出事(数据泄漏 / 合规问题)——无法追溯。
合作方的 RAG 体验
合作方不一定用专门 UI——可能通过:
- API 接入:B 的产品调 A 的 RAG API、B 员工间接用
- Embed UI:A 提供一个 iframe 嵌到 B 的应用里
- SSO 直登 A 的 UI:B 员工用 A 的界面
每种模式的权限约束不同——API 模式要有 quota / rate limit、embed 模式要有 iframe 安全。
计费和 quota
B2B 跨组织访问通常计费:
- 按 query 数计费
- 按 seat 计费
- 包月订阅
实现:
- 每个 query 附
org_id——根据 org 算账单 - Quota:超过 quota 的请求被 reject
- 账单透明度:B 方能看自己当月用量
这是 B2B SaaS RAG 产品的基本功能——参考 §21.18 多租户成本归因。
撤销权限的工程
合作结束、要撤销 B 的访问:
python
async def revoke_partner_access(partner_org_id):
# 1. 标记 partner 为 deactivated
await partner_registry.deactivate(partner_org_id)
# 2. invalidate 所有该 org 的 token
await auth_service.invalidate_tokens(org=partner_org_id)
# 3. 清 cache
await cache.purge_by_org(partner_org_id)
# 4. 归档(不删)访问日志
await audit_log.archive_org_access(partner_org_id)
# 5. 通知
await send_notification(f"{partner_org_id} access revoked")1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
分钟级完成——合同结束到访问终止不应超 24 小时。
共享知识的 curation
跨组织共享的知识本身也要特别处理:
- 标记
shareable_with_partners: true的文档才能被 B 看到 - 某些敏感词 / 章节自动 redact
- 共享的数据版本和内部可能不同(外部版本简化 / 脱敏)
不是内部所有文档都适合共享——需要明确 curation 策略。
合作方投诉和 SLA
合作方对 RAG 的期望和内部员工不同:
- 付费客户要求 SLA(99.9% 可用性 + 赔偿条款)
- 合同里约定 P99 延迟 / 支持响应时间
- 出事故要有赔偿
这意味着跨组织访问的SLA 可能严于内部——资源分配上给 B 更稳的 pool(ch4 §4.11 资源隔离)。
常见反模式
- 临时访问变永久:给了链接忘了过期、几个月后还能用
- 不区分内外:对 B 员工等同内部员工对待、权限过大
- 审计日志混在一起:B 访问记录不单独存、合规审计难
- 出事找不到负责人:给权限时没记 granted_by、追责难
- 撤销不彻底:账号禁了、cache / session 还在
跨组织权限的组织责任
跨组织不只是技术——涉及多角色:
- 销售:带回合作机会、给业务目标
- 法务:谈合同、审合规
- 工程:技术对接
- 运维:权限配置 / 监控
- 安全:审查安全风险
跨组织访问的 kick-off 会必须有这五方——少一方就有遗漏。
一个真实 B2B 事故
某 SaaS 给企业客户开通 RAG、客户员工反馈"能查到别客户的数据"。调查:
- 工程用了 user_id 作 scope、忘了加 org_id
- 客户 X 的员工 A 和客户 Y 的员工 B 的 user_id 碰巧连续、引用泄漏
- 未发现的原因:cache key 含 user_id、但上游 query 结果是跨 org 的
修复:所有 scope filter 必须含 org_id、不只 user_id。测试用例里加跨 org 污染检查。
教训:跨组织的边界是工程设计的 deepest invariant——比内部权限要严得多。
投资对应规模
跨组织功能的工程投入:
- SSO 联合登录:2-4 人周
- 临时访问链接:1-2 人周
- 审计和 report:2-3 人周
- 撤销 / 清理:1-2 人周
- 合规对接:需要法务时间、几周到几月
合计 2-3 人月起步——对 B2B 产品是必需、对纯内部项目无关。
7.14 跨书关联:从搜索系统继承权限思想
RAG 的 metadata + 权限模型几乎完全继承自企业搜索(Enterprise Search)。Google Search Appliance、Elastic 的 document-level security、Solr 的 post-filter security 讨论的问题和 RAG 一样。主要差异是 RAG 多了一层"生成"——如果权限过滤不严,不仅检索结果泄漏,LLM 还会把泄漏信息"自然语言地"说出来,杀伤力倍增。
这也是为什么 Enterprise Search 的三十年经验直接可用——权限模型、RBAC/ABAC 实现、audit log 这些子系统都不用重造。RAG 做好的关键是不重造这些轮子、专注在检索 + 生成的新工程问题上。
7.15 本章小结
- Metadata 是 RAG 索引的一等公民——没有 metadata 的 chunk 只能做语义相似,做不了"谁看什么"
- 三层 metadata:身份层(不可变)、业务层(可更新)、安全层(变更强制同步)
- 权限必须索引时嵌入 + 向量库 filter 下推——检索后过滤会召回污染、泄露存在性、浪费 rerank
- RBAC + 属性过滤混合覆盖 90% 企业场景
- 多租户三档隔离:弱(共享 + tenant_id)/ 中(每租户 collection)/ 强(每租户独立实例)——按客户分级
- PII 和合规 metadata 是底线——识别、标记、脱敏、审计四件套
- user_context 在 gateway 层构造,RAG 服务只消费——避免延迟和一致性问题
下一章讨论增量索引——metadata 和权限已就位,文档变了怎么更新、删了怎么清理、schema 变了怎么迁移。