LangChain自定义RAG加载器
引言
在构建基于LLM的应用程序时,从各种数据源加载和处理文档是RAG(检索增强生成)系统的关键环节。LangChain提供了灵活的文档加载和解析机制,允许开发者从各种来源(如PDF、网页、数据库等)提取信息,并将其转换为LLM可以利用的格式。本教程将深入探讨如何在LangChain中创建自定义文档加载器和解析器,帮助开发者更好地处理特定数据源和格式。
1. 文档加载的核心概念
文档加载的主要组件
LangChain中文档加载涉及以下核心组件:
| 组件 | 描述 |
|---|---|
| Document | 包含 text 和 metadata 的内容 |
| BaseLoader | 用于将原始数据转换为 Documents |
| Blob | 二进制数据的表示,可以位于文件或内存中 |
| BaseBlobParser | 解析 Blob 以生成 Document 对象的逻辑 |
Document对象
Document是LangChain中最基本的数据单元,包含两个主要部分:
- page_content:文本内容
- metadata:关于文档的元数据,如作者、来源、日期等
Document对象通常被格式化为提示,然后输入LLM,使其能够利用文档中的信息生成所需的响应(如摘要、问答等)。
2. 创建标准文档加载器
BaseLoader接口
要创建自定义文档加载器,需要继承BaseLoader类并实现以下方法:
| 方法名 | 说明 |
|---|---|
| lazy_load | 用于惰性逐个加载文档。用于生产代码。 |
| alazy_load | lazy_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. 最佳实践
选择正确的加载方式
-
惰性加载 vs 急切加载:
- 对于大型文件或生产环境,使用
lazy_load() - 对于小型文件或交互式环境,可以使用
load()
- 对于大型文件或生产环境,使用
-
同步 vs 异步:
- 对于Web应用或需要非阻塞IO的场景,实现异步方法
alazy_load()和aload() - 对于简单脚本或批处理任务,同步方法通常足够
- 对于Web应用或需要非阻塞IO的场景,实现异步方法
优化解析性能
- 分块处理:对于大型文档,考虑分块处理而不是一次加载全部内容
- 并行处理:对于多个文档,考虑使用并行处理提高性能
- 缓存结果:如果文档不经常变化,考虑缓存解析结果
丰富元数据
为文档添加丰富的元数据可以显著提高检索质量和用户体验:
- 文档来源:文件路径、URL、数据库表等
- 时间信息:创建日期、修改日期、加载日期
- 结构信息:页码、段落编号、章节等
- 内容类型:文本、HTML、PDF等
- 自定义字段:作者、标题、标签等
总结
自定义文档加载器和解析器是构建高质量RAG应用的关键组件。通过LangChain提供的灵活接口,开发者可以根据具体需求实现个性化的文档处理流程。
本教程介绍了如何创建标准文档加载器、Blob解析器,以及如何使用BlobLoader和GenericLoader简化文档处理流程。通过这些工具,开发者可以轻松地从各种数据源提取信息,并将其转换为LLM可以利用的格式,从而构建更强大、更智能的RAG应用。
随着应用规模的增长,可以考虑使用更高级的解析技术和优化策略,进一步提升系统的性能和用户体验。