Skip to content

四、文档切分

4.1 为什么要切分

获取 Document 对象后,需要将其切分成 Chunk(文本块),以 Chunk 为基本单位进行存储和检索。

问题说明
Token 限制LLM 的上下文窗口有限,无法一次性处理整篇长文档
检索不精确如果不切分,检索时返回整篇文档,噪声过多,LLM 容易产生幻觉
向量化成本高整篇文档做 Embedding,计算和存储成本都很高
语义模糊嵌入模型对短文本的语义表达更精确,长文本的向量表示会"稀释"关键信息

4.2 切分的核心考量

既然要切分,那么怎么切、在哪儿切就成了关键问题。切分需要平衡三个维度:

考量维度说明过犹不及的后果
长度控制每个块不能太大,也不能太小太大:检索噪声多;太小:语义不完整
语义完整尽量不让句子被切断,保持意思连贯断句会导致上下文断裂,影响理解
边界合理在自然分隔处切分,而非强行截断强行截断会丢失重要信息

基于这些考量,LangChain 提供了多种切分策略供选择。

4.3 切分策略对比

策略原理优点缺点
固定长度切分按固定字符/Token数切分简单直接,块大小均匀可能在不当位置断句
语义切分对相邻句子嵌入,找语义变化大的位置作为切分点语义完整性最好速度慢,块大小不均衡
递归多分隔符切分依次尝试多个分隔符切分,优先用大分隔符,逐步降级到小分隔符保持语义完整,块大小可控需要调整分隔符顺序

4.4 递归字符切分器

为什么选择它? 固定长度切分太粗暴,语义切分太慢且块大小不可控。递归字符切分器介于两者之间——既保证语义完整性,又能控制块大小,是实际应用中最常用的折中方案。

4.4.1 工作原理

核心思想:定义分隔符优先级列表(如 ["\n\n", "\n", " ", ""]),按优先级依次尝试切分:

  1. 先用最高优先级分隔符(如 \n\n)切分整段文本
  2. 检查每个切分块是否超过 chunk_size
  3. 若超限,对该块降级使用下一级分隔符继续切分
  4. 递归直到所有块都不超限,或降级到字符级切分(""

分隔符优先级的语义含义

分隔符语义边界说明
\n\n段落边界最优先保持段落完整
\n行边界段落超限时,尝试保持行完整
词边界行超限时,尝试保持词完整
""字符级最后手段,强制按字符截断

关键结论

  • 分隔符优先级越高,切分越粗粒度,语义完整性越好
  • 当分隔符无法匹配时,自动降级到下一级

4.4.2 重叠的作用

相邻块之间保留一部分重叠内容,防止切分边界处丢失关键信息。

4.4.3 代码示例

python
# pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import UnstructuredWordDocumentLoader

# 加载文档
docs = UnstructuredWordDocumentLoader(
    file_path="assets/sample.docx",
    mode="single"
).load()

# 切分为文本块
chunks = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。", "!", "?", "……", ",", ""],  # 分隔符优先级列表
    chunk_size=400,        # 每个块的最大长度
    chunk_overlap=50,      # 相邻块重叠长度
    length_function=len,   # 长度计算函数
).split_documents(docs)

# 统计块数量
print(f"切分得到 {len(chunks)} 个文本块\n")

# 可选:查看前几个块内容和元数据
for i, chunk in enumerate(chunks[:5]):  # 只看前 5 个
    print(f"=== Chunk {i} ===")
    print(chunk.page_content)
    print("metadata:", chunk.metadata)
    print("============\n")

切分后,每个 Chunk 仍然是一段文本。为了让计算机能够衡量"语义上有多相似",我们需要把文本转化为数值向量——这就是下一步"嵌入"要做的事。