RAG更新策略:文档局部更新后,知识库如何更新?
文档更新了200个字,整个知识库如何更新到最新状态?
这样的问题看着还是挺简单的,但动手的时候,好像又不那么简单。让我们一起看看
01
—
理论上的最优方法
-
定位变动的块:把新文档和旧文档按相同方式切块,比对每个块的内容。只找出内容发生变化的块。
-
更新索引
-
删除旧块对应的向量(根据块ID或文档ID+位置)
-
对新增/修改的块重新生成向量,插入索引
-
如果某块被删除,直接删掉
-
注意邻居块:如果改动导致块边界偏移(比如加了一大段文字),建议重算该部分前后几个块,保证上下文连续性。简单做法:把整段涉及的区域(前后各多取一块)重新切分并替换。
这样做,计算量减少 99%,速度飞快,成本极低。
理论上完全可行。
但实际上呢?即使最简单的固定大小分块策略,也会出现:
假如我们更新的这200个字导致当前块超过了分块大小,就会发生边界飘逸,导致上下两个块要重新分块,甚至级联到更多的块,这样极易导致周围块的上下文发生错位,语意不连贯甚至混乱,检索出来的结果自然也会是乱的。
如果是语义分块,你需要:
-
维护每个块的位置信息
-
处理块的分裂、合并、移位
-
保证新旧块之间的语义连续性
这样做真的很复杂!
为了不这么麻烦,LlamaIndex采取了以文档(Document)为最小粒度的策略。
—
LlamaIndex的做法
Document 的插入、删除、更新(update_ref_doc)和刷新(refresh_ref_docs)操作。LlamaIndex 将 Document 视为主要的数据操作单元,Node 是其内部的子对象,用户无法跳过 Document 直接对 Node 进行更新。
LlamaIndex 还有自动化摄入管道 (IngestionPipeline)策略,当你通过 IngestionPipeline 处理文档时,它会为每个文档计算一个“哈希值”作为“指纹”。后续再次运行时,它会自动跳过哈希值未变的文档,只重新处理发生过变化的文档,这从根本上避免了重复计算。
反正都是以文档为最小处理单元,也就没有必要逐字去做内容比对了。
—
自己动手,适配项目
LlamaIndex 之所以采取以文档而不是块为最小操作单元,也是因为它是一个通用框架,核心目标就是适用于大多数场景。而且其默认采用递归分块 (Recursive Chunking),采用优先分隔符(段落→句子→词),块大小不完全一致。管理块级别的内容,复杂度太高。
如果你项目中的文档平均大小在几万 token 以内,更新频率不高(一天几次几十次以内),直接用 LlamaIndex 的原生方法就好,别为了理论上的“最优”把自己搞崩溃。
-
自己维护文档 → 块的映射表
-
用固定分块策略(不要用语义切分,否则边界不可控)
-
每次更新,只重算变化的块及其邻居
-
直接操作向量数据库的
update接口
夜雨聆风