任何有效的检索增强生成(RAG)系统都基于其获取和处理相关信息的能力。虽然基础的文档加载涵盖简单的文本文件,但生产环境往往展现出远为复杂的实际情况:各种文件格式混合、大规模非结构化文档、噪声数据以及对丰富元数据的需求。本章将介绍用于加载和转换各种数据源的先进方法,为大规模可靠索引和检索做准备。正确处理这一阶段对您的RAG流程的性能、相关性和可维护性具有决定作用。处理多样数据源生产环境中的RAG系统很少只处理 .txt 文件。您会遇到包含扫描图像和复杂布局的PDF、具有复杂结构的HTML页面、CSV或JSON中的结构化数据,以及可能的专有格式。LangChain提供灵活的 DocumentLoader 抽象机制来处理这种多样性。使用内置加载器:LangChain生态系统包含为常见格式设计的众多加载器:PyPDFLoader / PyMuPDFLoader / PDFMinerLoader: 用于PDF文档。与PyPDFLoader相比,PyMuPDFLoader通常能提供更好的性能和复杂布局处理。UnstructuredPDFLoader(下文会提到)提供更高级的元素识别。WebBaseLoader: 从URL获取并解析HTML内容。常与BeautifulSoup4等HTML解析库配合使用,以进行精细控制(请确保bs4已安装)。CSVLoader: 从CSV文件加载数据,可指定源列和元数据。JSONLoader: 解析JSON文件,使用jq语法指定JSON结构的哪些部分构成文档的内容和元数据。UnstructuredFileLoader: 一个通用的选项,使用unstructured库。它自动识别文件类型(PDF、HTML、DOCX、PPTX、EML等),并提取标题、段落、列表和表格等内容元素。这通常是处理混合文件类型的良好起点。from langchain_community.document_loaders import UnstructuredFileLoader from langchain_community.document_loaders import WebBaseLoader import os # 使用UnstructuredFileLoader处理本地PDF的示例 pdf_path = "path/to/your/document.pdf" if os.path.exists(pdf_path): loader_pdf = UnstructuredFileLoader(pdf_path, mode="elements") docs_pdf = loader_pdf.load() # 'docs_pdf' 包含 Document 对象,通常代表不同的元素 # print(f"Loaded {len(docs_pdf)} elements from PDF.") else: print(f"PDF file not found at {pdf_path}") # 使用WebBaseLoader的示例 loader_web = WebBaseLoader("https://example.com/some_article") docs_web = loader_web.load() # print(f"Loaded {len(docs_web)} documents from web page.") # 注意:请确保已安装所需的依赖项,如 'unstructured', 'pdf2image', 'pytesseract', 'lxml', 'jq', 'bs4' # pip install unstructured[pdf,local-inference] beautifulsoup4 jq lxml pdfminer.six pymupdf开发自定义加载器:当内置加载器不够用时(例如,访问专有数据库、特定API格式或复杂的解析逻辑),您可以通过继承 langchain_core.document_loaders.BaseLoader 来创建自己的加载器。您主要需要实现 load 或 lazy_load 方法,该方法应返回 langchain_core.documents.Document 对象的列表或迭代器。from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document from typing import List, Iterator import requests # 用于API交互的示例依赖项 class CustomApiLoader(BaseLoader): """从自定义API端点加载数据。""" def __init__(self, api_endpoint: str, api_key: str): self.api_endpoint = api_endpoint self.headers = {"Authorization": f"Bearer {api_key}"} def lazy_load(self) -> Iterator[Document]: """一个按文档逐个生成(yield)的惰性加载器。""" try: response = requests.get(self.api_endpoint, headers=self.headers) response.raise_for_status() # 对不良响应(4xx或5xx)抛出HTTPError api_data = response.json() for item in api_data.get("items", []): # 假设API返回 {'items': [...]} content = item.get("text_content", "") metadata = { "source": f"{self.api_endpoint}/{item.get('id')}", "item_id": item.get("id"), "timestamp": item.get("created_at"), # 从API响应中添加其他相关的元数据字段 } if content: # 只生成包含实际内容的文档 yield Document(page_content=content, metadata=metadata) except requests.exceptions.RequestException as e: print(f"Error fetching data from API: {e}") # 适当处理错误,例如记录日志并返回空迭代器 return iter([]) # 错误时返回空迭代器 # 用法 # loader = CustomApiLoader(api_endpoint="https://my.api.com/data", api_key="YOUR_API_KEY") # for doc in loader.lazy_load(): # print(doc.metadata["source"])处理大型文档与高级分块大型语言模型(LLM)具有有限的上下文窗口。直接输入多兆字节文档是不可行的,并且通常会降低检索质量。因此,有效的分块是必要的,它将大型文档分割成更小、更连贯的块。基础分块之外:虽然 RecursiveCharacterTextSplitter 具有多功能性,但在生产环境中存在更精细的策略:MarkdownHeaderTextSplitter: 适合具有清晰Markdown结构(如 #、## 等标题)的文档。它根据标题进行分块,并将标题信息包含在元数据中,保持结构上下文。SemanticChunker: 这种方法基于语义而非固定字符数或分隔符来分割文本。LangChain提供的SemanticChunker(通常在langchain_experimental中)使用嵌入来根据语义相似性确定分割点。这会生成语境相关的块,但需要额外的计算资源。自定义分块逻辑: 对于具有独特、可预测结构的文档(例如法律合同、科学论文),您可以使用自定义Python代码,根据特定的章节标记、正则表达式或在加载期间识别的结构标签(例如通过UnstructuredFileLoader)进行分块。块大小与重叠: 仔细调整 chunk_size 和 chunk_overlap。较大的块保留更多上下文,但可能超出模型限制或稀释特定信息。重叠有助于保持块之间的上下文,但会增加冗余并提高存储/处理需求。找到合适的平衡点通常需要根据您的具体用例和检索策略进行实验和评估。from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter from langchain_core.documents import Document # 示例:MarkdownHeaderTextSplitter markdown_content = """ # 项目概述 这是主要摘要。 ## 需求 - 需求 1 - 需求 2 ### 子需求 A A的详细信息。 ## 设计 高级设计文档。 # 附录 额外信息。 """ headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on) md_header_splits = markdown_splitter.split_text(markdown_content) # 每个分割后的文档现在都包含指示其标题层次的元数据 # print(md_header_splits[1].page_content) # print(md_header_splits[1].metadata) # 输出可能为: # page_content='- Requirement 1\n- Requirement 2' # metadata={'Header 1': 'Project Overview', 'Header 2': 'Requirements'} # 示例:细致的递归分块 long_text = "..." # 假设这是一个很长的字符串 recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, # 目标字符数 chunk_overlap=150, # 块之间的重叠 length_function=len, is_separator_regex=False, separators=["\n\n", "\n", ". ", " ", ""] # 顺序很重要! ) docs = recursive_splitter.create_documents([long_text]) # print(f"Split into {len(docs)} chunks.")复杂预处理与转换原始加载的数据通常很混乱。转换步骤清理内容,提取有价值的元数据,并组织信息以便更好地检索。数据清理:删除样板内容: 删除重复的页眉、页脚、网站菜单(来自网页)、法律免责声明或无关部分。BeautifulSoup等库(用于HTML)或自定义正则表达式模式可以在加载后但在分块前应用。处理格式: 标准化空白符,修正编码问题,或删除可能干扰处理或嵌入的特殊字符。元数据提取与丰富:元数据对过滤搜索(例如,“查找来源X在日期Y之后创建的文档”)和为LLM提供上下文非常重要。来源信息: 始终存储来源(文件路径、URL、数据库ID)。结构元数据: 捕获章节标题、页码(来自PDF)或元素类型(标题、段落、表格——通常由UnstructuredFileLoader提供)。内容派生元数据: 从文档内容本身提取日期、作者、关键词或摘要,可能在转换阶段使用较小的LLM调用或基于规则的系统。LangChain的DocumentTransformer接口(例如 BeautifulSoupTransformer、EmbeddingsRedundantFilter)允许应用转换。您也可以实现自定义转换函数。from langchain_core.documents import Document from langchain_text_splitters import CharacterTextSplitter from langchain_community.document_transformers import BeautifulSoupTransformer import re from datetime import datetime, timezone def add_custom_metadata_and_clean(doc: Document) -> Document: """示例转换函数。""" # 1. 清理内容(例如,删除多余的空白符) cleaned_content = re.sub(r'\s+', ' ', doc.page_content).strip() # 2. 添加新元数据(例如,提取潜在日期) new_metadata = doc.metadata.copy() # 避免直接修改原始元数据 date_match = re.search(r'\b(20\d{2}-\d{2}-\d{2})\b', cleaned_content) if date_match: new_metadata['extracted_date'] = date_match.group(1) # 3. 添加处理时间戳或版本 new_metadata['processed_at'] = datetime.now(timezone.utc).isoformat() return Document(page_content=cleaned_content, metadata=new_metadata) # 假设 'initial_docs' 是来自加载器的一个 Document 对象列表 # transformed_docs = [add_custom_metadata_and_clean(doc) for doc in initial_docs] # 对HTML使用BeautifulSoupTransformer # loader = WebBaseLoader("...") # docs_html = loader.load() # bs_transformer = BeautifulSoupTransformer() # # 指定要提取的标签,移除不需要的标签 # docs_transformed_html = bs_transformer.transform_documents( # docs_html, # tags_to_extract=["p", "li", "div", "span"], # unwanted_tags=["header", "footer", "nav", "script", "style"] # ) 处理表格与图片:文档中的表格和图片构成挑战。简单的文本提取常常会破坏表格数据或完全忽略图片。表格: unstructured 等库可以尝试提取表格,通常将其转换为文档文本中的HTML或Markdown表示形式。另外,可能需要专门的表格提取工具,可以将结构化的表格数据单独存储并通过元数据进行关联。图片: 提取图表标题或周围文本通常是RAG最实用的方法。图像内容本身通常需要多模态模型,这会给RAG管线增加相当大的复杂度(这是一个超出标准LangChain RAG范围的话题)。生产工作流程图以下图表展示了一个典型的高级加载和转换管线:digraph G { rankdir=LR; node [shape=box, style="filled", fillcolor="#e9ecef", fontname="Arial"]; edge [fontname="Arial"]; rawData [label="原始数据\n(PDF, HTML, API, DOCX...)"]; loader [label="文档加载器\n(例如 UnstructuredFileLoader,\nCustomApiLoader)", fillcolor="#a5d8ff"]; transformer [label="文档转换器\n- 清理文本 (正则, BS4)\n- 添加/提取元数据\n- 结构处理", fillcolor="#96f2d7"]; splitter [label="文本分块器\n(例如 MarkdownHeaderTextSplitter,\nRecursiveCharacterTextSplitter,\n语义分块器)", fillcolor="#ffec99"]; finalDocs [label="已处理文档\n(内容 + 元数据)", shape=folder, fillcolor="#ced4da"]; rawData -> loader [label="加载"]; loader -> transformer [label="转换"]; transformer -> splitter [label="分块"]; splitter -> finalDocs [label="输出"]; }数据从原始来源经过加载、转换(清理、元数据丰富)和分块阶段,形成可用于索引的已处理文档。生产实践要点错误处理: 在加载和转换步骤中实现 try-except 块。全面记录错误。为问题文件决定策略:跳过它们,将它们移至错误队列,或尝试回退处理。可伸缩性与效率: 对于大型数据集,尽可能使用 lazy_load 迭代处理文档,以减少内存消耗。如果处理时间成为瓶颈,可考虑使用 concurrent.futures 等库或分布式任务队列(例如 Celery, Ray)并行化加载/转换过程。请注意,unstructured 等库可能计算密集型,特别是对于需要OCR的复杂PDF或基于图像的文档。幂等性: 确保在相同源数据上多次运行加载和转换过程,会产生相同的Document对象(内容和元数据)。这对于更新索引时保持一致性非常重要。使用稳定的标识符和确定性转换逻辑。通过投入建设精密的文档加载和转换管线,您为生产RAG系统奠定了坚实基础。处理各种格式、清理噪声、丰富元数据和智能分块是接下来将介绍的先进索引和检索方法的先决条件。