Skip to main content

数据隔离

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 知识问答系统中,数据隔离有几个维度:

3. DataScope 数据结构

3.1 可见性层级

这张图定义了数据可见性的层级模型——它决定了”谁能看到什么”。 三层从外到内呈同心圆嵌套关系(外层内容被内层包含): 关键的包含关系:public ⊂ internal ⊂ restricted。箭头从外指向内(“包含于”),而不是从内指向外。这个设计意味着:
  • 标记为 internal 的用户,检索时自动包含 publicinternal 的数据
  • 标记为 restricted 的用户,检索时自动包含全部三级数据
  • 不存在”只查 restricted 不查 internal”的情况——上级天然覆盖下级
为什么不是”各层级独立,用户属于哪个层级就只查哪个”? 因为在企业场景中,高层级用户(如合规审计员)查资料时,如果搜不到公司公告(public),会很困惑。嵌套模型让权限高的用户看到的信息更全,而不是更窄。 对应的 Milvus 过滤表达式(见 2.1 节代码):
  • 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 拼接成完整过滤表达式: 四个维度从上到下逐步收紧:先限定租户(最粗粒度),再限定数据集,再限定可见级别,最后检查角色。最终拼成的 Milvus 表达式类似:
为什么不用一个大而全的字段(如 access_level)把四个维度都编码进去? 因为运维场景中这四个维度的管理节奏完全不同:
  • tenant_id 几乎不变(一套部署服务一家公司)
  • dataset_id 在知识库版本更新时可能切换(从 staging 切到 production)
  • visibility 随文档敏感性逐文档设置
  • allowed_roles 随组织架构调整而增减
拆成四个独立字段后,每个维度的管理脚本可以独立运行,不需要拼一个复杂的编码规则。Milvus 的 AND 拼接天然支持这种多字段组合,性能没有额外损耗。

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() 实现

Milvus 表达式使用双引号包裹字符串值,因此只需要转义反斜杠和双引号。

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:创建场景目录和配置文件
步骤 2:维护 scenario.toml
步骤 3:编写 FAQ CSV
步骤 4:准备知识库资料 data/drawing_data/data/quality_data/data/safety_data/ 等既有 source 目录下放入 Markdown、PDF、Word、Excel 等资料。 步骤 5:执行入库
步骤 6:评测验证(可选但推荐) eval_sets/ 下补充该场景的回归样本,然后运行:
步骤 7:启动后验证 重启服务,在页面选择「工程项目资料助手」后提问新增 FAQ 或资料相关问题。维护既有场景时仍然是代码零修改,不需要改任何 Python 文件

8.4 场景配置如何影响主链路

8.5 场景边界检测

为了防止用户在 A 场景下提问 B 场景的问题(导致低质量召回),系统内置了场景边界检测。 detect_scenario_boundary() 会遍历所有已注册场景的 source_patterns,如果用户问题命中了其他场景的 source 正则,则返回提示:
工作流程
  1. 先用 score_source_matches() 计算当前场景的匹配分数。如果当前场景已有足够证据(>= CURRENT_SCENARIO_SAFE_SCORE),说明问题在本场景内有明确归属,直接放行。
  2. 遍历所有其他已注册场景,逐一计算 score_source_matches(),找到匹配分数最高的那个(best_score)。
  3. 如果最高分低于 MIN_OTHER_SCENARIO_SCORE(12 分),说明没有足够强的跨场景证据,不阻断。
  4. 否则返回 crossed=True 和匹配到的目标场景信息,由上游决定如何提示用户切换。
辅助函数 score_source_matches()score_source_map() 负责计算得分:命中 source_pattern 正则的次数乘以 10,加上匹配文本总长度,再减去 source 配置顺序的优先级偏移。这样分数高的 source 一定是”频率高、命中长、配置靠前”的那个。 这个机制保护了检索质量——用户不会因为场景选错而得到错误来源的资料。

8.6 场景配置与数据隔离的关系

场景配置定义了”这个问题应该去哪查”,数据隔离定义了”这个用户能看哪些数据”。两者叠加构成了完整的访问控制。

9. 本讲实践闭环

通过标准:同一 collection 中的数据不会跨场景、跨版本、跨租户混查。

9.1 本讲从 0 到 1 实现闭环

这一讲要实现的是“同一个 collection 里可以放多场景、多版本、多租户数据,但查询时不能混查”。实现顺序如下:
  1. 先定义 DataScope,描述一次请求允许访问的数据范围。
  2. 入库时把 tenant_iddataset_idvisibilityallowed_roles 写入每个 chunk metadata。
  3. 检索前把 DataScope 转成 Milvus expr。
  4. 最后用测试验证 expr 同时包含场景、版本、租户、数据集、source、角色条件。
实现完成后,相关代码结构应该是下面这张图: 来源:真实代码节选,见 qa_core/governance/data_scope.py
检索表达式不是简单拼 source,还要把场景、版本和权限边界都拼进去。 来源:真实代码逻辑压缩版,对应 qa_core/retrieval/filters.py::build_source_expr()
scenario_idtenant_iddataset_idvisibilityallowed_rolesDataScope.expr_clauses() 统一生成;source_filter 先过 valid_sources 白名单,再进入 Milvus expr。 入库标准化阶段必须补齐隔离字段,否则检索时再严格也查不到正确数据,或者出现跨域混查。 来源:真实代码调用点,见 qa_core/indexing/document_normalizer.py
验收时不用连接 Milvus,也可以先验证表达式字符串是否包含所有隔离条件。 来源:命令行验收,对应 tests/test_retrieval_and_prompt.py
闭环验证重点: 验收重点:数据隔离既要在入库 metadata 中存在,也要在检索 expr 中生效;只做其中一边都不完整。

10. 重点掌握

11. 本讲小结

  • DataScope 封装了一次查询的数据访问范围(租户、数据集、可见性、角色)
  • 可见性层级:public ⊂ internal ⊂ restricted,下层包含上层内容
  • 隔离字段在入库时写入每个 chunk 的 metadata,检索时拼入 Milvus 表达式
  • 双重保护:白名单校验 + 安全转义,防止表达式注入
  • 当前方案是轻量多租户方案,适合教学和演示,大规模场景需要更严格的隔离
下一讲文档入库与索引链路 — 文档加载、切分、FAQ 入库、增量清单