avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
  • 首页
主页 20 文档切片
文章

20 文档切片

发表于 最近 更新于 最近
作者 Administrator
34~44 分钟 阅读

文档切片

文档切片的核心是把清洗后的长文本切成大小合适、语义完整、带来源信息的 chunk,为后面的 Embedding 和向量检索做准备。

[TOC]

1. 学完本节要达到什么程度

前面已经得到:

cleaned_text  

本节要把它切成:

chunk 0  
chunk 1  
chunk 2  
...  

每个 chunk 都要有:

documentId  
chunkIndex  
content  
titlePath  
pageStart  
pageEnd  
charStart  
charEnd  
metadata  

切片不是随便按字数切。

切得好,后面检索更准。

切得差,RAG 很容易答非所问。

切片流程图:

graph TD;  
 A["cleaned_text"] --> B["识别标题 / 段落 / 页码"];  
 B --> C["按规则生成初始 chunks"]; C --> D{"chunk 太大吗?"};  
 D -- "是" --> D1["继续按分隔符递归拆分"];  
 D1 --> E; D -- "否" --> E{"chunk 太小或语义断裂吗?"};  
 E -- "是" --> E1["和相邻内容合并或增加 overlap"]; E1 --> F; E -- "否" --> F["补充 metadata<br/>titlePath / page / char range"]; F --> G["写入 rag_chunk"]; G --> H["后续生成 Embedding"];  

2. 为什么要切片

如果把整篇文档都送进向量库,会有问题:

文本太长  
向量表示不精准  
检索结果太粗  
上下文浪费  
模型难以定位答案  

如果切得太碎,也会有问题:

语义断裂  
标题丢失  
上下文不足  
回答缺少依据  

切片的目标是平衡:

足够小,便于检索  
足够完整,保留语义  
带 metadata,能回到来源  

3. chunk size

chunk size 表示每个 chunk 的大小。

常见单位:

字符数  
token 数  
段落数  
标题层级  

学习阶段可以先按字符。

中文文档可以先试:

chunk_size = 800  
chunk_overlap = 120  

英文文档可以先试:

chunk_size = 1000  
chunk_overlap = 150  

这不是固定答案。

后面要根据检索效果调整。

4. overlap

overlap,中文可以理解为“重叠”。

它表示相邻 chunk 之间保留一部分重复内容。

比如:

chunk 0:A B C D  
chunk 1:D E F G  

这里 D 就是重叠。

为什么需要 overlap?

因为答案可能跨越切片边界。

没有 overlap,边界附近的信息容易断。

但 overlap 也不能太大。

太大会导致:

重复内容多  
向量库变大  
检索结果重复  
成本增加  

学习阶段可以先用:

overlap = chunk_size 的 10% 到 20%```  
  
## 5. 按段落切片  
  
最简单的切片是按段落累加。  
  
伪代码:  
  
```python  
def split_by_paragraph(text: str, max_chars: int = 800):  
 paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] chunks = [] current = [] current_length = 0  
 for paragraph in paragraphs: paragraph_length = len(paragraph)  
 if current and current_length + paragraph_length > max_chars: chunks.append("\n\n".join(current)) current = [paragraph] current_length = paragraph_length else: current.append(paragraph) current_length += paragraph_length  
 if current: chunks.append("\n\n".join(current))  
 return chunks```  
  
优点:  
  
```text  
简单  
容易理解  
保留段落边界  

缺点:

没有 overlap标题信息可能丢  
超长段落处理不好  

6. 使用 RecursiveCharacterTextSplitter

LangChain 常用 RecursiveCharacterTextSplitter。

安装:

python -m pip install langchain-text-splitters```  
  
代码:  
  
```python  
from langchain_text_splitters import RecursiveCharacterTextSplitter  
  
text_splitter = RecursiveCharacterTextSplitter(  
 chunk_size=800, chunk_overlap=120, separators=[ "\n\n", "\n", "。",  
 "!",  
 "?",  
 ";",  
 ",",  
 " ", "", ],)  
  
chunks = text_splitter.split_text(cleaned_text)  

它会按分隔符递归尝试。

优先保留大结构:

段落  
换行  
句子  
词  
字符  

这比直接按固定长度切更自然。

7. 中文分隔符

中文没有英文那样明显的空格分词。

所以分隔符建议加入:

。  
!  
?  
;  
,  
、  

更完整一点:

separators = [  
 "\n\n", "\n", "。",  
 "!",  
 "?",  
 ";",  
 ",",  
 "、",  
 ".", "!", "?", ";", ",", " ", "",]  

这样不容易把中文句子切得太难看。

8. 按标题切片

Markdown 文档适合按标题切片。

比如:

# RAG 原理  
## 为什么需要 RAG## RAG 流程  

标题能提供上下文。

chunk 里最好保留标题路径:

RAG 原理 > 为什么需要 RAG```  
  
简单解析标题路径:  
  
```python  
def collect_title_path(lines: list[str], current_path: list[str], line: str):  
 if line.startswith("# "): return [line.removeprefix("# ").strip()]  
 if line.startswith("## "): return current_path[:1] + [line.removeprefix("## ").strip()]  
 if line.startswith("### "): return current_path[:2] + [line.removeprefix("### ").strip()]  
 return current_path```  
  
切片时把 `titlePath` 写入 metadata。  
  
检索结果返回时,用户就知道答案来自哪个章节。  
  
## 9. Chunk 数据结构  
  
Python 可以先定义:  
  
```python  
from pydantic import BaseModel  
  
  
class TextChunk(BaseModel):  
 document_id: str chunk_index: int content: str title_path: str | None = None page_start: int | None = None page_end: int | None = None char_start: int | None = None char_end: int | None = None metadata: dict = {}```  
  
数据库对应前面设计的 `rag_chunk`。  
  
重点是:  
  
```text  
content 用于 embeddingmetadata 用于过滤和溯源  
chunk_index 用于排序  

10. 保留页码

前面 PDF 解析时,如果保留了:

[PAGE 1]  

切片时可以尝试识别当前页码。

示例:

import re  
  
  
def detect_page_marker(line: str) -> int | None:  
 match = re.fullmatch(r"\[PAGE\s+(\d+)\]", line.strip()) if not match: return None  
 return int(match.group(1))```  
  
切片时记录:  
  
```text  
page_start  
page_end  

这样回答来源可以显示:

产品手册.pdf,第 3 页  

第一版页码不完美没关系。

关键是有这个意识。

11. 生成 chunk

基础函数:

from langchain_text_splitters import RecursiveCharacterTextSplitter  
  
  
def chunk_text(document_id: str, cleaned_text: str) -> list[TextChunk]:  
 splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=120, separators=[ "\n\n", "\n", "。",  
 "!",  
 "?",  
 ";",  
 ",",  
 "、",  
 " ", "", ], )  
 parts = splitter.split_text(cleaned_text) chunks = [] cursor = 0  
 for index, content in enumerate(parts): start = cleaned_text.find(content, cursor) if start < 0: start = cursor end = start + len(content) cursor = end  
 chunks.append(TextChunk( document_id=document_id, chunk_index=index, content=content, char_start=start, char_end=end, metadata={ "chunker": "recursive-character-v1", "chunk_size": 800, "chunk_overlap": 120, }, ))  
 return chunks```  
  
这里的 `find` 不是完美方案。  
  
但学习阶段可以先用。  
  
后面要更精确,可以在切片前自己维护字符偏移。  
  
## 12. 写入数据库  
  
Java 或 Python 都可以写 `rag_chunk`。  
  
如果 Java 控制入库,Python 返回 chunks:  
  
```json  
{  
 "documentId": "xxx", "chunks": [ { "chunkIndex": 0, "content": "...", "charStart": 0, "charEnd": 800, "metadata": {} } ]}  

Java 写入:

DELETE old chunks by document_id  
INSERT new chunks  
UPDATE rag_document.status = CHUNKED  

为什么先删旧 chunk?

因为清洗策略或切片策略改了以后,可能要重新切片。

同一个文档只保留当前版本的 chunk,第一版最简单。

后面可以加版本号。

13. 切片版本

metadata 里建议记录:

{  
 "chunker": "recursive-character-v1", "chunkSize": 800, "chunkOverlap": 120}  

原因:

方便排查  
方便重新切片  
方便比较不同策略效果  

后面如果换成按标题切片,可以变成:

markdown-header-v1  

14. 切片质量检查

切片后至少检查:

chunk 数量是否大于 0是否有空 chunk最大 chunk 是否明显超长  
平均 chunk 长度是否合理  
前几个 chunk 是否语义完整  
metadata 是否带 documentIdchunkIndex 是否连续  

简单统计:

def chunk_stats(chunks: list[TextChunk]) -> dict:  
 lengths = [len(chunk.content) for chunk in chunks]  
 return { "chunk_count": len(chunks), "min_length": min(lengths) if lengths else 0, "max_length": max(lengths) if lengths else 0, "avg_length": sum(lengths) // len(lengths) if lengths else 0, }```  
  
把统计结果写入日志。  
  
RAG 调试时很有用。  
  
## 15. 切片策略的工程化管理  
  
切片结果是检索系统真正搜索的最小单元。切片策略一旦变化,向量、索引和评测结果都会变化,因此不能只保存最终 chunk 文本。  
  
### 15.1 策略版本  
  
建议记录 `chunkerName`、`chunkerVersion`、`chunkSize`、`overlap`、分隔符集合和标题拼接方式。新策略上线时可以保留旧版本,先进行灰度评测,再决定是否全量重建。  
  
同一文档的不同切片版本不要混在同一检索集合中,否则返回结果难以解释。可以通过版本字段过滤,或为重建过程使用独立批次号。  
  
### 15.2 父子切片和上下文扩展  
  
较小 chunk 有利于精确召回,但给模型的上下文可能不完整。工程上可以同时保存父段落与子切片:使用子切片检索,命中后按需补充父段落或相邻片段。  
  
上下文扩展要设置边界,避免跨章节拼接无关内容。标题路径、页码和字符偏移可以帮助判断相邻片段是否真正属于同一语义单元。  
  
### 15.3 质量指标  
  
除了平均字符数,还应观察过短 chunk 比例、超长 chunk 比例、重复 chunk 比例、标题缺失比例和关键问题的 Recall@K。  
  
切片验收要使用真实问题。仅查看长度分布无法判断答案是否被切断,也无法发现表格、步骤列表和错误码被拆散的问题。  
  
## 16. 常见问题  
  
### 16.1 chunk size 多少最好  
  
没有固定答案。  
  
先用:  
  
```text  
800 字符  
120 overlap  

再根据检索效果调整。

如果答案经常缺上下文,可以适当增大。

如果检索结果很泛,可以适当减小。

16.2 overlap 越大越好吗

不是。

overlap 太大会让很多 chunk 很像。

检索结果容易重复。

一般先用 10% 到 20%。

16.3 Markdown 要按标题还是按字符

优先按标题保留结构。

但标题下面内容太长时,仍然要继续按字符或段落切。

常见策略:

先按标题分段  
每段太长再递归切片  

16.4 chunk 里要不要带标题

建议带。

比如 content 前面加:

标题路径:RAG 原理 > 检索流程  
  
正文...  

这样 embedding 时能带上章节语义。

但也要避免标题重复过多。

17. 练习清单

完成几件事情:

安装 langchain-text-splitters实现 recursive character 切片  
设置中文 separators生成 TextChunk 对象  
记录 chunkIndex记录 charStart 和 charEndmetadata 记录 chunker 版本  
写入 rag_chunk更新 rag_document.status = CHUNKED输出 chunk_stats检查空 chunk 和超长 chunk```  
  
建议目录:  
  
```text  
ai-agent-study  
├── python  
│   └── rag-service  
│       ├── chunkers.py  
│       ├── schemas.py  
│       └── main.py  
├── java  
│   └── rag-api  
│       └── PythonChunkClient.java  
└── docs  
 └── document-chunking.md  

18. 小结

本节的结论:

文档切片要在大小、语义完整性和来源信息之间做平衡。

最小链路:

cleaned_text  
 -> RecursiveCharacterTextSplitter -> TextChunk[] -> rag_chunk -> status=CHUNKED  

切片完成后,RAG 入库流程已经走完了文档处理的前半段。

下一步就可以开始生成 Embedding,并把 chunk 变成可检索的向量数据。

参考资料

  • LangChain Text Splitters
  • RecursiveCharacterTextSplitter
AI自学路线
AI
许可协议: 
分享

相关文章

6月 14, 2026

20 文档切片

文档切片 文档切片的核心是把清洗后的长文本切成大小合适、语义完整、带来源信息的 chunk,为后面的 Embedding 和向量检索做准备

6月 13, 2026

19 文本清洗

文本清洗 文本清洗的核心是在不破坏原文结构的前提下,去掉明显噪声,保留标题、段落、页码和来源信息,让后面的切

6月 13, 2026

18 文档解析

文档解析 文档解析的核心是把 PDF、Word、Markdown、TXT 等不同格式统一转换成可处理的文本,同时尽量保留页码、标题、来源等元数据

下一篇

上一篇

19 文本清洗

最近更新

  • 20 文档切片
  • 19 文本清洗
  • 18 文档解析
  • 17 文档上传
  • 16_01PostgreSQL 介绍

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy