DaemonDaemonAI LAB
DAEMON / ARTICLE

RAG 知识入库:Parent–Child 切块与增量同步

2026.09.16 · 13 MIN READ

RAG 入库的目标不是简单地“把文章转成向量”,而是稳定、低成本地把文档转换成可以持续更新的知识。 文档标准化、切块、指纹、增量匹配、向量生成和数据库更新。

1. 入库流程

text
Document
   ↓
Normalize
   ↓
Fingerprint
   ├─ 文档没变且 Chunk 存在 → Skip
   └─ 文档或配置变化
          ↓
     Section → Parent → Child
          ↓
       content_hash
          ↓
      新旧 Chunk 匹配
       ┌──────┴──────┐
       │             │
   Hash 相同      Hash 不同
       │             │
   保留旧 Row     插入新 Row
   复用/更新向量   生成新向量
       └──────┬──────┘
              ↓
       删除未匹配旧 Row
              ↓
     PostgreSQL + pgvector

只有 PUBLISHED 内容进入知识库;内容下线后删除对应 Chunk。

2. Parent–Child:小块向量化,大块保留上下文

单一 Chunk 大小很难同时兼顾召回精度和上下文完整度:

切块方式优点问题
小 Chunk主题集中上下文可能不足
大 Chunk上下文完整一个向量容易混入多个主题

因此采用 Parent–Child:

text
Parent A
├─ Child A1
├─ Child A2
└─ Child A3

当前切块方式:

层级规则
Section按 Markdown 标题划分
Parent约 2400 个字符
Child约 800 个字符
Overlap约 120 个字符

边界优先选择换行,其次是句号,最后才按长度硬切。Overlap 用来减少信息被切断造成的语义损失。

3. 入库数据结构

contents

字段作用
bodyMarkdown 正文
knowledge_hash文档 Fingerprint
statusDRAFTPUBLISHEDOFFLINE

updated_at 只能说明记录被修改过,不能说明真正参与 RAG 的输入发生了变化,所以不能单独作为重新入库依据。

knowledge_chunks

一行代表一个 Child,同时冗余保存 Parent 信息:

字段作用
id数据库内部主键
content_id所属文档
chunk_indexChild 在整篇文档中的顺序
textChild 文本
content_hashChild 内容 Hash
embeddingChild 向量
embedding_model向量使用的模型
parent_uidChild 所属 Parent 的分组 ID
parent_indexParent 在整篇文档中的顺序
parent_textParent 完整文本
section_pathMarkdown 章节路径

冗余保存 parent_text 是用少量存储换简单读取:命中 Child 后可以直接得到 Parent 上下文,不需要额外维护 Parent 表。

4. Fingerprint:文档级快路径

同步前先计算整篇文档的 Fingerprint:

python
fingerprint = sha256(
    normalized_text
    + chunk_config
    + embedding_model
    + embedding_dimension
    + pipeline_version
)

它包含真正会影响入库结果的内容和配置:

快速跳过必须同时满足:

text
Fingerprint 相同 AND 数据库中 Chunk 数量大于 0

检查 Chunk 数量是为了处理“文章下线后 Chunk 已被删除,但旧 Fingerprint 还在”的情况。

Fingerprint 管整篇文档。它相同就不切块、不调用 Embedding;它变化后,才进入 Chunk 级增量匹配。

5. Normalize:先统一文本再计算 Hash

计算 Hash 前需要统一文本,例如:

text
\r\n → \n
删除行尾空格
清理不必要的空白

Hash 判断的是标准化后的文本是否完全一致,不理解语义:

text
“部署到 ECS” ≠ “部署至 ECS”

两句话意思接近,但 Hash 不同,会作为变化内容重新向量化。

6. content_hash:Chunk 级增量匹配

每个 Child 计算:

python
content_hash = sha256(child_text)

content_hash 只包含 Child 文本,不包含模型、维度、位置或 Parent 信息。

向量复用需要同时满足:

text
content_hash 相同
+ embedding_model 相同
+ embedding 维度相同

处理规则:

text
Hash 相同 + 模型配置相同
→ 保留旧 Row
→ 复用旧 Vector

Hash 相同 + 模型配置变化
→ 保留旧 Row
→ 重新生成 Vector

Hash 不同
→ 插入新 Row
→ 生成新 Vector

未匹配的旧 Row
→ 删除

系统不会根据最近位置,让完全不同的新内容覆盖旧 Row。

content_hash 不是唯一 ID

一篇文章中可能重复出现相同文本,所以匹配结构是:

text
Hash → 一组候选旧 Chunk

而不是 Hash → 唯一 Chunk。匹配时按出现次数依次消费旧候选,确保重复文本也能正确配对。

7. parent_uid、位置和章节路径

parent_uid

Parent 使用确定性的 UUID5:

python
parent_uid = uuid5(
    namespace,
    f"{content_id}:{sha256(parent_text)}:{occurrence}"
)

parent_indexchunk_index

text
parent_index = Parent 在整篇文章中的顺序
chunk_index  = Child 在整篇文章中的顺序

section_path

section_path 保存 Markdown 标题形成的面包屑:

text
部署 / 阿里云 / ECS

它只用于展示知识来源,不参与 Embedding、向量匹配或 Parent 去重。

章节移动为什么可以复用向量

如果章节只是调整顺序或更换上级章节,而 Parent/Child 文本没有变化:

text
parent_uid 不变
content_hash 不变
→ 保留旧 Row
→ 复用旧 Vector
→ 更新 parent_index、chunk_index、section_path

性能收益来自 content_hash 命中后不再调用 Embedding。parent_uid 保持分组,section_path 更新展示位置。

如果章节自身标题或正文变化,Parent/Child 文本也会变化,需要重新生成对应向量。

8. Chunk 重排与事务

数据库存在唯一约束:

sql
UNIQUE(content_id, chunk_index)

当旧顺序 A → 0、B → 1 调整为 B → 0、A → 1 时,直接更新会发生索引冲突。

处理方式:

  1. 将保留的旧 Row 临时移动到负索引。
  2. 删除未匹配的旧 Row。
  3. 写入最终位置和新增 Row。
  4. 在同一事务中提交。

整个过程要么全部成功,要么全部回滚,避免知识库处于半更新状态。

9. 增量同步示例

旧 Chunk 为 1 2 3 4 5,新 Chunk 为 1 2 3 5 7

Chunk变化处理
1、2、3内容没变保留 Row,复用 Vector
5位置改变,内容没变保留 Row,复用 Vector,更新位置
7新内容插入 Row,生成 Vector
4已删除删除旧 Row

最终仍有 5 个 Chunk,但只需生成 1 个新 Embedding。

10. 费用与可观测性

Embedding 是入库中主要的外部付费调用。服务端日志记录:

text
provider
model
item_count
input_chars
input_tokens
total_tokens
dimensions
duration_ms
provider_request_id

日志使用 billable=true 标识可能计费的请求,但不打印 API Key、文章正文或完整输入。

需要重点观察:

11. 后续优化方向

优先级较高:

  1. Fingerprint 命中时真正做到数据库零写入。
  2. 只更新实际变化的 Row 字段。
  3. 对模型的 429、5xx 和超时增加有限重试。
  4. 防止同一文章被并发同步。

数据量明显增大后再考虑全局向量缓存和异步任务队列。

总结

text
Fingerprint → 文档级跳过
content_hash → Child 级增量匹配
Parent–Child → 小块向量化,大块保留上下文
相同内容 → 保留 Row,复用向量
不同内容 → 新增 Row,删除旧 Row
事务 → 保证整批同步一致性

入库优化的核心不是增加更多 ID,而是减少不必要的切块、Embedding 和数据库写入,同时保证每次同步结果完整一致。