17_LangChain自定义RAG加载器

74 阅读10分钟

LangChain自定义RAG加载器

引言

在构建基于LLM的应用程序时,从各种数据源加载和处理文档是RAG(检索增强生成)系统的关键环节。LangChain提供了灵活的文档加载和解析机制,允许开发者从各种来源(如PDF、网页、数据库等)提取信息,并将其转换为LLM可以利用的格式。本教程将深入探讨如何在LangChain中创建自定义文档加载器和解析器,帮助开发者更好地处理特定数据源和格式。

1. 文档加载的核心概念

文档加载的主要组件

LangChain中文档加载涉及以下核心组件:

组件描述
Document包含 text 和 metadata 的内容
BaseLoader用于将原始数据转换为 Documents
Blob二进制数据的表示,可以位于文件或内存中
BaseBlobParser解析 Blob 以生成 Document 对象的逻辑

Document对象

Document是LangChain中最基本的数据单元,包含两个主要部分:

  1. page_content:文本内容
  2. metadata:关于文档的元数据,如作者、来源、日期等

Document对象通常被格式化为提示,然后输入LLM,使其能够利用文档中的信息生成所需的响应(如摘要、问答等)。

2. 创建标准文档加载器

BaseLoader接口

要创建自定义文档加载器,需要继承BaseLoader类并实现以下方法:

方法名说明
lazy_load用于惰性逐个加载文档。用于生产代码。
alazy_loadlazy_load的异步变体
load用于急切将所有文档加载到内存中。用于交互式工作。
aload用于急切将所有文档加载到内存中。用于交互式工作。在2024-04添加到LangChain。

重要提示:在实现文档加载器时,不要通过lazy_load或alazy_load方法传递参数。所有配置都应通过初始化器(init)传递。这是LangChain的设计选择,以确保一旦实例化了文档加载器,它就具有加载文档所需的所有信息。

实现自定义文档加载器

下面是一个简单的自定义文档加载器示例,它从文本文件中加载数据,并为每一行创建一个Document对象:

from typing import List, Iterator
from langchain_core.documents import Document
from langchain_community.document_loaders.base import BaseLoader

class LineByLineLoader(BaseLoader):
    """从文本文件中逐行加载文档的加载器。"""
    
    def __init__(self, file_path: str, metadata: dict = None):
        """初始化加载器。
        
        Args:
            file_path: 要加载的文本文件路径
            metadata: 要添加到每个文档的元数据
        """
        self.file_path = file_path
        self.metadata = metadata or {}
        
    def lazy_load(self) -> Iterator[Document]:
        """惰性加载文档。
        
        每次产生一个文档,适用于大文件。
        """
        with open(self.file_path, 'r', encoding='utf-8') as file:
            for i, line in enumerate(file):
                if line.strip():  # 跳过空行
                    metadata = self.metadata.copy()
                    metadata["line_number"] = i + 1
                    metadata["source"] = self.file_path
                    yield Document(
                        page_content=line.strip(),
                        metadata=metadata
                    )
    
    async def alazy_load(self) -> Iterator[Document]:
        """异步惰性加载文档。
        
        这里我们简单地调用同步版本,但在实际应用中,
        您可能希望实现真正的异步文件读取。
        """
        for doc in self.lazy_load():
            yield doc
            
    # load()和aload()方法已由BaseLoader基类提供,
    # 它们分别调用lazy_load()和alazy_load()并将结果转换为列表

测试自定义加载器

为了测试我们的自定义加载器,我们需要创建一个包含一些内容的文本文件:

# 创建一个测试文件
with open("sample_data.txt", "w", encoding="utf-8") as f:
    f.write("这是第一行内容。\n")
    f.write("这是第二行内容。\n")
    f.write("这是第三行内容,包含一些特殊字符:!@#$%^&*()。\n")
    f.write("\n")  # 空行
    f.write("这是第五行内容。\n")

# 使用我们的自定义加载器
loader = LineByLineLoader("sample_data.txt", metadata={"source_type": "text_file"})

# 使用load()方法急切加载所有文档
documents = loader.load()

print(f"加载了 {len(documents)} 个文档")
for i, doc in enumerate(documents):
    print(f"文档 {i+1}:")
    print(f"  内容: {doc.page_content}")
    print(f"  元数据: {doc.metadata}")
    print()

提示load()方法在Jupyter Notebook等交互式环境中很有用。在生产代码中应避免使用它,因为急切加载假定所有内容都可以放入内存中,而这并不总是成立,特别是对于企业级数据。

3. 使用Blob和BlobParser

对于更复杂的文件处理需求,LangChain提供了Blob和BlobParser抽象,它们允许将文件加载逻辑与解析逻辑分离。

BaseBlobParser接口

BaseBlobParser是一个接口,它接受一个blob并输出一个Document对象列表。blob是一个表示数据的对象,可以存在于内存中或文件中。

from abc import ABC, abstractmethod
from typing import Iterator, List

from langchain_core.documents import Document

class BaseBlobParser(ABC):
    """解析blob并返回Document对象的基类。"""
    
    @abstractmethod
    def lazy_parse(self, blob: Blob) -> Iterator[Document]:
        """惰性解析blob。"""
        pass
    
    def parse(self, blob: Blob) -> List[Document]:
        """急切解析blob。"""
        return list(self.lazy_parse(blob))

实现自定义BlobParser

下面是一个简单的自定义BlobParser示例,它将文本blob解析为多个Document对象,每个段落一个:

from typing import Iterator, List, Optional
from langchain_core.documents import Document
from langchain_core.blobs import Blob
from langchain_community.document_loaders.parsers.base import BaseBlobParser

class ParagraphBlobParser(BaseBlobParser):
    """将文本blob按段落解析为Document对象的解析器。"""
    
    def __init__(self, separator: str = "\n\n"):
        """初始化解析器。
        
        Args:
            separator: 用于分隔段落的字符串
        """
        self.separator = separator
    
    def lazy_parse(self, blob: Blob) -> Iterator[Document]:
        """惰性解析blob。
        
        Args:
            blob: 要解析的blob
            
        Yields:
            每个段落的Document对象
        """
        content = blob.as_string()
        paragraphs = content.split(self.separator)
        
        for i, paragraph in enumerate(paragraphs):
            if paragraph.strip():  # 跳过空段落
                metadata = {
                    "source": blob.source or "",
                    "paragraph_index": i,
                    "content_type": blob.mimetype or "text/plain"
                }
                if blob.metadata:
                    metadata.update(blob.metadata)
                
                yield Document(
                    page_content=paragraph.strip(),
                    metadata=metadata
                )

使用Blob API

Blob是LangChain中表示二进制数据的对象,受Web API规范启发。下面是一些使用Blob API的基本示例:

from langchain_core.blobs import Blob

# 从字符串创建Blob
text_blob = Blob.from_text("这是一个测试文本。\n\n这是第二个段落。")
print(f"Blob mimetype: {text_blob.mimetype}")
print(f"Blob size: {text_blob.size} bytes")

# 从文件创建Blob
file_blob = Blob.from_path("sample_data.txt")
print(f"File blob source: {file_blob.source}")

# 使用我们的自定义解析器
parser = ParagraphBlobParser()
documents = parser.parse(file_blob)

print(f"\n解析了 {len(documents)} 个文档")
for i, doc in enumerate(documents):
    print(f"文档 {i+1}:")
    print(f"  内容: {doc.page_content}")
    print(f"  元数据: {doc.metadata}")
    print()

Blob元数据

Blob对象包含以下元数据:

# 创建带有自定义元数据的Blob
blob_with_metadata = Blob.from_text(
    "这是测试内容",
    metadata={"author": "张三", "date": "2024-04-01"},
    mimetype="text/plain"
)

print(f"Blob metadata: {blob_with_metadata.metadata}")
print(f"Blob mimetype: {blob_with_metadata.mimetype}")

4. 使用BlobLoader和GenericLoader

FileSystemBlobLoader

FileSystemBlobLoader是LangChain提供的一个BlobLoader实现,用于从文件系统加载Blob:

from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoader

# 创建一个加载器,加载指定目录中的所有.txt文件
loader = FileSystemBlobLoader(
    "data_directory",
    glob="**/*.txt",
    include_hidden=False
)

# 加载所有匹配的文件
blobs = list(loader.yield_blobs())
print(f"加载了 {len(blobs)} 个blob")

GenericLoader

GenericLoader是一个抽象,它将BlobLoader与BaseBlobParser结合在一起,提供了一种简单的方式来加载和解析文档:

from langchain_community.document_loaders.generic import GenericLoader
from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoader
from langchain_community.document_loaders.parsers import TextParser

# 创建一个通用加载器,加载和解析文本文件
loader = GenericLoader(
    FileSystemBlobLoader("data_directory", glob="**/*.txt"),
    TextParser()
)

# 加载并解析所有文档
documents = loader.load()
print(f"加载了 {len(documents)} 个文档")

自定义GenericLoader

您也可以创建自定义的GenericLoader子类,封装特定的加载和解析逻辑:

from langchain_community.document_loaders.generic import GenericLoader
from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoader

class CustomTextLoader(GenericLoader):
    """自定义文本文件加载器。"""
    
    def __init__(self, directory_path: str, glob_pattern: str = "**/*.txt"):
        """初始化加载器。
        
        Args:
            directory_path: 要加载文件的目录路径
            glob_pattern: 用于匹配文件的glob模式
        """
        blob_loader = FileSystemBlobLoader(
            directory_path,
            glob=glob_pattern,
            include_hidden=False
        )
        parser = ParagraphBlobParser()
        super().__init__(blob_loader, parser)

5. 高级用例

处理PDF文件

下面是一个处理PDF文件的自定义解析器示例:

from typing import Iterator, List, Optional
from langchain_core.documents import Document
from langchain_core.blobs import Blob
from langchain_community.document_loaders.parsers.base import BaseBlobParser
import PyPDF2
import io

class CustomPDFParser(BaseBlobParser):
    """自定义PDF解析器,将PDF文件解析为Document对象。"""
    
    def __init__(self, extract_images: bool = False):
        """初始化解析器。
        
        Args:
            extract_images: 是否提取PDF中的图像
        """
        self.extract_images = extract_images
    
    def lazy_parse(self, blob: Blob) -> Iterator[Document]:
        """惰性解析PDF blob。
        
        Args:
            blob: 要解析的PDF blob
            
        Yields:
            每页PDF的Document对象
        """
        # 获取PDF的二进制数据
        pdf_bytes = blob.as_bytes()
        
        # 使用PyPDF2解析PDF
        with io.BytesIO(pdf_bytes) as pdf_file:
            pdf_reader = PyPDF2.PdfReader(pdf_file)
            
            # 提取基本元数据
            metadata = {
                "source": blob.source or "",
                "content_type": "application/pdf",
                "total_pages": len(pdf_reader.pages)
            }
            
            # 提取PDF元数据
            if pdf_reader.metadata:
                for key, value in pdf_reader.metadata.items():
                    if key and value:
                        # 移除前缀'/'并转换为小写
                        clean_key = key.strip('/').lower() if isinstance(key, str) else key
                        metadata[clean_key] = value
            
            # 如果blob有额外元数据,添加它
            if blob.metadata:
                metadata.update(blob.metadata)
            
            # 处理每一页
            for i, page in enumerate(pdf_reader.pages):
                page_text = page.extract_text() or ""
                page_metadata = metadata.copy()
                page_metadata["page_number"] = i + 1
                
                yield Document(
                    page_content=page_text,
                    metadata=page_metadata
                )

处理网页内容

下面是一个处理HTML内容的自定义解析器示例:

from typing import Iterator, List, Optional
from langchain_core.documents import Document
from langchain_core.blobs import Blob
from langchain_community.document_loaders.parsers.base import BaseBlobParser
from bs4 import BeautifulSoup

class HTMLBlobParser(BaseBlobParser):
    """将HTML blob解析为Document对象的解析器。"""
    
    def __init__(self, extract_metadata: bool = True, extract_links: bool = True):
        """初始化解析器。
        
        Args:
            extract_metadata: 是否提取HTML元数据
            extract_links: 是否提取链接
        """
        self.extract_metadata = extract_metadata
        self.extract_links = extract_links
    
    def lazy_parse(self, blob: Blob) -> Iterator[Document]:
        """惰性解析HTML blob。
        
        Args:
            blob: 要解析的HTML blob
            
        Yields:
            Document对象
        """
        html_content = blob.as_string()
        soup = BeautifulSoup(html_content, 'html.parser')
        
        # 基本元数据
        metadata = {
            "source": blob.source or "",
            "content_type": "text/html"
        }
        
        # 提取HTML元数据
        if self.extract_metadata:
            # 提取标题
            title_tag = soup.find('title')
            if title_tag:
                metadata["title"] = title_tag.get_text()
            
            # 提取meta标签
            for meta in soup.find_all('meta'):
                name = meta.get('name') or meta.get('property')
                content = meta.get('content')
                if name and content:
                    metadata[f"meta_{name}"] = content
        
        # 提取链接
        if self.extract_links:
            links = []
            for a_tag in soup.find_all('a', href=True):
                links.append({
                    "text": a_tag.get_text().strip(),
                    "href": a_tag['href']
                })
            metadata["links"] = links
        
        # 提取正文内容
        # 移除script和style元素
        for script in soup(["script", "style"]):
            script.extract()
        
        # 获取文本
        text = soup.get_text()
        
        # 清理文本
        lines = (line.strip() for line in text.splitlines())
        chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
        text = '\n'.join(chunk for chunk in chunks if chunk)
        
        yield Document(
            page_content=text,
            metadata=metadata
        )

处理CSV和Excel文件

下面是一个处理表格数据的自定义解析器示例:

from typing import Iterator, List, Optional
from langchain_core.documents import Document
from langchain_core.blobs import Blob
from langchain_community.document_loaders.parsers.base import BaseBlobParser
import pandas as pd
import io

class TableBlobParser(BaseBlobParser):
    """将CSV或Excel blob解析为Document对象的解析器。"""
    
    def __init__(self, file_type: str = "auto", sheet_name: Optional[str] = None):
        """初始化解析器。
        
        Args:
            file_type: 文件类型,可以是'csv'、'excel'或'auto'
            sheet_name: Excel工作表名称,仅对Excel文件有效
        """
        self.file_type = file_type
        self.sheet_name = sheet_name
    
    def lazy_parse(self, blob: Blob) -> Iterator[Document]:
        """惰性解析表格blob。
        
        Args:
            blob: 要解析的blob
            
        Yields:
            每行数据的Document对象
        """
        # 确定文件类型
        file_type = self.file_type
        if file_type == "auto":
            if blob.mimetype == "text/csv" or (blob.source and blob.source.endswith('.csv')):
                file_type = "csv"
            elif blob.mimetype in ["application/vnd.ms-excel", 
                                  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] or \
                 (blob.source and (blob.source.endswith('.xls') or blob.source.endswith('.xlsx'))):
                file_type = "excel"
            else:
                raise ValueError(f"无法确定文件类型: {blob.mimetype or blob.source}")
        
        # 读取数据
        data_bytes = blob.as_bytes()
        df = None
        
        if file_type == "csv":
            df = pd.read_csv(io.BytesIO(data_bytes))
        elif file_type == "excel":
            df = pd.read_excel(io.BytesIO(data_bytes), sheet_name=self.sheet_name)
        
        # 基本元数据
        metadata = {
            "source": blob.source or "",
            "content_type": blob.mimetype or "",
            "row_count": len(df),
            "column_count": len(df.columns),
            "columns": list(df.columns)
        }
        
        # 为每行创建一个Document
        for i, row in df.iterrows():
            row_metadata = metadata.copy()
            row_metadata["row_index"] = i
            
            # 将行转换为字符串表示
            content = "\n".join([f"{col}: {row[col]}" for col in df.columns])
            
            yield Document(
                page_content=content,
                metadata=row_metadata
            )

6. 最佳实践

选择正确的加载方式

  1. 惰性加载 vs 急切加载

    • 对于大型文件或生产环境,使用lazy_load()
    • 对于小型文件或交互式环境,可以使用load()
  2. 同步 vs 异步

    • 对于Web应用或需要非阻塞IO的场景,实现异步方法alazy_load()aload()
    • 对于简单脚本或批处理任务,同步方法通常足够

优化解析性能

  1. 分块处理:对于大型文档,考虑分块处理而不是一次加载全部内容
  2. 并行处理:对于多个文档,考虑使用并行处理提高性能
  3. 缓存结果:如果文档不经常变化,考虑缓存解析结果

丰富元数据

为文档添加丰富的元数据可以显著提高检索质量和用户体验:

  1. 文档来源:文件路径、URL、数据库表等
  2. 时间信息:创建日期、修改日期、加载日期
  3. 结构信息:页码、段落编号、章节等
  4. 内容类型:文本、HTML、PDF等
  5. 自定义字段:作者、标题、标签等

总结

自定义文档加载器和解析器是构建高质量RAG应用的关键组件。通过LangChain提供的灵活接口,开发者可以根据具体需求实现个性化的文档处理流程。

本教程介绍了如何创建标准文档加载器、Blob解析器,以及如何使用BlobLoader和GenericLoader简化文档处理流程。通过这些工具,开发者可以轻松地从各种数据源提取信息,并将其转换为LLM可以利用的格式,从而构建更强大、更智能的RAG应用。

随着应用规模的增长,可以考虑使用更高级的解析技术和优化策略,进一步提升系统的性能和用户体验。