原始资料 -> Document Loader -> Document -> Text Splitter -> chunks -> Embedding -> Vector Store一、准备一份普通 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
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'}
三、用 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}
page_content:切出来的小段正文 metadata:来源文件、起始位置等信息
四、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,会多存一点重复内容,但检索出来的片段更不容易缺头少尾。

五、切完再写入向量库
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}----------------------------------------
六、反面示例:不切分会怎样
# 这里故意不使用 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'}
不切分:召回整份资料 切分后:召回相关片段
这篇只记住几件事
Text Splitter 切的是文本内容,与文件后缀无关,所以源文件可以是 TXT、PDF、Markdown、CSV,只要最后变成 Document,就可以继续切。 通用文本切分,优先理解 RecursiveCharacterTextSplitter。 chunk_size 控制每块长度。 chunk_overlap 让相邻 chunk 保留一点上下文。 切完以后,再把 chunks 写入向量库。
夜雨聆风