数据隔离
Written: 2026.06第 15 章跟敲代码:上一讲:知识库多版本管理codealong/chapters/ch15_data_isolation。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的qa_core/、scripts/等路径为准。
下一讲:文档入库与索引链路
1. 本讲目标
- 理解 RAG 系统中的数据隔离需求
- 掌握 DataScope 的结构和各字段含义
- 理解隔离字段如何拼入 Milvus 过滤表达式
- 理解轻量多租户方案的适用场景和局限性
2. 前置知识 — 多租户与数据隔离
2.1 什么是多租户
多租户(Multi-Tenancy) 是指同一个软件实例同时服务多个客户(租户),每个客户的数据必须完全隔离。2.2 RAG 系统中的隔离维度
在 RAG 知识问答系统中,数据隔离有几个维度:| 维度 | 问题 | 例子 |
|---|---|---|
| 租户隔离 | A 公司能看到 B 公司的资料吗? | tenant_id="company_a" 不应查到 tenant_id="company_b" 的数据 |
| 数据集隔离 | 生产环境和测试环境的数据混查? | dataset_id="production" 不应查到 dataset_id="test" 的数据 |
| 可见性 | 实习生能看到高管会议纪要吗? | visibility="restricted" 的内容不应被普通员工检索到 |
| 角色隔离 | HR 能看到财务数据吗? | HR 角色不应查到 allowed_roles=["finance_admin"] 的数据 |
3. DataScope 数据结构
3.1 可见性层级
这张图定义了数据可见性的层级模型——它决定了”谁能看到什么”。 三层从外到内呈同心圆嵌套关系(外层内容被内层包含):| 层级 | 典型数据 | 谁能看到 |
|---|---|---|
public | 公司公告、公开制度、产品手册 | 所有人(包括未登录用户) |
internal | 部门流程文档、操作手册、内部培训资料 | internal 用户 + restricted 用户(包含 public 内容) |
restricted | 高管会议纪要、薪酬方案、未公开合同 | 仅 restricted 用户(包含 public + internal 内容) |
public ⊂ internal ⊂ restricted。箭头从外指向内(“包含于”),而不是从内指向外。这个设计意味着:
- 标记为
internal的用户,检索时自动包含public和internal的数据 - 标记为
restricted的用户,检索时自动包含全部三级数据 - 不存在”只查 restricted 不查 internal”的情况——上级天然覆盖下级
visibility="public"→visibility in ["public"]visibility="internal"→visibility in ["public", "internal"]visibility="restricted"→visibility in ["public", "internal", "restricted"]
3.2 多维度隔离全景
这张图展示了数据隔离的完整拼图——不是只有 visibility 一个维度。 每条存入 Milvus 的 chunk 和 FAQ 都携带四个独立的隔离字段,检索时通过 AND 拼接成完整过滤表达式:| 维度 | 字段 | 解决的问题 | 典型值 |
|---|---|---|---|
| 租户隔离 | tenant_id | 不同公司/部门的数据不能互查 | "company_a", "company_b" |
| 数据集隔离 | dataset_id | 同一租户下,生产数据和测试数据不能混 | "production", "staging", "default" |
| 可见级别 | visibility | 同一数据集下,敏感资料仅限特定用户 | "public", "internal", "restricted" |
| 角色控制 | allowed_roles | 同一可见级别下,特定角色才能访问 | ["legal", "hr", "admin"] |
tenant_id几乎不变(一套部署服务一家公司)dataset_id在知识库版本更新时可能切换(从 staging 切到 production)visibility随文档敏感性逐文档设置allowed_roles随组织架构调整而增减
3.3 DataScope 的解析
4. 入库时的隔离字段
4.1 每个 chunk 的 metadata 中包含隔离信息
4.2 入库时指定数据范围
5. 检索时的过滤表达式
5.1 拼接完整过滤表达式
5.2 在前端请求中传入隔离参数
6. 安全转义
6.1 为什么要安全转义
Milvus 的过滤表达式是一个类 SQL 的字符串。如果直接把用户输入拼入表达式,存在注入风险:6.2 escape_expr_value() 实现
6.3 白名单 + 转义双重保护
7. 本方案的适用场景与局限
7.1 适用场景
- 轻量多租户:几个到几十个租户,通过 tenant_id 区分
- 教学和演示:展示多租户隔离的概念
- 企业内部:按部门、角色做数据隔离
7.2 当前局限
- 共享 Collection:所有租户的数据在同一个 Milvus Collection 中,通过表达式过滤实现逻辑隔离
- 角色过滤:
allowed_roles存储为数组,使用array_contains过滤,在小规模场景下可行 - 不是真正的多租户架构:如果扩展到数百个租户,建议使用 Milvus 的 Partition Key 功能
7.3 升级路径
如果项目需要更严格的隔离:8. 场景配置全貌 — 如何维护既有业务场景
虽然本讲的主题是数据隔离,但数据隔离和场景配置是紧密相关的。一个业务场景的完整配置决定了它的 source 白名单、数据范围、知识库版本和隔离策略。当前项目已经冻结为 8 个业务场景,一期不再新增第 9 个场景;这里重点讲清楚既有场景如何维护,以及为什么维护 source、FAQ 和资料不需要改主链路代码。8.1 场景配置的层级结构
8.2 scenario.toml 完整字段说明
以enterprise_knowledge 场景为例:
8.3 维护一个既有场景的完整步骤
假设要维护engineering_project_qa 场景,补充“图纸会审”资料。只需以下步骤:
步骤 1:创建场景目录和配置文件
data/drawing_data/、data/quality_data/、data/safety_data/ 等既有 source 目录下放入 Markdown、PDF、Word、Excel 等资料。
步骤 5:执行入库
eval_sets/ 下补充该场景的回归样本,然后运行:
8.4 场景配置如何影响主链路
8.5 场景边界检测
为了防止用户在 A 场景下提问 B 场景的问题(导致低质量召回),系统内置了场景边界检测。detect_scenario_boundary() 会遍历所有已注册场景的 source_patterns,如果用户问题命中了其他场景的 source 正则,则返回提示:
- 先用
score_source_matches()计算当前场景的匹配分数。如果当前场景已有足够证据(>= CURRENT_SCENARIO_SAFE_SCORE),说明问题在本场景内有明确归属,直接放行。 - 遍历所有其他已注册场景,逐一计算
score_source_matches(),找到匹配分数最高的那个(best_score)。 - 如果最高分低于
MIN_OTHER_SCENARIO_SCORE(12 分),说明没有足够强的跨场景证据,不阻断。 - 否则返回
crossed=True和匹配到的目标场景信息,由上游决定如何提示用户切换。
score_source_matches() 和 score_source_map() 负责计算得分:命中 source_pattern 正则的次数乘以 10,加上匹配文本总长度,再减去 source 配置顺序的优先级偏移。这样分数高的 source 一定是”频率高、命中长、配置靠前”的那个。
这个机制保护了检索质量——用户不会因为场景选错而得到错误来源的资料。
8.6 场景配置与数据隔离的关系
| 配置层 | 作用 | 数据隔离维度 |
|---|---|---|
valid_sources | 限制用户可选的 source | source 级过滤 |
faq_collection / doc_collection | 每个场景独立集合 | 物理级隔离 |
source_patterns | 自动推断 source | 意图驱动的过滤 |
DataScope (tenant/dataset/visibility/role) | 同一场景内的进一步隔离 | 逻辑级隔离 |
9. 本讲实践闭环
| 项目 | 内容 |
|---|---|
| 本讲类型 | 工程治理 |
| 实践产物 | DataScope、租户/数据集/可见性/角色过滤表达式 |
| 是否进入最终项目 | 是 |
| 验收方式 | 生成 Milvus expr,确认包含 scenario、kb_version、tenant、dataset、source 等条件 |
| 后续落点 | 第 8 讲检索过滤,第 16 讲入库 metadata 标准化 |
9.1 本讲从 0 到 1 实现闭环
这一讲要实现的是“同一个 collection 里可以放多场景、多版本、多租户数据,但查询时不能混查”。实现顺序如下:- 先定义
DataScope,描述一次请求允许访问的数据范围。 - 入库时把
tenant_id、dataset_id、visibility、allowed_roles写入每个 chunk metadata。 - 检索前把
DataScope转成 Milvus expr。 - 最后用测试验证 expr 同时包含场景、版本、租户、数据集、source、角色条件。
qa_core/governance/data_scope.py。
qa_core/retrieval/filters.py::build_source_expr()。
scenario_id、tenant_id、dataset_id、visibility、allowed_roles 由 DataScope.expr_clauses() 统一生成;source_filter 先过 valid_sources 白名单,再进入 Milvus expr。
入库标准化阶段必须补齐隔离字段,否则检索时再严格也查不到正确数据,或者出现跨域混查。
来源:真实代码调用点,见 qa_core/indexing/document_normalizer.py。
tests/test_retrieval_and_prompt.py。
| 验证项 | 验证方式 | 期望结果 |
|---|---|---|
| 入库 metadata | 查看 chunk metadata | 有 tenant/dataset/visibility/roles |
| 检索 expr | 单测检查字符串 | 包含场景、版本、租户、数据集 |
| source 白名单 | 传入非法 source | 被拒绝或忽略 |
| 角色隔离 | 切换 user_roles | 只能看到允许数据 |
| 场景隔离 | 切换 scenario | 不跨场景召回 |
10. 重点掌握
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | DataScope 的四维隔离结构:tenant_id(租户)、dataset_id(数据集)、visibility(可见性)、user_roles/allowed_roles(角色) | 数据隔离的完整模型 |
| ★★★ 必会 | 可见性层级嵌套关系:public ⊂ internal ⊂ restricted,上层用户自动覆盖下层内容 | 企业权限管理的常见模型 |
| ★★★ 必会 | 隔离字段写入 chunk metadata + 检索时拼入 Milvus expr 实现逻辑隔离 | 数据隔离的核心实现方式 |
| ★★ 理解 | 白名单校验 + 安全转义(escape_expr_value)的双重保护 | 防止表达式注入的关键设计 |
| ★★ 理解 | 场景配置(scenario.toml)的结构:valid_sources、faq/doc_collection、source_patterns 等 | 理解如何维护业务场景 |
| ★★ 理解 | 场景边界检测(detect_scenario_boundary):当问题明显属于其他场景时给出提示 | 防止选错场景导致低质量召回 |
| ★ 了解 | 轻量多租户方案的适用场景和局限性(数十个租户 vs 数百个租户需升级) | 了解设计边界 |
| ★ 了解 | source 自动推断(从 scenario.toml 的 source_patterns 匹配) | 回顾第 4 讲内容 |
11. 本讲小结
- DataScope 封装了一次查询的数据访问范围(租户、数据集、可见性、角色)
- 可见性层级:public ⊂ internal ⊂ restricted,下层包含上层内容
- 隔离字段在入库时写入每个 chunk 的 metadata,检索时拼入 Milvus 表达式
- 双重保护:白名单校验 + 安全转义,防止表达式注入
- 当前方案是轻量多租户方案,适合教学和演示,大规模场景需要更严格的隔离

