一、先知道什么是RAG
1. 一句话给 RAG 下定义
RAG = 给大模型(比如 ChatGPT、豆包、开源大模型)外挂了一个你自己的私有知识库,让大模型能精准、无差错地回答你私有数据里的内容,不会瞎编、不会用过时的知识。
你可以把 RAG 理解成:
- 大模型是一个超级会说话、会总结的「秘书」,但她的知识是固定的、过时的,而且完全不知道你电脑里的 PDF、文档、笔记这些私有内容
- RAG 就是你给这个秘书配的一个专属智能文件柜 + 精准检索神器,你把自己的所有文档、PDF、笔记都放进这个柜子里,秘书回答问题前,会先去这个柜子里精准找到对应的内容,再基于这些内容给你回答,绝对不会瞎编。
2. RAG 到底解决了什么核心痛点?
大模型本身有两个致命缺陷,RAG 就是专门解决这两个问题的:
-
大模型的知识是「过时的、固定的」
比如 2024 年的大模型,不知道 2025 年的新政策、新行业动态、你公司最新的产品手册,你问她,她要么瞎编,要么说「我的知识截止到 XX 时间」。
-
大模型完全不知道你的「私有数据」
你电脑里的公司的内部制度、项目文档、个人笔记,这些内容从来没有在互联网上出现过,大模型完全没见过,你直接问她,她 100% 会瞎编乱造。
-
RAG 的核心价值,就是让大模型能用上你的私有数据、最新的知识,回答 100% 贴合你的需求,不会出错、不会瞎编。
二、实操及讲解
模块 1:前置知识与核心认知
核心基础概念认知
- 文本块(Chunk):拆分后的每一段独立的短文本,是 RAG 系统中检索和向量化的最小单元
- 块大小(Chunk Size):每个文本块的最大长度(通常按字符数 /token 数计算),是分块最核心的参数
- 块重叠(Chunk Overlap):相邻两个文本块之间重复的内容长度,解决分块导致的上下文断裂问题
模块 2:文本分块核心规则与策略学习
1. 核心参数选型规则
块大小(Chunk Size)选型
| 场景 | 推荐块大小(字符数) | 对应 token 数(约) | 核心原则 |
|---|---|---|---|
| 通用知识库 / 网页文本 | 100-500 | 50-250 | 兼顾检索精度和上下文完整性 |
| 论文 / 长文本 / 书籍 | 500-1000 | 250-500 | 保证段落语义完整,避免拆分核心内容 |
| 问答 / 精准知识库 | 50-200 | 25-100 | 最大化提升检索匹配精度 |
| 通用红线 | 必须小于向量模型最大输入长度,预留 10%-20% 冗余空间 | - | 绝对不能超出模型输入限制 |
块重叠(Chunk Overlap)选型
- 通用规则:重叠长度为块大小的10%-20%,比如块大小 500 字符,重叠 50-100 字符
- 高连贯性长文本场景:重叠比例提升到20%-30%,彻底避免上下文断裂
- 核心原则:重叠部分必须包含完整的句子 / 语义单元,不能只重叠单个无意义的词语
2. 主流分块策略详解
| 分块策略 | 核心逻辑 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定长度分块(最基础) | 按固定的字符数 /token 数拆分文本,达到指定长度就拆分 | 实现简单、速度快、长度完全可控,适配绝大多数向量模型 | 可能拆分完整的句子 / 段落,导致语义断裂 | 通用知识库、网页文本、PDF 文档等绝大多数场景 |
| 语义单元分块(按段落 / 标题) | 先按文本天然结构拆分(标题、段落、句子),再对超长段落二次拆分 | 完全保留文本语义结构,不会拆分完整句子,语义完整性拉满 | 拆分后块长度不均匀,可能出现超长块,需要二次处理 | 结构化文档、论文、书籍、带标题层级的文档 |
| 带重叠滑动窗口分块(推荐) | 固定长度分块的基础上,相邻块之间保留指定长度的重叠内容 | 解决了上下文断裂问题,同时保留长度可控的优点,是 RAG 项目首选 | 少量内容冗余,轻微增加计算量 | 长文本、高连贯性要求的所有 RAG 场景 |
| 智能语义分块(进阶) | 基于大模型 / 语义相似度模型,自动识别文本语义边界,在转折处拆分 | 拆分效果最贴合语义逻辑,块之间语义独立性最强 | 实现复杂、速度慢、需要调用模型,成本高 | 法律 / 医疗等专业知识库、对检索精度要求极高的场景 |
PS: 带重叠的滑动窗口是什么意思?
带重叠滑动窗口(RAG 必懂)
一句话:切文本不一刀断开,后一块开头重复带上前一块末尾一小段文字,两块内容有重叠区域,防止一句话被劈成两半、关键信息丢失。
先对比两种切法(一眼看懂区别)
① 无重叠固定切块(普通一刀切)
设定:块长 500 字符,切完直接从 500 位置新开下一块
块 1:0~500
块 2:500~1000
块 3:1000~1500
坑:刚好一句话卡在 500 字符中间,前半句在块 1、后半句在块 2。用户提问只搜到块 2,后半句孤零零,看不懂前文,RAG 回答出错。
示例原文:订单超时未支付时,系统发送消息到RabbitMQ,消费者查询订单状态
一刀切拆分:
块 1:订单超时未支付时,系统发送消息到
块 2:RabbitMQ,消费者查询订单状态
搜 "消费者做什么" 只召回块 2,不知道什么场景下查订单。
② 带重叠滑动窗口(滑动切块)
参数:chunk_size=500(每块总长度),overlap=100(重叠100字符,重叠比例20%)
滑动步长 = 总块长−重叠 = 500−100=400,下一块不从 500 开始,从 400 开始
块 1:0~500(共 500 字)
块 2:400~900(从 400 起步,前面 400~500 这 100 个字和块 1 重复 = 重叠)
块 3:800~1300(800~900 和块 2 重复)
原来被劈断的那句话,后半句在块 2,前半句被重叠带进块 2 里,不管搜到块 1 还是块 2,整句话信息完整。
三个核心参数
- chunk_size:窗口大小 = 每一块总字符长度(默认 500)
- overlap:重叠字符数(块末尾复制到下一块开头,一般 10%~20%)
- step:滑动步长 = chunk_size−overlap(每次往后挪多少)
举例:500 块长、20% 重叠→重叠 100、步长 400。
为什么 RAG 一定要用重叠?
- 解决语义断裂:避免关键词、完整句子被切在两块中间,检索缺上下文(最核心目的)
- 提升检索命中率:跨边界的内容会同时存在两个块里,提问更容易匹配到完整信息
- 通用行业规范:普通文档重叠 10%~20%;法律 / 医疗专业文档 20%~30%
3. 实操代码
python
# -*- coding: utf-8 -*-
"""
@Created on : 2026/6/3 15:11
@creator : er_nao
@File :day_86.py
@Description :PDF长文档完整读取
"""
import pdfplumber
pdf_file_path = "C:\\Users\\hp\\Desktop\\NLP学习数据\\arctle2.pdf"
# 文本提取代码
def extract_full_text_from_pdf(pdf_file_path):
"""
提取PDF全量文本内容,返回拼接后的完整长文本
:param pdf_path: PDF文件路径
:return: 完整长文本字符串
"""
with pdfplumber.open(pdf_file_path) as pdf:
full_text_list = []
for page in pdf.pages:
page_text = page.extract_text(
x_tolerance=0.5, y_tolerance=0.5, keep_blank_chars=False
)
if page_text.strip():
full_text_list.append(page_text)
# 拼接所有页面的文本,用换行符分隔
full_text = "\n".join(full_text_list)
print(f"PDF文本提取完成,总字符数:{len(full_text)}")
return full_text
# 实操任务 1:基础固定长度文本分块代码
def fixed_length_chunking(text, chunk_size=500):
"""
基础固定长度文本分块函数
:param text: 输入的完整长文本
:param chunk_size: 每个文本块的最大字符数,默认500
:return: 分块后的文本块列表
"""
# 初始化结果列表
chunk_list = []
# 计算文本总长度
text_length = len(text)
# 按固定步长遍历文本,拆分块
for start_index in range(0, text_length, chunk_size):
# 计算当前块的结束索引
end_index = min(start_index + chunk_size, text_length)
# 截取当前块的文本
current_chunk = text[start_index:end_index]
# 过滤掉空块
if current_chunk.strip():
chunk_list.append(current_chunk)
# 输出分块结果统计
print(f"===== 固定长度分块完成 =====")
print(f"总文本长度:{text_length} 字符")
print(f"预设块大小:{chunk_size} 字符")
print(f"最终分块数量:{len(chunk_list)} 个")
print(f"单个块最大长度:{max([len(chunk) for chunk in chunk_list])} 字符")
print("=============================")
return chunk_list
# 实操任务 2:按段落 / 语义单元结构化分块代码
def paragraph_based_chunking(text, max_chunk_size=500):
"""
按段落/语义单元的结构化分块函数
核心逻辑:先按天然段落拆分,再对超长段落二次拆分,保证语义完整性
:param text: 输入的完整长文本
:param max_chunk_size: 单个块的最大字符数,默认1000
:return: 分块后的文本块列表
"""
# 第一步:按换行符拆分天然段落,过滤掉空段落
raw_paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
print(f"原始段落数量:{len(raw_paragraphs)}个")
# 第二步:遍历所有段落,拆分超长段落,生成最终块
chunk_list = []
for paragraph in raw_paragraphs:
paragraph_length = len(paragraph)
# 如果段落长度小于等于最大块大小,直接作为一个块
if paragraph_length <= max_chunk_size:
chunk_list.append(paragraph)
# 如果段落超长,进行二次固定长度拆分
else:
for start in range(0, paragraph_length, max_chunk_size):
end = min(start + max_chunk_size, paragraph_length)
sub_chunk = paragraph[start:end]
if sub_chunk.strip():
chunk_list.append(sub_chunk)
# 输出分块结果统计
print(f"===== 段落结构化分块完成 =====")
print(f"最终分块数量:{len(chunk_list)} 个")
print(f"单个块最大长度:{max([len(chunk) for chunk in chunk_list])} 字符")
print(f"单个块最小长度:{min([len(chunk) for chunk in chunk_list])} 字符")
print("=============================")
return chunk_list
# 实操任务 3:带重叠的滑动窗口分块代码(RAG 项目推荐)
def sliding_window_chunking(text, chunk_size=500, overlap_ratio=0.2):
"""
带重叠的滑动窗口分块函数(RAG项目首选方案)
核心逻辑:固定长度分块的基础上,相邻块之间保留指定比例的重叠内容,解决上下文断裂问题
:param text: 输入的完整长文本
:param chunk_size: 每个文本块的最大字符数,默认500
:param overlap_ratio: 相邻块的重叠比例,默认20%(0.2),推荐10%-30%
:return: 分块后的文本块列表
"""
# 计算重叠的字符数
overlap_length = int(chunk_size * overlap_ratio)
# 计算每次滑动的步长(块大小 - 重叠长度)
step_length = chunk_size - overlap_length
# 初始化结果列表
chunk_list = []
text_length = len(text)
for start_index in range(0, text_length, step_length):
# 计算当前块的结束索引
end_index = min(start_index+chunk_size, text_length)
# 截取当前块的文本
current_chunk = text[start_index:end_index]
# 过滤掉空块
if current_chunk.strip():
chunk_list.append(current_chunk)
# 输出分块结果统计
print(f"===== 滑动窗口分块完成 =====")
print(f"预设块大小:{chunk_size}字符")
print(f"重叠比例:{overlap_ratio*100}%,重叠长度:{overlap_length}字符")
print(f"滑动步长:{step_length}字符")
print(f"总文本长度:{text_length}字符")
print(f"最终分块数量:{len(chunk_list)}个")
print(f"单个快最大长度:{max([len(chunk) for chunk in chunk_list])}字符")
print("=============================")
return chunk_list
# 分块结果保存与校验代码
import json
# -------------------------- 结果保存路径 --------------------------
output_chunk_path = "C:\\Users\\hp\\Desktop\\NLP学习数据\\RAG文本分块结果.json"
# -----------------------------------------------------------------
def save_chunks_to_file(chunk_list, save_path):
"""
将分块结果保存为JSON文件,方便后续RAG系统使用
:param chunk_list: 分块后的文本块列表
:param save_path: 保存路径
"""
# 结构化保存,包含块序号、块内容、块长度
chunk_result = []
for idx, chunk in enumerate(chunk_list):
chunk_result.append({
"chunk_id": idx +1,
"chunk_text": chunk,
"chunk_length": len(chunk)
})
with open(save_path, 'w', encoding='utf-8') as f:
json.dump(chunk_result, f, ensure_ascii=False, indent=2)
print(f"分块结果已保存至:{save_path}")
print(f"共保存 {len(chunk_result)} 个文本块")
if __name__ == "__main__":
full_text = extract_full_text_from_pdf(pdf_file_path)
# 1:基础固定长度文本分块代码
# fixed_chunks = fixed_length_chunking(full_text, chunk_size=500)
# 打印前3个块,预览结果
# for i, chunk in enumerate(fixed_chunks[:3]):
# print(f"\n--- 第{i + 1}个文本块 ---")
# print(f"长度:{len(chunk)} 字符")
# print(f"内容:{chunk[:200]}...") # 只打印前200个字符预览
# 2:按段落 / 语义单元结构化分块代码
# paragraph_chunks = paragraph_based_chunking(full_text, chunk_size=500)
# 打印前3个块,预览结果
# for i, chunk in enumerate(paragraph_chunks[:3]):
# print(f"\n--- 第{i + 1}个文本块 ---")
# print(f"长度:{len(chunk)} 字符")
# print(f"内容:{chunk[:200]}...")
# 3:带重叠的滑动窗口分块代码(RAG项目推荐)
sliding_chunks = sliding_window_chunking(full_text, chunk_size=500, overlap_ratio=0.2)
# 打印前4个块,预览相邻块的重叠效果
for i, chunk in enumerate(sliding_chunks[:4]):
print(f"\n---第{i+1}个文件块---")
print(f"长度:{len(chunk)}字符")
print(f"内容:{chunk}...")
# 分块结果保存与校验代码
save_chunks_to_file(sliding_chunks, output_chunk_path)
