Skip to content

feat(ingestion): reuse content-addressed artifacts across rebuilds - #2467

Open
jasminewensm wants to merge 10 commits into
Tencent:mainfrom
jasminewensm:feat/ingestion-artifact-reuse
Open

feat(ingestion): reuse content-addressed artifacts across rebuilds#2467
jasminewensm wants to merge 10 commits into
Tencent:mainfrom
jasminewensm:feat/ingestion-artifact-reuse

Conversation

@jasminewensm

Copy link
Copy Markdown

🚀 概述

本 PR 将知识重建流程从 “删除全部旧数据并从零重新计算” 改造为 “基于内容寻址的增量复用”

本实现不仅增加了简单的函数结果缓存,还引入了一套持久化的 Artifact 生命周期与状态对账协议, 使系统能够在不绕过数据库、向量存储、Wiki、图谱和任务实时状态的前提下, 安全复用已经完成的昂贵计算结果。

核心设计原则: 只缓存可复用的计算结果,不缓存可变的全局状态。
改造前 改造后
重建前删除全部 Chunk 和向量 根据稳定身份对 Chunk 进行差异对账
未变化图片重复执行 OCR/Caption 复用正典多模态产物
未变化文本重复生成 Embedding 复用基于内容寻址的 Embedding 产物
完整重复执行 Wiki 单文档 Map 阶段 复用 Wiki 单文档 Map 产物
每个 Chunk 都重新执行 GraphRAG 抽取 复用单 Chunk 图谱抽取产物
任务重试时重复已经完成的 Provider 调用 已完成产物可跨任务重试和进程重启复用

✅ 核心验证结果

  • 连续执行三次相同输入时,首次计算后 Embedding 和 GraphRAG Provider 调用次数不再增加;
  • 只有一个 Chunk 发生变化时,只重新计算该 Chunk 的 Embedding 和 GraphRAG 抽取;
  • 进程重启后,OCR、Caption、Embedding、Wiki Map 和 Graph 抽取产物仍可继续复用;
  • Wiki Map 缓存命中时跳过纯 Map 模型调用,同时继续执行实时 Wiki Reduce;
  • Worker 崩溃后,其他 Worker 可以在租约过期后安全接管产物计算;
  • 不同租户的相同输入不会共享 Artifact;
  • 与最新上游代码完成整合后,Linux 聚焦测试和 Service Race 测试均已通过。

最终效果: 对于本 PR 实现的缓存产物层,首次成功计算后, 相同输入的后续重建不会继续增加对应 Provider 的调用次数。

Fixes #1679


🎯 问题背景

原有的知识重建流程大致如下:

删除现有 Chunk 和向量
  |
  v
重新解析文档
  |
  v
使用新的随机 UUID 创建 Chunk
  |
  +--> 生成 Embedding
  +--> 执行 OCR 和 Caption
  +--> 生成摘要和问题
  +--> 执行 Wiki Map 和 Reduce
  +--> 执行 GraphRAG 抽取

即使底层文档内容完全没有变化,完整的摄取链路仍然会重新执行。 同时,由于 Chunk ID 每次都使用随机 UUID 重新生成,向量、Wiki 引用、 图谱引用以及其他派生产物无法在多次重建之间保持稳定的逻辑身份。

这会导致:

  • 知识重建成本接近首次摄取成本;
  • 未变化图片重复产生昂贵的 VLM 调用;
  • 未变化文本重复产生 Embedding 调用;
  • 未变化输入重复执行 Wiki Map 和 GraphRAG 抽取;
  • 任务重试或 Worker 崩溃后重复已经完成的工作;
  • 配置变化时,实际未受影响的产物层也被重新计算;
  • 基于随机 Chunk ID 的引用在每次重建后失效。
现有的 attempt、supersede、retry 和 DLQ 机制能够改善任务编排与失败恢复, 但它们不能持久化复用已经成功完成的昂贵派生产物。

🧭 方案架构

文档内容
  |
  +--> OCR/Caption 正典产物
  |       图片字节和 VLM 依赖未变化时直接复用
  |
  v
生成稳定的 Chunk 身份
  |
  +--> 对账未变化、新增和失效的 Chunk
  |
  +--> Embedding 产物缓存
  |
  +--> Wiki 单文档 Map 产物缓存
  |
  +--> GraphRAG 单 Chunk 抽取缓存
  |
  v
执行实时状态对账
  |
  +--> 向量存储持久化与清理
  +--> Wiki 贡献者对账与 Reduce
  +--> 图谱持久化与对账
  +--> Supersede 检查与任务收尾

本实现并非简单的 Memoization,而是增加了一套包含所有权、租约、 Payload 校验、精确失效、崩溃恢复和实时状态对账的持久化 Artifact 生命周期。


🧩 已完成的改动

模块 实现内容 实现效果
可观测性 记录 Operation、Attempt、Request、Item、Subspan 和 Provider 调用信息 可以观测缓存命中与重复模型调用
稳定身份 增加基于内容派生的 stable_identityidentity_version Chunk 逻辑身份可跨重建保持稳定
Chunk 对账 复用未变化 Chunk,并在替代流程成功后淘汰失效 Chunk 重建不再依赖破坏式全量替换
Artifact 仓库 持久化、租户隔离,并支持所有权、租约、状态和摘要校验 产物可跨任务重试和 Worker 重启继续复用
OCR / Caption 根据图片内容和 VLM 依赖建立缓存,并冻结成功结果为正典文本 未变化图片不再重复调用 VLM
Embedding 根据归一化文本、模型、版本、配置和向量维度建立缓存 兼容且未变化的文本可以复用向量
Wiki Map 缓存单文档 Map 结果,并根据稳定身份重新绑定 Chunk 引用 未变化文档跳过 Wiki Map 的纯模型计算
GraphRAG 缓存单 Chunk 的实体与关系抽取结果 未变化 Chunk 跳过图谱抽取 Provider 调用
恢复机制 租约恢复、所有权协调、Payload 校验和租户隔离 任务重试可复用已完成工作,并安全恢复被遗弃的任务

已接入生产路径的 Artifact 类型

Artifact Kind 主要输入边界 缓存命中时跳过 仍然实时执行
multimodal.ocr 图片字节 + VLM + Prompt/配置版本 OCR VLM 调用 下游内容组装
multimodal.caption 图片字节 + VLM + Prompt/配置版本 Caption VLM 调用 下游内容组装
embedding.vector 归一化文本 + 模型 + 版本 + 向量维度 Embedding Provider 调用 向量存储持久化与对账
wiki.document-map 正典文档内容 + 模型 + Prompt/配置版本 Wiki 单文档 Map 的纯模型计算 贡献者对账、Reduce 和页面持久化
graph.chunk-extraction Chunk 内容 + 模型 + Prompt/配置版本 实体与关系抽取 图谱持久化与实时对账
1. 稳定 Chunk 身份与非破坏式差异对账

Chunk 新增以下稳定身份字段:

stable_identity
identity_version

稳定身份由归一化后的 Chunk 内容和稳定的文档内信息派生。

数据库中的 Chunk.ID 仍然与稳定身份分离。 这是有意保留的设计:GORM 软删除后,旧行的数据库主键仍然存在。 如果直接将确定性身份用作数据库主键,相同内容后续重建时可能发生主键冲突。

Chunk 对账流程现在会:

  • 复用未变化的 Chunk;
  • 创建真正新增的 Chunk;
  • 识别已经失效的 Chunk;
  • 在替代流程成功前继续保留当前可用状态;
  • 只在新的处理流程成功后禁用失效 Chunk;
  • 将引用重新绑定到对账后的 Chunk 数据库行;
  • 在数据库更新后对外部向量状态进行对账;
  • 拒绝过期或已经被 supersede 的对账快照。

同时保留了上游已有的可编辑 Chunk 语义:

  • ContentRevision
  • SourceContent
  • IndexStatus
  • Chunk 修订历史;
  • 乐观锁;
  • 防止重建流程静默覆盖用户编辑内容。
2. 持久化派生产物仓库

Artifact 支持以下生命周期状态:

pending
computing
succeeded
failed

每条 Artifact 记录包含以下身份与生命周期信息:

  • 租户范围;
  • Artifact 类型和 Artifact Key;
  • 输入摘要;
  • 模型 ID 和模型版本;
  • Prompt 版本;
  • 配置摘要;
  • 生产者版本;
  • Payload 编码方式和 Payload 摘要;
  • 所有者 Token 和租约过期时间;
  • 尝试次数、完成状态和失败信息。

Artifact 仓库支持:

  • 确定性 Artifact 查询;
  • 原子化生产者所有权获取;
  • 并发生产者协调;
  • 有界等待其他正在计算的生产者;
  • Worker 崩溃后的过期租约接管;
  • 失败 Artifact 重试;
  • 租户所有权隔离;
  • Payload 完整性校验;
  • 损坏或不兼容 Payload 的安全重算。
3. OCR 与 Caption 正典产物

多模态 Artifact Key 包含以下相关依赖:

  • 渲染后的图片字节;
  • VLM 模型 ID 和模型版本;
  • Prompt 版本;
  • 多模态处理配置;
  • 生产者版本。

缓存命中时:

  • 不再调用 VLM Provider;
  • 缓存中的 OCR/Caption 结果成为下游正典内容;
  • 相同图片字节可以产生稳定的下游输入。

多模态正典结果在源头隔离 VLM 的非确定性, 避免 OCR/Caption 的微小随机差异导致下游 Chunk、Embedding、 Wiki 和 Graph Artifact Key 连锁失效。

4. 基于内容寻址的 Embedding 产物

Embedding Artifact Key 包含:

  • 归一化文本摘要;
  • Embedding 模型 ID 和模型版本;
  • 向量维度;
  • 相关 Embedding 配置;
  • 生产者版本。

该设计支持:

  • 未变化重建之间复用 Embedding;
  • 在兼容调用方之间复用相同文本的向量;
  • 模型或向量维度变化时精确失效;
  • 同一批次中的部分缓存命中;
  • 向量维度和二进制 Payload 完整性校验;
  • 损坏或不兼容向量的安全重算。

只有 Embedding 计算结果会被缓存。 向量存储的持久化、清理和对账仍然实时执行。

5. Wiki 单文档 Map 产物

可复用的 Wiki 单文档 Map Payload 包含重建以下结果所需的信息:

  • 候选 Wiki 页面;
  • Map 结果使用的文档摘要信息;
  • Chunk 到页面的引用;
  • 文档贡献更新;
  • Map 阶段统计信息。

缓存中的来源引用使用稳定 Chunk 身份,而不是数据库行 ID。 恢复缓存时,系统会将稳定身份重新绑定到当前有效的 Chunk 数据库行。

Wiki Map 缓存命中后,以下操作仍然实时执行:

  • 查询当前贡献者;
  • 撤回失效贡献;
  • 检查知识和文档是否仍然有效;
  • 检查 Wiki 页面修订版本;
  • 处理乐观锁冲突;
  • 执行 Wiki 页面 Reduce 和合并;
  • 持久化最终 Wiki 页面。

Wiki Reduce 被有意排除在缓存范围之外, 因为其结果依赖多个文档当前的实时贡献者集合。

6. GraphRAG 单 Chunk 抽取产物

对于稳定身份和输入均未变化的 Chunk:

  • 跳过实体和关系抽取 Provider 调用;
  • 恢复已经缓存的 Graph 抽取结果;
  • 将引用重新绑定到当前 Chunk 状态。

图谱持久化仍然实时执行,确保最终图谱始终反映当前有效的文档和 Chunk 状态。


🛡️ 正确性约束

  • 缓存命中不能跳过实时对账: Wiki Reduce、向量持久化、图谱持久化和任务收尾仍然执行。
  • 稳定身份不是数据库主键: 避免与现有软删除语义产生主键冲突。
  • 重建不能覆盖用户编辑: Chunk 对账保留 ContentRevision、SourceContent 和乐观锁语义。
  • Artifact 不能跨租户复用: Artifact Key 和仓库操作均包含租户范围。
  • 缓存引用不能恢复陈旧数据库行 ID: 稳定引用会重新绑定到当前 Chunk 数据库行。
  • 损坏 Payload 不能作为有效缓存返回: Schema、Digest、编码和向量维度校验采用失败关闭策略。
  • 失去所有权的 Worker 不能覆盖接管结果: Artifact 完成时会再次检查所有权。
  • 被 supersede 的旧 Attempt 不能完成当前 Attempt: 现有 Supersede 检查仍然生效。

♻️ 缓存边界

可复用的计算结果 仍然实时执行的有状态操作
OCR 和 Caption 生成 向量存储持久化与清理
归一化文本 Embedding Wiki 贡献者对账与撤回
Wiki 单文档 Map Wiki 页面 Reduce 与合并
GraphRAG 单 Chunk 抽取 图谱持久化与实时对账
- Supersede 判断和摄取任务收尾
只有具有确定性或仅依赖单文档输入的计算结果会从 Artifact 缓存中恢复。 依赖当前可变状态的操作仍然实时执行。

🔄 精确失效行为

发生的变化 OCR / Caption Chunk Embedding Wiki Map Graph 抽取
内容未变化的重建 复用 对账 / 复用 复用 复用 复用
图片字节变化 重算受影响图片 重算受影响的下游内容 重算受影响的下游内容 重算受影响的下游内容 重算受影响的下游内容
VLM 模型或 Prompt 变化 重算 正典内容变化时重算 重算受影响的下游内容 重算受影响的下游内容 重算受影响的下游内容
分块配置变化 复用 重算 重算 重算 重算
Embedding 模型或维度变化 复用 复用 重算 复用 Graph 依赖未变化时复用
Wiki Prompt 或配置变化 复用 复用 复用 重算 复用
Graph Prompt 或配置变化 复用 复用 复用 复用 重算

🔐 恢复机制、租户隔离与 Payload 安全

  • 已完成 Artifact 可以在任务重试和进程重启后继续复用;
  • 并发 Worker 通过所有者 Token 和租约进行协调;
  • 被遗弃的 computing Artifact 可在租约过期后被其他 Worker 接管;
  • 旧所有者在失去租约后不能覆盖新所有者提交的 Artifact;
  • 失败 Artifact 可以重新尝试计算;
  • 损坏、身份不匹配或不兼容的 Payload 会被拒绝;
  • Embedding 向量维度和二进制 Payload 完整性会被校验;
  • Artifact 不能跨租户复用;
  • Artifact Payload 不保存数据库 Chunk 行 ID、凭据、Trace ID 或 Attempt ID;
  • 已被 supersede 的旧 Attempt 不能错误地完成当前摄取任务。

🗄️ 数据库与兼容行为

  • Chunk 新增持久化字段 stable_identityidentity_version
  • 新增持久化 derived_artifacts 表保存可复用产物状态;
  • 通过租户/Artifact Key 唯一约束和状态/租约索引支持安全查询与恢复;
  • 现有数据库 Chunk 主键和软删除语义保持不变;
  • 没有受支持稳定身份的旧 Chunk 会安全绕过需要稳定引用的缓存路径;
  • 第一次兼容计算为 Cache Miss,只有成功完成的 Artifact 才能在后续运行中复用;
  • 向量和图谱持久化仍然针对当前外部状态进行实时对账;
  • 已经更新项目所使用的数据库 Migration 和初始化 Schema。

✅ 预期行为

内容未变化的重建

解析并执行差异对账
  |
  +--> OCR/Caption 缓存命中
  +--> 稳定 Chunk 复用
  +--> Embedding 缓存命中
  +--> Wiki Map 缓存命中
  +--> GraphRAG 抽取缓存命中
  |
  v
继续执行必要的实时持久化、Wiki Reduce 和任务收尾

对于已经实现缓存的 Artifact 层,首次成功计算之后, 相同输入的后续重建不应继续增加对应 Provider 的调用次数。

文档部分内容发生变化

  • 未变化图片继续复用 OCR/Caption Artifact;
  • 未变化 Chunk 保留稳定身份;
  • 未变化文本继续复用 Embedding Artifact;
  • 只重新计算发生变化的 Embedding 和 GraphRAG 输入;
  • 受影响的 Wiki 单文档 Map 输入会被精确失效。

任务重试或崩溃恢复

  • 已经完成的 Artifact 直接复用;
  • 未完成或被遗弃的 Artifact 可以被恢复;
  • 无效或失败的 Artifact 会被安全重算;
  • 实时持久化和任务收尾流程仍然会收敛到当前有效状态。

🔗 与上游最新代码的兼容性

当前功能分支已经与最新的 upstream/main 完成同步。

同步过程中保留了上游以下行为:

  • 可编辑 Chunk;
  • Chunk 内容修订和乐观锁;
  • Source Content 和 Index Status;
  • Chunk 修订历史;
  • 生成问题时的 Revision 保护;
  • 摘要和自定义 Metadata 版本检查;
  • Reparse 状态重置与任务入队行为;
  • Wiki 页面修订和乐观锁冲突处理;
  • Wiki 来源追踪和 Source Context 处理;
  • 当前上游的导入、Metadata、索引和任务收尾逻辑。

当前功能分支没有落后于 upstream/main


🧪 验证结果

本实现已在 Windows 本地环境完成聚焦测试,并在与最新上游代码合并后, 通过 GitHub Actions 在 Linux 环境中完成验证。

Linux 环境 Ubuntu 24.04
Go 版本 Go 1.26.0
系统架构 linux/amd64
Race Detector 已在聚焦 Service 测试中启用
功能集成提交 275191ce
验证 Workflow 提交 12d9af1e01968ec95f6112612982b2d928d97736
验证结果 ✅ 所有聚焦 Linux 验证步骤均已通过

验收结果

验收场景 预期行为 结果
连续三次相同输入 首次之后不再增加 Embedding 和 GraphRAG Provider 调用 ✅ 通过
只有一个 Chunk 变化 只重新计算发生变化的 Chunk ✅ 通过
进程重启 五类持久化 Artifact 均可继续命中 ✅ 通过
Wiki Map 缓存命中 跳过 Map Provider 调用,同时继续执行实时 Reduce ✅ 通过
Graph Prompt 变化 只失效 Graph 抽取 Artifact ✅ 通过
Worker 租约过期 新 Worker 可以安全接管所有权 ✅ 通过
租户隔离 不同租户不能共享 Artifact ✅ 通过
Payload 损坏 拒绝缓存命中并安全重算 ✅ 通过
Chunk 对账失败 原子更新整体回滚,不留下部分差异 ✅ 通过
Linux Race 验证 聚焦 Service 测试不报告数据竞争 ✅ 通过
查看关键验收测试
  • TestIngestionCachesEndToEndAcrossRestartInvalidationAndCrashRecovery
  • TestIngestionArtifactReuse_ThreeIdenticalRebuildsDoNotIncreaseProviderCalls
  • TestIngestionArtifactReuse_OneChunkChanged
  • TestIngestionArtifactInvalidation_TenantIsolation
  • TestIngestionArtifactPayloads_ContainNoSecretsOrRowIDs
  • TestImageMultimodalArtifactCache_RebuildReusesOCRAndCaption
  • TestEmbeddingArtifactCachePartialHitPreservesOrder
  • TestWikiMapArtifactSecondRunSkipsPureMapChatAndStillReduces
  • TestWikiMapArtifactHitRebindsStableIdentityToRebuiltRowID
  • TestGraphExtractArtifactCachePartialHitAcrossChunks
  • TestApplyIngestionChunkReconcile_SQLite_AppliesAtomicManagedDiff
  • TestApplyIngestionChunkReconcile_SQLite_RejectsSupersededAttempt
查看 Linux 验证命令
test -z "$(gofmt -l \
  internal/contentkey \
  internal/artifactkey \
  internal/testutil/modelcount \
  internal/application/repository \
  internal/application/service)"

go test ./internal/contentkey/... -count=1

go test ./internal/artifactkey/... -count=1

go test ./internal/testutil/modelcount/... -count=1

go test ./internal/application/repository \
  -run 'Chunk|DerivedArtifact|Milvus|ChunkRevision|CreateChunks|Reconcile' \
  -count=1

go test -race ./internal/application/service \
  -run 'IngestionCache|IngestionArtifact|StableIdentity|Reconcile|WikiMap|GraphExtract|EmbeddingArtifact|Multimodal|ChunkRevision|ChunkEdit|Reparse|ProcessChunks' \
  -count=1

GitHub Actions 日志中的 Node.js 弃用警告来自 actions/checkout@v4actions/setup-go@v5, 不影响 Go 测试结果。


📚 设计文档


🔍 审查指南

查看实现提交列表
  1. ca153da0 - 增加摄取链路可观测性基线
  2. 8d0dfb0a - 持久化稳定的文本 Chunk 身份
  3. abe9b59e - 根据稳定身份对文档 Chunk 进行差异对账
  4. ff6c297d - 增加持久化派生产物缓存基础设施
  5. 33af18ad - 缓存 OCR 和 Caption 正典 Artifact
  6. ce2a612a - 复用基于内容寻址的 Embedding Artifact
  7. 68c5f3cd - 缓存 Wiki 单文档 Map Artifact
  8. 063877c7 - 缓存 GraphRAG 单 Chunk 抽取 Artifact
  9. 36750b32 - 完善精确失效、恢复、隔离和验收测试
  10. 275191ce - 与最新 upstream/main 同步并解决兼容问题

建议按照以下顺序审查:

  1. 稳定 Chunk 身份和差异对账;
  2. Artifact Key 与持久化仓库基础设施;
  3. OCR/Caption 和 Embedding 复用;
  4. Wiki Map 复用及实时 Reduce 边界;
  5. GraphRAG 抽取复用;
  6. 恢复、精确失效、租户隔离和 Payload 安全;
  7. 与最新上游代码的兼容性处理。

⚠️ 范围说明

  • Wiki Reduce 被有意排除在缓存范围之外,因为它依赖多个文档当前的实时贡献者集合。
  • 向量和图谱持久化仍然属于实时对账操作。
  • 数据库 Chunk 主键继续与稳定 Chunk 身份分离,以兼容现有软删除语义。
  • 缓存命中相关的验收结论适用于本 PR 实现的 Artifact 层: OCR、Caption、Embedding、Wiki 单文档 Map 和 GraphRAG 单 Chunk 抽取。
  • 临时 Linux 验证 Workflow 只存在于验证分支中,不包含在本 PR 的正式功能分支内。

Classify parser, VLM, chat, embedding, wiki, and graph ingestion operations.

Add provider request counters, pre-cache baseline tests, and cache_status=not_supported observations.
Add versioned normalization, deterministic UUIDv5 identities, duplicate ordinals, and parent-child identity assignment.

Persist stable identity separately from random chunk row IDs with non-unique cross-database lookup indexes.
Preserve existing chunk IDs for stable-identity matches and apply ingestion chunk differences transactionally.

Define idempotent BatchSave semantics across supported vector stores, use stable point IDs for Qdrant and Milvus, and lazily clean historical random-ID points.

Keep embedding recomputation enabled and introduce no artifact cache.
Persist tenant-scoped embedding vectors as derived artifacts and reuse exact-text matches across ingestion operations.

Support partial batch hits, binary float32 payloads, lease heartbeats, cancellation cleanup, fail-closed decoding, and cache-aware ingestion observations.
Persist tenant-scoped GraphRAG candidate nodes and relations using the derived-artifact lease protocol.

Support partial hits, concurrent claim sharing, lease renewal and takeover, corrupt fallback, deterministic normalization, and current chunk-ID rebinding.
@jasminewensm
jasminewensm marked this pull request as ready for review July 31, 2026 09:35
@jasminewensm

Copy link
Copy Markdown
Author

本 PR 已完成实现、自查和上游同步,现在可以开始审查。

相关 Artifact 复用测试已在 Ubuntu 24.04、Go 1.26.0 环境通过,其中包括 Service Race 测试。当前剩余 Workflow 因 PR 来自 Fork,正在等待维护者批准运行。

感谢审查。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 重建/重解析知识时应复用 OCR、Embedding、Wiki Map 等缓存,避免全量重算

1 participant