第十九章:知识库的动态更新与增量索引
19.1 为什么动态更新不能按记录增量处理
Demo 阶段的知识库往往一次性建好,但生产系统里的文档会持续变化。
RAG 更新和普通数据库更新的差异在于:文档哪怕只改一个字,也可能让 chunk 边界整体偏移,后续 chunk 的内容随之改变。
这意味着很多场景都不能按数据库记录那样"局部改一条",而要按文档或版本重建。
19.2 完整的更新链路
flowchart TB
S[数据源变化] --> D[1. 变更检测]
D --> C[2. 判断变更类型]
C -->|新增| ADD[解析 切分 向量化 写入]
C -->|修改| MOD[构建新版本 chunk 与索引]
C -->|删除| DEL[发布不含该文档的新版本]
ADD --> V[3. 一致性与权限校验]
MOD --> V
DEL --> V
V --> A[4. 原子切换 active alias]
A --> G[5. 灰度生效]
G --> M[6. 效果监控]
19.3 第一步:变更检测
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 轮询 + 内容哈希 | 定期扫描,对比文档内容的哈希值 | 实现简单、不依赖数据源改造 | 有延迟、大规模时扫描开销大 |
| 事件驱动 | 数据源变化时发消息(Webhook / 消息队列) | 实时、无扫描开销 | 需要数据源支持、需维护中间件 |
| 混合 | 事件驱动为主,定期全量对账兜底 | 可靠 | 复杂度最高 |
为什么用内容哈希而不是修改时间:很多系统的"修改时间"会因为元数据变动、重新保存、格式转换而更新,内容其实没变。用内容哈希能避免大量无谓的重建。
混合方案的必要性:事件驱动会丢消息(服务重启、网络故障、数据源没发出事件)。定期全量对账是唯一能发现"漏了什么"的手段——建议至少每天跑一次哈希对账。
19.4 第二步:版本化 Copy-on-Write + 原子 Alias
发布流程的关键约束在这里。文档开头插入一段话后,后续 chunk 的边界可能整体偏移,因而不应尝试在正在服务的索引中逐条“修补”。但“先删后增”会制造读空窗口:写入失败或索引尚未就绪时,用户既看不到旧版,也看不到新版。
正确做法是把一次文档变更作为不可变版本进行 copy-on-write:在候选版本中完整解析、切分、向量化和建索引;校验通过后,才把查询使用的 active alias(或路由指针)原子地切到新版本。旧版本保留到回滚窗口结束,再异步回收。
flowchart TB
A[doc_id: v17 active] --> B[构建隔离的 v18
解析、切分、向量化、索引]
B --> C[校验内容、ACL、召回与索引就绪]
C -->|通过| D[原子更新 active alias
v17 → v18]
C -->|失败| E[丢弃 v18,继续服务 v17]
D --> F[观察、可回滚]
F --> G[回收 v17]
这同时避免了边界偏移造成的残留旧 chunk 和删后写入的不可用窗口。查询必须在一次请求内绑定同一个已发布版本;不得让同一上下文混入两个版本的片段。合规删除另有物理擦除要求,不能只依赖延迟回收。
19.4.1 ID、版本与 Alias 设计
doc_id 用于定位文档;version_id 标识不可变构建产物;active alias 只指向一个经过验证的可查询版本。批量删除是旧版本回收和合规流程的一项能力,而不是用户查询路径上的发布机制。
推荐的元数据设计:
| 字段 | 用途 |
|---|---|
doc_id |
文档的稳定唯一标识 |
version_id |
不可变构建版本;查询与回滚的单位 |
chunk_id |
chunk 唯一标识,通常是 doc_id + version_id + 序号 |
content_hash |
变更检测与构建可复现性 |
active_alias / published_at |
原子发布、时效判断与灰度控制 |
source_path / page |
引用溯源 |
acl_tags / tenant_id |
权限过滤 |
doc_id、version_id 和 alias 解析路径都必须有索引或常数时间查找,否则回收、核验或切换会退化成全表扫描。
这个设计必须在建库时就做好——第三章 3.6.1 强调的「元数据事后补等于全量重建」,在这里体现得最直接。
19.5 第三步:删除的两个技术陷阱
19.5.1 HNSW 没有真正的删除
如第八章 8.2.1 所述,HNSW 图结构被破坏后难以修复,工程上普遍采用软删除 + 定期重建。
这会带来两个直接后果:
(1)软删除会累积。 被标记删除的向量仍占内存、仍参与图遍历、仍消耗 ef 预算,但会在结果中被过滤掉。累积到一定比例后,实际召回率会显著下降(第九章 9.6.4 的排查项之一)。
应对:监控软删除比例,超过阈值(比如 20%)触发重建。
(2)重建期间的可用性。 全量重建可能耗时数小时。始终采用构建新版本 → 校验 → 原子切换 alias,并保留旧版本供在途查询和回滚;代价是切换期间需要双份内存(第九章 9.3.3)。
19.5.2 合规删除的特殊要求
用户要求删除个人数据、或文档因合规原因必须移除时,软删除不够——数据仍然物理存在。
需要额外的流程:
- 记录删除请求,确保在下次重建后数据真正消失;
- 或使用支持真删除的索引结构(如 DiskANN 系);
- 检查关联位置:原文存储、缓存、日志、备份里的副本。只删向量而保留这些副本,合规审计仍然不通过。
19.6 第四步:一致性校验
更新完成后必须验证:
| 检查项 | 方法 |
|---|---|
| chunk 数量是否合理 | 与文档长度的比值是否在正常范围 |
| 发布版本是否完整 | version_id 的 chunk、ACL 与内容哈希均齐全 |
| Alias 是否一致 | 同一请求只解析到一个已发布版本,可原子回退 |
| 向量是否写入成功 | 抽样查询候选版本能否命中新内容 |
| 索引是否已就绪 | 候选版本进入 ANN 索引后才允许切换 alias |
很多向量库的新写入数据会先进入一个未建索引的缓冲区,要等到达到阈值或手动触发才合并进主索引。这段时间内,新数据可能检索不到,或者走的是低效路径。
这是第九章 9.6.4「召回率突然下降」排查清单里的第 2 条。
19.7 第五步:灰度与回滚
大批量更新(换 Embedding 模型、改切分策略、导入新语料)必须灰度。
flowchart LR
OLD[(旧索引)] --> R[流量路由]
NEW[(新索引)] --> R
R --> U[用户]
R -.->|对比指标| M[监控]
M -.->|异常| RB[回滚到旧索引]
做法:新旧索引并存,先切小比例流量,对比检索指标和线上反馈,确认无退化后再全量切换,并保留旧索引一段时间以便回滚。
代价是切换期需要双份存储,用来换取可回滚和对比窗口。
必须监控的对比指标:固定评测集的 Hit@K、线上点踩率、拒答率、P95 延迟。
19.8 权限隔离与多租户
这一块在企业场景里通常绕不开。
19.8.1 三种实现方式
| 方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 元数据过滤 | 每个 chunk 带权限标签,检索时按标签过滤 | 实现简单、单索引 | 踩第八章 8.4 的过滤陷阱 |
| 分区/分索引 | 每个租户或权限域独立索引 | 彻底隔离、无过滤陷阱 | 索引数量多、管理成本高 |
| 后过滤 | 检索后再按权限过滤 | 实现最简单 | 高选择性时结果可能为空;且有信息泄漏风险 |
常见做法是:租户级隔离用分索引,租户内的细粒度权限用元数据过滤。
19.8.2 三个必须注意的点
(1)后过滤有信息泄漏风险。 如果先检索再过滤,被过滤掉的结果虽然没返回,但"有结果被过滤"这个事实本身可能泄漏信息(比如返回结果数、延迟差异)。高敏感场景应使用预过滤或分索引。
(2)权限变更要能即时生效。 用户被移出某个部门后,应立即无法检索到该部门的文档。如果权限标签是写死在 chunk 元数据里的,权限变更就需要更新大量 chunk——更好的做法是存资源标识,在查询时映射到当前的权限。
(3)缓存必须带权限维度。 检索结果缓存如果不区分用户权限,会导致 A 用户的缓存被 B 用户命中——这是严重的越权漏洞。
19.9 时效性处理
同一份文档的新旧版本共存,或者知识本身有时效性时:
- 在元数据里记录生效时间和失效时间,检索时过滤掉已失效的;
- 同等相关性时按时间加权,优先返回新的;
- 在上下文中标注每个片段的日期,让模型知道时效(第十章 10.3.7);
- 定期清理过期内容——这属于知识库治理,是幻觉治理的最上游(第十七章 17.8)。
19.10 常见错误
19.10.1 在活动索引中局部更新或先删后增
chunk 边界会偏移,逐条修补会残留旧内容;先删后增又会产生读空窗口。应构建不可变新版本、验证后原子切换 alias。
19.10.2 没有 doc_id 或 doc_id 未建索引
无法批量删除,或删除操作退化成全表扫描。
19.10.3 用修改时间而非内容哈希检测变更
会触发大量无谓的重建。
19.10.4 只用事件驱动不做对账
事件会丢,必须有定期全量哈希对账兜底。
19.10.5 忽略软删除的累积
累积过多会导致召回率静默下降。
19.10.6 认为软删除满足合规要求
数据仍物理存在,且缓存、日志、备份里还有副本。
19.10.7 不检查索引是否已生效
新写入数据可能还在未建索引的缓冲区里。
19.10.8 大批量更新不灰度
发现退化时已经无法回滚。
19.10.9 权限缓存不带用户维度
会造成越权访问,是严重的安全漏洞。
19.10.10 权限标签写死在 chunk 里
权限变更需要更新大量 chunk,无法即时生效。
19.11 本章总结
- RAG 更新的特殊约束:文档改动会导致 chunk 边界整体偏移,无法局部更新;
- 完整链路:变更检测 → 分类处理 → 一致性校验 → 灰度生效 → 效果监控;
- 变更检测用内容哈希而非修改时间;事件驱动为主 + 定期全量对账兜底;
- 修改采用版本化 copy-on-write:先构建并验证完整新版本,再原子切换 alias;旧版本保留用于在途请求和回滚,随后回收;
- ID 与元数据设计要在建库时做好:
doc_id、version_id、chunk_id、content_hash、active_alias、acl_tags; - 删除语义取决于向量库实现:常见实现用 tombstone、segment compaction 或后台物理回收;需监控软删比例、验证召回,并确认合规删除最终覆盖索引、缓存与备份;
- 必须检查索引是否已生效,新数据可能还在未建索引的缓冲区;
- 大批量更新必须灰度,新旧索引并存、小流量对比、保留回滚能力;
- 权限隔离:租户级用分索引,细粒度用元数据过滤;注意后过滤的泄漏风险、权限变更的即时性、缓存必须带权限维度。
参考资料
- FreshDiskANN: A Fast and Accurate Graph-Based ANN Index for Streaming Similarity Search
- Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs
- ACORN: Performant and Predicate-Agnostic Search Over Vector Embeddings and Structured Data
- LightRAG: Simple and Fast Retrieval-Augmented Generation
- Retrieval-Augmented Generation for Large Language Models: A Survey