14-企业知识库增量更新-新增修改删除文档

企业知识库如何增量更新?新增、修改、删除文档的处理方案

系列:从零构建企业 RAG 知识库(第 14 篇)

1. 增量更新不是简单 upsert

文档变化会产生一组派生数据:解析块、Chunk、Embedding、关键词索引、图关系和缓存。更新必须保证:

text 复制代码
新版本完整可用之前,旧版本继续服务;
切换后,新请求只看到一个一致版本;
删除时,所有派生数据最终都被清理。

2. 使用内容哈希识别变化

python 复制代码
from dataclasses import dataclass
from hashlib import sha256
from typing import Literal


@dataclass(frozen=True)
class DocumentManifest:
    document_id: str
    tenant_id: str
    version: str
    content_hash: str
    status: Literal["building", "active", "failed", "retired", "deleted"]


def normalized_hash(content: bytes) -> str:
    """二进制哈希识别文件变化;解析配置变化另算策略版本。"""
    return sha256(content).hexdigest()

文件哈希相同但解析器、切分或 Embedding 版本改变时,仍需重建派生索引。

3. 计算增量动作

python 复制代码
@dataclass(frozen=True)
class SourceState:
    document_id: str
    content_hash: str


@dataclass(frozen=True)
class SyncAction:
    action: Literal["create", "rebuild", "delete", "skip"]
    document_id: str


def plan_sync(
    incoming: list[SourceState],
    indexed: list[SourceState],
) -> list[SyncAction]:
    incoming_map = {item.document_id: item for item in incoming}
    indexed_map = {item.document_id: item for item in indexed}
    actions: list[SyncAction] = []

    for document_id in sorted(incoming_map.keys() | indexed_map.keys()):
        new = incoming_map.get(document_id)
        old = indexed_map.get(document_id)
        if old is None:
            actions.append(SyncAction("create", document_id))
        elif new is None:
            actions.append(SyncAction("delete", document_id))
        elif new.content_hash != old.content_hash:
            actions.append(SyncAction("rebuild", document_id))
        else:
            actions.append(SyncAction("skip", document_id))
    return actions

document_id 必须来自稳定业务标识,不能使用随上传变化的临时文件名。

4. 两阶段发布避免半成品

python 复制代码
from typing import Protocol


class VersionRepository(Protocol):
    def create_building(self, manifest: DocumentManifest) -> None: ...
    def save_chunks(self, document_id: str, version: str, chunks: list[str]) -> None: ...
    def activate(self, document_id: str, version: str) -> None: ...
    def mark_failed(self, document_id: str, version: str, reason: str) -> None: ...


class IncrementalIndexer:
    def __init__(self, repository: VersionRepository) -> None:
        self.repository = repository

    def publish(
        self,
        manifest: DocumentManifest,
        chunks: list[str],
    ) -> None:
        if manifest.status != "building":
            raise ValueError("新版本必须从 building 开始")
        if not chunks:
            raise ValueError("空索引不能激活")
        try:
            self.repository.create_building(manifest)
            self.repository.save_chunks(
                manifest.document_id,
                manifest.version,
                chunks,
            )
            # 所有派生数据成功后再原子切换活动版本
            self.repository.activate(manifest.document_id, manifest.version)
        except Exception as exc:
            self.repository.mark_failed(
                manifest.document_id,
                manifest.version,
                type(exc).__name__,
            )
            raise

真实系统应让活动版本切换与索引别名/数据库状态一致,并验证并发读写。

5. 删除使用墓碑与异步清理

直接先删原文件可能导致派生向量无法追踪。更稳妥流程:

  1. 写入 Tombstone(删除墓碑),立即从查询中过滤;
  2. 清理向量、关键词索引、缓存、解析文本和原文件;
  3. 记录每个存储的清理状态;
  4. 验证检索和原文接口均不可访问;
  5. 按合规策略保留最小审计记录。

6. 幂等任务

python 复制代码
def indexing_job_key(
    document_id: str,
    content_hash: str,
    parser_version: str,
    chunk_version: str,
    embedding_model_id: str,
) -> str:
    raw = "|".join([
        document_id,
        content_hash,
        parser_version,
        chunk_version,
        embedding_model_id,
    ])
    return sha256(raw.encode("utf-8")).hexdigest()

同一键只应有一个活动任务,防止消息重投造成重复成本。

7. 可复验测试

python 复制代码
def test_sync_plan_covers_create_update_delete_and_skip() -> None:
    incoming = [
        SourceState("new", "h1"),
        SourceState("changed", "h2"),
        SourceState("same", "h3"),
    ]
    indexed = [
        SourceState("changed", "old"),
        SourceState("same", "h3"),
        SourceState("removed", "h4"),
    ]
    actions = {(item.document_id, item.action) for item in plan_sync(incoming, indexed)}
    assert actions == {
        ("new", "create"),
        ("changed", "rebuild"),
        ("same", "skip"),
        ("removed", "delete"),
    }

8. 对抗性审查

  • 同一文档并发更新使用乐观锁;
  • 旧版在新版完整前保持可用;
  • 删除先阻断查询,再清理全部派生数据;
  • 缓存键包含知识版本;
  • 解析器和 Embedding 变化也触发重建;
  • 失败任务可重试但不重复激活;
  • 定期做源系统与索引清单对账。

9. 总结

增量更新的核心是不可变版本、幂等任务、两阶段发布和可验证删除。upsert 只解决写入形式,不能独自保证知识的一致性。

相关推荐
yinshuzhineng12 小时前
如何提升生产线自动化水平,降低人工成本?
运维·人工智能·自动化·制造
今天你AiPy了吗12 小时前
AI桌面助手的隐私底线 数据可落本地不出域,还能一键切换云端,全能智能体
人工智能·python·ai·ai编程·智能体
u01030552712 小时前
H5转App遇API错误?关键在HTTPS与CORS
人工智能
2601_9563198812 小时前
近期手工规则转量化:按学习、表达、开发、验证推进
人工智能·python
青 春 记 忆13 小时前
LeetCode 104. 二叉树的最大深度|Python 解法详解
python·算法·leetcode
watersink13 小时前
机器学习关联分析
人工智能·算法·机器学习
码云骑士13 小时前
110-多模型热切换-API网关-LiteLLM代理-负载均衡故障转移
运维·python·负载均衡
AI服务老曹13 小时前
摄像机安装AI视频分析完整流程:从现场物理安装、硬解配置到算法POC效果验证
人工智能·算法·音视频
Ali8852013 小时前
Python字符串方法速查表大全
前端·python
开源量化GO13 小时前
近期量化实现:API 数据、策略逻辑和交易执行要连成一条线
人工智能·python