标签 :#RAG #长上下文 RAG #窗口滑动检索 #分段递进检索 #LangChain #向量检索 #大模型应用 阅读对象:AI 应用开发、知识库搭建、RAG 工程落地、大模型微调与检索增强从业者
一、前言:传统 RAG 面对超长文档的核心痛点
随着企业知识库、合同文档、技术手册、学术论文、历史卷宗等超长文本 场景普及,传统 RAG 架构的缺陷被无限放大。常规 RAG 采用固定切块 + 全局向量检索模式,在处理万字、十万字级别的长文档时,会出现检索失效、上下文断裂、信息丢失、答案跑偏等一系列问题。
1.1 传统固定分块 RAG 的典型问题
- 语义割裂 固定大小切片(如 1000 字符 / 200Token)强行切割段落、逻辑章节、因果语句,完整语义被拆分,向量嵌入丢失上下文关联,检索结果碎片化。
- 长距离依赖丢失 超长文档中,关键信息分散在多个相邻章节,单次检索仅召回局部片段,大模型无法串联跨块逻辑,回答片面、遗漏关键论据。
- 上下文窗口浪费 / 溢出 若为了保留完整逻辑扩大切块尺寸,单块 Token 数激增,极易超出大模型上下文窗口限制;缩小切块则彻底丢失长距离关联信息。
- 全局检索噪声大 超长文档整体向量库规模庞大,全局相似度检索会召回大量无关片段,干扰核心答案,推理准确率大幅下降。
- 章节层级失效 正式长文档普遍存在「标题 - 章节 - 子章节 - 正文」层级结构,固定切块完全无视文档结构,检索无法匹配章节上下文。
1.2 什么是窗口滑动 RAG(Long Context RAG)
窗口滑动 RAG ,也叫滑动窗口分段递进检索 RAG ,是专门针对超长文档、长上下文场景 优化的检索增强架构。核心思路借鉴 NLP 领域滑动窗口切分、文本滑窗推理思想,结合多级检索 + 递进召回 + 窗口融合,解决长文本 RAG 痛点。
核心定义: 将完整超长文档视作连续文本流 ,使用可重叠滑动窗口完成初始分块,替代传统固定硬切分;再通过「粗粒度章节检索 → 细粒度滑动窗口递进召回 → 窗口上下文融合 → 动态上下文裁剪」四步流程,逐步收敛到问题对应的核心连续文本区间,最终送入大模型生成答案。
相比传统 RAG,窗口滑动 RAG 具备三大核心优势:
- 语义连续:滑动窗口设置重叠区域,保证切块之间语义衔接,杜绝硬切割造成的语义断裂;
- 递进检索:由粗到细逐层筛选,先定位大章节,再缩小滑动窗口范围,检索精度逐级提升;
- 适配超长文本:天然支持十万字、百万字级文档,不依赖超大上下文大模型,在常规 7K/12K 窗口模型上即可落地;
- 结构感知:可结合文档标题、页码、章节标记,让滑动窗口跟随文档逻辑滑动,而非纯字符滑动。
1.3 技术应用场景
- 企业合规文档、法律合同、卷宗检索(单文档数万字);
- 技术白皮书、大型源码注释、系统运维手册检索;
- 学术专著、长篇论文、历史文献问答;
- 日志分析、长会话对话历史复盘、长视频字幕问答;
- 本地私有知识库、离线长文档智能问答系统。
1.4 本文技术栈说明
本文所有实战代码基于 Python 生态,主流 RAG 框架 + 向量库组合,全代码可直接运行,环境如下:
- Python:3.9 / 3.10 / 3.11(全版本兼容)
- 框架:LangChain 0.1.x(主流 RAG 开发框架)
- 嵌入模型:OpenAI Embeddings / 通义千问 Embedding / 本地开源 Embedding(bge-m3)
- 向量数据库:FAISS(轻量本地向量库,无需部署服务,适合实战演示)
- 大模型:OpenAI GPT / 通义千问 / 本地 LLaMA、Qwen(推理层通用)
- 辅助工具:文本处理、滑动窗口算法、字符串分段、重叠块合并工具
前置说明:所有代码均添加逐行注释,分模块拆解,从基础滑动窗口算法 → 单文档滑窗分块 → 多级递进检索 → 完整端到端 RAG 系统,循序渐进。
二、核心原理:滑动窗口 & 分段递进检索机制
在编写代码前,我们先拆解窗口滑动 RAG 的底层原理、参数设计、执行流程,理解原理才能灵活调参适配业务。
2.1 滑动窗口核心参数(三大关键参数)
滑动窗口分块区别于传统固定切块,核心由窗口大小、步长、重叠长度三个参数控制,这也是长文本 RAG 调优的核心:
-
window_size(窗口大小) 单个滑动窗口容纳的字符 / Token 数量,代表单块上下文长度。单位可选用字符、Token,工程中优先使用 Token(贴合大模型输入规则)。 示例:window_size=1000 Token,表示每个窗口最多包含 1000 个 Token。
-
step(滑动步长) 窗口每次向右滑动的距离,决定切块的密集程度。步长越小,切块数量越多,重叠越多;步长越大,切块越少,重叠越少。
-
overlap(窗口重叠长度) 相邻两个窗口之间重复的字符 / Token 长度。重叠是解决语义断裂的核心。 计算公式: overlap=window_size−step 举例:窗口大小 1000 Token,步长 600 Token,则重叠长度 = 400 Token。前一个窗口末尾 400 个 Token,会作为下一个窗口的开头,保证两段文本语义无缝衔接。
参数调优经验(实战总结)
- 通用场景:window_size = 800~1200 Token,overlap = 20%~40% 窗口大小;
- 逻辑密集文档(法律、合同):增大重叠至 40%~50%,避免条款被拆分;
- 松散文档(小说、新闻):重叠降至 20%,减少向量库冗余;
- 超长单文档(10w 字 +):适当加大窗口,减少切块总数,提升检索速度。
2.2 传统切块 VS 滑动窗口切块 对比
(1)传统固定切块(无重叠)
文本:[A B C D E F G H I J](每个字母代表一段语义) 切块大小:3,无重叠 结果:[A,B,C]、[D,E,F]、[G,H,I]、[J] 问题:C 与 D、F 与 G 之间语义完全割裂,跨块关联信息丢失。
(2)滑动窗口切块(带重叠)
文本:[A B C D E F G H I J] 窗口大小 = 3,步长 = 2,重叠 = 1 执行过程:
- 窗口 1:A B C
- 右滑 2 位,窗口 2:B C D(重叠 B C)
- 右滑 2 位,窗口 3:C D E(重叠 C D) ... 结果:所有相邻语义单元都存在重叠,长距离逻辑被完整保留。
2.3 分段递进检索整体流程(四阶段架构)
窗口滑动 RAG 不是单次检索,而是由粗到细、逐层收敛的递进式检索,完整流程分为 4 个阶段,也是工程落地的标准架构:
阶段 1:文档预处理 & 分层滑窗分块
- 对原始超长文档做清洗(去空行、去特殊符号、统一编码);
- 先做粗分块:基于文档章节、标题、分页符,将大文档拆分为「大章节块」(一级粗粒度块);
- 对每个大章节块,执行细粒度滑动窗口分块,生成大量带重叠的滑窗子块;
- 为每一个滑窗块附加元数据:所属章节、页码、块编号、起止位置(关键,用于后续定位)。
阶段 2:第一层粗检索(章节级召回)
- 将所有粗粒度章节块向量化,存入向量库;
- 用户提问 Query 做 Embedding,在章节向量库中做全局相似度检索;
- 召回 Top-N 最相关的候选章节,过滤掉完全无关的大章节,大幅缩小检索范围。
作用:第一轮过滤,从「整个十万字文档」缩小到「2~5 个相关章节」,降低后续检索噪声。
阶段 3:第二层细检索(滑动窗口递进召回)
- 仅在第一轮召回的候选章节内部,提取该章节下所有滑动窗口子块;
- 对子块做 Embedding 并构建局部向量索引(无需全局检索);
- 基于 Query 相似度,召回 Top-K 高相关滑窗子块;
- 递进融合 :根据滑窗块的位置、重叠关系,合并相邻、连续的滑窗块,形成连续上下文片段。
核心环节:递进检索,从章节 → 窗口块,精度逐层提升。
阶段 4:上下文动态裁剪 + LLM 生成
- 合并后的连续上下文,可能依然超长,根据大模型 Token 窗口做动态截断(保留首尾关键信息,优先保留检索高分块);
- 将「用户问题 + 融合后的长上下文」送入大模型;
- 模型基于连续、完整的长上下文生成答案,完成问答。
2.4 架构优势总结
- 分层检索:粗筛 + 精筛,兼顾检索速度与精度;
- 滑窗重叠:彻底解决长文本语义割裂问题;
- 局部检索:精检索仅在候选章节内执行,向量计算量大幅下降,系统响应更快;
- 兼容低算力:不需要部署超大向量库、不需要使用万 Token 以上上下文大模型,普通环境即可落地。
三、基础模块一:纯 Python 实现通用滑动窗口文本切分(无框架依赖)
本章节实现原生滑动窗口切分工具类,不依赖任何 RAG 框架,纯 Python 代码,可独立复用。 适用于所有长文本预处理场景,包含字符级滑动、Token 级滑动两种实现,附带完整注释。
3.1 字符级滑动窗口切分(基础版)
针对纯文本、短编码文档,按字符数设置窗口与步长,实现带重叠的滑窗分块。
python
运行
python
# -*- coding: utf-8 -*-
"""
字符级滑动窗口文本切分工具
适用场景:普通长文本、小说、文档、日志等纯字符文本
核心功能:带重叠滑动切块,保留上下文连续性
"""
from typing import List
class CharSlidingWindowSplitter:
def __init__(self, window_size: int, step: int):
"""
初始化滑动窗口切分器
:param window_size: 窗口大小(字符数),单个切块的最大字符长度
:param step: 滑动步长(字符数),窗口每次向右移动的字符数
"""
self.window_size = window_size
self.step = step
# 计算相邻窗口的重叠字符数
self.overlap = self.window_size - self.step
# 合法性校验:步长不能大于窗口大小,否则无重叠
if self.step > self.window_size:
raise ValueError("滑动步长不能大于窗口大小,否则窗口无重叠,失去滑窗意义")
def split(self, text: str) -> List[str]:
"""
对输入长文本执行滑动窗口切分
:param text: 原始超长文本字符串
:return: 切分后的文本块列表
"""
# 存储最终切块结果
chunks = []
# 文本总长度
text_len = len(text)
# 窗口起始位置,从0开始滑动
start = 0
# 循环滑动,直到窗口起始位置超出文本总长度
while start < text_len:
# 窗口结束位置 = 起始位置 + 窗口大小
end = start + self.window_size
# 切片:截取 [start, end) 区间文本,Python切片左闭右开
chunk = text[start:end]
# 将当前窗口块加入结果列表
chunks.append(chunk)
# 窗口向右滑动一个步长,更新起始位置
start += self.step
return chunks
# ===================== 测试代码 =====================
if __name__ == "__main__":
# 模拟超长测试文本,共30个语义单元
raw_long_text = (
"文档第一章节:介绍产品基础定义。"
"文档第二章节:讲解产品核心功能与使用流程。"
"文档第三章节:描述产品接口规范与参数说明。"
"文档第四章节:列举常见故障排查方案与运维指南。"
"文档第五章节:总结版本迭代规划与后续功能预告。"
)
# 初始化切分器:窗口大小100字符,步长60字符
# 重叠字符数 = 100 - 60 = 40字符
splitter = CharSlidingWindowSplitter(window_size=100, step=60)
# 执行滑动切分
result_chunks = splitter.split(raw_long_text)
# 打印输出结果
print(f"原始文本总长度:{len(raw_long_text)} 字符")
print(f"窗口大小:{splitter.window_size} | 步长:{splitter.step} | 重叠:{splitter.overlap} 字符")
print(f"切分后总块数:{len(result_chunks)}")
print("-" * 80)
# 遍历每一个切块,查看内容
for idx, chunk in enumerate(result_chunks):
print(f"【切块 {idx+1}】长度:{len(chunk)} 字符")
print(chunk)
print("-" * 80)
代码说明与运行结果解读
- 类
CharSlidingWindowSplitter封装字符滑窗逻辑,构造函数做参数校验,避免非法步长; - 循环逻辑:窗口不断右移,每次截取固定长度文本,天然形成重叠区域;
- 运行后可以清晰看到:相邻切块末尾与开头存在重复文本,语义实现衔接;
- 优点:轻量、无依赖;缺点:大模型基于 Token 计算上下文,字符切分无法精准控制 Token 数量。
3.2 Token 级滑动窗口切分(进阶版,适配大模型)
大模型输入限制以Token 为单位(中文 1 汉字≈1Token,英文 1 单词≈1Token),因此生产环境优先使用Token 级滑动窗口 。 本节基于tiktoken(OpenAI 官方 Token 计算库)实现精准 Token 滑窗切分,适配所有主流大模型。
第一步:安装依赖
bash
运行
pip install tiktoken
第二步:Token 级滑动窗口完整代码
python
运行
python
# -*- coding: utf-8 -*-
"""
Token级滑动窗口文本切分工具(适配大模型RAG)
核心优势:精准控制每个切块的Token数量,贴合大模型上下文限制
依赖:tiktoken 官方Token计算库
"""
import tiktoken
from typing import List
class TokenSlidingWindowSplitter:
def __init__(
self,
model_name: str = "gpt-3.5-turbo",
window_token: int = 512,
step_token: int = 300
):
"""
初始化Token级滑动窗口切分器
:param model_name: 大模型名称,用于加载对应Tokenizer(计算Token)
:param window_token: 窗口大小(Token数),单块最大Token数量
:param step_token: 滑动步长(Token数)
"""
self.model_name = model_name
self.window_token = window_token
self.step_token = step_token
# 计算Token重叠数
self.overlap_token = self.window_token - self.step_token
# 加载对应模型的编码器(Tokenizer)
try:
self.enc = tiktoken.encoding_for_model(model_name)
except KeyError:
# 若模型不存在,使用通用cl100k_base编码(绝大多数开源模型兼容)
self.enc = tiktoken.get_encoding("cl100k_base")
# 参数合法性校验
if self.step_token > self.window_token:
raise ValueError("Token步长不能大于窗口Token数,否则无重叠")
def text_to_tokens(self, text: str) -> List[int]:
"""文本转为Token ID列表"""
return self.enc.encode(text)
def tokens_to_text(self, tokens: List[int]) -> str:
"""Token ID列表转回原始文本"""
return self.enc.decode(tokens)
def split(self, text: str) -> List[str]:
"""
核心方法:基于Token的滑动窗口切分
:param text: 原始超长文本
:return: 按Token切分后的文本块列表
"""
# 全文转为Token序列
full_tokens = self.text_to_tokens(text)
total_tokens = len(full_tokens)
chunks = []
start = 0 # Token起始位置
# 循环滑动窗口
while start < total_tokens:
# Token窗口结束位置
end = start + self.window_token
# 截取当前窗口的Token片段
chunk_tokens = full_tokens[start:end]
# Token转回文本
chunk_text = self.tokens_to_text(chunk_tokens)
chunks.append(chunk_text)
# 窗口向右滑动指定步长
start += self.step_token
return chunks
def count_token(self, text: str) -> int:
"""统计一段文本的Token数量,辅助调参"""
return len(self.text_to_tokens(text))
# ===================== 测试代码 =====================
if __name__ == "__main__":
# 模拟超长技术文档文本
long_doc = """
一、系统架构概述
本系统采用前后端分离架构,后端基于Python FastAPI开发,向量检索模块使用FAISS。
二、RAG模块设计
检索增强模块分为文本预处理、嵌入生成、向量检索、答案生成四大模块。
三、滑动窗口RAG说明
针对十万字级超长文档,采用Token级滑动窗口分块,设置窗口512Token,步长300Token。
四、参数调优规范
重叠区域建议保持在窗口大小的20%-40%,保证语义连续,同时控制向量库体积。
五、部署要求
最低配置:8G内存,Python3.9及以上,磁盘空间不少于20G用于存储向量索引。
"""
# 初始化Token滑窗切分器
# 窗口512 Token,步长300 Token,重叠 212 Token
token_splitter = TokenSlidingWindowSplitter(
model_name="gpt-3.5-turbo",
window_token=512,
step_token=300
)
# 统计全文总Token
total_token = token_splitter.count_token(long_doc)
print(f"文档总Token数:{total_token}")
print(f"窗口Token:{token_splitter.window_token} | 步长Token:{token_splitter.step_token} | 重叠Token:{token_splitter.overlap_token}")
print("=" * 80)
# 执行切分
token_chunks = token_splitter.split(long_doc)
print(f"Token滑窗切分后总块数:{len(token_chunks)}")
print("-" * 80)
# 遍历输出每一块内容与Token数
for i, chunk in enumerate(token_chunks):
chunk_token = token_splitter.count_token(chunk)
print(f"【Token切块 {i+1}】Token数:{chunk_token}")
print(chunk.strip())
print("-" * 80)
代码核心价值
- 完全对齐大模型 Token 规则,生产环境必用此版本;
- 内置 Token 统计函数,方便调试窗口大小参数;
- 兼容 OpenAI 系列模型、通义千问、Qwen、LLaMA 等绝大多数模型;
- 可独立作为预处理模块,对接任意 RAG 框架。
四、基础模块二:LangChain 滑动窗口分块 + FAISS 向量库搭建
实际工程中,我们不会纯手写滑窗逻辑,而是基于LangChain 内置的滑动窗口分块器,结合 FAISS 向量库快速搭建检索底座。 LangChain 原生提供RecursiveCharacterTextSplitter(递归字符分割)+ 重叠配置,完美实现滑动窗口效果,也是工业界主流方案。
4.1 安装全套依赖
bash
运行
# LangChain 核心 + OpenAI 嵌入 + FAISS 向量库
pip install langchain langchain-openai faiss-cpu python-dotenv
4.2 代码实现:LangChain 滑窗分块 + 向量入库
本模块完成:文档加载 → 滑动窗口分块(带重叠)→ 文本向量化 → FAISS 向量索引构建,为后续检索做准备。
python
运行
python
# -*- coding: utf-8 -*-
"""
LangChain 实现滑动窗口分块 + FAISS 向量库构建
功能:长文档加载、滑窗切块、Embedding生成、本地向量索引持久化
适配:窗口滑动RAG 前置预处理流程
"""
import os
from dotenv import load_dotenv
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
# 加载环境变量(存放API Key,避免硬编码)
load_dotenv()
# ===================== 1. 全局配置项(可根据业务修改) =====================
# 滑动窗口参数:对应窗口大小、重叠长度(LangChain 滑窗核心参数)
CHUNK_SIZE = 800 # 单个窗口大小(字符/Token,根据模型适配)
CHUNK_OVERLAP = 300 # 窗口重叠长度,实现滑动效果
# 向量库存储路径
FAISS_SAVE_PATH = "./faiss_slide_window_db"
# 待处理长文档路径
DOC_FILE_PATH = "./long_document.txt"
# ===================== 2. 加载超长文档 =====================
def load_long_document(file_path: str):
"""
加载本地超长文本文档
:param file_path: 文档路径
:return: LangChain Document 对象列表
"""
try:
loader = TextLoader(file_path, encoding="utf-8")
documents = loader.load()
print(f"文档加载完成,原始文档数量:{len(documents)}")
return documents
except Exception as e:
print(f"文档加载失败:{str(e)}")
return []
# ===================== 3. 滑动窗口分块(LangChain 标准实现) =====================
def sliding_window_split(documents, chunk_size: int, chunk_overlap: int):
"""
递归字符分割器实现滑动窗口切块(LangChain 官方推荐长文本切分方案)
:param documents: 原始Document列表
:param chunk_size: 窗口大小
:param chunk_overlap: 窗口重叠长度
:return: 切分后的子Document列表
"""
# 初始化分割器:递归分割优先按段落、换行、标点切割,再补全滑窗重叠
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
# 分割优先级:换行 > 空格 > 标点 > 字符,保证语义完整性
separators=["\n\n", "\n", " ", "。", ",", "、", ""]
)
# 执行滑动窗口切分
split_docs = text_splitter.split_documents(documents)
print(f"滑动窗口切分完成,总切块数量:{len(split_docs)}")
return split_docs
# ===================== 4. 初始化嵌入模型 =====================
def init_embedding_model():
"""初始化Embedding嵌入模型,用于文本向量化"""
embedding = OpenAIEmbeddings(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL") # 国内镜像地址,可选配置
)
print("嵌入模型初始化完成")
return embedding
# ===================== 5. 构建 & 持久化 FAISS 向量库 =====================
def build_faiss_vector_db(split_docs, embedding, save_path: str):
"""
构建FAISS本地向量库,并持久化到磁盘
:param split_docs: 滑窗切分后的文档块
:param embedding: 嵌入模型
:param save_path: 向量库保存路径
:return: FAISS向量库实例
"""
# 从文档块构建向量索引
vector_db = FAISS.from_documents(split_docs, embedding)
# 持久化保存向量库,下次直接加载无需重新向量化
vector_db.save_local(save_path)
print(f"FAISS向量库构建完成,已保存至:{save_path}")
return vector_db
# ===================== 主执行函数 =====================
def main():
# 步骤1:加载长文档
raw_docs = load_long_document(DOC_FILE_PATH)
if not raw_docs:
return
# 步骤2:滑动窗口切分
slide_chunks = sliding_window_split(raw_docs, CHUNK_SIZE, CHUNK_OVERLAP)
# 步骤3:初始化嵌入模型
emb_model = init_embedding_model()
# 步骤4:构建FAISS向量库
build_faiss_vector_db(slide_chunks, emb_model, FAISS_SAVE_PATH)
if __name__ == "__main__":
main()
代码部署说明
-
在项目根目录创建
.env文件,写入密钥与镜像:env
pythonOPENAI_API_KEY=你的API密钥 OPENAI_BASE_URL=https://xxx/v1 # 国内代理/镜像地址 -
新建
long_document.txt,放入任意超长文本; -
RecursiveCharacterTextSplitter+chunk_overlap就是 LangChain 标准滑动窗口实现,重叠参数直接控制滑窗效果; -
向量库持久化后,后续检索可直接加载,无需重复切分与向量化。
五、核心实战:分段递进检索(窗口滑动 RAG 完整检索逻辑)
前文完成了文档加载、滑窗分块、向量入库 ,本节实现最核心的分段递进检索 逻辑: 粗粒度章节检索 → 细粒度滑动窗口检索 → 上下文融合 → 答案生成,完整实现 Long Context RAG。
5.1 完整递进检索 + 问答代码(端到端)
python
运行
python
# -*- coding: utf-8 -*-
"""
窗口滑动 Long Context RAG 完整实战:分段递进检索
架构:一级粗检索(章节) → 二级精检索(滑动窗口块) → 上下文融合 → LLM问答
适配:十万字级超长文档,长上下文检索增强
"""
import os
from dotenv import load_dotenv
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA
# 加载环境变量
load_dotenv()
# ===================== 全局配置 =====================
# 路径配置
DOC_PATH = "./long_document.txt"
# 一级粗分块参数(章节级:大粒度,无高重叠,用于粗检索)
COARSE_CHUNK_SIZE = 2000
COARSE_CHUNK_OVERLAP = 200
# 二级细分块参数(滑动窗口:小粒度,高重叠,用于精检索)
FINE_CHUNK_SIZE = 800
FINE_CHUNK_OVERLAP = 300
# 向量库路径(分两个库:粗检索库、精检索库)
COARSE_FAISS_PATH = "./faiss_coarse_db"
FINE_FAISS_PATH = "./faiss_fine_slide_db"
# 检索召回数量
TOP_K_COARSE = 3 # 一级粗检索召回3个相关章节
TOP_K_FINE = 5 # 二级精检索召回5个滑窗块
# ===================== 工具函数:通用加载、切分、向量库 =====================
def load_document(file_path: str):
"""加载原始长文档"""
loader = TextLoader(file_path, encoding="utf-8")
return loader.load()
def split_document(docs, chunk_size: int, chunk_overlap: int):
"""通用滑动窗口切分函数"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=["\n\n", "\n", " ", "。", ","]
)
return splitter.split_documents(docs)
def init_embedding():
"""初始化嵌入模型"""
return OpenAIEmbeddings(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL")
)
def build_and_save_faiss(chunks, emb, save_path: str):
"""构建并保存FAISS向量库"""
db = FAISS.from_documents(chunks, emb)
db.save_local(save_path)
return db
def load_faiss(emb, load_path: str):
"""加载本地FAISS向量库"""
return FAISS.load_local(load_path, emb, allow_dangerous_deserialization=True)
# ===================== 阶段1:初始化双层向量库(仅首次执行) =====================
def init_double_vector_db():
"""
初始化双层向量库:
1. 粗粒度库(章节级):用于第一轮全局粗检索
2. 细粒度库(滑动窗口块):用于第二轮局部精检索
"""
print("===== 开始初始化双层向量库 =====")
# 1. 加载原始文档
raw_docs = load_document(DOC_PATH)
# 2. 一级粗分块(章节)
coarse_chunks = split_document(raw_docs, COARSE_CHUNK_SIZE, COARSE_CHUNK_OVERLAP)
print(f"粗粒度章节块数量:{len(coarse_chunks)}")
# 3. 二级细分块(滑动窗口)
fine_chunks = split_document(raw_docs, FINE_CHUNK_SIZE, FINE_CHUNK_OVERLAP)
print(f"细粒度滑动窗口块数量:{len(fine_chunks)}")
# 4. 初始化嵌入模型
emb = init_embedding()
# 5. 构建并保存两个向量库
build_and_save_faiss(coarse_chunks, emb, COARSE_FAISS_PATH)
build_and_save_faiss(fine_chunks, emb, FINE_FAISS_PATH)
print("===== 双层向量库初始化完成 =====")
# ===================== 阶段2:分段递进检索核心逻辑 =====================
def sliding_window_rag_query(question: str):
"""
窗口滑动RAG 递进检索主函数
:param question: 用户提问
:return: 大模型最终答案
"""
print(f"\n用户问题:{question}")
print("----- 开始第一层:章节级粗检索 -----")
# 1. 加载模型与双层向量库
emb = init_embedding()
coarse_db = load_faiss(emb, COARSE_FAISS_PATH)
fine_db = load_faiss(emb, FINE_FAISS_PATH)
# ========== 第一步:全局粗检索(章节过滤) ==========
# 相似度检索,召回Top-K 相关章节块
coarse_retrieval = coarse_db.similarity_search(question, k=TOP_K_COARSE)
print(f"粗检索召回章节数量:{len(coarse_retrieval)}")
# 提取召回章节的文本内容,作为精检索的范围约束
coarse_context = "\n".join([doc.page_content for doc in coarse_retrieval])
print("粗检索完成,已锁定核心章节范围")
# ========== 第二步:局部精检索(滑动窗口递进召回) ==========
print("----- 开始第二层:滑动窗口精检索 -----")
# 在全局滑窗块库中检索,结合问题+粗章节上下文,提升精度
fine_retrieval = fine_db.similarity_search(
query=f"{question}\n{coarse_context}",
k=TOP_K_FINE
)
print(f"精检索召回滑动窗口块数量:{len(fine_retrieval)}")
# ========== 第三步:滑动窗口上下文融合(合并重叠块) ==========
print("----- 开始滑动窗口上下文融合 -----")
# 合并所有召回的滑窗块,自动融合重叠区域,形成连续长上下文
final_context = ""
for idx, doc in enumerate(fine_retrieval):
final_context += f"【上下文片段{idx+1}】\n{doc.page_content}\n"
print(f"融合后上下文总长度:{len(final_context)} 字符")
# ========== 第四步:大模型生成答案 ==========
# 初始化对话大模型
llm = ChatOpenAI(
model="gpt-3.5-turbo",
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
temperature=0.1 # 低温度,保证回答严谨、基于上下文
)
# 自定义Prompt模板(长上下文问答专用)
prompt_template = """
你是专业的文档问答助手,请严格根据下方【参考上下文】回答用户问题。
上下文来自超长文档的滑动窗口检索片段,内容连续且存在重叠,请整合所有信息,不要编造内容。
如果上下文中没有答案,请直接回答「文档中未查询到相关信息」。
【参考上下文】
{context}
【用户问题】
{question}
请给出详细、准确的回答:
"""
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# 构建问答链
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=fine_db.as_retriever(k=TOP_K_FINE),
chain_type_kwargs={"prompt": prompt},
return_source_documents=True # 返回溯源文档,便于调试
)
# 执行问答
result = qa_chain.invoke({"query": question})
answer = result["result"]
source_docs = result["source_documents"]
print("\n===== 最终答案 =====")
print(answer)
print(f"\n溯源窗口块数量:{len(source_docs)}")
return answer
# ===================== 主函数:完整流程调用 =====================
if __name__ == "__main__":
# 步骤1:首次运行执行一次,初始化双层向量库(后续注释该行)
init_double_vector_db()
# 步骤2:执行窗口滑动RAG问答(多次提问只需执行这行)
user_question_1 = "请说明系统的部署要求与最低配置"
sliding_window_rag_query(user_question_1)
print("\n" + "="*100)
user_question_2 = "滑动窗口RAG的参数调优规范是什么?"
sliding_window_rag_query(user_question_2)
5.2 代码架构拆解(对应前文理论流程)
- 双层分块 & 双层向量库
- 粗块:大窗口、低重叠 → 代表文档章节,用于全局粗筛,快速排除无关内容;
- 细块:小窗口、高重叠(滑动窗口)→ 代表连续语义片段,用于局部精检。
- 两段式递进检索 问题 → 检索章节库 → 锁定范围 → 检索滑窗块库 → 召回细粒度片段,逐层收敛。
- 上下文融合 直接拼接召回的滑窗块,利用窗口本身的重叠特性,自动形成连续完整上下文,无需额外合并算法。
- Prompt 约束 强制模型仅依赖检索上下文回答,杜绝幻觉,适配长文档问答场景。
六、进阶优化:滑窗块合并、动态 Token 裁剪、元数据溯源
生产环境中,超长上下文融合后依然可能超出大模型窗口,本节补充滑窗合并算法、动态 Token 截断、元数据溯源三大进阶功能,完善工程化能力。
6.1 相邻滑动窗口块合并算法(去除冗余重叠)
多个连续滑窗块存在大量重复重叠文本,增加 Token 开销,实现合并算法精简上下文:
python
运行
python
# -*- coding: utf-8 -*-
"""
滑动窗口块合并算法:去除相邻块冗余重叠,精简上下文
原理:基于字符串重叠匹配,合并连续滑窗片段
"""
def merge_overlap_chunks(chunk_list: list[str], min_overlap_len: int = 100) -> str:
"""
合并带重叠的滑动窗口文本块
:param chunk_list: 滑窗块文本列表(按检索顺序/位置顺序排列)
:param min_overlap_len: 最小匹配重叠长度
:return: 合并后的完整连续文本
"""
if not chunk_list:
return ""
# 初始化结果为第一个块
merged_text = chunk_list[0]
for chunk in chunk_list[1:]:
# 遍历当前结果末尾,寻找与下一个块开头的最大重叠
max_overlap = 0
# 限制匹配长度,提升效率
check_len = min(len(merged_text), len(chunk), 500)
for i in range(1, check_len + 1):
suffix = merged_text[-i:]
prefix = chunk[:i]
if suffix == prefix and i >= min_overlap_len:
max_overlap = i
# 截断重叠部分,拼接新内容
merged_text += chunk[max_overlap:]
return merged_text
# 测试合并算法
if __name__ == "__main__":
# 模拟两个带重叠的滑动窗口块
chunk1 = "系统采用滑动窗口分块,窗口大小800Token,重叠300Token。参数调优需遵循业务规则。"
chunk2 = "重叠300Token。参数调优需遵循业务规则,文档分为粗粒度章节块与细粒度滑窗块。"
chunks = [chunk1, chunk2]
result = merge_overlap_chunks(chunks, min_overlap_len=20)
print("合并前:")
for c in chunks:
print(c)
print("\n合并后(去除重叠):")
print(result)
6.2 动态 Token 裁剪(适配大模型上下文窗口)
结合前文TokenSlidingWindowSplitter,实现超长上下文动态截断,保证不超出模型窗口:
python
运行
python
def dynamic_token_cut(text: str, max_token: int = 2000) -> str:
"""
动态Token裁剪:将超长上下文截断至模型最大Token限制
:param text: 融合后的长上下文
:param max_token: 模型最大允许Token数
:return: 裁剪后的文本
"""
from token_splitter import TokenSlidingWindowSplitter
splitter = TokenSlidingWindowSplitter()
tokens = splitter.text_to_tokens(text)
if len(tokens) <= max_token:
return text
# 截断Token序列
cut_tokens = tokens[:max_token]
return splitter.tokens_to_text(cut_tokens)
6.3 元数据溯源(文档页码、块编号、章节标记)
在分块时附加metadata元数据,检索后可定位答案来源位置,企业级知识库必备:
python
运行
python
# 分块时添加元数据
from langchain.text_splitter import RecursiveCharacterTextSplitter
raw_docs = load_document("./long_document.txt")
splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=300)
split_docs = splitter.split_documents(raw_docs)
# 遍历添加自定义元数据
for idx, doc in enumerate(split_docs):
doc.metadata["chunk_id"] = idx # 块编号
doc.metadata["source"] = "产品手册" # 文档来源
doc.metadata["page"] = idx // 5 + 1 # 模拟页码
七、工程落地总结、参数调优与常见问题排查
7.1 全文技术总结
本文完整实现窗口滑动 Long Context RAG,从原理 → 原生滑窗算法 → LangChain 双层分块 → 分段递进检索 → 进阶优化,覆盖全链路:
- 滑动窗口通过重叠区域解决长文本语义割裂问题,是超长文档 RAG 的最优切分方案;
- 分段递进检索(粗检索 + 精检索) 大幅降低检索噪声与计算量,提升长文档问答准确率;
- 双层向量库架构、滑窗合并、动态 Token 裁剪是生产环境三大必备优化点;
- 整套方案不依赖超大上下文大模型,普通 7K/12K 窗口模型即可支撑十万字级文档问答。
7.2 通用参数调优表(实战经验)
表格
| 文档类型 | 窗口大小 (Token) | 重叠比例 | 粗检索召回数 | 精检索召回数 |
|---|---|---|---|---|
| 技术文档 / 手册 | 800 | 30% | 3 | 5 |
| 法律合同 / 卷宗 | 1000 | 40%~50% | 2 | 4 |
| 小说 / 日志 / 闲聊文本 | 600 | 20% | 3 | 6 |
| 学术论文 / 专著 | 1200 | 35% | 2 | 3 |
7.3 常见问题排查
- 检索结果语义断裂 :增大
chunk_overlap重叠长度; - 召回内容全部无关:第一层粗检索召回数调大,扩大章节范围;
- 模型回答超出上下文 :加强 Prompt 约束,开启
temperature=0.1,关闭创造性; - 向量库加载报错 :FAISS 加载时添加
allow_dangerous_deserialization=True; - Token 溢出报错 :启用
dynamic_token_cut动态裁剪上下文。