006:RAG 入门-面试官问你,RAG 为什么要切块?

本文是 refine-rag 系列教程的第六篇,我们来学习一下什么是切块,应该怎么样去切块?
本文所有代码都在:https://github.com/zonezoen/refine-rag

前言

前面花费了挺多时间去学习怎么读取数据的,那么在读取数据之后,我们要做的操作就是切块,那么什么是切块?为什么要切块呢?往下看

什么是切块?为什么要切块?

简单来说,切块就是把“长篇大论”切成“语义胶囊”。

它把成千上万字的文章,按照一定的逻辑(比如每 500 字一段,或者按段落)切分成一个个独立的小块。这样,AI 在处理信息时,就不需要每次都“读全集”,而是“精准点餐”。

那为什么要切块的?有以下几点原因:

  1. 大模型的上下文有 token 的长度限制
  2. 切块可以突出表示某个向量的特征,如果说某个大文本的向量能表示10个特征,那么这个向量表达就会很模糊,会影响检索精度
  3. 嵌入模型有限制,大多数主流的 Embedding 模型(如 text-embedding-3-small)通常只能处理 512 或 8192 个 Token
  4. 省钱,大模型 api 是按 toekn 收费的,省时间,上下文长度越长,大模型处理的时间越长

怎么切块?

目前常用的切块方法有 5 种,下面我们逐一介绍:

1. 固定切块(CharacterTextSplitter)

最简单的切块方式,按固定字符数分割文本。这种切块方式很简单,但是缺点也很明显:很容易切断语义完整性,比如:一个句子被切分成了两个块,那么这个句子的语义就会丢失。

文件名: 01-固定切块.py

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter

loader = TextLoader("../99-doc-data/黑悟空/黑悟空wiki.txt")
data = loader.load()

text_splitter = CharacterTextSplitter(
    chunk_size=100,      # 每个块的大小
    chunk_overlap=5      # 块之间的重叠大小
)

chunks = text_splitter.split_documents(data)
for chunk in chunks:
    print("====== 切块分页 ======")
    print(chunk.page_content)

参数说明:

  • chunk_size=100:每个块最多 100 个字符
  • chunk_overlap=5:相邻块之间重叠 5 个字符(避免信息丢失)

优点:

  • 实现简单,速度快
  • 块大小可控

缺点:

  • 容易在句子中间断开
  • 破坏语义完整性

适用场景: 结构简单的文本、需要快速处理的场景

2. 递归切块(RecursiveCharacterTextSplitter)

按优先级递归尝试不同的分隔符,保持文本结构。

文件名: 02-递归切块.py

from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = TextLoader("../99-doc-data/黑悟空/黑悟空wiki.txt")
data = loader.load()

# 定义分割符列表,按优先级依次使用
separators = [
    "\n\n",  # 双换行符(段落分隔)
    "\n",    # 单换行符(行分隔)
    "。",    # 中文句号
    ".",     # 英文句号
    "!",    # 中文感叹号
    "!",     # 英文感叹号
    "?",    # 中文问号
    "?",     # 英文问号
    ";",    # 中文分号
    ";",     # 英文分号
    ",",    # 中文逗号
    ",",     # 英文逗号
    " ",     # 空格
    ""       # 最后按字符分割
]

recursive_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=5,
    separators=separators,
    length_function=len
)

r_chunks = recursive_text_splitter.split_documents(data)
for chunk in r_chunks:
    print("====== 递归切块分页 ======")
    print(chunk.page_content)

工作原理:

  1. 先尝试用 \n\n(段落)分割
  2. 如果块还是太大,用 \n(行)分割
  3. 如果还是太大,用 (句号)分割
  4. 依此类推,直到满足 chunk_size

优点:

  • 保持文本结构(段落、句子)
  • 比固定切块更智能
  • 适用范围广

缺点:

  • 仍可能在不合适的地方断开

适用场景: 普通文章、博客、没有明确标题的文本

3. 代码切块(Language-specific Splitter)

专门为代码设计的切块器,保持代码结构完整。

文件名: 03-代码切块.py

from langchain_text_splitters import Language, RecursiveCharacterTextSplitter

GAME_CODE = """
class CombatSystem:
   def __init__(self):
       self.health = 100
       self.stamina = 100
   
   def update(self, delta_time):
       self._update_stats(delta_time)
       self._handle_combat()

class InventorySystem:
   def __init__(self):
       self.items = {}
       self.capacity = 20
   
   def add_item(self, item_id, quantity):
       if item_id in self.items:
           self.items[item_id] += quantity
       else:
           self.items[item_id] = quantity
"""

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=0
)

py_docs = python_splitter.create_documents([GAME_CODE])
for i, chunk in enumerate(py_docs, 1):
    print(f"\n--- 第 {i} 个代码块 ---")
    print(chunk.page_content)

支持的主流的20多种语言,工作原理: 按类、函数、方法等代码结构分割,保持代码的完整性和可读性

优点:

  • 保持代码结构完整
  • 不会在函数中间断开
  • 支持多种编程语言

缺点:

  • 仅适用于代码

适用场景: 代码文档、技术教程、API 文档

4. 语义切块(SemanticChunker)

最智能的切块方式,根据语义相似度分割文本。

文件名: 04-LangChain-语义分块-DeepSeek.py

from langchain_community.document_loaders import TextLoader
from langchain_experimental.text_splitter import SemanticChunker
from langchain_huggingface import HuggingFaceEmbeddings

# 1. 加载文档
loader = TextLoader("../99-doc-data/黑悟空/黑悟空wiki.txt", encoding="utf-8")
docs = loader.load()

# 2. 设置嵌入模型(用于计算语义相似度)
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5",
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True}
)

# 3. 创建语义分块器
semantic_splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",  # 使用百分位数阈值
    breakpoint_threshold_amount=90           # 90% 不相似度时分割
)

# 4. 执行语义分块
semantic_chunks = semantic_splitter.split_documents(docs)

for i, chunk in enumerate(semantic_chunks, 1):
    print(f"\n--- 第 {i} 个语义块 ---")
    print(f"长度: {len(chunk.page_content)} 字符")
    print(f"内容: {chunk.page_content[:200]}...")

工作原理:首先将文本按句子分割,然后计算相邻句子的语义相似度(使用 embedding),如果相似度低于阈值时,创建新的分块

示例对比:

传统分块(可能在句子中间断开):

块1: "黑神话悟空是一款动作游戏。游戏基于西游"
块2: "记改编。主角是孙悟空的转世。"

语义分块(保持语义完整):

块1: "黑神话悟空是一款动作游戏。游戏基于西游记改编。"  # 游戏介绍
块2: "主角是孙悟空的转世。"  # 角色介绍

参数说明:

  • breakpoint_threshold_type="percentile":使用百分位数阈值(推荐)
  • breakpoint_threshold_amount=90:90% 不相似度时分割
    • 85:块很多(细粒度)
    • 90:块适中(推荐)
    • 95:块较少(粗粒度)

优点:

  • 保持语义连贯性
  • 自动识别主题边界
  • 检索质量最高

缺点:

  • 速度慢(比传统分块慢 50 倍)
  • 需要 embedding 模型

适用场景: 问答系统、长文档分析、高质量检索

5. 按段落和标题切块(最推荐)

最实用的切块方式,按文档的自然结构(标题、段落)分割。

文件名: 05-按段落标题切块.py

2.1 Markdown 标题切块

from langchain_text_splitters import MarkdownHeaderTextSplitter

markdown_document = """
# 黑神话:悟空

## 游戏简介
《黑神话:悟空》是一款动作角色扮演游戏。

## 游戏玩法
### 战斗系统
游戏的战斗系统流畅爽快。

### 技能系统
玩家可以学习72变等经典技能。
"""

# 定义要按哪些标题级别切块
headers_to_split_on = [
    ("#", "一级标题"),      # H1
    ("##", "二级标题"),     # H2
    ("###", "三级标题"),    # H3
]

# 创建 Markdown 标题切块器
markdown_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers=False  # 保留标题在内容中
)

# 执行切块
md_chunks = markdown_splitter.split_text(markdown_document)

for chunk in md_chunks:
    print(f"内容: {chunk.page_content}")
    print(f"元数据: {chunk.metadata}")  # 包含标题层级信息

输出示例:

内容: # 黑神话:悟空
元数据: {'一级标题': '黑神话:悟空'}

内容: ## 游戏简介
《黑神话:悟空》是一款动作角色扮演游戏。
元数据: {'一级标题': '黑神话:悟空', '二级标题': '游戏简介'}

内容: ### 战斗系统
游戏的战斗系统流畅爽快。
元数据: {'一级标题': '黑神话:悟空', '二级标题': '游戏玩法', '三级标题': '战斗系统'}

2.2 段落切块

from langchain_text_splitters import RecursiveCharacterTextSplitter

# 段落之间用双换行符分隔的文本
paragraph_document = """
《黑神话:悟空》是一款动作角色扮演游戏。

游戏采用虚幻引擎5开发,画面表现力极强。

玩家在游戏中扮演孙悟空的转世。
"""

# 优先按段落(双换行符)分割
paragraph_splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。", " ", ""],
    chunk_size=500,
    chunk_overlap=50
)

chunks = paragraph_splitter.split_text(paragraph_document)

2.3 混合切块(推荐)

# 先按标题切块
md_chunks = markdown_splitter.split_text(markdown_document)

# 再对大块进行段落切分
final_chunks = []
for header_chunk in md_chunks:
    if len(header_chunk.page_content) > 200:
        # 块太大,再按段落切分
        sub_splitter = RecursiveCharacterTextSplitter(
            separators=["\n\n", "\n", "。", " ", ""],
            chunk_size=200,
            chunk_overlap=20
        )
        sub_chunks = sub_splitter.split_text(header_chunk.page_content)
        
        # 保留标题元数据
        for sub_chunk in sub_chunks:
            from langchain_core.documents import Document
            final_chunks.append(Document(
                page_content=sub_chunk,
                metadata=header_chunk.metadata
            ))
    else:
        final_chunks.append(header_chunk)

优点:

  • 保持逻辑完整性
  • 每个块都有明确的主题
  • 元数据包含标题信息,便于溯源

缺点:

  • 需要文档有明确的结构

适用场景: 说明书、论文、技术文档、Markdown 文档

切块方法对比

以下各种切块策略对比,可以根据使用场景来选择,当然了,也可以多种组合使用,比如技术文档,先基于 markdown 分段,然后再使用递归或者代码切块。

切块策略 速度 质量 适用场景 推荐度
固定切块 ⚡⚡⚡ ⭐⭐ 简单文本、快速处理、要求较低的需求 ⭐⭐
递归切块 ⚡⚡⚡ ⭐⭐⭐⭐ 普通文章、博客、无明显分段的文章 ⭐⭐⭐⭐
代码切块 ⚡⚡⚡ ⭐⭐⭐⭐ 代码文档、技术教程 ⭐⭐⭐⭐
语义切块 ⭐⭐⭐⭐⭐ 问答系统、长文档 ⭐⭐⭐⭐
段落标题切块 ⚡⚡⚡ ⭐⭐⭐⭐⭐ 说明书、论文、Markdown、HTML 格式文档 ⭐⭐⭐⭐⭐

学习路径

  1. 简易RAG 学习
  2. LCEL 语法学习
  3. LangChain 读取数据
    1. LangChain 读取文本数据
    2. LangChain 读取图片数据
    3. LangChain 读取 PDF 数据
    4. LangChain 读取表格数据
  4. 文本切块
  5. 向量嵌入
  6. 向量存储
  7. 检索前处理
  8. 索引优化
  9. 检索后处理
  10. 响应生成
  11. 系统评估

项目地址

本文所有代码示例都在 GitHub 开源:

https://github.com/zonezoen/refine-rag

欢迎 Star 和 Fork,一起学习 RAG 技术!

posted @ 2026-03-10 17:11  zone7  阅读(105)  评论(0)    收藏  举报