窗口滑动 RAG(Long Context RAG):超长文档分段递进检索 实战详解

标签 :#RAG #长上下文 RAG #窗口滑动检索 #分段递进检索 #LangChain #向量检索 #大模型应用 阅读对象:AI 应用开发、知识库搭建、RAG 工程落地、大模型微调与检索增强从业者

一、前言:传统 RAG 面对超长文档的核心痛点

随着企业知识库、合同文档、技术手册、学术论文、历史卷宗等超长文本 场景普及,传统 RAG 架构的缺陷被无限放大。常规 RAG 采用固定切块 + 全局向量检索模式,在处理万字、十万字级别的长文档时,会出现检索失效、上下文断裂、信息丢失、答案跑偏等一系列问题。

1.1 传统固定分块 RAG 的典型问题

  1. 语义割裂 固定大小切片(如 1000 字符 / 200Token)强行切割段落、逻辑章节、因果语句,完整语义被拆分,向量嵌入丢失上下文关联,检索结果碎片化。
  2. 长距离依赖丢失 超长文档中,关键信息分散在多个相邻章节,单次检索仅召回局部片段,大模型无法串联跨块逻辑,回答片面、遗漏关键论据。
  3. 上下文窗口浪费 / 溢出 若为了保留完整逻辑扩大切块尺寸,单块 Token 数激增,极易超出大模型上下文窗口限制;缩小切块则彻底丢失长距离关联信息。
  4. 全局检索噪声大 超长文档整体向量库规模庞大,全局相似度检索会召回大量无关片段,干扰核心答案,推理准确率大幅下降。
  5. 章节层级失效 正式长文档普遍存在「标题 - 章节 - 子章节 - 正文」层级结构,固定切块完全无视文档结构,检索无法匹配章节上下文

1.2 什么是窗口滑动 RAG(Long Context RAG)

窗口滑动 RAG ,也叫滑动窗口分段递进检索 RAG ,是专门针对超长文档、长上下文场景 优化的检索增强架构。核心思路借鉴 NLP 领域滑动窗口切分、文本滑窗推理思想,结合多级检索 + 递进召回 + 窗口融合,解决长文本 RAG 痛点。

核心定义: 将完整超长文档视作连续文本流 ,使用可重叠滑动窗口完成初始分块,替代传统固定硬切分;再通过「粗粒度章节检索 → 细粒度滑动窗口递进召回 → 窗口上下文融合 → 动态上下文裁剪」四步流程,逐步收敛到问题对应的核心连续文本区间,最终送入大模型生成答案。

相比传统 RAG,窗口滑动 RAG 具备三大核心优势:

  • 语义连续:滑动窗口设置重叠区域,保证切块之间语义衔接,杜绝硬切割造成的语义断裂;
  • 递进检索:由粗到细逐层筛选,先定位大章节,再缩小滑动窗口范围,检索精度逐级提升;
  • 适配超长文本:天然支持十万字、百万字级文档,不依赖超大上下文大模型,在常规 7K/12K 窗口模型上即可落地;
  • 结构感知:可结合文档标题、页码、章节标记,让滑动窗口跟随文档逻辑滑动,而非纯字符滑动。

1.3 技术应用场景

  1. 企业合规文档、法律合同、卷宗检索(单文档数万字);
  2. 技术白皮书、大型源码注释、系统运维手册检索;
  3. 学术专著、长篇论文、历史文献问答;
  4. 日志分析、长会话对话历史复盘、长视频字幕问答;
  5. 本地私有知识库、离线长文档智能问答系统。

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 调优的核心:

  1. window_size(窗口大小) 单个滑动窗口容纳的字符 / Token 数量,代表单块上下文长度。单位可选用字符、Token,工程中优先使用 Token(贴合大模型输入规则)。 示例:window_size=1000 Token,表示每个窗口最多包含 1000 个 Token。

  2. step(滑动步长) 窗口每次向右滑动的距离,决定切块的密集程度。步长越小,切块数量越多,重叠越多;步长越大,切块越少,重叠越少。

  3. 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. 窗口 1:A B C
  2. 右滑 2 位,窗口 2:B C D(重叠 B C)
  3. 右滑 2 位,窗口 3:C D E(重叠 C D) ... 结果:所有相邻语义单元都存在重叠,长距离逻辑被完整保留。

2.3 分段递进检索整体流程(四阶段架构)

窗口滑动 RAG 不是单次检索,而是由粗到细、逐层收敛的递进式检索,完整流程分为 4 个阶段,也是工程落地的标准架构:

阶段 1:文档预处理 & 分层滑窗分块
  1. 对原始超长文档做清洗(去空行、去特殊符号、统一编码);
  2. 先做粗分块:基于文档章节、标题、分页符,将大文档拆分为「大章节块」(一级粗粒度块);
  3. 对每个大章节块,执行细粒度滑动窗口分块,生成大量带重叠的滑窗子块;
  4. 为每一个滑窗块附加元数据:所属章节、页码、块编号、起止位置(关键,用于后续定位)。
阶段 2:第一层粗检索(章节级召回)
  1. 将所有粗粒度章节块向量化,存入向量库;
  2. 用户提问 Query 做 Embedding,在章节向量库中做全局相似度检索;
  3. 召回 Top-N 最相关的候选章节,过滤掉完全无关的大章节,大幅缩小检索范围。

作用:第一轮过滤,从「整个十万字文档」缩小到「2~5 个相关章节」,降低后续检索噪声。

阶段 3:第二层细检索(滑动窗口递进召回)
  1. 仅在第一轮召回的候选章节内部,提取该章节下所有滑动窗口子块
  2. 对子块做 Embedding 并构建局部向量索引(无需全局检索);
  3. 基于 Query 相似度,召回 Top-K 高相关滑窗子块;
  4. 递进融合 :根据滑窗块的位置、重叠关系,合并相邻、连续的滑窗块,形成连续上下文片段

核心环节:递进检索,从章节 → 窗口块,精度逐层提升。

阶段 4:上下文动态裁剪 + LLM 生成
  1. 合并后的连续上下文,可能依然超长,根据大模型 Token 窗口做动态截断(保留首尾关键信息,优先保留检索高分块);
  2. 将「用户问题 + 融合后的长上下文」送入大模型;
  3. 模型基于连续、完整的长上下文生成答案,完成问答。

2.4 架构优势总结

  1. 分层检索:粗筛 + 精筛,兼顾检索速度与精度;
  2. 滑窗重叠:彻底解决长文本语义割裂问题;
  3. 局部检索:精检索仅在候选章节内执行,向量计算量大幅下降,系统响应更快;
  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)
代码说明与运行结果解读
  1. CharSlidingWindowSplitter封装字符滑窗逻辑,构造函数做参数校验,避免非法步长;
  2. 循环逻辑:窗口不断右移,每次截取固定长度文本,天然形成重叠区域;
  3. 运行后可以清晰看到:相邻切块末尾与开头存在重复文本,语义实现衔接;
  4. 优点:轻量、无依赖;缺点:大模型基于 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)
代码核心价值
  1. 完全对齐大模型 Token 规则,生产环境必用此版本
  2. 内置 Token 统计函数,方便调试窗口大小参数;
  3. 兼容 OpenAI 系列模型、通义千问、Qwen、LLaMA 等绝大多数模型;
  4. 可独立作为预处理模块,对接任意 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()
代码部署说明
  1. 在项目根目录创建.env文件,写入密钥与镜像:

    env

    python 复制代码
    OPENAI_API_KEY=你的API密钥
    OPENAI_BASE_URL=https://xxx/v1  # 国内代理/镜像地址
  2. 新建long_document.txt,放入任意超长文本;

  3. RecursiveCharacterTextSplitter + chunk_overlap 就是 LangChain 标准滑动窗口实现,重叠参数直接控制滑窗效果;

  4. 向量库持久化后,后续检索可直接加载,无需重复切分与向量化。

五、核心实战:分段递进检索(窗口滑动 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 代码架构拆解(对应前文理论流程)

  1. 双层分块 & 双层向量库
    • 粗块:大窗口、低重叠 → 代表文档章节,用于全局粗筛,快速排除无关内容;
    • 细块:小窗口、高重叠(滑动窗口)→ 代表连续语义片段,用于局部精检
  2. 两段式递进检索 问题 → 检索章节库 → 锁定范围 → 检索滑窗块库 → 召回细粒度片段,逐层收敛。
  3. 上下文融合 直接拼接召回的滑窗块,利用窗口本身的重叠特性,自动形成连续完整上下文,无需额外合并算法。
  4. 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 双层分块 → 分段递进检索 → 进阶优化,覆盖全链路:

  1. 滑动窗口通过重叠区域解决长文本语义割裂问题,是超长文档 RAG 的最优切分方案;
  2. 分段递进检索(粗检索 + 精检索) 大幅降低检索噪声与计算量,提升长文档问答准确率;
  3. 双层向量库架构、滑窗合并、动态 Token 裁剪是生产环境三大必备优化点;
  4. 整套方案不依赖超大上下文大模型,普通 7K/12K 窗口模型即可支撑十万字级文档问答。

7.2 通用参数调优表(实战经验)

表格

文档类型 窗口大小 (Token) 重叠比例 粗检索召回数 精检索召回数
技术文档 / 手册 800 30% 3 5
法律合同 / 卷宗 1000 40%~50% 2 4
小说 / 日志 / 闲聊文本 600 20% 3 6
学术论文 / 专著 1200 35% 2 3

7.3 常见问题排查

  1. 检索结果语义断裂 :增大chunk_overlap重叠长度;
  2. 召回内容全部无关:第一层粗检索召回数调大,扩大章节范围;
  3. 模型回答超出上下文 :加强 Prompt 约束,开启temperature=0.1,关闭创造性;
  4. 向量库加载报错 :FAISS 加载时添加allow_dangerous_deserialization=True
  5. Token 溢出报错 :启用dynamic_token_cut动态裁剪上下文。
相关推荐
吃好睡好便好1 小时前
矩阵的加减运算
开发语言·人工智能·学习·线性代数·算法·matlab·矩阵
葫三生1 小时前
多模态视角下的一部当代东方创世史诗 ——《论三生原理》?(扩版)
人工智能·科技·算法·机器学习·开源
AI周红伟1 小时前
agent-skills 一键落地实操指南-运行指南-周红伟
大数据·人工智能·elasticsearch·搜索引擎
百度Geek说1 小时前
告别死锁和陈旧语法、告别性能瓶颈:新手Gopher 秒变 Go 语言大神
人工智能·go
weixin_421607552 小时前
短剧出海的AI 视频翻译技术方案:从单集打样到批量交付的工程全链路
人工智能·ffmpeg
代码女神经2 小时前
用AI思维,重构供应链物流产品闭环
大数据·人工智能·重构
m0_380167142 小时前
最适合交易机器人的加密数据 API:CoinGlass API 指南
人工智能·ai·区块链
雪隐2 小时前
AI股票小助手00-导言
人工智能·后端
RD_daoyi2 小时前
Google 网站收录全流程解析:抓取、索引与排名机制详解
前端·javascript·人工智能·学习·搜索引擎·html