← RAG 检索增强

知识库文档更新或删除后,片段和向量怎么保持一致?

高频 中等 知识库更新与一致性 · 第 1 / 2 问 更新于 2026/09/29
RAG知识库维护数据一致性向量库增量更新
本题落地项目AI智能客服与工单处理系统

简化版

一篇文档会派生出解析文本 → 片段 → 向量三层数据,上游任何一层变了,下游都要作废重建:换文件、重新解析、重新切分、改了用于过滤的元数据,都要清掉旧片段和旧向量。业务库和向量库分开存时,一个事务管不到两边,所以要安排删除顺序:先删向量、再删片段,出错时最多「少一条向量、重新向量化能补回」,而不会「片段没了、向量还在,检索命中一段不存在的资料」。另外两条底线:删除失败不能吞异常;先确认新数据能生成,再清旧数据。

详细版

文档(原文件、标题、分类/城市等元数据)
  └─ 解析文本
       └─ 片段(chunk)
            └─ 向量(embedding)
上游发生的变化要作废什么做法
换了原文件解析文本、片段、向量清空三层,状态退回「待解析」
重新解析 / 解析失败片段、向量解析失败也要清,旧片段已不对应当前文件
重新切分片段、向量先删向量再删片段,再写新片段
修改片段内容这条片段的向量删向量或置空,等重新向量化
修改过滤用的元数据冗余了这些字段的片段和向量清掉重来,否则过滤条件对不上
删除文档 / 知识库下面所有层自下而上:向量 → 片段 → 文档 → 知识库

三条原则:

  1. 自下而上删:先删最下游(向量),再删上游,任何一步失败都只会留下「可以重建」的缺口。
  2. 失败要暴露:删除向量失败必须报错,不能静默跳过。
  3. 先生成后替换:新文本解析成功、新向量生成成功,再删旧的;失败时旧数据保留,检索不会突然少一块。

完整版教学

一、为什么「改了文档」会让检索出错

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智能客服与工单处理系统》,上面这些追问你都会迎刃而解。

本题落地项目登峰造极AI智能客服与工单处理系统基于RAG、意图识别和AI回复建议,实现知识库问答、客户会话、工单创建、优先级识别、人工处理、服务质检和统计分析,适合学习FAQ资料、客服业务闭环、满意度评价和过程追踪。SpringbootLangChain4JRAGPGVectorEmbedding源码+SQL喂饭学习教程配套面试文档环境安装文档项目运行文档 学习这个项目 也可以学AI Agent旅游行程智能规划平台地狱锤炼 查看项目