ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

RAG增量更新:如何避免旧知识污染?

RAG增量更新:如何避免旧知识污染? 很多团队第一次做 RAG流程都差不多上传文档、解析文本、切 chunk、生成 embedding、写入向量库然后在问答时做相似度检索。Demo 跑通以后真正的问题才出现文档更新了怎么办比如一份《售后政策》从 v1 改到 v2旧条款已经废弃但向量库里还躺着旧 chunk。用户问“七天无理由规则是什么”系统可能同时召回新旧两段内容模型再把它们揉成一个看似合理、实际错误的答案。RAG 的增量更新不是“把新文档再导入一次”。它本质上是一个索引一致性问题业务系统里的知识变了向量索引也要能删除旧版本、写入新版本并且让检索链路知道当前哪些内容才是可信的。旧知识污染比召回不到更危险召回不到用户通常能感知到系统“不知道”。但召回到过期内容模型会很自然地生成一个确定语气的错误答案。这类问题在企业知识库里很常见场景如果只追加会发生什么制度文档修订新旧制度同时被召回接口文档更新Agent 仍然按旧参数调用FAQ 答案调整客服答案出现前后不一致产品价格变更模型引用过期价格权限文档迁移已废弃权限规则继续生效向量库不是数据库的替代品。它擅长相似度检索但不天然知道“哪条知识已经被业务废弃”。这个判断必须由索引设计来表达。给每个 chunk 带上可重建的身份在 RAG 里一个原始文档通常会被拆成多个 chunk。真正进入向量库的不是“文档”而是“文档片段”。所以增量更新的关键不是只记录文件名而是让每个 chunk 都能追溯回原始文档和版本。建议至少保留这些 metadata字段作用docId原始文档稳定 ID删除旧 chunk 时使用contentHash判断内容是否变化避免重复 embeddingversion文档版本便于排查和灰度chunkIndexchunk 顺序便于定位和上下文拼接sourceUri原始来源如文件地址、CMS 链接、数据库主键updatedAt索引更新时间便于审计tenantId多租户场景下的隔离字段Spring AI 的 Document 支持文本内容和 metadata。官方文档中也明确了 VectorStore 提供 add、delete、similaritySearch 等操作并支持基于 metadata 的 filter expression。也就是说增量更新不应该绕开框架能力而应该把“文档身份”设计进 metadata。一条更稳的更新链路可落地的流程可以这样设计这里有一个重要细节不要把向量库当作唯一状态来源。生产项目里最好有一张普通业务表记录索引状态例如knowledge_index_state- doc_id- content_hash- version- indexed_at- status它的作用不是参与相似度检索而是回答几个工程问题这份文档有没有被索引过当前索引对应哪个版本上次失败停在哪一步是否需要重建向量库负责“查相似内容”业务表负责“管理索引生命周期”。这两个职责分开系统会好维护很多。Spring AI 中的关键代码下面的代码只展示核心思路检测内容变化删除旧 chunk重新切分并写入。具体向量库可以是 PgVector、Milvus、Redis、Elasticsearch、Qdrant 等接口层面都围绕 VectorStore 展开。Servicepublic class KnowledgeIndexService {private final VectorStore vectorStore;private final KnowledgeIndexStateRepository stateRepository;private final TokenTextSplitter splitter TokenTextSplitter.builder().withChunkSize(800).withMinChunkSizeChars(350).withKeepSeparator(true).build();public KnowledgeIndexService(VectorStore vectorStore,KnowledgeIndexStateRepository stateRepository) {this.vectorStore vectorStore;this.stateRepository stateRepository;}public void reindex(KnowledgeFile file) {String docId file.docId();String contentHash sha256(file.content());KnowledgeIndexState state stateRepository.findByDocId(docId);if (state ! null contentHash.equals(state.contentHash())) {return;}// docId 应使用系统生成的稳定 ID例如 UUID/ULID避免拼接外部输入vectorStore.delete(docId docId );Document raw new Document(file.content(), Map.of(docId, docId,version, file.version(),contentHash, contentHash,sourceUri, file.sourceUri(),updatedAt, Instant.now().toString()));ListDocument chunks splitter.apply(List.of(raw));ListDocument indexedChunks IntStream.range(0, chunks.size()).mapToObj(i - {Document chunk chunks.get(i);MapString, Object metadata new LinkedHashMap(chunk.getMetadata());metadata.put(chunkIndex, i);return new Document(chunk.getContent(), metadata);}).toList();vectorStore.add(indexedChunks);stateRepository.save(new KnowledgeIndexState(docId,contentHash,file.version(),Instant.now(),INDEXED));}private String sha256(String text) {// 生产代码建议封装异常并统一字符集return DigestUtils.sha256Hex(text);}}这段代码背后的重点不是 TokenTextSplitter而是“可重建”。只要知道 docId就能删除这个文档历史产生的所有 chunk只要知道 contentHash就能判断是否需要重新 embedding只要保留 chunkIndex就能定位某个答案来自哪一段。Spring AI 2.0.0 文档中TokenTextSplitter.builder() 是推荐创建方式旧构造器已经不适合继续作为示例。具体 API 可能会随版本变化实际项目中应以官方文档为准。先删后写不是唯一答案上面的“先删旧 chunk再写新 chunk”适合大多数后台重建任务但它有一个短暂窗口如果删除成功、写入失败检索会暂时查不到这份文档。如果知识库对一致性要求很高可以改成“双版本 active 标记”的方式新版本 chunk 写入向量库metadata 带上 version v2。业务表把当前 active version 从 v1 切到 v2。检索时通过 filter expression 只查 active version。后台异步删除旧版本 chunk。这种方式更接近数据库里的灰度发布。代价是检索时必须带版本过滤索引状态表也要设计得更严谨。如果只是内部知识库问答先删后写通常够用如果是客服、合同、风控、操作指令类场景建议用版本切换避免半更新状态影响线上回答。检索链路也要理解版本很多人只在写入侧做版本检索侧却不加限制结果旧版本仍然可能被召回。Spring AI 的 QuestionAnswerAdvisor 可以基于 SearchRequest 配置相似度阈值、topK也支持动态 filter expression。比如在多租户或版本场景下检索时可以限制只查当前租户、当前知识库范围String answer chatClient.prompt().user(question).advisors(a - a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION,tenantId t_1001 status ACTIVE)).call().content();这里不要把 filter 只理解成“权限隔离”。它也是 RAG 工程化里控制知识有效性的手段。一个成熟的 RAG 系统写入侧和检索侧必须共享同一套 metadata 约定。真实项目里还要补三件事第一索引任务要可重试。文档解析、embedding、向量库写入都有可能失败不要把它们塞进一个不可观察的同步接口里。更稳的做法是通过 MQ 或任务表异步处理失败后记录错误原因和重试次数。第二删除要分批。Spring AI 文档也提醒大量删除可能给后端向量库带来压力filter 删除的性能还和具体实现有关。大文档、批量导入、整库重建时要控制批次大小。第三答案要能溯源。每个 chunk 的 metadata 里保留 docId、sourceUri、version 和 chunkIndex不仅是为了更新也是为了在用户质疑答案时能定位来源。RAG 系统没有溯源能力排查成本会非常高。把 RAG 做到能回答问题并不难难的是当知识不断变化时系统仍然能回答“当前正确的内容”。对 Java 后端开发者来说向量库只是新型索引真正决定系统稳定性的还是那些熟悉的东西ID 设计、状态表、异步任务、重试、审计和一致性边界。
返回列表