乐于分享
好东西不私藏

「让 AI 读懂你的文档」文本分割与 Embedding 实战

「让 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)

原理

  1. 先按 \n\n(段落)分割

  2. 如果段落太长,按 \n(换行)分割

  3. 如果还太长,按句号分割

  4. 以此类推,直到块足够小

实测效果:比固定长度分割,召回率提升 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])

原理

  1. 把文本切成句子

  2. 计算相邻句子的 Embedding 相似度

  3. 相似度突然下降的地方 = 语义断点

  4. 在断点处切断

效果:比递归分割,召回率再提升 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

多文档效果不稳定

每个文档格式不同

先统一格式再分割


总结

三个核心要点:

  1. 文本分割是 RAG 的第一步,也是最容易出问题的步骤

  2. 256 字块 + 64 字重叠,是中文文档的最优默认参数

  3. 有结构的文档(手册、合同)按结构分割,效果比固定长度好 30%+


下一步 & 变现钩子

文本分割看起来是”体力活”,但其实是最能拉开差距的地方。 很多人搭建知识库效果不好,不是算法不行,是文档没切对。

搞定分割,下一步就是调优。下一篇《RAG 效果调优》会讲 5 个核心参数怎么调。

如果你也在处理文档分割的问题,欢迎扫码聊一聊。 我可以帮你评估现有文档结构,给出具体的分割策略建议。

备注”分割”,送你一份《常见文档分割策略对比表》👇


📌 关联阅读

关注「林间昭语」公众号,回复以下关键词领取资料:

  • 回复”知识库” → 领取《RAG 交付自检清单》

  • 回复”分割” → 领取《常见文档分割策略对比表》

  • 回复”选型” → 领取《向量数据库选型评估表》

  • 回复”调优” → 领取《RAG 调优参数手册》


关注「林间昭语」,用技术创造可能。点击上方蓝色公众号名称 → 设为星标 🌟,第一时间收到干货。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 「让 AI 读懂你的文档」文本分割与 Embedding 实战

猜你喜欢

  • 暂无文章