知识库文档更新或删除后,片段和向量怎么保持一致?
简化版
一篇文档会派生出解析文本 → 片段 → 向量三层数据,上游任何一层变了,下游都要作废重建:换文件、重新解析、重新切分、改了用于过滤的元数据,都要清掉旧片段和旧向量。业务库和向量库分开存时,一个事务管不到两边,所以要安排删除顺序:先删向量、再删片段,出错时最多「少一条向量、重新向量化能补回」,而不会「片段没了、向量还在,检索命中一段不存在的资料」。另外两条底线:删除失败不能吞异常;先确认新数据能生成,再清旧数据。
详细版
文档(原文件、标题、分类/城市等元数据)
└─ 解析文本
└─ 片段(chunk)
└─ 向量(embedding)
| 上游发生的变化 | 要作废什么 | 做法 |
|---|---|---|
| 换了原文件 | 解析文本、片段、向量 | 清空三层,状态退回「待解析」 |
| 重新解析 / 解析失败 | 片段、向量 | 解析失败也要清,旧片段已不对应当前文件 |
| 重新切分 | 片段、向量 | 先删向量再删片段,再写新片段 |
| 修改片段内容 | 这条片段的向量 | 删向量或置空,等重新向量化 |
| 修改过滤用的元数据 | 冗余了这些字段的片段和向量 | 清掉重来,否则过滤条件对不上 |
| 删除文档 / 知识库 | 下面所有层 | 自下而上:向量 → 片段 → 文档 → 知识库 |
三条原则:
- 自下而上删:先删最下游(向量),再删上游,任何一步失败都只会留下「可以重建」的缺口。
- 失败要暴露:删除向量失败必须报错,不能静默跳过。
- 先生成后替换:新文本解析成功、新向量生成成功,再删旧的;失败时旧数据保留,检索不会突然少一块。
完整版教学
一、为什么「改了文档」会让检索出错
RAG 检索命中的是片段和向量,不是原文件。只要派生数据没跟着更新,检索看到的就是旧世界:
售后政策 v1:「7 天无理由退货」 → 切出片段 #12,向量 V12
管理员上传 v2:「15 天无理由退货」
只重新切了片段(#12 被删,新增 #31),忘了删向量 V12
用户问「多少天能退?」
→ 向量检索命中 V12(相似度 0.83),V12 指向的片段 #12 已不存在
→ 要么拼接上下文时报错,要么把 V12 里冗余存的旧正文「7 天」交给模型
最后一种最危险:页面不报错,回答却是过期规则。这类问题只有翻引用来源才发现,所以一致性必须靠流程保证,而不是靠人记得。
易错点:向量库里常常冗余存了片段正文,方便检索后直接拼上下文。冗余越多,没清干净时「看起来正常、其实是旧数据」的风险越大。
二、先画出派生关系,再决定「谁变了清谁」
把一篇文档的数据看成一条依赖链,规则就很清楚:某一层变了,它下游的所有层都要作废。
| 层 | 从哪来 | 谁依赖它 |
|---|---|---|
| 原文件 | 用户上传 | 解析文本 |
| 解析文本 | 解析原文件 | 片段 |
| 片段 | 切分解析文本 | 向量、引用来源 |
| 向量 | 向量化片段 | 检索 |
容易漏的是元数据。如果检索前要按城市、知识库、文档类型收窄,而这些字段又冗余存进了片段表和向量表(为了检索时不联表),那么文档的元数据一改,片段上的冗余值就过期了:按新城市过滤搜不到它,按旧城市过滤反而命中。所以「修改用于过滤的元数据」和「换文件」一样,要作废片段。
三、跨库删除:为什么顺序比事务更重要
很多项目把业务数据放在 MySQL、向量放在 PostgreSQL + PGVector 或专用向量库。一个本地事务只管一个数据库连接,两边无法一起提交或回滚。这时就要比较两种顺序出错后的结果:
| 顺序 | 第二步失败时的状态 | 后果 |
|---|---|---|
| 先删向量,再删片段 | 片段还在,向量没了 | 这条片段暂时检索不到,重新向量化即可补回 |
| 先删片段,再删向量 | 片段没了,向量还在 | 检索命中「孤儿向量」,引用一段已不存在的资料 |
第一种是可恢复的缺口,第二种是会被用户看到的错误。所以结论是:先删下游(向量),再删上游(片段)。
同样的道理也适用于「在 MySQL 事务里删旧片段、写新片段,中间顺手删了向量库的旧向量」:MySQL 回滚能把片段恢复原样,但向量已经删掉、回滚不了。结果是片段还在、向量缺失,属于可恢复的状态,不需要为此引入分布式事务。
四、删除失败为什么不能吞掉
一段常见的「防御式」写法:
try {
vectorStore.deleteBySegmentId(id);
} catch (Exception ignored) {
// 删不掉就算了,不影响主流程
}
segmentMapper.deleteById(id);
它让主流程「看起来更稳」,实际上制造了孤儿向量:向量删除失败被吞掉,片段照样删了,之后每次检索都有可能命中这条向量。正确做法是删除失败直接抛出,让这次操作整体失败、页面提示出来,宁可当场报错,也不能悄悄留下脏数据。
记忆钩子:删除路径上的异常要「大声」,写入路径上的失败要「留痕」。
五、「先清后建」与「先建后替」
重建数据有两种顺序:
先清后建:删旧片段/旧向量 → 解析/切分/向量化 → 写新数据
失败时:旧的已经没了,新的没生成,这篇文档暂时检索不到
先建后替:解析/切分/向量化 → 成功后删旧数据 → 写新数据
失败时:旧数据还在,检索照常,只是内容没更新
能先建后替就先建后替,至少做到把会失败的步骤放在清理之前:原文读不出来就不要动旧片段;向量模型调用失败就不要删旧向量;批量重建前先确认向量模型配置可用,而不是清空了才发现 Key 没填。
还有一个容易被忽略的细节:失败状态要能落库。如果把「写失败状态 → 抛异常」包在同一个数据库事务里,事务回滚会把刚写的「解析失败」「向量化失败」一起撤掉,页面上永远看不到失败标记。需要保留失败状态的方法,不能整段包在事务里。
六、批量删除要复用单条删除
删除知识库或批量删除文档时,最省事的写法是一条 delete ... where id in (...) 删掉文档表。但单条删除里带着「先删向量、再删片段、再删文件」的整套级联清理,一条 SQL 只删了最上层,下面几层全成了孤儿。
错误:delete from document where id in (1,2,3) → 片段、向量都留下了
正确:for id in [1,2,3]: deleteDocument(id) → 每篇都走完整清理
批量操作逐条调用单条删除,性能稍差,但保证「删一条」和「删一批」的效果完全一样。数据量大时可以把每一层改成按文档 ID 集合批量删,但顺序不能变。
七、从覆盖式重建到增量更新
上面的做法都属于覆盖式重建:一篇文档变了,就把它的片段和向量整体删掉重来。它简单可靠,适合文档量不大、更新不频繁的知识库。
文档量上来之后,就要考虑增量:给片段稳定 ID 和内容哈希,只重算内容变化的片段;删除用墓碑标记,查询时过滤;换切分策略或 Embedding 模型时建新索引、回填完成后切换。这部分见「RAG 索引如何增量更新?」。
八、常见误区与追问
- 误区:删了文档记录,派生数据以后再清也行。 残留的片段和向量会被检索命中,回答引用不存在的资料,必须同一次操作里清理干净。
- 误区:跨库操作一定要上分布式事务。 安排好「先删下游、再删上游」的顺序,出错时只留下可重建的缺口,多数场景不需要分布式事务。
- 误区:删除向量失败可以忽略。 失败后片段已删、向量还在,就成了孤儿向量,删除失败必须抛出。
- 误区:重新解析前先把旧片段清掉最干净。 解析失败时旧片段也没了,应该解析成功后再清旧数据。
- 误区:只有换文件才需要重建。 修改用于检索过滤的元数据,同样会让冗余在片段上的字段过期。
- 追问:为什么失败状态有时写不进数据库? 写失败状态和抛异常在同一个事务里,异常触发回滚,把失败状态一起撤掉了。
- 追问:批量删除为什么不直接写一条 in 语句? 单条删除里有级联清理,批量必须复用它,否则下层数据会残留。
九、加强记忆
一篇文档派生出解析文本、片段、向量三层,上游任何一层变化,下游都要作废,别漏了换文件、解析失败、改片段内容和改过滤元数据这几种情况。业务库和向量库分开时事务管不到两边,靠顺序兜底:先删向量再删片段,出错只会少一条可重建的向量,不会留下命中不存在资料的孤儿向量。删除失败必须抛出,失败状态不能被事务回滚掉。重建时把会失败的步骤放在清理之前,能先建后替就先建后替。批量删除逐条复用单条删除。文档量大了再从覆盖式重建升级到增量更新。
项目实战落地
项目里怎么做的
《AI智能客服与工单处理系统》的业务数据在 MySQL,片段向量在 PostgreSQL + PGVector 的 customer_service_segment_vector 表,两边靠 segment_id 对应:
- 三处共用一个清理方法:编辑时换了文件(按
stored_name判断)、重新解析、解析失败,都调用clearGeneratedData,顺序是先删 PGVector 里这篇文档的向量,再删 MySQL 里的片段,最后重置文档的解析和向量化状态; - 删单条片段也是先删向量:
deleteById先deleteBySegmentId删向量、再删片段,然后按剩余片段重新汇总文档的向量化状态;批量删除循环调用它; - 删除失败必须抛:
PgVectorService的两个删除方法捕获到异常直接抛出,教程写明不能写成catch (Exception ignored); - 失败状态不被回滚:
parseById和vectorizeById都不加@Transactional,失败分支先把状态写成「解析失败」「向量化失败」再抛异常。
《AI Agent旅游行程智能规划平台》用的是「先建后替」:原文解析成功、切出片段之后才删旧片段和向量;新向量生成并通过维度校验后才删旧向量;文档改了城市或类型,冗余在 travel_chunk 上的收窄字段就过期了,直接清掉片段和向量并打回「待解析」。
为什么这样取舍
- 客服项目先删向量、再删片段:知识库资料被客服回答直接引用,孤儿向量会让回答引用一份已删除的资料,而少一条向量只需要重新向量化。
- 旅游项目先建后替:攻略语料是批量导入的,某篇文件损坏或模型调用失败时,保留旧数据比让这篇攻略暂时检索不到更合适。
面试官还会追问
- 重新生成片段的方法加了
@Transactional,而删除 PGVector 旧向量那一步不在这个事务里,回滚不了,为什么不会出问题? - 向量表是第一次向量化时才自动创建的,用户上传文档后没向量化就直接删除,删除语句会怎样?项目怎么处理?
学完《AI智能客服与工单处理系统》,上面这些追问你都会迎刃而解。