理解文档切分的五大策略,深入剖析 TextSplitter 父类中三个核心方法的源码逻辑及其调用关系。
| 原因 | 说明 |
|---|
| Token 限制 | 大模型有上下文长度限制,完整文档可能超过上限导致截断和信息缺失 |
| 检索精准度 | 大文档中包含大量无关信息,会干扰大模型输出;小块检索更精准 |
| 成本控制 | 不必要的内容会消耗更多 Token,增加 API 调用成本 |
| 策略 | 描述 | 优点 | 缺点 |
|---|
| 按句子切分 | 以句号为分隔符 | 语义完整 | 粒度可能不均匀 |
| 固定字符数切分 | 达到指定字符数就切一刀 | 简单可控 | 可能造成语义断裂 |
| 固定字符数 + 重叠窗口 | 在固定切分基础上,前后 chunk 之间有重叠 | 缓解语义断裂 | 会产生冗余内容 |
| 递归字符切分 ⭐ | 按优先级依次尝试分隔符(段落 → 换行 → 空格 → 字符),递归切分直到满足 chunk size | 兼顾语义完整性和长度控制(最常用) | 实现稍复杂 |
| 按语义内容切分 | 基于向量相似度判断语义边界 | 语义最优 | 需要模型参与,成本高、速度慢,chunk 大小不均匀 |
class TextSplitter:
def split_text(self, text: str) -> List[str]: ...
def create_documents(
self, texts: List[str], metadatas: list = None
) -> List[Document]: ...
def split_documents(
self, documents: List[Document]
) -> List[Document]: ...
split_documents (方法三)
└── 提取每个 Document 的 page_content → 构成 List[str]
└── create_documents (方法二)
└── 遍历每个字符串 → 调用 split_text (方法一)
└── 将切分后的字符串封装为 Document 对象
| 参数 | 默认值 | 说明 |
|---|
chunk_size | 4000 | 每个 chunk 的最大字符数 |
chunk_overlap | 200 | 相邻 chunk 的重叠字符数 |
length_function | len | 计算文本长度的函数(默认 Python 内置 len) |
separator | \n\n | 分隔符(子类可自定义) |
注意:chunk_size 和 chunk_overlap 的单位是字符(characters),不是 Token 或字节。
| 你的输入是什么 | 应调用的方法 |
|---|
一段纯文本 str | split_text(text) |
多个纯文本 List[str] | create_documents(texts) |
多个 Document List[Document] | split_documents(documents) |
三个方法本质是层层封装的关系,没有本质区别,根据输入类型选择即可。
def split_text(self, text: str) -> List[str]:
"""将单段文本切分为多个字符串"""
...
def create_documents(
self, texts: List[str], metadatas: Optional[List[dict]] = None
) -> List[Document]:
"""底层调用了 split_text"""
documents = []
for i, text in enumerate(texts):
for chunk in self.split_text(text):
documents.append(Document(
page_content=chunk,
metadata=metadatas[i] if metadatas else {}
))
return documents
def split_documents(
self, documents: List[Document]
) -> List[Document]:
"""底层调用了 create_documents"""
texts = [doc.page_content for doc in documents]
metadatas = [doc.metadata for doc in documents]
return self.create_documents(texts, metadatas)
from langchain.text_splitter import CharacterTextSplitter
splitter = CharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separator="\n"
)
text = "这是一段很长的文本内容..."
chunks = splitter.split_text(text)
texts = ["第一段文本", "第二段文本"]
docs = splitter.create_documents(texts)
from langchain_community.document_loaders import TextLoader
loader = TextLoader("./document.txt")
documents = loader.load()
split_docs = splitter.split_documents(documents)
- chunk_size 单位混淆:明确是字符数而非 Token 数。中文 1 个汉字 = 1 个字符,英文 1 个字母 = 1 个字符。
- chunk_overlap 单位同样为字符:设置重叠窗口大小时注意与 chunk_size 的比例关系。
- 抽象方法不能直接调用:
split_text 是抽象方法,必须通过子类实例调用,不能直接实例化 TextSplitter。 - 按语义切分的代价:虽然效果最好,但需要模型参与向量化计算,增加时间和费用成本,且 chunk 大小极不均匀。
- 递归切分是最佳实践:兼顾了语义完整性和长度控制,是当前最主流的方案。
- 父类设计模式:TextSplitter 遵循模板方法模式:在父类中定义了
create_documents 和 split_documents 的骨架逻辑,将核心切分算法 split_text 作为抽象方法留给子类实现。这种设计使得所有 Splitter 对外接口完全统一。 - 方法选择策略:实际开发中最常用的是
split_documents(documents),因为上游 Loader 产出的就是 List[Document],可以直接对接。只有在特殊场景(如手动构造测试文本)时才使用前两个方法。 - 与 Loader 的衔接:
BaseLoader 也有 load_and_split(text_splitter) 方法,可以直接在加载环节完成切分。但建议分开操作(先 load 再 split),便于调试中间结果和灵活调整切分参数。 - 接下来的重点:掌握 CharacterTextSplitter 和 RecursiveCharacterTextSplitter 两个最常用的子类,理解其 separator 和 separators 参数的差异。