「RAG」RAG 知识库如何实现动态与持续更新?
0 前言
本文内容整理自公众号「小林面试笔记」的文章 19. RAG 知识库如何实现动态与持续更新?,用于记录与总结 RAG 相关的核心面试知识点。
「RAG 知识库如何实现动态与持续更新?」,可以这样作答:
核心挑战在于:文档一变,对应的 chunk 和向量都要跟着变,而且必须做到增量处理,不能每次全量重建。通用方案是给每篇文档算一个内容 hash,通过轮询或监听数据源变更来检测新增、修改、删除,一旦发现变化,先删掉旧的向量,再重新切割入库(先删后增)。
细节上还有几个补充:检测变更前先用最后修改时间做粗筛,再对有变化的文档算 hash,能过滤掉绝大部分不动的内容;删除时靠 chunk ID 的文档 ID 前缀或 metadata 里的文档 ID 字段准确定位该文档的所有 chunk;对实时性要求高的场景,用 Kafka 等消息队列做事件驱动,实现秒级入库;生产环境推荐组合是「事件驱动 + hash 检测 + 先删后增」,实在不放心再叠一层灰度更新,新旧版本并行、验证后切换。
1 为什么更新 RAG 知识库比更新普通数据库麻烦?
先想想普通数据库为什么简单:每条记录是独立的,改一条 UPDATE 一条,目标明确、互不影响。
RAG 知识库完全不是这个逻辑。一篇文档和向量库是"一对多"的关系——一篇几十上百页的文档会被切成一堆 chunk,每个 chunk 再各自 Embedding 变成向量存进向量库。麻烦就麻烦在:文档内容一变,切割结果就可能完全不同——chunk 的数量变了、边界变了、每块的内容也变了。
所以工程上最可靠的更新逻辑不是"局部修补",而是**「先删后增」**:先删掉这篇旧文档对应的所有 chunk,再把新文档重新切割、Embedding、写入。为什么不推荐局部更新?因为局部更新的前提是 Chunking 策略完全稳定(比如固定按 token 窗口切割),而这种假设在真实业务里往往不成立——今天改一个段落,明天换一次算法,切割边界就漂移了,旧 chunk 和新内容根本不"对位"。与其在这种不确定性上博弈,不如直接走「先删后增」,简单可靠:

抽象来看,知识库的变更只有三种操作类型:
- 新增:最简单,完整走「切割 → Embedding → 写入」一套流程,没有历史包袱;
- 修改:最易踩坑。切割边界一变,原来第 3 个 chunk 里的内容可能被拆到新版本的第 3、4 个 chunk 里——局部打补丁彻底行不通,必须推倒重来。这就像装修拆了一面墙,原来的插座位置全对不上了,只能重新布线;
- 删除:最直接,文档下线了,把它对应的所有 chunk 从向量库里清干净,不能留"僵尸 chunk"——否则检索还是会命中文档已不存在的过期内容:

2 如何知道文档是否发生了变化?
先删后增解决了"怎么更",下一个问题就是"怎么知道该更了"。系统怎么感知到一篇文档"变没变"?
最常用的方案是内容 hash:入库时给每篇文档算一个摘要(MD5 或 SHA256),和文档 ID、它对应的 chunk ID 列表一起存起来(放 Redis 或数据库都行)。下次同步时重新算一遍,对比一下:hash 相同说明没变,跳过;hash 不同说明内容动了,触发重新处理。

这个方案有三个明显优点:hash 算得极快;哪怕只改了一个标点符号 hash 也会变,不漏任何变更;检测成本低到可以反复跑。
还有个工程优化值得提:直接全量算 hash 对百万篇文档来说仍是开销,所以可以先用"最后修改时间"做轻量粗筛——只对时间戳变化过的文档算 hash。比如每晚同步一次,百万篇里真正改了的可能就几千篇,这一层粗筛就能过滤掉 99% 的文档,开销再降一个量级。
3 文档 ID 和 chunk ID 的设计
「先删后增」里有个隐藏前提:说删就能删得干净。删除一篇文档的所有 chunk 前,你得先能"按文档找到它的所有 chunk"。这要求从一开始就把文档和 chunk 的关联关系设计好,两种常见做法:
做法一:chunk ID 带文档 ID 前缀。 比如文档 ID 是 product_manual_v3,它的 chunk 就叫 product_manual_v3_chunk_001、product_manual_v3_chunk_002……删除时按前缀批量查找、批量删除,一条命令搞定。
做法二:chunk 的 metadata 里存文档 ID 字段。 每个 chunk 写入时打上 source_doc_id: "product_manual_v3" 这样的标签,向量库大多支持按 metadata 字段过滤,删除时筛出所有 source_doc_id = 文档ID 的 chunk 批量清除。
两条路都行,共同点是关联关系要在入库设计时就定好——别等真要删了,才发现翻遍库都找不齐这篇文档的 chunk。
4 两种主流的变更感知方式
系统通过什么方式"定期"知道文档变了?有两种主流方案,各有适用场景:
第一种:定时轮询(Polling)。 按固定间隔(比如每天凌晨两点、或者每小时)扫描一遍,对粗筛后的文档重算 hash 对比。优点:实现最简单,不依赖任何外部系统,一个定时任务就搞定。缺点也明显:有延迟——文档改了,要等到下一个轮询周期才生效;文档量大的时候,全量扫描本身也是一笔开销。
第二种:事件驱动(Event-Driven)。 数据源一有变更就发消息(Kafka、RabbitMQ 或者直接 Webhook),收到消息立刻处理,秒级生效。适合对实时性要求高的场景:客服知识库里的退款政策改了要马上生效,新闻资讯类内容要第一时间入库。代价是要数据源支持发消息,架构也更复杂一些。

有个细节值得点一下:不一定非要上大而全的消息队列。Confluence、Notion、语雀这些协作平台本身就支持 Webhook——文档一保存就主动推一个 HTTP 请求给你,订阅一下就能实现秒级更新,架构轻量很多。
选择思路一句话:更新频率低、实时性要求不高就用轮询;更新频繁、要求秒级生效就用事件驱动。
5 全量重建是最后的手段
事件驱动、增量更新都是"精细活",还有一种省事到底的方案——全量重建:不搞增量逻辑,直接把整个知识库重新处理一遍。
它的优点恰好是增量方案的痛点:逻辑最简单——不用维护文档和 chunk 的对应关系,不用做 hash 检测,也不用担心旧 chunk 漏删,反正全推倒重来。
但缺点同样绕不开:文档量大时,耗时间、耗 Embedding 的 API 费用;重建期间知识库要么不可用,要么一直在用旧数据。
所以全量重建只适合两种场合:
- 规模真的小——几十篇文档,几分钟重跑完事,增量逻辑的复杂度反而不划算;
- 知识库结构大改——换了 Embedding 模型、改了 Chunking 策略,新旧向量根本不兼容,增量更新无从谈起,只能推倒重来。
一句话:全量重建不是不能用,是只能用在"小"或"改"的场景,常态更新别指望它。
6 灰度更新:稳妥地切换新版本
生产环境的核心知识库,直接"删旧写新"风险不小——万一新版本有问题,想回滚却发现旧 chunk 已经删光了。稳妥的做法是把灰度更新加进来:
- 并行写入:新版本 chunk 带上
version=new标签入库,旧的version=old保留不动; - 验证比较:拿一批真实问题跑新旧两版,对比答案质量;
- 切换:验证通过后,把检索时的版本过滤条件从
old切到new——这一刻起线上命中的全是新版本; - 清理:最终清掉旧版 chunk,完成更新。

这个方案很像软件发布里的蓝绿部署:新旧两套并存,切换即时生效,出了任何问题都能秒级切回去,而且切换不涉及重新入库。对知识库质量要求很高的场景(金融、医疗领域的问答系统),这种谨慎的更新策略非常有必要。
把几种更新方案的特点放在一起对比,实际选型时对照着看:
| 方案 | 延迟 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 定时轮询 | 分钟~小时级 | 低 | 文档更新频率低、实时性要求不高 |
| Webhook 触发 | 秒级 | 中 | 数据源支持 Webhook(Confluence、Notion、语雀) |
| 消息队列 | 秒级 | 中高 | 大规模、高并发更新,生产环境首选 |
| 全量重建 | 分钟~小时级 | 低 | 文档量小,或知识库结构大改,不推荐常规使用 |
7 面试总结
这道题考察的是工程思维的完整性,答题的核心是把"更新"这件事拆成一个闭环:先删后增的正确姿势(一对多、切割边界会变)→ 怎么感知变化(内容 hash + 修改时间粗筛)→ 怎么删得干净(chunk 与文档的关联设计)→ 按实时性选感知方式(轮询 / 事件驱动)→ 稳妥切换(灰度、蓝绿部署)。
几个容易被追问的细节提前准备:
- 为什么不能只更新"变了的那个 chunk"? 因为切割边界会变化,新版本里"对应"的 chunk 根本不存在,内容和旧 chunk 对不上位,局部补丁没有意义;
- hash 存在哪? Redis 或数据库,和文档 ID、chunk ID 列表一起存;
- 轮询间隔怎么定? 从业务能容忍的更新延迟倒推,等不起就换事件驱动。
最后记住两句话:更新知识库,先想清楚怎么删(先删后增、关联设计是地基);生产落地推荐「事件驱动 + hash 检测 + 先删后增」的组合,要求再高就叠加灰度更新,宁可多花一次入库的功夫,也不要删了才发现回不去。
参考
本文图片均来源于公众号「小林面试笔记」,版权归原作者所有。