Appearance
indexing相关技术名词
接下来我们来看一下 indexing 阶段你需要掌握技术名词:
- Semantic Chunking
- Recursive Chunking
- Contextual Headers
- Document Augmentation
- Hierarchical Indexing
- 元数据抽取
- 去重与清洗
- 增量更新
这一大堆名词里面,其实可以分成这么三类:
- 切割 chunk 技术
- chunk 增强
- 索引治理
1. 切割 chunk 技术
顾名思义,就是将文档做切块的时候,具体怎么去切。
1-1. Semantic Chunking
按照语义边界来切分文档,而不是单纯按照固定字符数或 token 数切。
它的核心目标是:让每个 chunk 尽量保持语义完整,避免一句话被硬拆开,或者一段完整说明被截断。
常见做法包括:
- 按句子切
- 按段落切
- 按标题层级切
- 按语义相似度变化点切
举个例子:
- 错误切分(固定长度):
[React Hooks 是...] [一个函数式编程...] [它允许你...] - 语义切分:
[React Hooks 是什么 + 为什么出现][useState 的作用][useEffect 的作用]
1-2. Recursive Chunking
Recursive Chunking 指的是递归式切分策略。它不是一上来就按固定长度硬切,而是按一组优先级分隔符逐层尝试切分,例如:
- 先按标题或段落分
- 如果某段还是太长,再按句子分
- 如果句子还太长,再按标点分
- 最后实在不行,再按字符或 token 长度切
它本质上是一种“尽量保留语义结构,同时满足长度约束”的折中策略。
举个例子,一篇技术文档很长:
- 先按
##标题切成几个部分; - 其中某个部分仍然超长,就再按段落切;
- 某段还太长,再按句子切。
这样切出来的 chunk 通常比简单固定长度切分更自然。
NOTE
langchain 里的 RecursiveCharacterTextSplitter 就是这个思路的代表实现。
1-3. 如何落地
下面来看一下,在我们的 RAG SDK 里面,这一类切割 chunk 技术具体应该怎么落地。
先说一个基本原则:
SDK 负责提供切割能力的抽象与最小默认实现,而具体使用哪一种切割策略,则由开发者来决定。
为什么要这样设计?
因为 chunk 切割虽然属于 indexing 阶段的核心能力,但不同项目对切割策略的要求差异很大:
- 有的文档结构规整,固定长度切分就够了;
- 有的文档层级分明,更适合递归切分;
- 有的文档非常依赖语义完整性,需要语义切分;
- 有的业务还会结合标题、章节、段落甚至模型能力来决定切分方式。
因此,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 中,落地方式遵循这样一个思路:
- indexing 提供 Chunker 抽象
- SDK 内置 SimpleChunker 作为最小默认实现
- adapters 负责接入 LangChain 等第三方切割器
- 开发者也可以自行实现更复杂的 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 切分更依赖业务数据本身。不同项目在这一层的需求差异非常大:
- 有的项目只需要补充标题信息;
- 有的项目需要给 chunk 增加上下文 header;
- 有的项目需要提取复杂 metadata;
- 有的项目还会引入摘要、关键词、问答对等增强信息。
因此,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 里,大致遵循下面这套落地方式:
metadataBuilder:适合做结构化 metadata 增强transformers:适合做文档级增强- 自定义
Chunker:适合做直接作用于 chunk 内容的增强 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 内核。
更合理的做法是:
- 当前先保持单层 chunk 索引模型
- 如果业务真的需要层级索引,先在业务层自行维护
- 等后续
runtime和索引存储协议都稳定后,再作为高级索引能力单独设计
也就是说,Hierarchical Indexing 更像是未来演进方向,而不是当前阶段要立即内置的能力。
3. 增量更新:长期属于 indexing,但当前阶段先不实现
增量更新从归属上看,确实应该属于 indexing,因为它本质上就是索引生命周期管理的一部分。
但是它和普通索引流程相比,会额外引入很多问题:
- 怎么判断文档是否发生变化
- 怎么识别旧 chunk 需要被删除
- chunk id 是否要保持稳定
- store 是否支持删除和替换
- 如何保证重复执行时结果一致
这些问题一旦展开,会直接影响当前索引流程、向量存储接口,甚至结果统计结构。
因此,对当前 SDK 来说,更合理的落地方式不是马上实现,而是先在设计上留出空间。例如:
- 保持
document.id稳定 - 保持 chunk id 规则可预测
- 后续让
VectorStore具备删除或覆盖能力
等这些基础条件成熟之后,再正式支持增量更新。
所以,这一类能力在我们的 RAG SDK 中,大致遵循这样一套落地思路:
- 去重与清洗:当前就可以落地,通过
transformers和shouldIndex由开发者提供规则 - Hierarchical Indexing:属于高级索引形态,当前先不进入内核
- 增量更新:长期应属于
indexing,但当前阶段先不实现,只保留演进空间
这样做的好处是,SDK 既能覆盖当前最需要的基础能力,又不会在还没稳定核心流程的时候,过早引入过重的复杂度。
-EOF-