第14讲:知识库多版本管理¶
上一讲:应用入口与环境前置校验 下一讲:数据隔离与多租户设计
本讲目标¶
- 理解为什么 RAG 系统需要知识库版本管理
- 掌握版本状态机的设计(STAGED → ACTIVE → ARCHIVED)
- 理解版本切换的实现(O(1) 操作,不批量更新 Milvus)
- 理解版本号生成的设计(时间戳 + 配置哈希)
- 理解入库质量报告/质量门禁为什么是版本激活前置条件
本讲地图¶
本图对应本讲功能闭环,展示从输入到本讲交付物的主干路径。节点与主项目代码文件和函数保持一致,后续章节消费的能力只作为交付边界出现。
图 1:第 14 讲功能闭环地图¶
flowchart TD
C14_GEN["版本号<br/>generate_kb_version()"]
C14_COMMON["公共时间工具<br/>utc_now() / utc_file_stamp()"]
C14_MODEL["版本对象<br/>KnowledgeBaseVersion"]
C14_MYSQL["MySQL 存储基类<br/>_MySqlStore"]
C14_STORE["版本仓库<br/>KnowledgeBaseVersionStore"]
C14_ENSURE["确保版本<br/>ensure_version()"]
C14_RESULT["入库记录<br/>record_ingest_result()"]
C14_ACTIVE["激活归档<br/>activate_version() / archive_version()"]
C14_API["版本 API<br/>list/activate/archive payload"]
C14_QUERY{{"在线接入<br/>active_kb_version"}}
C14_COMMON --> C14_GEN
C14_GEN --> C14_MODEL
C14_MYSQL --> C14_STORE
C14_MODEL --> C14_STORE
C14_STORE --> C14_ENSURE
C14_ENSURE --> C14_RESULT
C14_RESULT --> C14_ACTIVE
C14_ACTIVE --> C14_QUERY
C14_STORE --> C14_API
C14_API --> C14_ACTIVE
style C14_GEN fill:#F8FAFC,stroke:#64748B,stroke-width:2px
style C14_COMMON fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C14_MODEL fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C14_MYSQL fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C14_STORE fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C14_ENSURE fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C14_RESULT fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C14_ACTIVE fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C14_API fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C14_QUERY fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
节点与代码对齐¶
| 节点 | 对齐文件 | 函数/对象 | 本章职责 |
|---|---|---|---|
| 版本号 | qa_core/governance/kb_versions.py |
generate_kb_version() |
生成带场景和时间信息的知识库版本号。 |
| 公共时间工具 | qa_core/common.py |
utc_now() / utc_file_stamp() |
统一版本时间和文件时间戳格式。 |
| 版本对象 | qa_core/governance/kb_versions.py |
KnowledgeBaseVersion |
保存版本状态、描述、时间和入库统计。 |
| MySQL 存储基类 | qa_core/memory/base.py |
_MySqlStore |
提供 SQLAlchemy 引擎和安全表名校验。 |
| MySQL Schema 初始化 | qa_core/storage/mysql_schema.py |
create_kb_version_tables() |
集中维护版本表和 active 指针表的 DDL。 |
| 版本仓库 | qa_core/governance/kb_versions.py |
KnowledgeBaseVersionStore |
读取和写入 MySQL 版本表与 active 指针。 |
| 确保版本 | qa_core/governance/kb_versions.py |
ensure_version() |
入库时创建或复用 STAGED 版本。 |
| 入库记录 | qa_core/governance/kb_versions.py |
record_ingest_result() |
记录 doc/faq 数量和 source。 |
| 激活归档 | qa_core/governance/kb_versions.py |
activate_version() / archive_version() |
控制当前在线检索可见版本。 |
| 版本 API | qa_core/api/kb_versions.py |
list/activate/archive payload |
提供轻量版本管理接口。 |
| 在线接入 | qa_core/pipeline/runtime.py |
active_kb_version |
查询时把版本写入 RAGQueryContext。 |
第一部分:前置知识 — 为什么 RAG 需要版本管理¶
1.1 不加版本管理的风险¶
假设你用一个脚本把 500 个业务文档写入 Milvus。运行完毕后:
版本管理的核心价值:让知识库的更新成为可逆操作。
版本管理还承担一个发布边界:新版本写入 Milvus 只是进入 STAGED,不等于可以上线。上线前必须有质量依据,当前项目通过入库质量报告和质量门禁决定是否允许激活。
1.2 版本管理的典型需求¶
| 需求 | 说明 |
|---|---|
| 安全入库 | 新版本先写入,不影响线上正在使用的版本 |
| 灰度验证 | 新版本入库后先评测,确定没问题再切换 |
| 快速回滚 | 新版本效果不好,一键切回旧版本 |
| 对比评测 | 同一个问题可以分别在新旧版本上验证召回效果 |
| 长期保留 | 历史版本不删除,作为 A/B 测试和故障分析的依据 |
第二部分:版本状态机¶
2.1 三种状态¶
stateDiagram-v2
[*] --> STAGED : 入库完成
STAGED --> ACTIVE : 激活版本<br/>仅更新 MySQL active 指针
STAGED --> ARCHIVED : 直接归档<br/>从未激活的版本
ACTIVE --> STAGED : 回滚/新版本激活<br/>旧 ACTIVE 转为 STAGED
ACTIVE --> ARCHIVED : 长期不用后归档
ARCHIVED --> [*] : Milvus 数据保留<br/>可手动清理
note right of ACTIVE
在线检索表达式:
kb_version == "active_version"
同一场景只有一个 ACTIVE
end note
note right of STAGED
已写入 Milvus
评测验证中
用户检索不可见
end note
安全入库与激活流程¶
flowchart TD
Start(["执行 rebuild_kb_version.py"]) --> Create["1️⃣ 创建 STAGED 版本<br/>版本号含时间戳+配置哈希"]
Create --> Ingest["2️⃣ 入库存入 STAGED<br/>FAQ 入库 + 文档入库<br/>写入 kb_version=新版本号"]
Ingest --> Report["3️⃣ 生成入库质量报告<br/>解析失败/低质量chunk/<br/>FAQ空值/冲突检测"]
Report --> Gate{"4️⃣ 入库质量门禁"}
Gate -->|"✅ 通过"| Activate["5️⃣ 激活版本<br/>更新 MySQL active 指针<br/>旧 ACTIVE 降为 STAGED<br/>Milvus 数据不更新"]
Gate -->|"❌ 不通过"| Abort["❌ 终止激活<br/>STAGED 版本保留<br/>线上仍用旧 ACTIVE<br/>用户无感知"]
Activate --> Online["✅ 新版本上线<br/>下一次检索自动使用<br/>expr: kb_version==新版本号"]
style Create fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
style Gate fill:#FFFBEB,stroke:#D97706,stroke-width:2px
style Activate fill:#ECFDF5,stroke:#059669,stroke-width:2px
style Abort fill:#FEF2F2,stroke:#DC2626,stroke-width:2px
style Online fill:#ECFDF5,stroke:#059669,stroke-width:3px
- STAGED:版本已写入 Milvus,但线上检索不使用。通常用于新入库的版本,等待评测验证。
- ACTIVE:当前在线检索使用的版本。同一场景只有一个 ACTIVE 版本。
- ARCHIVED:不再使用的历史版本。数据和 Milvus chunk 都保留,但不参与在线检索。
2.2 质量门禁与状态转换¶
第 14 章需要讲清一个边界:activate_version() 本身只负责切换 MySQL active 指针;它不判断资料质量。资料质量由 scripts/rebuild_kb_version.py --quality-gate --activate 串起来:先生成入库质量报告,再执行门禁,门禁通过才调用 activate_version()。
质量报告在本章只作为“是否允许激活”的前置条件出现,不展开检测算法。具体检查项,例如解析失败、低质量 chunk、FAQ 空值、重复问题和 FAQ/文档冲突,放在第 17 讲系统展开。
下面的代码展示的是步骤 5(激活)和归档操作。步骤 1(创建版本)见 ensure_version(),步骤 2(入库写入)见第 16 讲,步骤 3-4(质量报告和门控)见第 17 讲。
2.3 激活操作的轻量性¶
关键设计:激活版本只更新 MySQL 中的一行 active 指针,不碰 Milvus。
如果激活需要修改所有 chunk 的 metadata,一个 10 万条 chunk 的知识库需要很长时间。通过把版本切换放在检索表达式中,版本切换变成了 O(1) 操作。
2.4 为什么采用 STAGED 候选版本 + active 指针切换¶
多版本入库不要理解成“把资料重新写一遍”,而要理解成一次知识库发布流程。新资料可能出现解析失败、切分异常、FAQ 口径冲突、source 配错、召回退化等问题。如果直接覆盖线上向量数据,一旦新版本质量有问题,用户会立刻受到影响,而且很难快速回滚。
本项目采用的是控制面和数据面分离:
| 层次 | 保存内容 | 主要职责 |
|---|---|---|
| Milvus 数据面 | 所有版本的 FAQ 向量和文档 chunk 向量 | 保存可检索数据,不负责判断哪个版本上线 |
| MySQL 控制面 | kb_versions、kb_active_versions |
保存版本状态、active 指针和 previous 指针 |
| 在线检索表达式 | kb_version == active_version |
让查询只命中当前 active 版本 |
所以完整业务逻辑是:
注意两个边界:
- 写入 Milvus 不等于上线:STAGED 版本已经在 Milvus 中存在,但线上检索不会命中它,因为线上检索只查 active
kb_version。 - 质量报告不等于质量门禁:质量报告负责记录事实,例如失败文件、重复 FAQ、低质量 chunk;质量门禁负责根据阈值决定是否阻断激活。
这种设计的好处是:
| 问题 | 直接覆盖式入库 | 多版本发布式入库 |
|---|---|---|
| 新资料有问题 | 线上立即受影响 | STAGED 阶段被拦截 |
| 需要回滚 | 很难恢复旧向量 | 切回 previous active 指针 |
| 发布速度 | 可能要修改大量向量数据 | 只更新 MySQL active 指针 |
| 排查问题 | 不知道哪次入库引入问题 | 每个版本有独立版本号、报告和统计 |
| 多场景管理 | 容易互相影响 | 每个场景独立 active 指针 |
企业项目中也经常做跨版本增量构建。当前项目采用的是物理复制式增量:未变化文件不重新 embedding,但会把旧 chunk 和 dense 向量复制到新版本,这样新版本仍然是一个完整快照,线上查询只需要过滤 kb_version == active_version。
大规模企业知识库还可以升级为引用式增量:未变化 chunk 不复制,而是用 valid_from_seq / valid_to_seq 描述 chunk 在哪些版本中有效。两种方案的取舍如下:
| 方案 | 查询方式 | 优点 | 代价 |
|---|---|---|---|
| 物理复制式增量 | kb_version == active_version |
查询简单、版本完整、适合中小规模 | 未变化 chunk 会重复占用空间 |
| 引用式增量 | valid_from_seq <= active_seq 且未失效 |
节省空间、适合百万级 chunk | 检索表达式、回滚、质量报告和 GC 都更复杂 |
本课程主项目选择物理复制式增量,是为了先把“候选版本入库 → 质量门禁 → active 指针切换 → 回滚”的主线讲清楚。引用式增量作为企业规模化优化方向,在第 16 讲入库章节补充理解。
第三部分:版本号设计¶
3.1 版本号生成¶
3.2 为什么版本号包含配置哈希¶
设计意图:从版本号可以直接判断两个版本是否使用同一套配置。
如果两个版本的 hash 相同但日期不同,说明是同一套配置下的数据更新(新增/修改了文档)。 如果 hash 不同,说明 Embedding 模型、Reranker 模型或 Chunk 方案有变化,需要重点关注召回质量的对比。
第四部分:MySQL 版本控制面¶
4.1 表结构¶
当前项目把知识库版本控制面保存到 MySQL,而不是本地 JSON 文件。核心是两张表:
| 表 | 作用 |
|---|---|
kb_versions |
保存每个版本的状态、模型配置快照、collection、入库统计 |
kb_active_versions |
保存每个场景当前 active 版本和 previous 版本 |
kb_versions 的关键字段:
| 字段 | 说明 |
|---|---|
scenario_id |
业务场景 |
kb_version |
知识库版本号 |
status |
STAGED / ACTIVE / ARCHIVED |
doc_collection / faq_collection |
对应 Milvus collection |
embedding_model_version / reranker_model_version / chunk_schema_version |
入库配置快照 |
sources_json / stats_json |
已入库 source 和统计信息 |
kb_active_versions 的关键字段:
| 字段 | 说明 |
|---|---|
scenario_id |
业务场景 |
active_kb_version |
在线检索默认使用的版本 |
previous_kb_version |
上一个 active 版本,用于快速回滚 |
表结构创建逻辑集中在 qa_core/storage/mysql_schema.py:
KnowledgeBaseVersionStore.ensure_tables() 只调用 schema 初始化函数,再做版本状态校准。这样表结构初始化和版本状态机读写分开:
| 模块 | 职责 |
|---|---|
qa_core/storage/mysql_schema.py |
维护 MySQL 控制面表结构和幂等 schema 补齐逻辑 |
qa_core/governance/kb_versions.py |
负责版本状态机、active 指针、激活、归档和回滚 |
scripts/rebuild_kb_version.py |
负责编排入库、质量门禁,通过后才激活版本 |
当前项目暂不引入 Alembic。课程一期先讲清控制面/数据面分离和 active 指针切换;生产环境如果需要严格版本化 DDL,可以把 mysql_schema.py 中的表结构迁移成 Alembic revisions。
4.2 KnowledgeBaseVersionStore 类¶
第五部分:与 Milvus 检索的集成¶
5.1 写入时携带版本信息¶
每条 FAQ 和 chunk 入库时,这些字段都会被写入 metadata:
5.2 检索时过滤版本¶
5.3 评测用历史版本¶
第六部分:全量重建的安全流程¶
这一部分是第 14 章需要和第 16、17 章衔接的地方:第 16 章负责把资料写入新版本,第 17 章负责解释质量报告如何检查;第 14 章负责解释为什么质量报告不通过时不能激活版本。
执行顺序:
对应真实代码在 scripts/rebuild_kb_version.py:
这段逻辑表达的是:版本已经入库不代表可以上线;只有质量门禁通过,才允许切换 active 指针。
关键安全点:即使新的 STAGED 版本已经写入了 Milvus,只要没有执行激活步骤,线上检索仍然使用旧的 ACTIVE 版本。用户完全无感知。
本讲实践闭环¶
| 项目 | 内容 |
|---|---|
| 本讲类型 | 工程治理 |
| 实践产物 | KnowledgeBaseVersionStore、版本状态机、激活和回滚能力、激活前质量门禁边界 |
| 是否进入最终项目 | 是 |
| 验收方式 | 创建 staged 版本,生成入库质量报告,门禁通过后激活为 active,再归档旧版本 |
| 后续落点 | 第 16 讲入库生成版本,第 17 讲质量门禁控制激活 |
通过标准:任意时刻每个场景只有一个 active 版本,版本可追踪、可回滚;质量门禁失败时不会切换线上版本。
本讲从 0 到 1 实现闭环¶
这一讲的核心是把“重建知识库”变成可追踪、可回滚的发布动作。实现顺序如下:
stateDiagram-v2
[*] --> STAGED: 新建版本并入库
STAGED --> ACTIVE: 质量门禁通过 + activate
ACTIVE --> ARCHIVED: 新版本激活后旧版本归档
ARCHIVED --> ACTIVE: 回滚到历史版本
STAGED --> ARCHIVED: 构建失败或废弃
- 先定义版本状态:
STAGED、ACTIVE、ARCHIVED。 - 再实现版本清单 store,把每个场景的版本列表和 active 指针持久化。
- 然后在入库脚本里先创建 staged 版本,入库后生成质量报告。
- 质量门禁通过后再激活,失败则保留 staged 且线上仍用旧 active。
- 最后保证同一个场景任意时刻只有一个 active 版本。
实现完成后,相关代码结构应该是下面这张图:
flowchart LR
VersionStore["qa_core/governance/kb_versions.py<br/>版本记录/状态机/active 指针"] --> Rebuild["scripts/rebuild_kb_version.py<br/>创建 STAGED<br/>质量通过后激活"]
VersionStore --> Filters["qa_core/retrieval/filters.py<br/>按 active kb_version 检索"]
VersionStore --> Admin["qa_core/api/kb_versions.py<br/>版本查询/管理接口"]
Rebuild --> Reports["reports/<br/>构建与质量报告"]
来源:真实代码逻辑压缩版,对应 qa_core/governance/kb_versions.py::KnowledgeBaseVersion。
激活版本不是改 Milvus 数据,而是修改 MySQL 控制面里的 active 指针。这样切换速度快,也能随时回滚。
来源:真实代码逻辑压缩版,对应 qa_core/governance/kb_versions.py::activate_version()。
注意:真实代码激活新版本时,旧 ACTIVE 会降为 STAGED,不是自动归档为 ARCHIVED。归档是单独的 archive_version() 操作,并且不能归档当前 active 版本。
入库时,每个 chunk 都要写入 scenario_id 和 kb_version。线上检索通过 Milvus expr 只查当前 active 版本。
来源:真实代码调用点,见 qa_core/governance/kb_versions.py 和 qa_core/retrieval/filters.py。
验收时先创建 staged,再激活,再检查旧 active 是否归档。
来源:命令行验收,对应 scripts/rebuild_kb_version.py。
Docker Compose 模式下执行前,先确认项目根目录已经存在 .env.compose。
闭环验证重点:
| 验证项 | 验证方式 | 期望结果 |
|---|---|---|
| 新建版本 | 执行 --new-version |
生成 STAGED 版本 |
| 入库质量报告 | 默认生成,或显式使用 --quality-gate |
报告写入 reports/ingestion/,包含当前 kb_version |
| 激活版本 | 加 --quality-gate --activate |
门禁通过后新版本变 ACTIVE |
| 旧版本转为 STAGED | 多次激活 | 旧 ACTIVE 变 STAGED,保留回滚能力 |
| 检索过滤 | 查看 expr | 包含当前 kb_version |
| 回滚能力 | 指定历史版本激活 | active 指针切回历史版本 |
| 手动归档 | 调用 archive | 非 active 版本变 ARCHIVED |
验收重点:知识库更新必须可追踪、可回滚,不能直接覆盖线上数据。
重点掌握¶
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | 版本状态机:STAGED(已入库待验证)→ ACTIVE(在线检索使用)→ ARCHIVED(归档保留) | 知识库版本管理的核心模型 |
| ★★★ 必会 | O(1) 版本切换:激活只更新 MySQL active 指针,不更新 Milvus 数据 | 理解版本切换为什么是即时的 |
| ★★★ 必会 | 控制面 / 数据面分离:Milvus 保存所有版本数据,MySQL 控制哪个版本上线 | 理解为什么 STAGED 数据不会污染线上 |
| ★★★ 必会 | 版本号设计:kb_{scenario_id}_{timestamp}_{config_hash},配置哈希体现版本差异 |
从版本号直接判断配置是否变化 |
| ★★ 理解 | resolve_active_version() 的三级优先级:请求传入 > 环境变量 > MySQL active 指针 | 理解版本解析流程 |
| ★★ 理解 | version_metadata() 将版本信息写入每个 chunk 的 metadata,检索时通过 expr 过滤 | 写入和检索的版本关联机制 |
| ★★ 理解 | 安全入库流程:创建 STAGED → 入库 → 生成质量报告 → 质量门禁 → 激活 | 生产环境的完整安全操作 |
| ★★ 理解 | 质量报告记录事实,质量门禁给出是否允许激活的结论 | 避免把“发现问题”和“阻断上线”混成一个概念 |
| ★★ 理解 | 质量报告在第 14 章是版本激活前置条件,具体检查算法在第 17 章展开 | 避免把版本治理和质量评估混成一章 |
| ★ 了解 | 跨版本增量构建仍然产出完整的新 kb_version | 理解企业项目如何减少重复 embedding,同时保持版本确定性 |
| ★ 了解 | 评测时可以显式指定历史版本做对比 | 了解版本管理的扩展用途 |
本讲小结¶
- 版本管理让知识库更新成为可逆操作:入库 → 评测 → 激活 →(效果不好)→ 回滚
- 版本状态机:STAGED(待验证)→ ACTIVE(在线使用)→ ARCHIVED(归档保留)
- 版本切换是 O(1) 操作:只更新 MySQL 中的 active 指针,不更新 Milvus 数据
- 控制面和数据面分离:Milvus 保存各版本数据,MySQL 决定哪个版本在线
- 版本号 = 时间戳 + 配置哈希,可以肉眼判断先后和配置差异
- 每个场景独立 active 指针,不同行业场景可以独立管理知识库版本
- 入库质量报告是激活前置条件:报告或门禁失败时,新版本保持 STAGED,线上仍使用旧 ACTIVE
- 质量报告记录问题,质量门禁决定是否激活,两者职责不能混淆
- 增量构建也要产出完整版本,线上始终只查一个 active
kb_version - 评测脚本可以显式指定历史版本,实现新老版本对比
下一讲:数据隔离与多租户 — 租户/数据集/角色隔离、Milvus 表达式过滤