news 2026/6/26 17:58:32

RAG 知识库别只会追加:Java 项目里如何做增量更新

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG 知识库别只会追加:Java 项目里如何做增量更新

很多团队第一次做 RAG,流程都差不多:上传文档、解析文本、切 chunk、生成 embedding、写入向量库,然后在问答时做相似度检索。

Demo 跑通以后,真正的问题才出现:文档更新了怎么办?

比如一份《售后政策》从 v1 改到 v2,旧条款已经废弃,但向量库里还躺着旧 chunk。用户问“七天无理由规则是什么”,系统可能同时召回新旧两段内容,模型再把它们揉成一个看似合理、实际错误的答案。

RAG 的增量更新不是“把新文档再导入一次”。它本质上是一个索引一致性问题:业务系统里的知识变了,向量索引也要能删除旧版本、写入新版本,并且让检索链路知道当前哪些内容才是可信的。

旧知识污染比召回不到更危险

召回不到,用户通常能感知到系统“不知道”。但召回到过期内容,模型会很自然地生成一个确定语气的错误答案。

这类问题在企业知识库里很常见:

场景如果只追加会发生什么
制度文档修订新旧制度同时被召回
接口文档更新Agent 仍然按旧参数调用
FAQ 答案调整客服答案出现前后不一致
产品价格变更模型引用过期价格
权限文档迁移已废弃权限规则继续生效

向量库不是数据库的替代品。它擅长相似度检索,但不天然知道“哪条知识已经被业务废弃”。这个判断必须由索引设计来表达。

给每个 chunk 带上可重建的身份

在 RAG 里,一个原始文档通常会被拆成多个 chunk。真正进入向量库的不是“文档”,而是“文档片段”。所以增量更新的关键不是只记录文件名,而是让每个 chunk 都能追溯回原始文档和版本。

建议至少保留这些 metadata:

字段作用
docId原始文档稳定 ID,删除旧 chunk 时使用
contentHash判断内容是否变化,避免重复 embedding
version文档版本,便于排查和灰度
chunkIndexchunk 顺序,便于定位和上下文拼接
sourceUri原始来源,如文件地址、CMS 链接、数据库主键
updatedAt索引更新时间,便于审计
tenantId多租户场景下的隔离字段

Spring AI 的Document支持文本内容和 metadata。官方文档中也明确了VectorStore提供adddeletesimilaritySearch等操作,并支持基于 metadata 的 filter expression。也就是说,增量更新不应该绕开框架能力,而应该把“文档身份”设计进 metadata。

一条更稳的更新链路

可落地的流程可以这样设计:

这里有一个重要细节:不要把向量库当作唯一状态来源。生产项目里最好有一张普通业务表记录索引状态,例如:

knowledge_index_state - doc_id - content_hash - version - indexed_at - status

它的作用不是参与相似度检索,而是回答几个工程问题:这份文档有没有被索引过?当前索引对应哪个版本?上次失败停在哪一步?是否需要重建?

向量库负责“查相似内容”,业务表负责“管理索引生命周期”。这两个职责分开,系统会好维护很多。

Spring AI 中的关键代码

下面的代码只展示核心思路:检测内容变化,删除旧 chunk,重新切分并写入。具体向量库可以是 PgVector、Milvus、Redis、Elasticsearch、Qdrant 等,接口层面都围绕VectorStore展开。

@Service public 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() )); List<Document> chunks = splitter.apply(List.of(raw)); List<Document> indexedChunks = IntStream.range(0, chunks.size()) .mapToObj(i -> { Document chunk = chunks.get(i); Map<String, 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 标记”的方式:

  1. 新版本 chunk 写入向量库,metadata 带上version = v2
  2. 业务表把当前 active version 从v1切到v2
  3. 检索时通过 filter expression 只查 active version。
  4. 后台异步删除旧版本 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();
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 17:57:51

Unity Mod Manager终极指南:5分钟学会游戏模组管理

Unity Mod Manager终极指南&#xff1a;5分钟学会游戏模组管理 【免费下载链接】unity-mod-manager UnityModManager 项目地址: https://gitcode.com/gh_mirrors/un/unity-mod-manager 你是否曾经因为复杂的模组安装过程而放弃尝试&#xff1f;或者因为模组冲突导致游戏…

作者头像 李华
网站建设 2026/6/26 17:55:30

AI辅助安全研究:7天挑战苹果MIE防护的极限攻防实战

1. 项目概述&#xff1a;一次极限安全研究的实战复盘最近在安全研究圈子里&#xff0c;一个话题讨论得挺热&#xff1a;一个名不见经传的小型安全团队&#xff0c;声称在短短7天内&#xff0c;成功绕过了苹果公司号称价值50亿美元投入的MIE&#xff08;内存完整性执行&#xff…

作者头像 李华
网站建设 2026/6/26 17:53:59

Wand-Enhancer完整指南:如何为WeMod游戏平台实现高级功能增强

Wand-Enhancer完整指南&#xff1a;如何为WeMod游戏平台实现高级功能增强 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/gh_mirrors/we/Wand-Enhancer Wand-Enhancer是一款专为WeMod…

作者头像 李华
网站建设 2026/6/26 17:50:14

2026下半年云手机深度测评:从架构、稳定性解析各类云手机适配场景

随着移动端程序自动化运行、应用测试、远程移动办公业务不断普及&#xff0c;基于 ARM 虚拟化技术的云端手机服务得到大范围应用。很多从业者在选型时&#xff0c;容易只关注月租价格&#xff0c;忽略底层架构、后台保活能力、批量管控功能以及隐形收费项目。 本次测试统一搭建…

作者头像 李华