2024本地RAG实战测评:高效构建与优化指南(中上篇)
O、猎奇的文档
为了验证后续RAG系统的检索效果,我特意用AI生成了一份约2700字的《不正经有限公司 正经管理办法》。这份文档充斥着大量违反常理的公司条例,例如“员工必须使用左手接听电话”、“会议室严禁讨论任何正经事务”等。
这样做的目的非常明确:当你向构建完成的知识库提问时,如果系统返回的是这些荒诞条款,而非通用的公司制度内容,那就构成了确凿证据——你的私有知识库已成功生效,它精准地“记住”并优先检索了你所注入的独特内容。
一、为什么需要文档切片
在上一环节的思路梳理中,我们明确了两个核心要点:
- 句向量的核心价值在于高效检索。它能依据问题或关键词,快速定位语义最相近的句子。
- 然而,这些被检索到的句子,最终仍需封装进提示词(Prompt),交由基于词向量的大语言模型(LLM)进行理解并生成回答。
基于此流程,一个关键矛盾便凸显出来:
- 句向量对应的文本块长度必须严格控制。若每次对话都嵌入数千字的文档片段,不仅会急剧消耗Token(导致成本飙升与响应延迟),还会干扰LLM抓取重点,严重影响回答的准确性。
- 反之,文本块也不能过短。信息残缺的短句容易引发断章取义,导致检索结果意义模糊,无法为回答提供有效支撑。
因此,当前行业的主流解决方案是:在将文本转化为句向量之前,先执行一道“切片”工序。目标清晰:
- 确保每个待向量化的文本块长度适中。
- 确保每个文本块都是一个语义完整、逻辑独立的单元。
在着手编写代码前,建议先通过可视化工具网站(chunkviz.com)进行实验,直观感受不同切片策略的效果差异,从而为你的文档选定最优的切分方案。
二、了解切分的基本概念
打开 chunkviz.com 网站,你将看到如下操作界面:
这里涉及四个核心参数:
- text: 待切分的原始文档文本。
- Splitter (切分器): 执行切割任务的工具。不同的Splitter针对特定文本类型优化,例如通用文本、代码、Markdown标题结构等各有专攻。
- ChunkSize (块长度): 你期望每个文本块包含的字符(或Token)数量上限。这是控制块大小的核心参数。
- ChunkOverlap (块间重叠长度): 为避免完整的句子被生硬地切割到两个不同的块中,可以设置一个重叠量。例如,后一个块的开头部分会包含前一个块的结尾部分,以此保障上下文的连贯性。
仅看概念可能略显抽象。该网站的优势在于提供了直观的可视化效果。保持上图中的默认配置,你将看到如下色彩分布图:
此图清晰地展示了切分过程。由于选择了“Token Splitter”,它基本按照 `ChunkSize = 500` 的设定,将文本划分为三个主要片段:
- 蓝色 + 绿色1 = 片段1
- 绿色1 + 黄色 + 绿色2 = 片段2
- 绿色2 + 粉色 + 绿色3 = 片段3
请注意那些绿色的部分,它们正是由 `ChunkOverlap` 参数控制的重叠片段。这种设计能有效防止句子被拦腰截断,其代价是关键信息可能同时出现在相邻的两个块中。但权衡利弊,此举无疑是利远大于弊的。
三、选择适合你的Splitter
选择不同的 `Splitter` 对最终拆分结果的影响,远大于单纯调整 `ChunkSize` 和 `ChunkOverlap` 的数值。因为不同的拆分器内置了针对特定文本格式的解析逻辑。
我们稍作调整,将配置切换为 `MarkdownHeaderSplitter`,再看效果:
效果立竿见影!它不再机械地按固定长度切割,而是智能地识别出文档的二级标题(##),并以此作为语义边界进行切分。如此一来,每个切片都对应一个完整的章节,语义完整性得到极大提升。
针对不同场景,例如切分Java代码,还有更多专用Splitter可选。但就我们当前处理的Markdown文档而言,选用 `MarkdownHeaderTextSplitter` 无疑是最佳策略。
在实际工程实践中,更常见的做法是采用“两步走”策略,兼顾语义结构与长度限制:
保留灵魂 (MarkdownHeaderTextSplitter):首先依据 #、##、### 等标题层级进行切分。这一步的精妙之处在于,它会将所属的标题信息作为元数据(Metadata)附加在每个文本块上。这相当于为每段文本加装了GPS定位器,未来大模型在处理时,能立刻知晓其出处章节,上下文脉络一目了然。
控制体型 (RecursiveCharacterTextSplitter):如果某个标题下的内容仍然过长(例如超出了底层向量模型如bge-small的512长度限制),则进行二次切分。此次按段落、句子进行切割,并设置合理的块大小(如400字符)和重叠量(如50字符),以完美适配向量模型的“消化”能力。
四、在代码里切分
理论明晰后,接下来进入实战环节。你可以尝试运行以下代码来测试和验证整个切片流程:
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
# 1. 模拟一篇真实的 Markdown 文档
markdown_text = """
# DeepSeek 使用指南
## 1. 简介
DeepSeek 是一家专注于大语言模型的初创公司。它的特点是性价比极高,且模型能力在开源界名列前茅。
## 2. API 调用
### 2.1 准备工作
你需要先去官网注册账号,并获取一个 API Key。请妥善保管这个 Key。
### 2.2 Python 代码示例
这里是一段如何用 Python 发起请求的代码...(假设这里有一万字)
"""
# --- 第一步:按 Markdown 标题切分 ---
# 定义需要识别的标题级别及其在元数据中的字段名
headers_to_split_on = [
("#", "主标题"), # 识别 #
("##", "二级标题"), # 识别 ##
("###", "三级标题"), # 识别 ###
]
# 初始化 Markdown 切分器
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False # 保留文本中的 # 号标记(若需移除可设为 True)
)
# 执行第一步切分
md_header_splits = markdown_splitter.split_text(markdown_text)
# --- 第二步:按字符长度二次切分 ---
# 若某个标题下的内容过长(例如超过 bge-small 的 512 限制),则进行细化切割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=400, # 限制每块最大 400 字符
chunk_overlap=50 # 设置 50 字符的重叠以防止断句
)
# 执行第二步切分 (注意此处使用 split_documents,因为输入已是结构化数据)
final_splits = text_splitter.split_documents(md_header_splits)
# --- 查看结果 ---
print(f"一共切成了 {len(final_splits)} 块\n")
for i, doc in enumerate(final_splits):
print(f"--- 第 {i+1} 块 ---")
print(f"? 内容: {doc.page_content.strip()}")
# 除了切片内容,它还生成了至关重要的元数据
print(f"?️ 标签(Metadata): {doc.metadata}\n")
执行上述脚本,你将看到类似下图的输出:
你可能已经注意到,除了生成切片内容,Markdown Splitter还自动生成了metadata。这有用吗?
极其有用!这堪称企业级应用的“王牌功能”。未来在进行向量检索时,你可以这样运用它:
results = collection.query(
query_texts=["怎么写 Python 代码?"],
n_results=2,
# 王牌用法:限定仅在主标题为“DeepSeek 使用指南”的范围内进行向量搜索!
where={"主标题": "DeepSeek 使用指南"}
)
这意味着你可以实现带过滤条件的精准检索。例如,当你的知识库同时包含公司制度、技术文档、产品手册等多类资料时,用户提出技术问题,你可以限定仅在“技术文档”这个主标题下进行搜索,完全避开无关的制度条款,从而大幅提升检索的准确性与效率。这对于构建复杂、多领域的企业知识库而言,是不可或缺的核心能力。
至此,你已经掌握了文档切片的核心原理与基础实践。这是构建高质量RAG系统的基石。下一步,我们将基于这些规整的文本块,着手生成我们的【句向量】数据库。
下一步预告
本节课我们完成了文档切片——这是决定RAG检索质量最关键的预处理步骤。地基已经夯实,接下来我们将开始浇筑主体结构:将这些文本切片转化为向量,并存入数据库,构建起真正可检索的智能知识库。






