> ## Documentation Index
> Fetch the complete documentation index at: https://www.yuan111.asia/doc/llms.txt
> Use this file to discover all available pages before exploring further.

# 07 · 文档与切分：把知识变成模型能用的"块"

# 07 · 文档与切分：把知识变成模型能用的"块"

> **本片目标**：让模型认识公司文档——先讲 `Document`（LangChain 里知识的基本单位），再讲切分（为什么必须切、怎么切）。
> **新增规定性：7**（知识单位：`Document` 有固定字段；切分器有固定参数）
> **数据字典**：Document / RecursiveCharacterTextSplitter。
> **进程线程模型**：切分是纯 CPU 文本操作，毫秒级，无阻塞。
> **网络模型**：无（本片不联网；embedding 联网是第 08 篇的事）。

***

## 1. 上集回顾

第 06 篇模型能记住对话了。但用户问"转正需要什么条件"，模型只能瞎编——它没看过你们公司的新人培训手册。

**直觉方案**：把整本新人培训手册塞进 system 提示。但三个问题立刻出现：

1. **塞不下**：新人培训手册两万多字符，加上对话历史很容易超上下文窗口；
2. **塞得下也太贵**：每次问答都带全文，token 费爆炸；
3. **检索定位难**：问"转正"，模型要在整本手册里找相关内容——找不到就乱答。

**所以正确路线不是"全塞"，而是"先召回相关内容，再只塞相关内容"**（这就是 RAG）。而"召回"的前提是：**把文档切成一块块**，每块是一个检索单元。

***

## 2. 数据字典：Document（知识的原子单位）

```python theme={null}
from langchain_core.documents import Document

doc = Document(
    page_content="转正要求：累计至少 60 篇日总结，答辩平均分 75 分以上。",
    metadata={"source": "新人培训手册.md", "section": "转正要求"},
    id="chunk_17",
)
```

| 字段             | 类型                    | 说明                                  |                      |
| -------------- | --------------------- | ----------------------------------- | -------------------- |
| `page_content` | `str`                 | **正文**（检索、embedding、喂给模型都用它）        |                      |
| `metadata`     | `dict`                | **附属信息**（来源、页码、章节…）。不参与检索，但随块走，用于溯源 |                      |
| `id`           | \`str                 | None\`                              | 可选的唯一标识（向量库里做去重/更新用） |
| `type`         | `Literal["Document"]` | 恒为 `"Document"`                     |                      |

**关键**：`metadata` 是 RAG 溯源的地基——第 09 篇回答"答案来自哪块"就靠它。

***

## 3. 数据字典：RecursiveCharacterTextSplitter（递归字符切分器）

```python theme={null}
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "。", "！", "？", "；", " ", ""],  # ← 中文必须显式给！
    chunk_size=200,        # 目标块大小（字符数）
    chunk_overlap=20,      # 相邻块重叠字符数
    keep_separator=True,
    length_function=len,
    is_separator_regex=False,
)
```

| 参数                   | 类型          | 默认                               | 说明                                 |
| -------------------- | ----------- | -------------------------------- | ---------------------------------- |
| `separators`         | `list[str]` | `["\n\n","\n"," ",""]`（**英文默认**） | 按顺序尝试的分隔符。**中文必须加 `。！？；`，否则句子被腰斩** |
| `chunk_size`         | `int`       | 400                              | 目标块大小                              |
| `chunk_overlap`      | `int`       | 200                              | 相邻块重叠——防语义断裂的"胶水"                  |
| `keep_separator`     | `bool`      | True                             | 分隔符是否保留在块里                         |
| `length_function`    | `Callable`  | `len`                            | 量块大小的函数（也可用 token 计数）              |
| `is_separator_regex` | `bool`      | False                            | separators 是否正则                    |

**切分逻辑（递归）**：

1. 先尝试按 `\n\n`（段落）切；
2. 切出的块还太大 → 按 `\n`（行）切；
3. 还太大 → 按 `。！？；`（句子）切；
4. 还太大 → 按空格 → 最后按字符硬切。

**为什么 overlap 必要**：切块切在句中被切断的语义，靠重叠块"粘"回来——第 2 块的开头重复第 1 块的结尾，检索时不会漏掉跨块的关键信息。

***

## 4. 关键方法

| 方法                 | 签名                                                        | 返回值              | 用途                                 |
| ------------------ | --------------------------------------------------------- | ---------------- | ---------------------------------- |
| `split_text`       | `split_text(text: str) -> list[str]`                      | `list[str]`      | 切字符串（新人培训手册.md 是文本文件，用它）           |
| `split_documents`  | `split_documents(docs: list[Document]) -> list[Document]` | `list[Document]` | 切 Document（**metadata 自动复制到每个小块**） |
| `create_documents` | `create_documents(texts: list[str]) -> list[Document]`    | `list[Document]` | 从多个文本直接切出 Document                 |

**新人培训手册实测**（`data/新人培训手册.md`，22201 字符）：

```python theme={null}
with open("data/新人培训手册.md", encoding="utf-8") as f:
    text = f.read()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=200, chunk_overlap=20,
    separators=["\n\n", "\n", "。", "！", "？", "；", " ", ""],
)
chunks = splitter.split_text(text)   # → 实测切出 163 块，每块 ≤200 字符
```

***

## 5. 进程线程模型

```
读文件(磁盘IO) → 切分(纯CPU字符串操作) → list[str]
```

* 切分是**纯内存 CPU 操作**，22201 字符毫秒级完成，不涉及网络、不阻塞。
* 生产环境对超大文档集切分时，考虑并行切分（第 12 篇 batch）；但单文件切分无压力。

***

## 5.5 chunk\_size 对比实验：粒度如何影响块数

**chunk\_size 是切分最重要的旋钮**。实测新人培训手册（22201 字符）在不同 chunk\_size 下的结果：

| chunk\_size | 块数  | 平均块长   | 特点               |
| ----------- | --- | ------ | ---------------- |
| 100         | 339 | 65 字符  | 很碎，检索单元小，但上下文碎片多 |
| 200         | 163 | 136 字符 | **本系列默认**——平衡点   |
| 500         | 59  | 376 字符 | 块更完整，但检索粒度粗      |
| 1000        | 26  | 853 字符 | 接近"整段"级别，检索会漏细节  |

```
chunk_size= 100 → 339 块
chunk_size= 200 → 163 块
chunk_size= 500 →  59 块
chunk_size=1000 →  26 块
```

**权衡本质**：

* **块越小**：检索单元越小、越精确（能捞到具体某条规则），但一个问题的答案可能被切成多块，喂给模型时要拼多个块（上下文碎片多、token 浪费）；
* **块越大**：每块信息越完整（一条规则自成一个块），但检索粒度粗——问一个细节，可能整块都偏题。

**这是第 15 篇调参的核心依据**：粗排 `k` 值（召回几块）+ `final_k`（最终喂模型几块）都是在这个粒度权衡上选的。

***

## 6. 网络模型

本片**完全不联网**——文档读取和切分都是本地操作。这是 RAG 管道里唯一"不出网"的环节。从第 08 篇开始，embedding 和模型调用会重新联网。

***

## 7. 验证：跑起来

配套代码 `code/07_load_split.py`：

1. 读新人培训手册；
2. 用**英文默认分隔符**切一次（演示句子被腰斩的坑）；
3. 用**中文分隔符**切一次，打印块数、每块前 60 字、块长度；
4. 看 overlap 的效果（相邻块首尾重复）；
5. **chunk\_size 对比**：100/200/500/1000 四种粒度各切一次，看块数怎么变。

```powershell theme={null}
cd enterprise-rag-course\code
python 07_load_split.py
```

**预期输出（节选）**：

```
===== 坑：英文默认分隔符切中文 =====
块 0: "# 英大财险项目新人培训全手册\n\n> 版本：v1.0..."   ← 块在换行处断开
（默认分隔符只有 \n\n、\n、空格，没有 。！？；，句子/表格被腰斩）

===== 正解：中文分隔符 =====
共切 163 块
块 0  (102字符): # 英大财险项目新人培训全手册\n\n> 版本：v1.0（整合自培训材料全部文档）...
块 1  (177字符): - [第一章：入职指南](#第一章入职指南)\n- [第二章：...   ← 与块0末尾重叠 20 字符

===== ⑤ chunk_size 对比 =====
chunk_size= 100 → 339 块
chunk_size= 200 → 163 块
chunk_size= 500 →  59 块
chunk_size=1000 →  26 块
```

***

## 8. 边界

* **chunk\_size 没有"标准答案"**——检索粒度 vs 上下文效率的权衡。200\~500 字符常见；语义完整的长段落可更大。第 15 篇讲调参。
* **文本文件（.txt/.md）用 `split_text`**；PDF/网页等多格式文档有专用 loader（`langchain_community.document_loaders` 停更，1.x 用各官方集成包或 pypdf 等），本系列聚焦文本文件。
* **metadata 会复制到每个小块**——大文档切出的 163 块都带 `{"source": "新人培训手册.md"}`，溯源没问题；但注意别把巨大的对象塞进 metadata。

***

## 推荐资料（延伸阅读）

* [LangChain 官方文档 · 文本切分器集成](https://docs.langchain.com/oss/python/integrations/splitters/) —— 官方支持的切分器清单
* [langchain-core API 参考 · documents](https://reference.langchain.com/python/langchain-core/documents/) —— Document / BaseMedia 数据结构

***

## 9. 未完待续

文档切好了，163 块。现在问题：用户问"转正需要什么条件"，**怎么让机器知道"哪几块最相关"？**

关键词搜索（`if "转正" in chunk`）太脆弱——用户可能问"我多久能转正"，里面没有"条件"二字。**让机器理解"意思相近"**，这就是第 08 篇：向量化与向量库。

→ [08 · 向量化与向量库](/doc/doc/enterprise-rag-course/08-向量化与向量库)
