为 RAG 系统拆分大型文档是必要的,主要是因为大型语言模型(LLMs)的上下文窗口限制,以及对精准检索的需求。将使用 Python 和常用库来加载和分块文本数据。我们假设您有一个正常运行的 Python 环境,并且已经安装了所需库。对于这些示例,我们将主要使用 LangChain 库中的组件,该库为常见的 RAG 任务提供了有用的抽象。如果您尚未安装,通常可以通过 pip 完成:pip install langchain langchain-community准备我们的示例数据首先,让我们定义一些可以处理的示例文本内容。假设这段文本来自一个关于虚构的“AstroCortex”项目的较大文档。# 示例文档内容 astro_cortex_text = """ AstroCortex 项目:概览 AstroCortex 是一项旨在绘制宇宙生命体神经通路的创新计划。我们的主要目标是理解这些生命体如何感知宇宙的基本作用力并与之互动。该项目结合了量子物理学、天体生物学和计算神经科学。 方法包括部署配备神经扫描仪的超空间探测器。这些探测器收集数据特征,然后将它们传回我们位于地球的“深思集群”进行分析。初步数据显示,其思维模式复杂且非线性,与地球生命截然不同。我们正在使用高级机器学习模型来解码这些模式。挑战包括在遥远距离上的信号衰减以及数据的独特性,这需要新颖的分析技术。 伦理考量很重要。通信尝试严格非侵入性,遵循2267年建立的最高指令协议。确保任何被发现智能生命的自主性和安全是我们的最高优先级。潜在益处包括对宇宙结构以及我们在其中位置的无与伦比的见解。资金由星际科学基金会和各私人捐助者提供。该项目时间线跨越数十年,反映了这项工作的复杂性和规模。未来阶段将尝试建立基于数学原理的基本通信。 """ # 在实际情况中,您可能需要从文件中加载。 # 为简单起见,我们将直接使用此字符串。 # 如果从名为 'astrocortex.txt' 的 .txt 文件加载: # from langchain_community.document_loaders import TextLoader # loader = TextLoader('astrocortex.txt') # documents = loader.load() # 这将返回一个 Document 对象列表 # astro_cortex_text = documents[0].page_content # 如果从文件加载,提取文本固定大小分块最直接的方法是将文本分割成固定字符长度的块。这种方法简单,但有时会不自然地切分句子或语义。为此,我们将使用 LangChain 的 CharacterTextSplitter。这里的一个主要参数是 chunk_overlap,它定义了前一个块末尾有多少字符会在下一个块的开头重复。这种重叠有助于在块边界之间保持上下文。from langchain.text_splitter import CharacterTextSplitter # 初始化分割器 char_splitter = CharacterTextSplitter( separator="\n\n", # 首先尝试如何分割文本(例如,按段落) chunk_size=250, # 期望的最大块大小(按字符计) chunk_overlap=50, # 块之间重叠的字符数 length_function=len, # 测量块长度的函数(默认即可) is_separator_regex=False, # 将分隔符视为字面字符串 ) # 分割文本 fixed_chunks = char_splitter.split_text(astro_cortex_text) # 让我们查看这些块 print(f"原始文本长度: {len(astro_cortex_text)}") print(f"创建的块数: {len(fixed_chunks)}\n") for i, chunk in enumerate(fixed_chunks): print(f"--- 块 {i+1} (长度: {len(chunk)}) ---") print(chunk) print("-" * 20 + "\n") 运行此代码将输出这些块。请注意 chunk_size 如何作为最大限制,并且分割器会首先尝试遵循 separator。注意一个块的末尾和下一个块的开头,以查看 chunk_overlap 的效果。例如,块1的末尾可能会在块2的开头重新出现,从而提供连续性。尽管简单,但如果 chunk_size 限制附近没有找到自然断点(如段落),固定大小分块可能会在句子中途切断。递归字符分块一种更常用且通常更有效的策略是递归字符分割。此方法尝试根据优先级列表中的分隔符来分割文本。它从较大的语义单元(例如段落的双换行符)开始,如果一个块仍然过大,则使用逐渐更小的分隔符(单换行符、空格等)递归地分割更小的片段。这种方法倾向于比简单的固定大小分割更有效地将相关内容保持在一起。LangChain 的 RecursiveCharacterTextSplitter 实现了这一点。from langchain.text_splitter import RecursiveCharacterTextSplitter # 初始化递归分割器 recursive_splitter = RecursiveCharacterTextSplitter( # 尝试首先按段落分割,然后按句子,再按单词 separators=["\n\n", "\n", " ", ""], chunk_size=250, chunk_overlap=50, length_function=len, is_separator_regex=False, ) # 分割文本 recursive_chunks = recursive_splitter.split_text(astro_cortex_text) # 查看结果 print(f"原始文本长度: {len(astro_cortex_text)}") print(f"递归分割器创建的块数: {len(recursive_chunks)}\n") for i, chunk in enumerate(recursive_chunks): print(f"--- 递归块 {i+1} (长度: {len(chunk)}) ---") print(chunk) print("-" * 20 + "\n")比较 RecursiveCharacterTextSplitter 和 CharacterTextSplitter 的输出。您很可能会发现,即使使用相同的 chunk_size 目标,递归方法也能更好地保留块内的段落和句子结构。它优先沿 \n\n(段落)分割,然后是 \n(行),再是空格,这通常会形成语义上更连贯的块。选择您的策略哪种分块策略最好?固定大小 (CharacterTextSplitter):简单,块长度可预测。如果它经常在句子中途分割,在保留语义方面可能效果较差。递归 (RecursiveCharacterTextSplitter):通常更受青睐。尝试保持语义边界(段落、句子),从而形成更连贯的块,尽管块大小可能略有更多变化。理想的 chunk_size 和 chunk_overlap 很大程度上取决于:您的数据:具有清晰段落结构的文档会从递归分割中极大受益。嵌入模型:有些模型有最佳输入长度。下游大型语言模型:它的上下文窗口限制影响了每次可处理的文本量。您的检索目标:小块允许更精准的检索,但可能遗漏更广的上下文。大块捕捉更多上下文,但可能不够具体。通常需要进行实验。尝试不同的分割器和参数,然后评估形成的块在 RAG 管道检索过程中表现如何(我们很快会介绍,并在第6章进行评估)。这个实践练习演示了如何将原始文本转换为已处理的块。数据准备的下一步涉及为这些块生成向量嵌入,并将它们存储在向量数据库中,从而使它们可被检索组件搜索。