Skip to content

indexing相关技术名词 ​

接下来我们来看一下 indexing 阶段你需要掌握技术名词:

  1. Semantic Chunking
  2. Recursive Chunking
  3. Contextual Headers
  4. Document Augmentation
  5. Hierarchical Indexing
  6. 元数据抽取
  7. 去重与清洗
  8. 增量更新

这一大堆名词里面,其实可以分成这么三类:

  1. 切割 chunk 技术
  2. chunk 增强
  3. 索引治理

1. 切割 chunk 技术 ​

顾名思义,就是将文档做切块的时候,具体怎么去切。

1-1. Semantic Chunking ​

按照语义边界来切分文档,而不是单纯按照固定字符数或 token 数切。

它的核心目标是:让每个 chunk 尽量保持语义完整,避免一句话被硬拆开,或者一段完整说明被截断。

常见做法包括:

  • 按句子切
  • 按段落切
  • 按标题层级切
  • 按语义相似度变化点切

举个例子:

  • 错误切分(固定长度):[React Hooks 是...] [一个函数式编程...] [它允许你...]
  • 语义切分:[React Hooks 是什么 + 为什么出现][useState 的作用][useEffect 的作用]

1-2. Recursive Chunking ​

Recursive Chunking 指的是递归式切分策略。它不是一上来就按固定长度硬切,而是按一组优先级分隔符逐层尝试切分,例如:

  1. 先按标题或段落分
  2. 如果某段还是太长,再按句子分
  3. 如果句子还太长,再按标点分
  4. 最后实在不行,再按字符或 token 长度切

它本质上是一种“尽量保留语义结构,同时满足长度约束”的折中策略。

举个例子,一篇技术文档很长:

  1. 先按##标题切成几个部分;
  2. 其中某个部分仍然超长,就再按段落切;
  3. 某段还太长,再按句子切。

这样切出来的 chunk 通常比简单固定长度切分更自然。

NOTE

langchain 里的 RecursiveCharacterTextSplitter 就是这个思路的代表实现。

1-3. 如何落地 ​

下面来看一下,在我们的 RAG SDK 里面,这一类切割 chunk 技术具体应该怎么落地。

先说一个基本原则:

SDK 负责提供切割能力的抽象与最小默认实现,而具体使用哪一种切割策略,则由开发者来决定。

为什么要这样设计?

因为 chunk 切割虽然属于 indexing 阶段的核心能力,但不同项目对切割策略的要求差异很大:

  1. 有的文档结构规整,固定长度切分就够了;
  2. 有的文档层级分明,更适合递归切分;
  3. 有的文档非常依赖语义完整性,需要语义切分;
  4. 有的业务还会结合标题、章节、段落甚至模型能力来决定切分方式。

因此,SDK 不应该把所有切割策略都写死,而应该把“切割”抽象成统一接口,再让开发者按需接入具体实现。

在当前的 RAG SDK 中,这一层抽象就是 Chunker。

它的职责很单纯:接收一个 Document,返回一组 Chunk。至于内部到底是固定长度切、递归切,还是语义切,主流程并不关心。

基于这个设计,切割技术的落地方式大致可以分成下面几种。

1. 使用 SDK 内置的基础切割器

在当前实现里,SDK 提供了一个最基础的 SimpleChunker,它按照固定字符数进行切分,并支持 overlap。

这种方式适合:

  • demo
  • 单元测试
  • 规则比较简单的文档
  • 先把 indexing 主流程快速跑通

也就是说,SimpleChunker 更像是一个最小可用实现,同时也是默认兜底策略。

2. 通过 adapters 接入第三方切割器

如果开发者不满足于简单固定长度切分,就可以通过 adapters 去接入第三方生态中的 chunking 能力。

例如在当前项目里,已经有了 LangChain 的 TextSplitter 适配器。这样一来,开发者就可以直接使用 LangChain 里的切割器,例如:

  • RecursiveCharacterTextSplitter

然后通过适配器把它转换成 SDK 可消费的 Chunker。

这种方式非常适合像 Recursive Chunking 这样的技术,因为它本身已经有成熟的第三方实现,没必要在 SDK 内核里重复造一遍。

3. 由开发者自己实现自定义 Chunker

如果业务对切分规则有很强的定制需求,例如:

  • 要按特定标题层级切分
  • 要结合文档语义做切分
  • 要为不同类型的文档使用不同切分策略

那么最合适的方式,就是由开发者自己实现一个 Chunker。

这样做的好处是,SDK 只负责提供统一接口和主流程编排,而具体策略完全由业务侧掌控,不会把强业务逻辑耦合进核心包。

所以,像 Semantic Chunking 这类技术,更适合通过“自定义 Chunker”或者“第三方适配器”的方式接入,而不是直接内置到当前 SDK 的默认实现中。

总结一下,切割 chunk 技术在我们的 RAG SDK 中,落地方式遵循这样一个思路:

  1. indexing 提供 Chunker 抽象
  2. SDK 内置 SimpleChunker 作为最小默认实现
  3. adapters 负责接入 LangChain 等第三方切割器
  4. 开发者也可以自行实现更复杂的 chunking 策略

这样一来,SDK 既有最基本的可用能力,又保留了足够的扩展空间。

2. chunk增强 ​

所谓 chunk 增强,指的就是目前 chunk 已经有了,怎样让它更适合检索。

2-1. Contextual Headers ​

Contextual Chunk Headers 指的是在 chunk 前面补充上下文标题或结构信息,让 chunk 即使脱离原文,也仍然保留必要背景。因为文档切块后,一个 chunk 往往会失去它原本所在的章节语境。如果只看块内文本,可能不知道它在讲什么主题。

所以,会在 chunk 前增加类似:

  • 文档标题
  • 章节标题
  • 小节标题
  • 所属模块名称

这样的上下文头信息。

举个例子,假设原文来自一篇产品文档,标题层级如下:

  • 《支付系统文档》
  • 3.退款流程
  • 3.2 审核规则

某个 chunk 只包含一句:审核通过后,退款将在 1 到 3 个工作日内到账。

如果孤立看这句话,不一定知道它讲的是退款。

这时可以给 chunk 增加 contextual header:

支付系统文档 > 退款流程 > 审核规则 审核通过后,退款将在 1 到 3 个工作日内到账。

这样检索和生成时,语义会更完整。

2-2. Document Augmentation ​

Document Augmentation 指的是在索引前对原始文档做增强,补充额外信息,让后续检索效果更好。这种增强不一定改变原文主体内容,但会让文档更适合被切分、检索和理解。

增强的方式很多,例如:

  • 给文档补标题
  • 给段落补摘要
  • 给 FAQ 补标准问法
  • 给知识点补关键词
  • 给表格转自然语言描述

举个例子:

原文:

React Hooks 是一种...

增强

- summary: "介绍 Hooks 概念"
- keywords: ["React", "Hooks", "useState"]
- QA: "什么是 Hooks?"

React Hooks 是一种...

2-3. 元数据抽取 ​

元数据抽取指的是在索引阶段,从文档中提取结构化信息,放进 metadata,方便后续检索、过滤、排序和展示。这些 metadata 往往不是正文主体,但对系统很重要。

常见 metadata 包括:

  • 文档标题
  • 来源路径
  • 作者
  • 更新时间
  • 所属知识库
  • 标签
  • 权限级别
  • 章节标题

例子

一篇文档正文是:

所有正式员工每年享有 10 天带薪年假。

可以抽出的 metadata 有:

  • title: 年假制度
  • department: HR
  • audience: 正式员工
  • category: 假期政策

这样以后就可以做:

  • 只检索 HR 文档
  • 只检索正式员工适用内容
  • 检索后在界面展示来源标题

2-4. 如何落地 ​

上面这些能力虽然都属于 chunk 增强,但它们的落地方式并不完全一样。

在我们的 RAG SDK 中,核心思路仍然是:SDK 提供流程扩展点,具体增强策略由开发者按需接入。

为什么不直接把这些增强能力全部内置进去?

因为增强这一层,比 chunk 切分更依赖业务数据本身。不同项目在这一层的需求差异非常大:

  1. 有的项目只需要补充标题信息;
  2. 有的项目需要给 chunk 增加上下文 header;
  3. 有的项目需要提取复杂 metadata;
  4. 有的项目还会引入摘要、关键词、问答对等增强信息。

因此,SDK 更适合做的是提供稳定的挂载点,而不是把所有增强策略写死。

在当前的设计里,chunk 增强主要可以通过下面几个位置来落地。

1. 通过 metadataBuilder 做元数据增强

这是当前 SDK 里最直接、也最适合的一种落地方式。

开发者在调用 runIndexing 时,可以传入一个 metadataBuilder。这个函数会在 chunk 已经切好之后执行,用来给 chunk 补充额外 metadata。

因此,像下面这些能力,都非常适合通过它来做:

  • 元数据抽取
  • 补充标题信息
  • 记录来源路径
  • 增加文档分类、标签、权限信息
  • 给 chunk 标记所属章节

也就是说,metadataBuilder 更适合做“结构化增强”。

2. 通过 transformers 做文档级增强

如果增强发生在“文档层”,而不是“chunk 层”,那么更适合放在 transformers 里。

例如:

  • 给原始文档补标题
  • 给文档补摘要
  • 给 FAQ 补标准问法
  • 给表格转成自然语言
  • 给文档补关键词描述

这些增强通常发生在切 chunk 之前,因此更适合先对 Document 做处理,再交给后续 chunker 去切分。所以,像 Document Augmentation 这类能力,更适合通过 transformers 来落地。

3. 通过自定义 Chunker 做内容级增强

有些增强不是简单补 metadata,也不是纯文档预处理,而是会直接改变 chunk 内容本身。

Contextual Headers 就是一个很典型的例子。

因为它往往需要在 chunk 内容前面拼接:

  • 文档标题
  • 章节标题
  • 模块名称

这类增强通常和“怎么切 chunk”关系非常紧密,因此最自然的做法往往是:

  • 在自定义 Chunker 中直接处理
  • 或者在 chunking 之后增加一层 chunk 后处理逻辑

目前我们的 SDK 的设计,还没有单独的“chunk 后处理器”接口,所以如果开发者要做 Contextual Headers,更现实的方式通常是:

  • 自定义一个 Chunker
  • 或者在原有 chunker 基础上做二次封装

4. 第三方增强能力通过 adapters 接入

如果增强过程依赖外部生态,例如:

  • 使用 LangChain 的某种增强能力
  • 使用模型服务生成摘要或关键词
  • 使用第三方 parser 提取结构信息

那么更合适的方式,是先通过 adapters 把第三方能力接进来,再交给 transformers、Chunker 或 metadataBuilder 使用。

也就是说,adapters 解决的是“怎么接入外部能力”,而真正的增强逻辑仍然挂在 indexing 流程提供的扩展点上。

总结一下,chunk 增强这一层在我们的 RAG SDK 里,大致遵循下面这套落地方式:

  1. metadataBuilder:适合做结构化 metadata 增强
  2. transformers:适合做文档级增强
  3. 自定义 Chunker:适合做直接作用于 chunk 内容的增强
  4. adapters:适合接入第三方增强能力

这样设计的好处是,SDK 内核仍然保持简洁,只负责提供流程和扩展点;而真正和业务强相关的增强策略,则交给开发者按需实现。

3. 索引治理技术 ​

首先我们需要了解什么是索引。

所谓索引,就类似于书籍的目录、字典的索引,本质上是为了加速检索的。其实之前在关系型数据库时代,也有索引的概念。有了索引之后,就可以 避免全表扫描,能够更快的定位到要查找的内容。

RAG中一个典型的索引如下:

ts
type IndexItem = {
  id: string
  content: string        // chunk 内容
  embedding: number[]    // 向量
  metadata: {
    source: string
    title?: string
    tags?: string[]
  }
}

本质上就是一堆 chunk + embedding + metadata

3-1. 去重与清洗 ​

去重与清洗属于 索引前的数据治理

指的是在做索引之前,先把重复、噪音、脏数据处理掉,避免它们进入向量库。这是非常常见、也非常重要的一步。因为如果原始数据质量差,后面的 embedding 和检索再好,也很难得到理想效果。

常见操作包括:

  • 去除重复文档
  • 去除重复 chunk
  • 去掉空白内容
  • 去掉模板页眉页脚
  • 去掉乱码和无意义字符
  • 标准化格式

例子

假设公司知识库里有 3 份几乎一样的“报销制度”,只是文件名不同。如果不去重,检索时很可能召回 3 条几乎相同的 chunk,浪费上下文窗口。

再比如一些 PDF 解析出来后,每页都有:

公司内部资料 请勿外传

这类重复页眉如果不清洗,也会污染 chunk 内容。

3-2. Hierarchical Indexing ​

Hierarchical Indexing 指的是一种建立索引的方式。所谓分层,就是不只建立一种粒度的索引,而是同时建立多层级的知识表示。这样做的目的,是让系统在检索时可以先粗定位,再细召回,或者根据问题复杂度选择不同粒度的信息。

下面是普通索引和层级索引的对比,假设一篇文档长这样:

文档:员工手册

第 2 章 请假制度
  2.1 病假
    段落A:病假申请方式
    段落B:病假证明要求
  2.2 事假
    段落C:事假审批流程

如果没有 Hierarchical Indexing,你可能只是把它切成几个 chunk:

  • chunk_1
  • chunk_2
  • chunk_3

这些 chunk 在向量库里是平铺的,系统只知道:

  • 这是一段内容
  • 这是它的 embedding

但如果做了 Hierarchical Indexing,系统还会额外知道:

  • 这个 chunk 属于哪篇文档
  • 属于哪个 chapter
  • 属于哪个 section
  • 它前后相邻的 chunk 是谁
  • 它的父节点是谁

很多时候,存进去的并不是什么特别复杂的树结构,而是这种“带层级 metadata 的记录”。比如:

ts
type IndexItem = {
  id: string
  content: string
  embedding: number[]
  metadata: {
    doc_id: string
    doc_title: string
    chapter_id?: string
    chapter_title?: string
    section_id?: string
    section_title?: string
    parent_id?: string
    prev_id?: string
    next_id?: string
    level?: "document" | "section" | "chunk"
  }
}

也就是说,向量数据库里通常还是一条条记录,只是每条记录带了层级信息。

比如某个 chunk 可能是这样:

ts
{
  "id": "chunk_23",
  "content": "病假超过3天,需提供医院诊断证明。",
  "embedding": [0.12, -0.44, 0.73, ...],
  "metadata": {
    "doc_id": "doc_1",
    "doc_title": "员工手册",
    "chapter_id": "chapter_2",
    "chapter_title": "请假制度",
    "section_id": "section_2_1",
    "section_title": "病假",
    "parent_id": "section_2_1",
    "prev_id": "chunk_22",
    "next_id": "chunk_24",
    "level": "chunk"
  }
}

比如用户问:

病假需要什么证明

系统先检索到:

chunk_23: 病假超过3天,需提供医院诊断证明。

如果没有层级信息,系统可能只能把这句话单独返回。

但如果有 Hierarchical Indexing,系统就可以:

  • 看它的 section_id = section_2_1
  • 知道它属于“病假”这一节
  • 再把整个 section 拉出来
  • 或者再拼上 prev_id / next_id 对应的相邻 chunk

于是最终返回给模型的是更完整的上下文。

这就是层级索引真正的价值。

3-3. 增量更新 ​

增量更新属于 索引生命周期管理,指的是索引时不必每次都全量重建,而是只处理新增、修改或删除的那部分数据。

它解决的问题是:当知识库规模变大后,全量重建成本太高,效率太低。

增量更新通常依赖一些机制,例如:

  • 文档版本号
  • 更新时间
  • 内容哈希
  • 文件变更记录
  • chunk级diff

例子

假设你的知识库有 10000 篇文档,但今天只改了其中 5 篇。

如果没有增量更新,每次都要重新切分、embedding、写库全部 10000 篇,成本很高。

如果有增量更新,就只处理这 5 篇:

  • 新文档新增索引
  • 修改过的文档重新切块并覆盖旧向量
  • 删除的文档从向量库移除

这样索引效率会高很多。

3-4. 如何落地 ​

这一类能力,和前面的 chunk 切分、chunk 增强不太一样。

  • 前两类能力,核心是“怎么把文档处理成更适合检索的 chunk”
  • 这一类能力更关注的是:如何让整个索引系统长期保持可用、稳定、可维护。

因此,它们在 SDK 中的落地方式也会有所不同:有些适合直接提供扩展点,有些则更适合放到后续阶段再逐步实现。

1. 去重与清洗:由 SDK 提供入口,由开发者提供规则

去重与清洗最适合落到 transformers 和 shouldIndex 这两个扩展点上。

原因很简单,这类能力高度依赖数据源:

  • Markdown 文档怎么清洗
  • PDF 内容怎么去页眉页脚
  • FAQ 数据怎么去重
  • 哪些文档应该直接跳过

这些规则很难由 SDK 统一写死,因此更合理的做法是:

  • transformers 负责做文档清洗、格式规范化、噪音移除
  • shouldIndex 负责决定某个文档是否值得进入索引流程

也就是说,SDK 提供的是清洗和过滤的挂载点,而具体去重与清洗规则由开发者根据业务数据来实现。

2. Hierarchical Indexing:当前不建议放进内核,属于后续高级能力

层级索引虽然属于 indexing 阶段,但它并不是在现有流程里简单加一个步骤就能完成的。

因为一旦引入层级索引,就意味着很多基础设计都要跟着变化,例如:

  • 索引结果不再只有一种粒度
  • store 里要保存多层级结构
  • retrieval 阶段也要理解这些层级关系
  • runtime 可能要支持“先文档、再章节、再 chunk”的消费方式

因此,这类能力不太适合在当前 MVP 阶段直接塞进 indexing 内核。

更合理的做法是:

  1. 当前先保持单层 chunk 索引模型
  2. 如果业务真的需要层级索引,先在业务层自行维护
  3. 等后续 runtime 和索引存储协议都稳定后,再作为高级索引能力单独设计

也就是说,Hierarchical Indexing 更像是未来演进方向,而不是当前阶段要立即内置的能力。

3. 增量更新:长期属于 indexing,但当前阶段先不实现

增量更新从归属上看,确实应该属于 indexing,因为它本质上就是索引生命周期管理的一部分。

但是它和普通索引流程相比,会额外引入很多问题:

  • 怎么判断文档是否发生变化
  • 怎么识别旧 chunk 需要被删除
  • chunk id 是否要保持稳定
  • store 是否支持删除和替换
  • 如何保证重复执行时结果一致

这些问题一旦展开,会直接影响当前索引流程、向量存储接口,甚至结果统计结构。

因此,对当前 SDK 来说,更合理的落地方式不是马上实现,而是先在设计上留出空间。例如:

  • 保持 document.id 稳定
  • 保持 chunk id 规则可预测
  • 后续让 VectorStore 具备删除或覆盖能力

等这些基础条件成熟之后,再正式支持增量更新。

所以,这一类能力在我们的 RAG SDK 中,大致遵循这样一套落地思路:

  1. 去重与清洗:当前就可以落地,通过 transformers 和 shouldIndex 由开发者提供规则
  2. Hierarchical Indexing:属于高级索引形态,当前先不进入内核
  3. 增量更新:长期应属于 indexing,但当前阶段先不实现,只保留演进空间

这样做的好处是,SDK 既能覆盖当前最需要的基础能力,又不会在还没稳定核心流程的时候,过早引入过重的复杂度。


-EOF-