「让 AI 读懂你的文档」文本分割与 Embedding 实战
关联阅读:前一篇《向量数据库怎么选》
一、最大的坑:你的文档”碎”得不对
很多人觉得 RAG 难,是难在”效果调优”。
但实际上,60% 的问题出在第一步:文本分割。
你可能遇到过这种情况:
-
问 AI “这份合同的关键条款是什么?”它答了一堆无关内容
-
明明文档里有答案,但 AI 硬是说”没找到”
-
检索出来了,但召回的内容东一块西一块,看不懂在说什么
这些问题,90% 是因为你的文档没有被正确地”切开”。
二、为什么文本分割这么重要?
2.1 原理:AI 是怎么”读”文档的?
RAG 的流程是:
文档 → 分块 → 向量化 → 存入向量库 ↓用户问题 → 向量化 → 检索最相似的块 → 送给 LLM
关键点:AI 不是读整篇文档,而是一次只读一个块。
如果这个块太小 → 上下文不够,AI 看不懂 如果这个块太大 → 语义太杂,AI 注意力分散 如果切的位置不对 → 把一句完整的话切成两半,意思全变了
2.2 一个真实的失败案例
某客户的产品手册,每页是一个产品型号,格式是:
【产品名称】XXX【产品型号】XXX【功能特点】- 功能1: xxx- 功能2: xxx最初用固定 500 字分块,结果:
第一个块包含了”产品名称”和”产品型号”
第二个块从”功能特点”中间断开
第三个块只有后半部分功能
检索”这个产品有哪些功能”时,召回的内容全是碎的,用户体验极差。
后来改成按”【】”标题分割,每个块都是一个完整的产品卡片,问题立刻解决。
三、四种文本分割策略
3.1 固定长度分割(最简单,但效果最差)
from langchain.text_splitter import CharacterTextSplittersplitter = CharacterTextSplitter( chunk_size=500, chunk_overlap=50)chunks = splitter.split_text(text)
问题:完全不考虑语义,该断的地方不断,不该断的地方乱断。
适用场景:只有纯文本,没有任何结构
3.2 递归字符分割(推荐默认选项)
Langchain 推荐的做法,按优先级逐层尝试分割:
from langchain.text_splitter import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter( chunk_size=256, # 目标块大小(字符) chunk_overlap=64, # 重叠区域 separators=[ "\n\n", # 第一优先:按段落分割 "\n", # 第二优先:按换行分割 "。", # 第三优先:按句号分割 ",", # 第四优先:按逗号分割 "" # 最后:按字符分割 ])chunks = splitter.split_text(text)
原理:
-
先按
\n\n(段落)分割 -
如果段落太长,按
\n(换行)分割 -
如果还太长,按句号分割
-
以此类推,直到块足够小
实测效果:比固定长度分割,召回率提升 15-20%。
3.3 按标题/结构分割(最适合有结构的文档)
对于有明确结构的文档(手册、报告、合同),按结构分割效果最好:
from langchain.text_splitter import MarkdownTextSplitter# 按 Markdown 标题分割splitter = MarkdownTextSplitter( chunk_size=500, overlap=50)chunks = splitter.split_text(markdown_text)
或者用更灵活的方案:
import refrom langchain.text_splitter import TextSplitterclass StructuredSplitter(TextSplitter): def __init__(self, pattern, **kwargs): self.pattern = re.compile(pattern) super().__init__(**kwargs) def split_text(self, text): # 按章节标题分割 sections = self.pattern.split(text) return [s.strip() for s in sections if s.strip()]# 按 "第X章" 或 "【XXX】" 分割splitter = StructuredSplitter( pattern=r'(第\d+章|【[^】]+】)', chunk_size=500)chunks = splitter.split_text(contract_text)
3.4 语义分割(进阶方案,效果最好但实现复杂)
用 Embedding 模型判断”语义断点”,在意义完整的地方才切断:
from langchain_experimental.text_splitter import SemanticChunkerfrom langchain_openai import OpenAIEmbeddings# 按语义断点分割splitter = SemanticChunker( embeddings=OpenAIEmbeddings(), breakpoint_threshold_amount=0.95 # 相似度阈值)chunks = splitter.create_documents([long_text])
原理:
-
把文本切成句子
-
计算相邻句子的 Embedding 相似度
-
相似度突然下降的地方 = 语义断点
-
在断点处切断
效果:比递归分割,召回率再提升 5-10%。
缺点:速度慢,成本高(需要调用更多 Embedding API)。
四、分块参数的实战经验
4.1 块大小:256 是中文最优值
|
块大小 |
召回率 |
精确率 |
适用场景 |
|---|---|---|---|
|
128 |
中 |
高 |
语义极密集的内容 |
|
256 |
高 |
高 |
大多数中文文档 |
|
512 |
高 |
中 |
结构清晰的长文档 |
|
1024 |
中 |
低 |
不推荐 |
为什么是 256?
-
中文 256 字符 ≈ 300-400 token
-
刚好够 LLM 理解一小段完整内容
-
又不至于信息太杂
4.2 重叠:20-25% 是黄金比例
chunk_overlap = int(chunk_size * 0.25) # 256 * 0.25 = 64
作用:防止块边界切断语义,让检索更连贯。
五、Embedding 模型的选择与优化
5.1 为什么 Embedding 决定召回上限?
如果说文本分割是”切菜”,Embedding 就是”把菜变成向量”。
如果菜切得再整齐,但向量转化得不对,AI 依然找不到。
5.2 中文场景模型选型
|
模型 |
中文效果 |
速度 |
推荐度 |
|---|---|---|---|
|
BGE-large-zh |
⭐⭐⭐⭐⭐ |
中 |
首选 |
|
BGE-base-zh |
⭐⭐⭐⭐ |
快 |
备选 |
|
M3E |
⭐⭐⭐⭐ |
快 |
特定场景 |
|
text2vec |
⭐⭐⭐ |
快 |
轻量场景 |
5.3 Embedding 实战代码
from langchain_community.embeddings import HuggingFaceBgeEmbeddings# 初始化(支持本地部署,数据不出网)embedding = HuggingFaceBgeEmbeddings( model_name="BAAI/bge-large-zh", model_kwargs={ "device": "cpu" # 或 "cuda" 如果有 GPU }, encode_kwargs={ "normalize_embeddings": True # 归一化,检索更准 })# 单句向量化query_vec = embedding.embed_query("劳动合同最长期限是多久?")print(f"向量维度: {len(query_vec)}")# 批量向量化doc_vecs = embedding.embed_documents([ "第一章 总则", "第二章 劳动者的权利和义务", "第三章 劳动合同的订立"])
六、完整实战:从文档到向量
from langchain_community.document_loaders import PyPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_community.embeddings import HuggingFaceBgeEmbeddingsfrom langchain_community.vectorstores import Qdrant# 1. 加载文档loader = PyPDFLoader("产品手册.pdf")pages = loader.load_and_split()# 2. 文本分割splitter = RecursiveCharacterTextSplitter( chunk_size=256, chunk_overlap=64, separators=["\n\n", "\n", "。", ","])chunks = splitter.split_documents(pages)# 3. 向量化embedding = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-large-zh")# 4. 存入向量数据库vectorstore = Qdrant.from_documents( documents=chunks, embedding=embedding, client=client, collection_name="product_manual")print(f"✅ 处理完成:{len(pages)} 页 → {len(chunks)} 个块")
七、常见问题与解决方案
|
问题 |
原因 |
解决方案 |
|---|---|---|
|
召回率低 |
块太小或太大 |
调 chunk_size 到 256 |
|
检索不连贯 |
块之间没重叠 |
加 chunk_overlap |
|
相关内容搜不到 |
Embedding 模型不对 |
换成 BGE-large-zh |
|
结构化文档效果差 |
没按结构分割 |
用 MarkdownSplitter |
|
多文档效果不稳定 |
每个文档格式不同 |
先统一格式再分割 |
总结
三个核心要点:
-
文本分割是 RAG 的第一步,也是最容易出问题的步骤
-
256 字块 + 64 字重叠,是中文文档的最优默认参数
-
有结构的文档(手册、合同)按结构分割,效果比固定长度好 30%+
下一步 & 变现钩子
文本分割看起来是”体力活”,但其实是最能拉开差距的地方。 很多人搭建知识库效果不好,不是算法不行,是文档没切对。
搞定分割,下一步就是调优。下一篇《RAG 效果调优》会讲 5 个核心参数怎么调。
如果你也在处理文档分割的问题,欢迎扫码聊一聊。 我可以帮你评估现有文档结构,给出具体的分割策略建议。
备注”分割”,送你一份《常见文档分割策略对比表》👇
📌 关联阅读
关注「林间昭语」公众号,回复以下关键词领取资料:
回复”知识库” → 领取《RAG 交付自检清单》
回复”分割” → 领取《常见文档分割策略对比表》
回复”选型” → 领取《向量数据库选型评估表》
回复”调优” → 领取《RAG 调优参数手册》
关注「林间昭语」,用技术创造可能。点击上方蓝色公众号名称 → 设为星标 🌟,第一时间收到干货。
夜雨聆风