avatar

一条在知识海洋的咸鱼

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

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

20 文档切片

发表于 28天前 更新于 28天前
作者 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 变成可检索的向量数据。

c8c9c540-a497-4d5e-aee9-9ecc368b7673.jpeg
这个是切片完成之后的效果

参考资料

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

相关文章

7月 10, 2026

47 LangGraph Node:拆分意图、检索、订单与工单节点

LangGraph Node:拆分意图、检索、订单与工单节点 Node 应该职责清晰、输入输出稳定、可独立测试。不

7月 9, 2026

46 LangGraph State:设计可恢复的 Agent 状态

LangGraph State:设计可恢复的 Agent 状态 State 是 Agent 节点之间共享的数据契约。它决定任务能否暂停、恢复、测试和排查。

7月 9, 2026

45 业务接口设计:订单、工单、确认与日志 API

业务接口设计:订单、工单、确认与日志 API Agent 最终调用的是业务接口。接口越清楚,模型ว

下一篇

21 Embedding 向量化

上一篇

19 文本清洗

最近更新

  • 47 LangGraph Node:拆分意图、检索、订单与工单节点
  • 46 LangGraph State:设计可恢复的 Agent 状态
  • 45 业务接口设计:订单、工单、确认与日志 API
  • 44 售后工单 Agent 需求分析
  • 43 Agent 与 Workflow:固定流程和动态决策的边界

热门标签

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

目录

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

使用 Halo 主题 Chirpy