在上一篇文章中,介绍了实现RAG功能的整个流程,以及LangChain提供了哪些组件来实现RAG功能,想了解RAG实现过程可以参考:LangChain框架入门09:什么是RAG?。
本文将从RAG准备阶段第一个步骤:文档加载,详细介绍LangChain中的文档加载器组件,LangChain内置的文档加载器可以加载各种格式的知识库文档,而无需开发者自己编写,可以非常便捷地实现文档加载功能。
文中所有示例代码:github.com/wzycoding/l…
一、什么是文档加载器
在LangChain中,文档加载器用于将各种格式的文档转换为Document对象,LangChain提供了大量的文档加载器,支持从各种来源加载文档,如文件、数据源、URL等。
常用的LangChain文档加载器如下:
| 文档加载器 | 作用 |
|---|---|
| CSVLoader | 从CSV加载文档 |
| JSONLoader | 从JSON数据加载文档 |
| PyPDFLoader | 从PDF数据加载文档 |
| UnstructuredHTMLLoader | 从HTML数据加载文档 |
| UnstructuredMarkdownLoader | 从Markdown加载文档 |
| UnstructuredExcelLoader | 从Excel文件加载数据 |
每一个文档加载器都有自己特定的参数和方法,但它们有一个统一的load()方法来完成文档的加载,load()方法会返回一个Document类的对象列表,因为这些文档加载器都继承自BaseLoader基类,它们的继承关系如下:
在
BaseLoader类中,定义了load()方法,用来加载文档对象,在方法内部又调用了lazy_load()懒加载方法
def load(self) -> List[Document]:
"""Load data into Document objects."""
return list(self.lazy_load())
在lazy_load()方法中,判断了子类是否重写了load()方法,如果重写了,则调用当前类的load()方法,如果没有重写则抛出异常,因此在子类中,要重写load()方法或lazy_load()方法。
def lazy_load(self) -> Iterator[Document]:
"""A lazy loader for Documents."""
if type(self).load != BaseLoader.load:
return iter(self.load())
raise NotImplementedError(
f"{self.__class__.__name__} does not implement lazy_load()"
)
如果LangChain提供的文档加载器无法满足业务需求,我们也可以自己实现自定义加载器,通过继承BaseLoader,并实现其中的load()方法,来编写自定义文档加载器的加载逻辑。
二、Document文档类
文档加载器无论从什么来源进行文档加载,最终都是为了将文档信息解析为Document对象,下面一起来看看Document类中重要属性:
Document类中,主要包含两个重要属性:
page_content:表示文档的内容,类型是字符串
metadata :与文档本身无关的元数据信息。可以保存文档 ID、文件名等任意信息,类型是字典
三、UnstructuredMarkdownLoader文档加载器使用
下面以UnstructuredMarkdownLoader为例来介绍文档加载器的用法:
首先需要安装unstructured相关依赖
pip install "unstructured[md]" nltk
执行命令,生成依赖版本快照
pip freeze > requirements.txt
使用UnstructuredMarkdownLoader读取md文件示例如下:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
# 1.创建文档加载器,并指定路径
document_load = UnstructuredMarkdownLoader(file_path="LangChain框架入门09:什么是RAG?.md")
# 2.加载文档
documents = document_load.load()
# 3.打印文档内容
print(f"文档数量:{len(documents)}")
for document in documents:
print(f"文档内容:{document.page_content}")
print(f"文档元数据:{document.metadata}")
执行结果如下,默认情况下UnstructuredMarkdownLoader把md文档内容加载成了一个Document对象,并且自动将文件名添加到了Document对象的元数据中。
文档数量:1
文档内容:当我们在跟大语言模型交流时,会发现大语言模型只能回答训练数据范围内的事情,如果你问它一些超出训练数据之外的事情,AI的回答往往就是胡言乱语,或者给出错误的答案。
(文档内容省略...)
文档元数据:{'source': 'LangChain框架入门09:什么是RAG?.md'}
在底层Unstructured包会为不同的文本片段创建不同的“元素”。默认情况下会将这些元素合并在一起,可以通过指定 mode="elements" 来将不同元素进行分离,解析成多个文档。
document_load = UnstructuredMarkdownLoader(file_path="LangChain框架入门09:什么是RAG?.md",
mode="elements")
重新执行代码,可以看到加载的文档数为12,文档的内容也按照不同元素进行了拆分。
文档数量:12
文档内容:当我们在跟大语言模型交流时,会发现大语言模型只能回答训练数据范围内的事情,如果你问它一些超出训练数据之外的事情,AI的回答往往就是胡言乱语,或者给出错误的答案。
文档元数据:{'source': 'LangChain框架入门09:什么是RAG?.md', 'languages': ['zho'], 'filename': 'LangChain框架入门09:什么是RAG?.md', 'filetype': 'text/markdown', 'last_modified': '2025-08-03T15:10:28', 'category': 'UncategorizedText', 'element_id': '026c499e414187c4ffe540d4c89ce4f3'}
文档内容:那么我们不禁会想到:大模型拥有如此强大的能力,是否能让大模型掌握一些特定领域的知识,甚至接入企业内部的知识库数据?来帮助我们实现业务需求。
文档元数据:{'source': 'LangChain框架入门09:什么是RAG?.md', 'languages': ['zho'], 'filename': 'LangChain框架入门09:什么是RAG?.md', 'filetype': 'text/markdown', 'last_modified': '2025-08-03T15:10:28', 'category': 'UncategorizedText', 'element_id': '971d5c14b54653977c847a7802d531a6'}
(省略...)
四、实现自定义文档加载器
在实际开发中,基于文件的不同类型和不同格式,有时通过这些LangChain提供的文档加载器很难满足业务需求,例如需要根据特定规则提取文本片段,这时就需要开发自定义加载器,只需要定义一个自定义文档加载器类,并继承前面提到的BaseLoader类。
假设有如下需求,对chat_record.txt文件进行文档加载,内容如下,要求将不同人物的每一句对话加载成一个文档,并且在文档的元数据中添加人物的名字。
小明:你是哪个学校的?
小刚:我是光明中学的,我叫李小刚
小明:我是第一中学的,我叫赵小明
小刚:你的学号是多少?
小明:我的学号是,202409876
小刚:我的学号是,202407827
自定义文档加载器代码如下:
from typing import List
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
class ChatRecordLoader(BaseLoader):
file_path: str
def __init__(self, file_path: str):
self.file_path = file_path
def load(self) -> List[Document]:
"""加载聊天记录文档"""
documents = []
with open(self.file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# 如果包含中文冒号,则进行分割
if ":" in line:
user_name, content = line.split(":", 1)
documents.append(
Document(
page_content=content.strip(),
metadata={"user_name": user_name.strip()}
)
)
else:
# 不符合格式要求直接跳过
continue
return documents
chat_record_loader = ChatRecordLoader(file_path="chat_record.txt")
documents = chat_record_loader.load()
print(f"文档数量:{len(documents)}")
for document in documents:
print(f"文档内容:{document.page_content}")
print(f"文档元数据:{document.metadata}")
执行结果如下,chat_record.txt文件被解析成6个文档,并且每个文档的元数据都保存了user_name。
文档数量:6
文档内容:你是哪个学校的?
文档元数据:{'user_name': '小明'}
文档内容:我是光明中学的,我叫李小刚
文档元数据:{'user_name': '小刚'}
文档内容:我是第一中学的,我叫赵小明
文档元数据:{'user_name': '小明'}
文档内容:你的学号是多少?
文档元数据:{'user_name': '小刚'}
文档内容:我的学号是,202409876
文档元数据:{'user_name': '小明'}
文档内容:我的学号是,202407827
文档元数据:{'user_name': '小刚'}
五、总结
本文首先介绍了什么是文档加载器,以及常见文档加载器和它们的类继承关系。随后,以 UnstructuredMarkdownLoader 为例,演示了如何加载 Markdown 文件,并对比了默认模式与 mode="elements" 模式下的不同拆分效果。
最后,通过一个聊天记录文件加载的案例,展示了如何根据业务需求定制自定义文档加载器,实现将聊天记录逐句加载为 Document 对象,并在 metadata 中附加额外信息。
通过本文相信大家已经掌握了文档加载器的用法,LangChain 提供了丰富的内置加载器来快速适配常见文档格式,同时也为开发者预留出了灵活的扩展空间,使得无论是处理结构化数据还是非结构化文本,都能很好的处理文档的加载,在后续的文章中,将会介绍LangChain的文档分割器的用法,敬请期待。