# 文档转换器与字符分割器组件的使用

文档加载器读取到 Document 后,通常还不能直接写入向量库。原始文档可能存在内容过长、格式不统一、语言不一致、噪声过多、metadata 不完整等问题。

因此,在 RAG 数据入库前,经常需要执行文档转换。文档转换的输入是 Document 列表,输出仍然是 Document 列表,只是内容、结构或 metadata 被处理过。

RAG 架构中的文档转换与加载链路

常见转换动作包括:

  • 文档切分。
  • 文档合并。
  • 文档过滤。
  • 文档翻译。
  • HTML 转文本。
  • 内容压缩。
  • metadata 标记。
  • 关键信息抽取。

其中,文本分割器是最常见的一类文档转换器。

# DocumentTransformer

LangChain 中的文档转换器基类是 BaseDocumentTransformer。它的核心方法是:

方法 作用
transform_documents() 同步转换文档列表
atransform_documents() 异步转换文档列表

从职责上看,DocumentTransformer 不关心文档从哪里来,也不关心最终写入哪个向量库。它只负责把传入的 Document 列表转换成新的 Document 列表。

这让 RAG 入库链路可以拆成:

Loader -> Document -> Transformer -> Splitter -> Embedding -> VectorStore
1

文档转换器与文本分割器类关系

# TextSplitter

TextSplitter 可以看作一类特殊的 DocumentTransformer。它负责把长文本拆成更小的 chunk,让后续向量检索更加稳定。

如果不做切分,长文档直接入库会带来几个问题:

  • 文档太长,向量表达会被大量无关内容稀释。
  • 检索命中后,放进 Prompt 的内容太大,浪费 token。
  • 长文档可能超过模型上下文限制。
  • 答案所在位置被大量上下文包围,模型难以定位。

如果切得太碎,也会有问题:

  • 每个 chunk 信息不足。
  • 原本完整的语义被切断。
  • 相邻 chunk 缺少上下文。

所以分割器的核心目标不是“切得越小越好”,而是让 chunk 足够短、足够完整、足够适合召回。

# 字符分割器示例

字符分割器可以根据指定分隔符切分文本,再按照 chunk 大小和重叠长度合并成文档片段。

示例代码:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter

# 1. 加载对应的文档
loader = UnstructuredMarkdownLoader("./项目API文档.md")
documents = loader.load()

# 2. 构建文本分割器
text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=500,
    chunk_overlap=50,
    add_start_index=True,
)

# 3. 分割文本
chunks = text_splitter.split_documents(documents)

for chunk in chunks:
    print(f"块大小:{len(chunk.page_content)}, 元数据:{chunk.metadata}")

print(len(chunks))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这段代码中:

参数 作用
separator 优先使用的文本分隔符
chunk_size 每个 chunk 的目标大小
chunk_overlap 相邻 chunk 的重叠长度
add_start_index 是否在 metadata 中记录起始位置

add_start_index=True 很重要。它会在 metadata 中记录 chunk 在原文中的起始位置,后续做引用、回溯和问题排查时很有用。

# 切分后的 metadata

分割器处理 Document 时,通常会继承原始 Document 的 metadata,并追加切分相关字段。例如原始文档中有 source,切分后每个 chunk 仍然应该保留 source。

如果启用 add_start_index=True,metadata 中还会出现 start_index,用于表示该 chunk 在原文中的起始字符位置。

示例:

{
    "source": "./项目API文档.md",
    "start_index": 6579,
}
1
2
3
4

这类 metadata 对生产排查非常关键。比如用户反馈答案不对,可以根据 source 和 start_index 回到原始文档定位上下文。

# 最新版 LangChain 用法提示

最新版 LangChain 中,文本分割器已经拆到 langchain_text_splitters 包中。通用文本场景下,官方更推荐 RecursiveCharacterTextSplitter,因为它会优先保留段落、句子、词等自然结构。

字符分割器仍然可以使用,但更适合格式非常稳定、明确知道分隔符的文本。例如日志块、固定格式段落、双换行分段的 Markdown。

更推荐的通用写法:

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    add_start_index=True,
)

chunks = text_splitter.split_documents(documents)
1
2
3
4
5
6
7
8
9

如果明确要按双换行切分,可以继续使用 CharacterTextSplitter:

from langchain_text_splitters import CharacterTextSplitter

text_splitter = CharacterTextSplitter(
    separator="\n\n",
    chunk_size=500,
    chunk_overlap=50,
    add_start_index=True,
)
1
2
3
4
5
6
7
8

最新版写法中,重点不是“必须换掉 CharacterTextSplitter”,而是要根据文本结构选择分割器:通用文本优先递归字符分割器,结构明确时可以用字符分割器。

# 拓展

文档转换器不只有切分。实际 RAG 入库前还可以加入多种转换:

  • 清洗转换:去掉空白、页眉页脚、导航噪声。
  • 过滤转换:删除过短、无意义或重复文档。
  • 翻译转换:把多语言内容统一成目标语言。
  • 摘要转换:为长文档生成摘要字段。
  • 问答转换:把陈述型内容转换成问答形式。
  • metadata 增强:增加分类、标签、权限字段。

这些转换可以串联起来,但要控制复杂度。每加一个转换步骤,都应该能解释它解决什么问题,并能回放转换前后的效果。

# 常见问题

# chunk_size 应该设置多大?

没有固定答案。常见做法是从 500 到 1200 字符或相应 token 范围开始试,结合文档类型、Embedding 模型、LLM 上下文长度和评估集调整。

# chunk_overlap 越大越好吗?

不是。重叠可以保留上下文,但过大会导致重复内容变多、索引膨胀、检索结果重复。一般先设置为 chunk_size 的 10% 到 20%,再根据效果调整。

# 为什么切分后检索结果重复?

可能是 overlap 太大,也可能是原文重复、chunk 太小、top_k 过高。可以降低 overlap、去重、使用 MMR 或增加重排序。

# 面试题

# DocumentTransformer 和 TextSplitter 的关系是什么?

DocumentTransformer 是文档转换器的抽象,TextSplitter 可以看作一种特殊的文档转换器,用于把长 Document 切分成多个小 Document。

# CharacterTextSplitter 和 RecursiveCharacterTextSplitter 有什么区别?

CharacterTextSplitter 主要按指定分隔符切分;RecursiveCharacterTextSplitter 会按分隔符优先级递归切分,尽量保留段落、句子等自然结构。

# 为什么 RAG 入库前必须切分文档?

因为长文档直接入库会降低向量表达质量,也会让检索结果过长、噪声过多。合理切分能提高召回质量和上下文利用效率。

# 生产问题排查

问题 常见原因 处理方式
检索不到答案 chunk 太大或切断关键语义 调整 chunk_size,增加 overlap,换递归分割器
检索结果重复 overlap 太大、原文重复、top_k 过高 降低 overlap,去重,使用 MMR
答案缺少上下文 chunk 太小或没有重叠 增大 chunk_size 或 chunk_overlap
无法回溯原文 metadata 缺少 source/start_index 开启 add_start_index,保留 source
token 成本过高 chunk 太大、top_k 太高 控制 chunk_size 和 top_k
表格内容被切乱 普通文本分割器不理解表格 对表格单独解析或使用结构化切分策略