文档加载器(Document loaders

RAG 介绍

RAG 概念

从本小节开始,我们将重点放在 RAG 阶段(Retrieval-Augmented Generation,检索增强生成)。这是当前大语言模型应用的核心模式。RAG 的流程相对复杂,为了更好的理解 RAG,我们先用 AI 搜索来引出 RAG。

  • 对于【AI 大模型】来说,它最擅长的是语义理解和文本总结,最不擅长的就是获取实时的信息。因为大模型的训练数据是有截止日期的!
  • 对于【搜索引擎】来说,它最擅长的就是获取实时的信息,缺点是信息分散,每次都需要人为进行总结。
  • 大模型与搜索引擎的结合,就是给 AI 配备了一个活字典,让 AI 可以随时进行查阅。

下图展示了一个最简单的 AI 搜索工作流程,搜索引擎在这里充当知识库,结合我们的查询语句,大模型便可以从知识库中获取相应的查询结果:

有了以上流程的铺垫,接下来,正式进入 RAG 的学习。

首先,先来思考一个问题搜索引擎可以帮我们解决实时数据的获取,但获取到的数据也是受限的。它只能获取到公开在网络中的数据,而无法获取到一些本地数据,或企业内部的私有数据等,此时该如何?

答案是使用 RAG(检索增强生成)技术!当用户向 LLM 提问时,系统首先在知识库(如公司内部文档)中进行语义搜索,找到最相关的内容,然后将这些内容和问题一起交给 LLM 来生成答案。与 AI 搜索类比,本质是知识库改变了,从搜索引擎线上搜索改为了本地或私有知识库中搜索。

RAG 流程

RAG 的流程分为【离线数据处理】和【在线检索】两个过程。

上面提到,RAG 知识库可以是本地文档、公司内部文档等一些私有化数据。但这些私有数据或文档实际上并不能很好地被直接进行检索访问。因此需要将这些私有化数据构建成可以被检索的知识库,这就是离线数据处理要干的事情。经过离线数据后,知识则会按照某种格式以及排列方式存储在知识库中,等待被使用。

在线检索则是我们依赖知识库查询,通过大模型生成结果的过程。

过程如下图所示:

这张图将会是我们后续要学习 LangChain 组件知识地图,所有的组件都会一一进行讲解,现在我们只需掌握其流程,接触相关概念即可。

文档加载(Document Loading:加载多种不同来源加载文档。LangChain 提供了 100 多种不同的文档加载器,包括 PDF 在内的非结构化的数据、SQL 在内的结构化的数据,以及 Python、Java 之类的代码等。

文本分割(Splitting:文本分割器把 Documents 切分为指定大小的块。

存储(Storage:存储涉及到两个环节,分别是:

  • 将切分好的文档块进行嵌入(Embedding),即将文档块转换成向量的形式。
  • Embedding 后的向量数据,存储到向量数据库中。

检索(Retrieval:数据存入向量数据库后。当我们需要进行数据检索时,会通过某种检索算法找到与输入问题相似的文档块。

输出(Output:把问题以及检索出来的文档块一起提交给 LLMLLM 会通过问题和检索出来的提示一起来生成更加合理的答案。


我们现在已经知道了 RAG 的完整流程,但也仅是知道 RAG 是什么,至于流程中为什么要进行文档加载、文本分割、存储,我们还无从了解。因此后续的各个 LangChain 组件讲解时都会涉及每个步骤被设计出来的原因。

RAG 示例

讲这么多,不如见一见。接下来就来演示一个 RAG 系统。在这个示例中,我们

  • 提供了:《租房项目 Q&A》文档
  • 希望:通过聊天方式,提问关于项目的任何问题,最终得到答案。
  • 要求:最多只用三句话回答,要简明扼要。

代码如下(不用理解代码含义,只需要看结果即可):

from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

# 定义聊天模型
model = ChatOpenAI(model="gpt-4o-mini")
# 定义嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 配置 Redis 客户端
redis_url = "redis://192.168.100.238:6379"
config = RedisConfig(
    index_name="qa",
    redis_url=redis_url,
    metadata_schema=[
        {"name": "category", "type": "tag"},
        {"name": "num", "type": "numeric"},
    ],
)
# 定义 Redis 向量存储
vector_store = RedisVectorStore(embeddings, config=config)
# 生成检索器
retriever = vector_store.as_retriever()

# 定义提示词模板
prompt = ChatPromptTemplate.from_messages(
    [
        (
            "human",
            """你是负责回答问题的助手。使用以下检索到的上下文片段来回答问题。如果你不知道答案,就说你不知道。最多只用三句话,回答要简明扼要。
            Question: {question}
            Context: {context}
            Answer:""",
        ),
    ]
)

# 将文档转换为字符串
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# 定义链
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

# 循环输入问题
while True:
    # 获取用户输入
    question = input("\n请输入您的问题(输入'退出'或'quit'结束程序):").strip()

    # 检查是否退出
    if question.lower() in ["退出", "quit"]:
        print("程序已结束,再见!")
        break

    # 检查输入是否为空
    if not question:
        print("问题不能为空,请重新输入。")
        continue

    # 执行链,流式输出
    print("回答:", end="", flush=True)
    chunks = []
    for chunk in rag_chain.stream(question):
        chunks.append(chunk)
        print(chunk, end="", flush=True)
    print()  # 换行

运行代码:

请输入您的问题(输入'退出'或'quit'结束程序):介绍一下这个项目
回答:这个项目是一个基于脚手架的微服务在线租房系统,旨在模仿贝壳、安居客等流行应用,具备真实的交互体验和架构设计。通过结合理论和实践,我在这个项目中加深了对 Java 编程语言的理解。这个项目的灵感来源于我在大学时和同学合租的经历。

请输入您的问题(输入'退出'或'quit'结束程序):项目设计难点有哪些?
回答:项目设计的难点主要包括非技术方面的需求把控和技术方面的明确项目范围与目标。缺乏产品经理的介入使得设计过程中需自行绘制原型图并拆分功能,这增加了设计的复杂性。同时,测试和质量保证也是项目中的重要挑战,需要合理安排时间以确保项目质量。

请输入您的问题(输入'退出'或'quit'结束程序):介绍数据存储的相关设计
回答:数据存储的设计中,使用MySQL作为关系数据库管理系统,并结合MyBatis简化SQL操作。为了更好地应对高并发场景,设计了Redis缓存方案来优化内存利用和减轻数据库压力。引入OSS对象存储是为了实现无限扩展性,相对于本地存储和MySQL,它具备更高的性能和成本效益。

请输入您的问题(输入'退出'或'quit'结束程序):详细介绍 Redis 与 mysql 数据一致性方案
回答:Redis与MySQL的数据一致性方案通常采用“双写一致性”模式,通过Cache-Aside方法实现。在这一方案中,应用程序在读取数据时先查询Redis缓存,如果不存在再查询MySQL数据库,并将结果缓存到Redis中;在更新数据时,需同时更新MySQL和Redis,以确保两者状态一致。此方法也包括设置合理的缓存过期时间和使用布隆过滤器,以解决缓存穿透等问题。

请输入您的问题(输入'退出'或'quit'结束程序):

我们提供的文档越详细,生成的结果越符合预期。


Document 文档类

要想实现 RAG,首先就需要从源中获取数据,即加载数据或文档。这是通过 LangChain 的文档加载器完成的。LangChain 文档加载器可以将各种数据源加载成一系列的文档对象 Document

class langchain_core.documents.base.Document 用于存储一段文本和相关元数据的类,我们可以直接定义 LangChain 文档列表,如下所示:

from langchain_core.documents import Document
documents = [
    # 单个Document对象通常表示较大文档的一个块
    Document(
        # 内容字符串
        page_content="狗是很好的伴侣,以忠诚和友好而闻名。",
        # 元数据字典
        # 元数据属性可以捕获有关文档源、与其他文档的关系以及其他信息的信息。
        metadata={"source": "mammal-pets-doc"},
    ),
    Document(
        page_content="猫是独立的宠物,经常享受自己的空间。",
        metadata={"source": "mammal-pets-doc"},
    ),
]

这里我们定义了一个 documents 文档列表,其内包含了两个 Document 文档对象。通常,单个 Document 对象表示较大文档的一个块 / 页。每个 Document 对象,包含了以下参数:

  • id:可选的文档标识符。理想情况下,这应该在整个文档集合中是唯一的,并格式化为 UUID,但不会强制执行。
  • page_content:字符串文本
  • metadata:与内容关联的任意元数据。类型为 dict [Optional]

加载 PDF 文档

将本地的 PDF 文档加载到 LangChain 中,其实就是将 PDF 文档转换为一个个 Document 对象。这时就需要我们使用 PyPDFLoader 文档加载器完成这一功能。

class langchain_community.document_loaders.pdf.PyPDFLoader 类,有以下关键函数:

  • init() 初始化函数,入参 file_path,表示要加载的 PDF 文件的路径。
  • load() -> list[Document]:将数据加载到文档对象中。返回文档对象列表。

现在,让我们加载一个本地 PDF 文档看下效果。

from langchain_community.document_loaders import PyPDFLoader
file_path = "../Docs/PDF/脚手架级微服务租房平台Q&A.pdf"
loader = PyPDFLoader(file_path)
# 将 PDF 文件的每一页转换为一个独立的 Document 对象,并存储在列表 docs 中。
docs = loader.load()

接下来,来看看 Document 重要的一些参数:

print(f"问: PDF 文件的总页数为: \n{len(docs)}\n")
print(f"问: 第一页文本内容的前200个字符是: \n{docs[0].page_content[:200]}\n")
print(f"问: 第一页元数据: \n{docs[0].metadata}")

打印结果如下:

问: PDF 文件的总页数为: 
32

问: 第一页文本内容的前200个字符是: 
脚手架级微服务租房平台
通用问题
1. 为什么做这个项目?
回答1: (出于兴趣爱好开发)
大学期间,我和同学在外合租过一段时间,使用了一些租房平台,于是我有个想法,自己能不能开发一个租房平台,可以让我将理论知识与实践相结合。我希望通过实际项目来加深对Java编程语言和相关技术的理解。于是我便查找了一些资料,看了一些开源项目,进行了一些改进。
回答2: (开源项目的解释)
这个

问: 第一页元数据: 
{'producer': 'pdfcpu v0.8.1 dev', 'creator': 'Chromium', 'creationdate': '2025-08-28T17:52:34+08:00', 'moddate': '2025-08-28T17:52:34+08:00', 'source': '../PDF/脚手架级微服务租房平台Q&A.pdf', 'total_pages': 32, 'page': 0, 'page_label': '1'}

现在许多 LLM 支持对多模态输入(例如图像)进行推理。在某些应用程序中,例如对具有复杂布局、图表或扫描的 PDF 进行问答,可以跳过 PDF 解析,直接将 PDF 页面转换为图像并将其直接传递给模型可能是更准确的。


加载 Markdown 文件

将本地的 Markdown 文档加载到 LangChain 中,需要我们使用 UnstructuredMarkdownLoader 文档加载器完成这一功能。

class langchain_community.document_loaders.markdown.UnstructuredMarkdownLoader 类,有以下关键函数:

init() 初始化函数,所需参数:

1. file_path:表示要加载的 Markdown 文件的路径。

2. mode:加载文件时要使用的模式。可以是 singleelements。默认为 single

  • single:文档将作为单个 Document 对象返回。
  • elements:会将文档拆分为 TitleNarrativeText 等不同类型的元素。

load() -> list[Document]:将数据加载到文档对象中。返回文档对象列表。

LangChain 实现的 UnstructuredMarkdownLoader 需要依赖 Unstructured 包。因此在使用前我们需要先安装它:

pip install "unstructured[md]" nltk

现在,让我们使用 single 模式加载一个本地 Markdown 文档看下效果。

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Document

markdown_path = "../Docs/Markdown/脚手架级微服务租房平台Q&A.md"
# single 模式,加载后,默认只有一个 Document 对象
loader = UnstructuredMarkdownLoader(markdown_path)
data = loader.load()

assert len(data) == 1
assert isinstance(data[0], Document)
print(data[0].page_content[:200])
print(data[0].metadata)

打印结果如下:

通用问题

为什么做这个项目?

回答1

{'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md'}

接下来再看下 elements 模式下加载本地 Markdown 文档的效果:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Document

markdown_path = "../Docs/Markdown/脚手架级微服务租房平台Q&A.md"
# single 模式,加载后,默认只有一个 Document 对象
loader = UnstructuredMarkdownLoader(markdown_path, mode="elements")
data = loader.load()

print(f"问: 文档个数为: \n{len(data)}\n")
print("问: 前三个文档数据: ")
for document in data[:3]:
    print(f"{document}\n")

打印结果如下:

问: 文档个数为:
441

问: 前三个文档数据:
page_content='通用问题' metadata={'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category_depth': 0, 'languages': ['zho'], 'file_directory': '../Docs/Markdown', 'filename': '脚手架级微服务租房平台Q&A.md', 'filetype': 'text/markdown', 'last_modified': '2025-08-29T10:56:36', 'category': 'Title', 'element_id': '3a0670f9bfd58576e430ef11def41593'}

page_content='为什么做这个项目?' metadata={'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'category_depth': 2, 'emphasized_text_contents': ['为什么做这个项目?'], 'emphasized_text_tags': ['b'], 'languages': ['zho'], 'file_directory': '../Docs/Markdown', 'filename': '脚手架级微服务租房平台Q&A.md', 'filetype': 'text/markdown', 'last_modified': '2025-08-29T10:56:36', 'parent_id': '3a0670f9bfd58576e430ef11def41593', 'category': 'Title', 'element_id': 'fcb08b2a85942455eecebb9467ffca4c'}

page_content='回答1: (出于兴趣爱好开发)' metadata={'source': '../Docs/Markdown/脚手架级微服务租房平台Q&A.md', 'emphasized_text_contents': ['回答1: (出于兴趣爱好开发)'], 'emphasized_text_tags': ['b'], 'languages': ['zho'], 'file_directory': '../Docs/Markdown', 'filename': '脚手架级微服务租房平台Q&A.md', 'filetype': 'text/markdown', 'last_modified': '2025-08-29T10:56:36', 'parent_id': 'fcb08b2a85942455eecebb9467ffca4c', 'category': 'UncategorizedText', 'element_id': 'a6fc0b5a457d21234bf1c4a6ae0a18db'}

可以看见文档被分成了 441 个。根据什么规则拆分呢?答案是根据类型拆分。在元数据中,有一个表示类型的字段:category。这些类型都是现代文档解析库(如 Unstructured.io)中用于分类 Markdown(MD)文档或从其他格式(如 PDF, Word, HTML)解析后转换为 Markdown 的常见元素类型。

让我们把当前文档包含的所有类型拿出来看看:

print(set(document.metadata["category"] for document in data))

结果如下:

{'Image', 'Title', 'ListItem', 'Table', 'NarrativeText', 'UncategorizedText'}

含义如下:

  • Image:图像。使用 ![alt text](image-url) 语法插入的图片。
  • Title:标题。这里包含了一级、二级、三级等标题。
  • ListItem:列表项。以 -*+ 开头的无序列表,或以 1.2. 等开头的有序列表项。
  • Table:表格。使用 |- 语法创建的表格。
  • NarrativeText:叙述性文本。一个或多个连续的段落。
  • UncategorizedText:未分类文本。通常是:表格中的脚注或注释、图片下方的简短说明、项目符号中非常简短的词组、文档页眉 / 页脚中的日期或页码等元数据。

这里的标题都是用 Title 表示,那么如何区分到底是几级标题呢?首先,每个文档对象的元数据中都包含一个 element_id 用来识别其唯一性,除此之外,还有一个 parent_id 元素,用来表示其归属于哪个文档对象下,可以用来表示层级关系。如示例中的前三个文档对象,就有明显的层级关系:

page_content='通用问题' metadata={
    'element_id': '3a0670f9bfd58576e430ef11def41593',
    'category': 'Title',
}

page_content='为什么做这个项目?' metadata={
    'parent_id': '3a0670f9bfd58576e430ef11def41593',
    'element_id': 'fcb08b2a85942455eecebb9467ffca4c',
    'category': 'Title',
}

page_content='回答1: (出于兴趣爱好开发)' metadata={
    'parent_id': 'fcb08b2a85942455eecebb9467ffca4c',
    'category': 'UncategorizedText',
    'element_id': 'a6fc0b5a457d21234bf1c4a6ae0a18db'
}

经过这种设计,就可以还原出整个 MD 文档的内容了。

对于 LangChain 来说,能加载的文档类型远不止这些,它还能加载网页、一些云提供商文件、社交媒体平台文档等,更多文档加载器见这里

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐