乐于分享
好东西不私藏

07 | RAG文档切分 Text Splitter

07 | RAG文档切分 Text Splitter
CSV、JSON、PDF、TXT、Markdown等文档都可以通过不同loader变成LangChain的Document。但进入向量库之前,还有一个很关键的步骤:文档切分。
Text Splitter 在 RAG 里的位置是这里:
原始资料 -> Document Loader -> Document -> Text Splitter -> chunks -> Embedding -> Vector Store
它不负责读取文件,也不负责向量化。它负责把一个长 Document 切成多个更适合检索的小 Document。
所以 Text Splitter 也和不同后缀的文件类型没有强绑定关系。PDF、Markdown、CSV、TXT,只要前面已经被 loader 处理成 Document,后面都可以继续切。
为什么要切?因为 RAG 检索时,我们通常不想把整篇资料塞给模型,而是希望召回“刚好能回答问题的那一小段”。
比如用户问:未发货订单能不能退款?
系统最好只拿到退款规则相关段落,而不是把发票、会员、商品质量问题一起搬出来。模型不是不能读,主要是没必要给它一坨。

一、准备一份普通 TXT 资料

本篇用一个.txt文档来说明。场景是客服知识库:退款、发票、会员续费、商品质量等售后规则都写在同一份文本里。
from pathlib import Path# 示例文件放在 rag/data 目录,和前面几篇保持一致。data_dir = Path("data")data_dir.mkdir(exist_ok=True)kb_path = data_dir / "support_policy.txt"# 准备一份客服知识库文本。# 真实项目里,这个文件通常来自客服后台、知识库系统或运营维护的文档。kb_path.write_text(    "售后知识库说明\n\n"    "一、未发货订单退款\n"    "如果订单还没有发货,用户可以在订单详情页申请退款。系统会自动拦截发货流程,退款通常会在 1 到 3 个工作日内原路退回。"    "如果订单已经进入仓库打包状态,退款申请可能需要人工审核。\n\n"    "二、已发货订单退款\n"    "如果订单已经发货,用户需要先等待商品送达,再根据实际情况申请退货退款。"    "退货时需要保持商品包装完整,并上传物流单号。仓库签收并验货通过后,退款会在 3 到 7 个工作日内退回。\n\n"    "三、发票下载\n"    "电子发票可以在订单详情页下载。如果用户找不到发票入口,可以引导用户进入:我的订单 -> 订单详情 -> 发票信息。"    "如果发票抬头填写错误,需要联系人工客服重新开具。\n\n"    "四、会员自动续费\n"    "会员服务默认不会强制续费。用户如果开通了自动续费,可以在 App 的会员中心关闭。"    "关闭自动续费后,已经购买的会员权益不会立即失效,会持续到当前会员周期结束。\n\n"    "五、商品质量问题\n"    "如果用户收到商品后发现破损、缺件或无法正常使用,需要在签收后 48 小时内提交售后申请。"    "用户需要上传商品照片、外包装照片和问题说明。客服审核通过后,可以安排换货或退款。\n",    encoding="utf-8",)

二、先加载成 Document

TextLoader 适合读取 .txt、日志、纯文本说明这类文件。它会把文本读成 LangChain 的 Document,后面 Text Splitter 就切这个 Document.page_content。
from langchain_community.document_loaders import TextLoaderloader = TextLoader(str(kb_path), encoding="utf-8")documents = loader.load()document = documents[0]print("Document 数量:", len(documents))print("正文长度:", len(document.page_content))print(document.page_content[:300])print(document.metadata)
输出:
Document 数量: 1正文长度: 481售后知识库说明一、未发货订单退款如果订单还没有发货,用户可以在订单详情页申请退款。系统会自动拦截发货流程,退款通常会在 1 到 3 个工作日内原路退回。如果订单已经进入仓库打包状态,退款申请可能需要人工审核。二、已发货订单退款如果订单已经发货,用户需要先等待商品送达,再根据实际情况申请退货退款。退货时需要保持商品包装完整,并上传物流单号。仓库签收并验货通过后,退款会在 3 到 7 个工作日内退回。三、发票下载电子发票可以在订单详情页下载。如果用户找不到发票入口,可以引导用户进入:我的订单 -> 订单详情 -> 发票信息。如果发票抬头填写错误,需要联系人工客服重新开具。四、{'source': 'data/support_policy.txt'}
注意,现在只有 1 个 Document,里面装着整份客服知识库。
如果直接把它写进向量库,检索出来也会是一大段。用户只是问退款,你却把发票、会员、质量问题都带上,信息量很足,但不够精准。

三、用 RecursiveCharacterTextSplitter 切分

通用文本切分,优先从 RecursiveCharacterTextSplitter 开始。它会按一组分隔符从大到小尝试切:先按段落,再按句子,再按标点,最后才按字符兜底。
from langchain_text_splitters import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter(    # 每个 chunk 尽量控制在 180 个字符以内,便于观察切分边界。    chunk_size=180,    # 相邻 chunk 保留 40 个字符重叠,避免规则在切口处断掉。    chunk_overlap=40,    # 默认按字符长度计算 chunk 大小;这里显式写出来,方便理解。    length_function=len,    # separators 不是正则表达式,只是普通字符串分隔符。    is_separator_regex=False,    # 中文文本优先按段落、句号、逗号切分。    separators=["\n\n", "。", ",", " ", ""],    # 把 chunk 在原文中的起始位置写入 metadata["start_index"]。    add_start_index=True,)chunks = splitter.split_documents(documents)print("切分后 chunk 数量:", len(chunks))for i, chunk in enumerate(chunks, start=1):    print("-" * 40)    print("chunk", i, "长度:", len(chunk.page_content))    print(chunk.page_content)    print(chunk.metadata)
输出:
切分后 chunk 数量: 4----------------------------------------chunk 1 长度: 107售后知识库说明一、未发货订单退款如果订单还没有发货,用户可以在订单详情页申请退款。系统会自动拦截发货流程,退款通常会在 1 到 3 个工作日内原路退回。如果订单已经进入仓库打包状态,退款申请可能需要人工审核。{'source': 'data/support_policy.txt', 'start_index': 0}----------------------------------------chunk 2 长度: 97二、已发货订单退款如果订单已经发货,用户需要先等待商品送达,再根据实际情况申请退货退款。退货时需要保持商品包装完整,并上传物流单号。仓库签收并验货通过后,退款会在 3 到 7 个工作日内退回。{'source': 'data/support_policy.txt', 'start_index': 109}----------------------------------------chunk 3 长度: 177三、发票下载电子发票可以在订单详情页下载。如果用户找不到发票入口,可以引导用户进入:我的订单 -> 订单详情 -> 发票信息。如果发票抬头填写错误,需要联系人工客服重新开具。四、会员自动续费会员服务默认不会强制续费。用户如果开通了自动续费,可以在 App 的会员中心关闭。关闭自动续费后,已经购买的会员权益不会立即失效,会持续到当前会员周期结束。{'source': 'data/support_policy.txt', 'start_index': 208}----------------------------------------chunk 4 长度: 93五、商品质量问题如果用户收到商品后发现破损、缺件或无法正常使用,需要在签收后 48 小时内提交售后申请。用户需要上传商品照片、外包装照片和问题说明。客服审核通过后,可以安排换货或退款。{'source': 'data/support_policy.txt', 'start_index': 387}
切分后的每个 chunk 仍然是 Document。也就是说,它既有:
  • page_content:切出来的小段正文
  • metadata:来源文件、起始位置等信息
这就是 RAG 后面做“引用来源”和“排查召回结果”的基础。

四、chunk_size 和 chunk_overlap

这两个参数要多说两句,
chunk_size 控制每块大概多长,而chunk_overlap 控制相邻两块重叠多少内容。
为什么要重叠?因为一条规则可能刚好卡在切分边界上。如果完全不重叠,前半句在上一块,后半句在下一块,检索时就像把一句话从中间撕开。比如下面的例子:
sample_text = "未发货订单可以直接申请退款系统会自动拦截发货流程如果订单进入仓库打包状态退款申请可能需要人工审核退款通常会在1到3个工作日内原路退回"no_overlap_splitter = RecursiveCharacterTextSplitter(    chunk_size=32,    chunk_overlap=0,    separators=[""],)with_overlap_splitter = RecursiveCharacterTextSplitter(    chunk_size=32,    chunk_overlap=10,    separators=[""],)print("不保留重叠:")for text in no_overlap_splitter.split_text(sample_text):    print(text)print("\n保留 10 个字符重叠:")for text in with_overlap_splitter.split_text(sample_text):    print(text)
输出:
不保留重叠:未发货订单可以直接申请退款系统会自动拦截发货流程如果订单进入仓库打包状态退款申请可能需要人工审核退款通常会在1到3个工作日内原路退回保留 10 个字符重叠:未发货订单可以直接申请退款系统会自动拦截发货流程如果订单进入仓库流程如果订单进入仓库打包状态退款申请可能需要人工审核退款通常会在人工审核退款通常会在1到3个工作日内原路退回

这段输出可以这样看:

chunk_size=32表示每个 chunk 最多大约 32 个字符,所以这段话会被切成 3 块。

不保留重叠时,上一块结尾是:

...如果订单进入仓库

下一块接着突兀的说:

打包状态退款申请可能需要人工审核...

这两块单独看都不完整。尤其第一块停在“进入仓库”,读者还不知道进入仓库之后会怎样。

保留chunk_overlap=10后,下一块会从上一块结尾附近重新带一小段:

流程如果订单进入仓库打包状态...

重叠不是为了重复凑字数,而是为了让下一块带上上一块的尾巴。这样即使检索只命中第二块,也能看到前后关系。

即,没有 overlap,切口很干净,但上下文容易断。  有 overlap,会多存一点重复内容,但检索出来的片段更不容易缺头少尾。

经验上可以先这样选:
不要迷信固定数字。真正要看的是:切出来的 chunk 是否能独立表达一个意思,如果一次切不好,那就多切几次。

五、切完再写入向量库

Text Splitter 本身不负责向量化,也不负责检索。切完以后,再把 chunks 交给 Embedding 和 Vector Store。
from langchain_core.vectorstores import InMemoryVectorStorefrom langchain_ollama import OllamaEmbeddingsembeddings = OllamaEmbeddings(    model="qwen3-embedding:latest",    base_url="http://localhost:11434",)# 注意这里写入的是 chunks,不是原始 documents。# 这样检索返回的是更精确的小段内容。vectorstore = InMemoryVectorStore.from_documents(    documents=chunks,    embedding=embeddings,)results = vectorstore.similarity_search(    "未发货订单能退款吗?",    k=2,)for doc in results:    print(doc.page_content)    print(doc.metadata)    print("-" * 40)
输出:
售后知识库说明一、未发货订单退款如果订单还没有发货,用户可以在订单详情页申请退款。系统会自动拦截发货流程,退款通常会在 1 到 3 个工作日内原路退回。如果订单已经进入仓库打包状态,退款申请可能需要人工审核。{'source': 'data/support_policy.txt', 'start_index': 0}----------------------------------------二、已发货订单退款如果订单已经发货,用户需要先等待商品送达,再根据实际情况申请退货退款。退货时需要保持商品包装完整,并上传物流单号。仓库签收并验货通过后,退款会在 3 到 7 个工作日内退回。{'source': 'data/support_policy.txt', 'start_index': 109}----------------------------------------
从这个结果中也可以看到,为了回答“为发货订单能退款吗”,基于overloap它把两个有关联的chunk都给拿出来了。

六、反面示例:不切分会怎样

为了对比一下,现在把原始 documents 直接写进向量库。
注意:原始 documents 只有 1 个 Document,也就是整份 support_policy.txt。
# 这里故意不使用 chunks,而是把完整 documents 写进向量库。raw_vectorstore = InMemoryVectorStore.from_documents(    documents=documents,    embedding=embeddings,)raw_results = raw_vectorstore.similarity_search(    "未发货订单能退款吗?",    k=1,)for doc in raw_results:    print(doc.page_content)    print(doc.metadata)
输出:
售后知识库说明一、未发货订单退款如果订单还没有发货,用户可以在订单详情页申请退款。系统会自动拦截发货流程,退款通常会在 1 到 3 个工作日内原路退回。如果订单已经进入仓库打包状态,退款申请可能需要人工审核。二、已发货订单退款如果订单已经发货,用户需要先等待商品送达,再根据实际情况申请退货退款。退货时需要保持商品包装完整,并上传物流单号。仓库签收并验货通过后,退款会在 3 到 7 个工作日内退回。三、发票下载电子发票可以在订单详情页下载。如果用户找不到发票入口,可以引导用户进入:我的订单 -> 订单详情 -> 发票信息。如果发票抬头填写错误,需要联系人工客服重新开具。四、会员自动续费会员服务默认不会强制续费。用户如果开通了自动续费,可以在 App 的会员中心关闭。关闭自动续费后,已经购买的会员权益不会立即失效,会持续到当前会员周期结束。五、商品质量问题如果用户收到商品后发现破损、缺件或无法正常使用,需要在签收后 48 小时内提交售后申请。用户需要上传商品照片、外包装照片和问题说明。客服审核通过后,可以安排换货或退款。{'source': 'data/support_policy.txt'}
这时虽然也能检索到相关内容,但返回的是整份售后知识库。
这就是切分前后的差别:
  • 不切分:召回整份资料
  • 切分后:召回相关片段
RAG 不是只要“找得到”,还要“拿得准”。这也是 Text Splitter 存在的核心价值。

这篇只记住几件事

  1. Text Splitter 切的是文本内容,与文件后缀无关,所以源文件可以是 TXT、PDF、Markdown、CSV,只要最后变成 Document,就可以继续切。
  2. 通用文本切分,优先理解 RecursiveCharacterTextSplitter。
  3. chunk_size 控制每块长度。
  4. chunk_overlap 让相邻 chunk 保留一点上下文。
  5. 切完以后,再把 chunks 写入向量库。

相关学习资料