RAG 学习笔记:从分块到检索优化

RAG 学习笔记:从分块到检索优化

这是我的 RAG 系列学习总笔记,包含两大部分:基础分块(把文档切好的 6 种方式)和检索优化(让检索更准更全的 10 种方法)。
每个知识点都配有:生活化比喻 → 原理 → 带注释的可运行代码 → 真实运行结果 → 踩坑记录。

学习路线图(本笔记在 RAG 全流程中的位置)

RAG 完整流程: 文档加载 → 【分块】→ 向量化 → 【检索优化】→ 重排序 → 生成
                              ↑                ↑
                        第一部分讲这里     第二部分讲这里
  • 第一部分:基础分块——切好块是地基(6 种方式:固定长度/递归字符/句子/结构/对话/语义)
  • 第二部分:检索优化——检索是 RAG 最关键的"In"(Garbage In, Garbage Out):数据源 4 法 + 查询转换 4 法 + 架构级 2 法

第一部分:基础分块(Chunking)

分块(Chunking)是 RAG 流程中的关键环节:把长文档切成小块,再做向量化、检索、生成。
切得好不好,直接决定检索质量和最终回答的准确度。本篇整理 5 种基础分块方式 + 1 种高级分块方式(语义分块)。


一、为什么要分块

  1. 大模型输入有长度限制(context window),整篇文档塞不进去。
  2. 检索时希望精准:只把"相关的一小段"喂给模型,而不是整篇文档。
  3. 所以要先把文档切成小块,再做向量化、检索。

⚠️ 注意:分块是纯本地字符串运算,不联网、不调 API。需要 API 的是后面的"向量化"和"生成"步骤。


二、固定长度分块(Fixed-size Chunking)

核心思想

按固定字符长度切分文本,相邻分块之间保留一定重叠(overlap)。

核心公式

stride(步长)= chunk_size - chunk_overlap

窗口大小为 chunk_size,但每次只向前走 stride 步。因为 stride < chunk_size,所以相邻窗口会重叠 chunk_overlap 个字符。

关键参数

参数 含义
separator 按什么切。英文用 " "(空格),中文用 ""(字符)
chunk_size 每个分块的最大字符数(上限,不是目标值
chunk_overlap 相邻分块重叠的字符数,防止语义被硬切断
length_function 长度计量方式,len = 按字符数

为什么 chunk 实际长度常小于 chunk_size?

chunk_size上限而非目标值。分块器以单词为单位累加:只要加上下一个完整单词还不超限就继续,一旦会超限就停手。所以实际长度 = 若干完整单词的总长,通常 < chunk_size

overlap 为什么能修复硬切断

  • overlap=0:窗口走满,第 N 和 N+1 字符之间的词被硬切断,只属于一个分块。
  • overlap>0:被切断的词会同时出现在两个相邻分块里,检索时都能命中。

代码

"""
============================================================
RAG 分块(Chunking)—— 基础分块之【固定长度分块】(Fixed-size Chunking)
============================================================

【它是什么】
    把一段长文本,按固定长度(chunk_size)切成多个小块。
    这是 RAG 里最简单的一种分块方式。

【为什么 RAG 要分块】
    大模型输入有长度限制(context window),整篇文档塞不进去;
    而且检索时,我们希望只把"相关的一小段"喂给模型,而不是整篇。
    所以要先把文档切成小块,再做向量化、检索。

【本 demo 用的工具】
    LangChain 的 CharacterTextSplitter —— 字符级固定长度分块器。
    注意:它完全是【本地字符串运算】,不需要联网、不需要调任何 API。
    (RAG 里需要 API 的是后面的"向量化"和"生成"步骤,分块这步不用。)
"""

# 导入 LangChain 的字符分块器(纯本地工具,不联网)
from langchain_text_splitters import CharacterTextSplitter

# ------------------------------------------------------------
# 1. 准备原始文本(待分块的语料)
# ------------------------------------------------------------
# 这里用英文文本,单词之间用空格隔开。
# 英文用 separator=" "(按空格切)最自然,因为不会把一个单词从中间劈开。
sample_text = (
    "LangChain was created by Harrison Chase in 2022. It provides a framework for developing applications "
    "powered by language models. The library is known for its modularity and ease of use. "
    "One of its key components is the TextSplitter class, which helps in document chunking."
)

# ------------------------------------------------------------
# 2. 创建分块器,设置参数
# ------------------------------------------------------------
# 四个核心参数:
#   - separator:      按"什么"来切。这里用空格" ",即以单词为单位。
#                     (中文因为没有词间空格,通常用 "" 按字符切)
#   - chunk_size:     每个分块的最大长度(这里是字符数,因为 length_function=len)。
#                     设成 100,表示每个块尽量不超过 100 个字符。
#   - chunk_overlap:  相邻两个分块之间的"重叠"字符数。
#                     设成 20,表示相邻块会有 20 个字符是重复的。
#                     作用:防止一句话被从中间切断导致语义丢失。
#   - length_function:怎么计量"长度"。len = 按字符数计量。
#                     (也可以换成按 token 数计量,给 embedding 用更准)
#
# 衍生概念:步长 stride = chunk_size - chunk_overlap = 100 - 20 = 80
#   意思是窗口每次向前推进 80 个字符,而不是 100。
#   走得比窗口短,于是相邻窗口重叠了 20 字符 → 这就是 overlap。
#
# 参数提取成变量(好处:下面的打印直接引用变量,改参数时打印自动跟着变,
# 不会出现"参数改了、打印文案忘了改"的不一致——实测踩过的坑)
CHUNK_SIZE = 100      # 每块最大 100 字符
CHUNK_OVERLAP = 20    # 相邻块重叠 20 字符

text_splitter = CharacterTextSplitter(
    separator=" ",                      # 按空格(单词边界)切
    chunk_size=CHUNK_SIZE,              # 每块最大长度
    chunk_overlap=CHUNK_OVERLAP,        # 相邻块重叠长度
    length_function=len,                # 用 len() 计量长度(字符数)
)

# ------------------------------------------------------------
# 3. 执行分块
# ------------------------------------------------------------
# 关键区别(两种调用方式,结果都是"分块",但返回类型不同):
#
#   split_text(text)      → 返回 List[str]
#                           切出来的就是纯字符串列表。
#                           适合:只想看看切成了几段文本。
#
#   create_documents([text]) → 返回 List[Document]
#                           切出来的每一段被包成 LangChain 的 Document 对象,
#                           每个 Document 里有:
#                             .page_content  → 文本内容
#                             .metadata      → 元数据(可附加来源、页码等)
#                           适合:接入后续 RAG 流程(向量化、检索)。
#
# RAG 项目里几乎都用 create_documents(),因为后续环节只认 Document 对象。
docs = text_splitter.create_documents([sample_text])

# ------------------------------------------------------------
# 4. 打印结果
# ------------------------------------------------------------
print("=" * 60)
print(f"原始文本长度:{
     
     len(sample_text)} 个字符")
print(f"分块参数:chunk_size={
     
     CHUNK_SIZE}, chunk_overlap={
     
     CHUNK_OVERLAP}"
      f"  =>  stride(步长)={
     
     CHUNK_SIZE - CHUNK_OVERLAP}")
print(f"共切分为 {
     
     len(docs)} 个分块(Document 对象)")
print("=" * 60)

for i, doc in enumerate(docs):
    print(f"\n--- Chunk {
     
     i + 1} ---")
    print(f"类型: {
     
     type(doc).__name__}")               # Document
    print(f"长度: {
     
     len(doc.page_content)} 个字符")
    print(f"内容: {
     
     doc.page_content}")

▶ 运行结果python 固定长度分块.py 实测输出,纯本地脚本输出稳定):

============================================================
原始文本长度:272 个字符
分块参数:chunk_size=100, chunk_overlap=20  =>  stride(步长)=80
共切分为 4 个分块(Document 对象)
============================================================

--- Chunk 1 ---
类型: Document
长度: 100 个字符
内容: LangChain was created by Harrison Chase in 2022. It provides a framework for developing applications

--- Chunk 2 ---
类型: Document
长度: 97 个字符
内容: applications powered by language models. The library is known for its modularity and ease of use.

--- Chunk 3 ---
类型: Document
长度: 93 个字符
内容: and ease of use. One of its key components is the TextSplitter class, which helps in document

--- Chunk 4 ---
类型: Document
长度: 27 个字符
内容: helps in document chunking.

适用场景

简单、快速。适合对语义边界要求不高的场景。


三、递归字符分块(Recursive Character Splitting)

核心思想

固定长度分块只认一个分隔符,容易把语义从中间切断。递归分块有一组按优先级排列的分隔符,先尽量在自然边界切,切不开才退回字符级硬切。

分隔符优先级(默认)

["\n\n"  →  "\n"  →  " "  →  ""]
 段落       行      空格    字符

算法原理(递归三步走)

  1. 用第 1 个 separator(如 \n\n)把文本切成若干段。
  2. 对每一段:
    • 若长度 ≤ chunk_size → 直接当作一个分块。
    • 若长度 > chunk_size → 换下一个 separator 再切,回到步骤 2。
  3. 所有 separator 都试过还太长,最后用空字符 "" 硬切。
  4. 切完的小块再按 chunk_size 装回大块(允许 overlap 拼接)。

与固定长度分块的核心区别

  • 固定长度:只认一个 separator,切完拉倒。
  • 递归:一组 separator,优先语义边界,“尽量在自然边界切,实在不行才硬切”。

为什么 RAG 实战都用它

兼顾三点:

  • 长度可控(有 chunk_size 上限)
  • 语义完整(优先在段落/句子边界切)
  • 上下文保留(有 overlap)

代码

"""
============================================================
RAG 分块(Chunking)—— 基础分块之【递归字符分块】
(Recursive Character Text Splitting)
============================================================

【它和"固定长度分块"的区别(核心!)】

固定长度分块(CharacterTextSplitter):
    只认一个 separator,切分方式单一,容易把语义从中间硬切断。

递归字符分块(RecursiveCharacterTextSplitter):
    有一【组】按优先级排列的分隔符,从最希望切的地方开始尝试:
        ["\n\n"  →  "\n"  →  " "  →  ""]
        段落       行      空格    字符
    先尽量按段落切;段落还太长,就退一步按行切;行还太长,按空格(单词)切;
    单词还太长,最后才按字符硬切。
    => "尽量在自然边界切,实在不行才硬切",所以语义保持得更好。

【算法原理(三步走)】
    1. 用第 1 个 separator(如 \n\n)把文本切成若干段;
    2. 对每一段:
         - 若长度 <= chunk_size      → 直接当作一个分块,完事;
         - 若长度 >  chunk_size      → 换下一个 separator(\n)再切,回到步骤2;
    3. 所有 separator 都试过了还太长,最后用空字符 "" 硬切。
    4. 切完的小块,再按 chunk_size 装回大块(允许 overlap 重叠拼接)。

【为什么 RAG 实战都用它】
    它兼顾了"长度可控"和"语义完整":
    - 优先在段落/句子边界切,分块更自然;
    - 又有 chunk_size 上限和 overlap,保证检索时的颗粒度和上下文。
"""
from langchain_text_splitters import RecursiveCharacterTextSplitter

# ------------------------------------------------------------
# 1. 准备原始文本(特意设计 4 种段落,演示完整的"递归降级链")
# ------------------------------------------------------------
# 每段演示一条不同的降级路径(chunk_size=80):
#   段A:短段落(46字符 ≤ 80)      → 第①刀(\n\n)切出后直接成块,无需降级
#   段B:长段落(>80)但内部有换行\n → 降到第②刀(\n)按"行"切,每行独立成块
#   段C:长段落(>80)且内部无换行   → 降到第③刀(空格)按"单词"切
#   段D:含一个 87 字符的超长"单词"  → 单词本身就 >80,逼出第④刀("")字符硬切
sample_text = """RAG stands for Retrieval-Augmented Generation.

Chunking splits a long document into smaller pieces before indexing.
Each chunking strategy has its own trade-offs.
You should choose one based on your document type.

Recursive splitting tries paragraph boundaries first, then line boundaries, then word boundaries, and only falls back to raw characters as the last resort.

A single very long identifier like checkpoint_v1.2.3_20240601_experimental_run_42_abcdef1234567890_github_actions_release cannot fit into one chunk."""

# ------------------------------------------------------------
# 2. 创建递归字符分块器
# ------------------------------------------------------------
# 核心参数:
#   - separators:  分隔符列表,按优先级从高到低。
#                  不写的话用默认值 ["\n\n", "\n", " ", ""],对中英文都适用。
#   - chunk_size:  每个分块的最大长度(字符数)。
#   - chunk_overlap: 相邻分块的重叠字符数(和固定长度分块里的 overlap 一个意思)。
#   - length_function: 长度计量方式,len = 按字符数。
#
# 这里故意把 chunk_size 设小一点(80),让"段落太长要再细分"的情况出现,
# 这样你能看到递归过程:第一段 113 字 > 80,会被进一步用后续 separator 切。
#
# 参数提取成变量:下面的打印直接引用,改参数时打印自动跟着变,
# 不会出现"参数改了、打印忘了改"的不一致(实测踩过的坑)
CHUNK_SIZE = 80      # 每块最大 80 字符
CHUNK_OVERLAP = 20   # 相邻块重叠 20 字符

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=CHUNK_SIZE,
    chunk_overlap=CHUNK_OVERLAP,
    length_function=len,
    # separators=["\n\n", "\n", " ", ""],  # 默认值,可省略;需要时可自定义
)

# ------------------------------------------------------------
# 3. 执行分块
# ------------------------------------------------------------
# 同样用 create_documents() 返回 Document 对象(RAG 后续环节只认这个类型)。
docs = text_splitter.create_documents([sample_text])

# ------------------------------------------------------------
# 4. 打印结果
# ------------------------------------------------------------
print("=" * 70)
print(f"原始文本长度:{
     
     len(sample_text)} 个字符")
print(f"分块参数:chunk_size={
     
     CHUNK_SIZE}, chunk_overlap={
     
     CHUNK_OVERLAP}")
print(f"分隔符优先级(从高到低):\\n\\n(段落) → \\n(行) → 空格(单词) → 空字符(硬切)")
print(f"共切分为 {
     
     len(docs)} 个分块")
print("=" * 70)

for i, doc in enumerate(docs):
    print(f"\n--- Chunk {
     
     i + 1} (长度 {
     
     len(doc.page_content)} 字符)---")
    print(doc.page_content)
    print("-" * 70)

▶ 运行结果python 递归字符分块.py 实测输出,纯本地脚本输出稳定):

======================================================================
原始文本长度:521 个字符
分块参数:chunk_size=80, chunk_overlap=20
分隔符优先级(从高到低):\n\n(段落) → \n(行) → 空格(单词) → 空字符(硬切)
共切分为 11 个分块
======================================================================

--- Chunk 1 (长度 46 字符)---
RAG stands for Retrieval-Augmented Generation.
----------------------------------------------------------------------

--- Chunk 2 (长度 68 字符)---
Chunking splits a long document into smaller pieces before indexing.
----------------------------------------------------------------------

--- Chunk 3 (长度 46 字符)---
Each chunking strategy has its own trade-offs.
----------------------------------------------------------------------

--- Chunk 4 (长度 50 字符)---
You should choose one based on your document type.
----------------------------------------------------------------------

--- Chunk 5 (长度 75 字符)---
Recursive splitting tries paragraph boundaries first, then line boundaries,
----------------------------------------------------------------------

--- Chunk 6 (长度 79 字符)---
line boundaries, then word boundaries, and only falls back to raw characters as
----------------------------------------------------------------------

--- Chunk 7 (长度 34 字符)---
raw characters as the last resort.
----------------------------------------------------------------------

--- Chunk 8 (长度 34 字符)---
A single very long identifier like
----------------------------------------------------------------------

--- Chunk 9 (长度 79 字符)---
checkpoint_v1.2.3_20240601_experimental_run_42_abcdef1234567890_github_actions_
----------------------------------------------------------------------

--- Chunk 10 (长度 27 字符)---
7890_github_actions_release
----------------------------------------------------------------------

--- Chunk 11 (长度 26 字符)---
cannot fit into one chunk.
----------------------------------------------------------------------

四、按句子分块(Sentence-based Chunking)

核心思想

完整的句子为最小单位来组合分块,保证每个分块里的句子是完整的,语义不被破坏。

与前两种的区别

分块方式 最小单位 是否会切断句子
固定长度 字符/单词
递归字符 段→行→词 可能(本质还是凑字符数)
按句子 完整句子 绝不会

算法原理(贪心累加)

  1. 用 NLTK 的 sent_tokenize 把全文切成一句句的完整句子。
  2. 准备一个空块,逐句往里加:
    • 若加上这句还不超 max_chars → 加进去。
    • 若加上这句会超 max_chars → 当前块收工,新开一块,从上一块末尾的 overlap_sentences 句开始,再加当前这句。
  3. overlap_sentences:相邻块共享几句话(句子级 overlap,比字符级语义更完整)。

关键参数

参数 含义
max_chars 每个分块的最大字符数(上限)
overlap_sentences 相邻块重叠的句子数

代码

"""
============================================================
RAG 分块(Chunking)—— 基础分块之【按句子分块】(Sentence-based)
============================================================

【它是什么】
    以【完整的句子】为最小单位来组合分块。
    核心思想:保证每个分块里的句子是完整的,语义不被破坏。

【和前面两种分块的区别】

    固定长度分块(CharacterTextSplitter):
        只按字符数硬切,可能把句子从中间劈开 → 语义破坏。

    递归字符分块(RecursiveCharacterTextSplitter):
        尽量在段落/句子边界切,但本质还是"凑字符数",可能一句话被拆到两个块。

    按句子分块(本 demo):
        最小单位就是"一个完整句子",绝不会把句子劈开。
        多个句子凑在一起,直到接近长度上限为止。

【算法原理(贪心累加)】
    1. 用 NLTK 的 sent_tokenize 把全文切成一句句的【完整句子】;
    2. 准备一个空块 current_chunk,逐句往里加:
         - 若加上这句还不超 max_chars  → 加进去;
         - 若加上这句会超 max_chars    → 当前块收工(存起来),
                                         新开一块,从【上一块的末尾 overlap_sentences 句】开始,
                                         再加当前这句。
    3. overlap_sentences:相邻块之间共享几句话(和字符级 overlap 同理,保上下文)。

【适用场景】
    短句多的文本(新闻、问答、对话);不适合超长单句(如某些法律条文)。
"""
from nltk.tokenize import sent_tokenize


def chunk_by_sentences(text, max_chars=500, overlap_sentences=1):
    """
    按句子分块(贪心累加 + 句子级重叠)

    参数:
        text:              原始文本
        max_chars:         每个分块的最大字符数(上限,不是目标值)
        overlap_sentences: 相邻块之间重叠的句子数(保上下文,类似 chunk_overlap)

    返回:
        chunks: 分块后的字符串列表
    """
    # ---- 第 1 步:把全文切成一句句的【完整句子】 ----
    # sent_tokenize 会识别句号/问号/叹号等,返回句子列表,每个句子都是完整的。
    sentences = sent_tokenize(text)

    chunks = []            # 存放最终的所有分块
    current_chunk = ""     # 正在累加的当前块
    start_index = 0        # 当前块从第几句开始(用于实现句子级 overlap)

    # ---- 第 2 步:逐句贪心累加 ----
    for i, sentence in enumerate(sentences):
        # 试探:如果把当前这句加到 current_chunk,会不会超 max_chars?
        # (多 +1 是为了预留句间空格)
        if len(current_chunk) + len(sentence) + 1 <= max_chars:
            # 没超 → 加进当前块(前面已有内容就补个空格)
            if current_chunk:
                current_chunk += " " + sentence
            else:
                current_chunk = sentence
        else:
            # 超了 → 当前块收工,存起来
            if current_chunk:
                chunks.append(current_chunk)
            # ---- 第 3 步:新开一块,带上 overlap ----
            # 新块从 "倒数第 overlap_sentences 句" 开始,让相邻块共享几句(保上下文)。
            # 比如 overlap_sentences=1,新块就把上一块的【最后 1 句】作为开头。
            start_index = max(0, i - overlap_sentences)
            current_chunk = " ".join(sentences[start_index : i + 1])

    # ---- 收尾:最后一块别忘了存 ----
    if current_chunk:
        chunks.append(current_chunk)

    return chunks


# ------------------------------------------------------------
# 准备测试文本(多句话,能看出"按句累加"和"句子重叠"的效果)
# ------------------------------------------------------------
sample_text = (
    "Artificial intelligence is a branch of computer science. "
    "It aims to build systems capable of human-like reasoning. "
    "In recent years, deep learning has driven rapid progress. "
    "Large language models like GPT and BERT achieve strong results. "
    "Retrieval-Augmented Generation combines search with generation. "
    "It retrieves relevant documents before producing an answer. "
    "Chunking is a key step that affects retrieval quality. "
    "Sentence-based chunking keeps each sentence intact."
)

# ------------------------------------------------------------
# 调用:每个块最多 200 字符,相邻块重叠 1 句
# ------------------------------------------------------------
# 参数提取成变量:下面的打印直接引用,改参数时打印自动跟着变
MAX_CHARS = 200            # 每块最大 200 字符
OVERLAP_SENTENCES = 1      # 相邻块重叠 1 句

chunks = chunk_by_sentences(
    sample_text,
    max_chars=MAX_CHARS,
    overlap_sentences=OVERLAP_SENTENCES,
)

# ------------------------------------------------------------
# 打印结果
# ------------------------------------------------------------
print("=" * 70)
print(f"原始文本长度:{
     
     len(sample_text)} 个字符")
print(f"分块参数:max_chars={
     
     MAX_CHARS}, overlap_sentences={
     
     OVERLAP_SENTENCES}")
print(f"共切分为 {
     
     len(chunks)} 个分块")
print("=" * 70)

for i, chunk in enumerate(chunks, 1):
    print(f"\n--- Chunk {
     
     i} (长度 {
     
     len(chunk)} 字符)---")
    print(chunk)
    print("-" * 70)

▶ 运行结果python 句子分块.py 实测输出,纯本地脚本输出稳定):

======================================================================
原始文本长度:467 个字符
分块参数:max_chars=200, overlap_sentences=1
共切分为 4 个分块
======================================================================

--- Chunk 1 (长度 172 字符)---
Artificial intelligence is a branch of computer science. It aims to build systems capable of human-like reasoning. In recent years, deep learning has driven rapid progress.
----------------------------------------------------------------------

--- Chunk 2 (长度 185 字符)---
In recent years, deep learning has driven rapid progress. Large language models like GPT and BERT achieve strong results. Retrieval-Augmented Generation combines search with generation.
----------------------------------------------------------------------

--- Chunk 3 (长度 178 字符)---
Retrieval-Augmented Generation combines search with generation. It retrieves relevant documents before producing an answer. Chunking is a key step that affects retrieval quality.
----------------------------------------------------------------------

--- Chunk 4 (长度 106 字符)---
Chunking is a key step that affects retrieval quality. Sentence-based chunking keeps each sentence intact.
----------------------------------------------------------------------

适用场景

短句多的文本:新闻、问答、对话。不适合超长单句(如某些法律条文)。

依赖说明

使用前需安装 nltk 并下载分句模型:

pip install nltk
import nltk
nltk.download('punkt_tab')   # 分句所依赖的模型数据

如果下载慢(数据在 GitHub),可稍后重试;punkt_tab 约 300KB,本身不大。


五、按结构分块(Structure-based Chunking)

核心思想

根据文档的结构标签(Markdown 标题、HTML 标签、JSON 层级等)来分块,而不是按字符数硬切。

常见的结构标签:

  • Markdown# ## ### 标题
  • HTML<h1> <h2> <p> <div> 等标签
  • JSON:嵌套层级 key
  • 代码:函数 / 类 定义

与前三种的根本区别

前三种都按字符长度句子切,跟文档本身的结构无关,一个分块里可能混入两个不相关的章节。

按结构分块按文档自身的层级结构切,一个分块正好对应文档里的一个章节/小节,语义高度内聚。

两个独有优势

  1. 语义高度内聚:一个分块 = 一个完整小节,绝不会把两个不相关章节混在一起。
  2. 自带结构化元数据:把标题路径写进 metadata,检索时能知道"这段话属于哪个章节"。

例如检索到的某段,其 metadata 为:

{
   
   'Header 1': '概述', 'Header 2': '安装', 'Header 3': '基础安装'}

这意味着能利用"这段话属于哪一章哪一节"的信息,做章节级过滤、排序,或作为额外上下文喂给大模型。

代码(按 Markdown 标题切)

"""
============================================================
RAG 分块(Chunking)—— 基础分块之【按结构分块】(Structure-based)
============================================================

【它是什么】
    根据文档的【结构标签】来分块,而不是按固定字符数硬切。
    常见的结构标签:
        - Markdown:# ## ### 标题
        - HTML:    <h1> <h2> <p> <div> 等标签
        - JSON:    嵌套层级 key
        - 代码:    函数 / 类 定义

【它和前面三种分块的根本区别】

    固定长度 / 递归字符 / 句子分块:
        都是【按字符长度】或【按句子】切,跟文档本身的结构无关。
        → 同一个分块里可能混入两个不相关的章节。

    按结构分块:
        按【文档自身的层级结构】切。
        → 一个分块正好对应文档里的一个【章节/小节】,语义高度内聚。
        → 而且会把【标题路径】写进 metadata,检索时能知道"这段话属于哪个章节"。

【本 demo 用什么】
    LangChain 的 MarkdownHeaderTextSplitter:
        按 Markdown 标题(# / ## / ###)把文档切成"标题 + 该标题下的正文"。
        每个 chunk 自动带上 metadata,记录它属于哪一级标题(标题路径)。

【算法原理(直观)】
    1. 扫描 Markdown,遇到 # 就开始一个新章节,遇到 ## 就开一个子章节……
    2. 每个【最小标题】下面紧跟的正文,连同它所有上级标题路径,组成一个分块。
    3. 所以:分块的边界 = 标题,分块的内容 = 该标题下的正文。
"""
from langchain_text_splitters import MarkdownHeaderTextSplitter

# ------------------------------------------------------------
# 1. 准备一段带结构的 Markdown 文本
# ------------------------------------------------------------
# 故意写成多级标题,让"按结构切"的效果看得出来:
#   一级标题下有正文 + 两个二级标题,二级标题下又有正文和三级标题。
md = """# 概述

LangChain 是一个用于开发大语言模型应用的开源框架。

## 安装

可以使用 pip 安装 LangChain。

### 基础安装

运行 `pip install langchain` 即可完成安装。

### 完整安装

运行 `pip install langchain[all]` 会安装全部可选依赖。

## 核心概念

LangChain 包含模型、提示、链、代理等核心组件。

# 进阶用法

本节介绍一些高级特性,如自定义检索器和多轮对话。
"""

# ------------------------------------------------------------
# 2. 定义"按哪些标题级别来切"
# ------------------------------------------------------------
# header_to_split_on 是一个列表,每一项是 (标题符号, 元数据字段名):
#   - "#":  一级标题  → 切出来的分块 metadata 里字段名叫 "Header 1"
#   - "##": 二级标题  → 字段名叫 "Header 2"
#   - "###":三级标题  → 字段名叫 "Header 3"
# 这就是"按结构切"的核心:告诉分块器,文档的结构层次是怎么定义的。
header_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

# ------------------------------------------------------------
# 3. 创建分块器并执行分块
# ------------------------------------------------------------
markdown_splitter = MarkdownHeaderTextSplitter(header_to_split_on)

# 注意:MarkdownHeaderTextSplitter 只有一个 split_text 方法,
# 不需要 chunk_size / chunk_overlap —— 因为它是【按标题结构】切,不是按长度切。
# 它返回的是 Document 对象列表(自带 metadata,记录标题路径)。
md_header_splits = markdown_splitter.split_text(md)

# ------------------------------------------------------------
# 4. 打印结果
# ------------------------------------------------------------
print("=" * 70)
print(f"原始 Markdown 文本长度:{
     
     len(md)} 个字符")
print(f"共切分为 {
     
     len(md_header_splits)} 个分块(每个分块 = 一个最小标题下的正文)")
print("=" * 70)

for i, doc in enumerate(md_header_splits, 1):
    print(f"\n--- Chunk {
     
     i} ---")
    print(f"标题路径 (metadata): {
     
     doc.metadata}")
    print(f"正文内容:")
    print(doc.page_content)
    print("-" * 70)

▶ 运行结果python 结构分块.py 实测输出,纯本地脚本输出稳定):

======================================================================
原始 Markdown 文本长度:241 个字符
共切分为 6 个分块(每个分块 = 一个最小标题下的正文)
======================================================================

--- Chunk 1 ---
标题路径 (metadata): {'Header 1': '概述'}
正文内容:
LangChain 是一个用于开发大语言模型应用的开源框架。
----------------------------------------------------------------------

--- Chunk 2 ---
标题路径 (metadata): {'Header 1': '概述', 'Header 2': '安装'}
正文内容:
可以使用 pip 安装 LangChain。
----------------------------------------------------------------------

--- Chunk 3 ---
标题路径 (metadata): {'Header 1': '概述', 'Header 2': '安装', 'Header 3': '基础安装'}
正文内容:
运行 `pip install langchain` 即可完成安装。
----------------------------------------------------------------------

--- Chunk 4 ---
标题路径 (metadata): {'Header 1': '概述', 'Header 2': '安装', 'Header 3': '完整安装'}
正文内容:
运行 `pip install langchain[all]` 会安装全部可选依赖。
----------------------------------------------------------------------

--- Chunk 5 ---
标题路径 (metadata): {'Header 1': '概述', 'Header 2': '核心概念'}
正文内容:
LangChain 包含模型、提示、链、代理等核心组件。
----------------------------------------------------------------------

--- Chunk 6 ---
标题路径 (metadata): {'Header 1': '进阶用法'}
正文内容:
本节介绍一些高级特性,如自定义检索器和多轮对话。
----------------------------------------------------------------------

注意:MarkdownHeaderTextSplitter 没有 chunk_size / chunk_overlap 参数——它是按标题结构切,不是按长度切。

实战技巧:两阶段分块

结构分块通常不单独用,而是配合其他分块器做两阶段:

# 第一阶段:先按结构切(保证语义内聚 + 带标题路径)
md_splits = MarkdownHeaderTextSplitter(header_to_split_on).split_text(md)

# 第二阶段:对每个结构块,如果太长,再用递归字符分块细切
recursive_splitter = RecursiveCharacterTextSplitter(chunk_size=500)
final_docs = recursive_splitter.split_documents(md_splits)

这样既有结构化的 metadata,又有长度可控的分块——这是处理技术文档/PDF 的标准做法。


六、按对话分块(Dialogue/Conversation-based Chunking)

核心思想

根据对话的角色(user / assistant / system)或**对话轮次(turn)**进行分块,确保每个分块保留完整的对话上下文。

为什么对话数据需要单独的分块方式

普通文本分块(按字符/句子/段落切)会把"一问一答"拆散:把 user 的问题和 assistant 的回答切到不同块里,上下文就丢了。对话分块的核心是保持问答的成对完整

三种角色消息

消息类型 作用
SystemMessage 系统设定(“你是一个助手”),通常全局唯一,放在对话开头
HumanMessage 用户的提问
AIMessage AI 的回答

一组连续的 HumanMessage + AIMessage 就构成"一轮对话"(turn)。

本 demo 的做法

  1. 用 LangChain 的 Message 类把对话表示成结构化数据,明确每条消息的角色。
  2. TokenTextSplitter 对每条消息的内容按 token 数进一步细分,防止单条消息(比如 AI 的长回答)过长。

关于 TokenTextSplitter

  • token 数(而非字符数)切分。token 是大模型处理文本的最小单位,对英文大约 1 token ≈ 0.75 个单词。
  • 用 token 计量更贴合模型的实际输入限制。
  • 关键参数:
    • chunk_size:每个分块的最大 token 数。
    • chunk_overlap:相邻分块重叠的 token 数(保上下文)。

代码

"""
============================================================
RAG 分块(Chunking)—— 基础分块之【按对话分块】(Dialogue/Conversation-based)
============================================================

【核心思想】
    根据【对话的角色】(user / assistant / system)或【对话轮次(turn)】进行分块,
    确保每个分块保留完整的对话上下文。

【为什么对话数据需要单独的分块方式】
    普通文本分块(按字符/句子/段落切)会把"一问一答"拆散:
    把 user 的问题和 assistant 的回答切到不同块里,上下文就丢了。
    对话分块的核心是【保持问答的成对完整】。

【三种角色消息】
    SystemMessage   : 系统设定("你是一个助手"),通常全局唯一,放在对话开头
    HumanMessage    : 用户的提问
    AIMessage       : AI 的回答

【本 demo 做法】
    1. 用 LangChain 的 Message 类把对话表示成结构化数据。
    2. 用 TokenTextSplitter 对每条消息的内容按 token 数切分。

【关于 TokenTextSplitter】
    按【token 数】切分(不是字符数)。token 是大模型处理文本的最小单位,
    对英文大约 1 token ≈ 0.75 个单词。用 token 计量更贴合模型的实际输入限制。
"""
from langchain_text_splitters import TokenTextSplitter
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

# 准备一段多轮对话数据
messages = [
    SystemMessage(content="You are a helpful assistant."),
    HumanMessage(content="What is RAG?"),
    AIMessage(content="RAG stands for Retrieval-Augmented Generation. It combines a retrieval system with a generation model to answer questions."),
    HumanMessage(content="Why is chunking important?"),
    AIMessage(content="Chunking splits documents into smaller pieces for better retrieval and context management."),
    HumanMessage(content="What is an embedding?"),
    AIMessage(content="An embedding is a vector representation of text, enabling semantic similarity search."),
]

# 创建 Token 分块器
text_splitter = TokenTextSplitter(chunk_size=50, chunk_overlap=10)

# 遍历每条消息,对其内容做 token 级切分
for msg in messages:
    print(f"--- {
     
     type(msg).__name__} ---")
    chunks = text_splitter.split_text(msg.content)
    for chunk in chunks:
        print(chunk)
    print()

▶ 运行结果python 对话分块.py 实测输出,纯本地脚本输出稳定):

--- SystemMessage ---
You are a helpful assistant.

--- HumanMessage ---
What is RAG?

--- AIMessage ---
RAG stands for Retrieval-Augmented Generation. It combines a retrieval system with a generation model to answer questions.

--- HumanMessage ---
Why is chunking important?

--- AIMessage ---
Chunking splits documents into smaller pieces for better retrieval and context management.

--- HumanMessage ---
What is an embedding?

--- AIMessage ---
An embedding is a vector representation of text, enabling semantic similarity search.

适用场景

  • 客服对话记录、聊天记录的 RAG 知识库。
  • 多轮问答数据集。
  • 任何"问答成对"结构的数据。

依赖说明

pip install langchain-core langchain-text-splitters tiktoken
  • langchain-core:提供 SystemMessage / HumanMessage / AIMessage 三种角色消息类。
  • langchain-text-splitters:提供 TokenTextSplitter
  • tiktokenTokenTextSplitter 的底层 token 计数依赖(不装会报 ImportError)。

七、语义分块(Semantic Chunking)—— 高级

核心思想

前面的分块方式都按长度/句子/结构表面规则切,和内容本身无关。语义分块按内容含义切:把语义相近的句子聚到一个块里,当检测到"语义跳变"(话题转换)时,就断开成新块。

它为什么更强

<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值