文档加载器

RAG介绍(检索增强生成)

RAG概念

对于【AI大模型】来说,它最擅长的是语义理解和文本总结,最不擅长的就是获取实时的信息。因为大模型的训练数据是有截止日期的!

对于【搜索引擎】来说,它最擅长的就是获取实时的信息,缺点是信息分散,每次都需要人为进行总结。

  • 大模型与搜索引擎的结合,就是给AI配备了一个活字典,让AI可以随时进行查阅。

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

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

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

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

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

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

在这里插入图片描述

  • 文档加载(Document Loading):加载多种不同来源加载文档。LangChain提供了100多种不同的文档加载器,包括PDF在内的非结构化的数据、SQL在内的结构化的数据,以及Python、Java之类的代码等。
  • 文本分割(Splitting):文本分割器把Documents切分为指定大小的块。
  • 存储(Storage):存储涉及到两个环节:
    • 将切分好的文档块进行嵌入(Embedding),即将文档块转换成向量的形式。
    • 将Embedding后的向量数据,存储到向量数据库中。
  • 检索(Retrieval):数据存入向量数据库后。当我们需要进行数据检索时,会通过某种检索算法找到与输入问题相似的文档块。
  • 输出(Output):把问题以及检索出来的文档块一起提交给LLM,LLM会通过问题和检索出来的提示一起来生成更加合理的答案。
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)
    for chunk in rag_chain.stream(question):
        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,以确保两者状态一致。此方法也包括设置合理的缓存过期时间和使用布隆过滤器,以解决缓存穿透等问题。

通过聊天方式,提问关于项目的任何问题,最终得到答案。要求:最多只用三句话回答,要简明扼要。我们提供的文档越详细,生成的结果越符合预期

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 文档看下效果 + Document重要的一些参数:

from langchain_community.document_loaders import PyPDFLoader

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

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. 为什么做这个项目?
出于兴趣爱好开发)
大学期间,我和同学在外合租过一段时间,使用了一些租房平台,于是我有个想法,自己能不能开发一个租房平台,可以让我将理论知识与实践相结合。我希望通过实际项目来加深对Java编程语言和相关技术的理解。于是我便查找了一些资料,看了一些开源项目,进行了一些改进。
开源项目的解释)
这个

问:第一页元数据:
{'producer': 'pdfcpu v0.8.1 dev', 'creator': 'Chromium', 'creatondate': '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__() 初始化函数,所需参数:
    • file_path:表示要加载的Markdown文件的路径。
    • 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"
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': 'fcb08b2a85942455eecebb94677ffca4c'}

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': 'fcb08b2a85942455eecebb94677ffca4c', 'category': 'UncategorizedText', 'element_id': 'a6fc0b5a457d21234bf1c4a6ae0a18db'}

可以看见文档被分成了441个。根据什么规则拆分呢?答案是根据类型拆分

在元数据中,有一个表示类型的字段:category。这些类型都是现代文档解析库(如Unstructured.io)中用于分类Markdown(MD)文档或从其他格式(如PDF, Word, HTML)解析后转换为Markdown的常见元素类型。

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

print(set(doc.metadata["category"] for doc 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={
    'category': 'Title',
    'element_id': '3a0670f9bfd58576e430ef11def41593'
}

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

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

总结

🧠 LangChain 文档加载器

一、什么是 RAG(检索增强生成)?

  • 痛点:大模型(LLM)只学过过去的数据,不知道实时信息(比如今天的天气、公司内部文档)。
  • 搜索引擎能拿到实时信息,但返回结果很乱,要自己一条条看。
  • RAG = 大模型 + 你自己的知识库
    步骤:
    1. 把你的文档(PDF、Markdown等)提前处理好,存到“知识库”里。
    2. 当用户提问时,先去知识库中搜索相关的内容。
    3. 把搜索到的内容和用户的问题一起交给大模型,让它结合这些信息回答。

一句话:RAG 让大模型能“查资料”再回答,就像开卷考试。


二、RAG 的两大阶段

  1. 离线数据处理(提前准备):
    • 加载文档 → 切分成小块 → 转换成向量(数字形式) → 存入向量数据库。
  2. 在线检索(提问时):
    • 用户提问 → 去向量数据库找相关内容 → 把内容和问题发给大模型 → 得到答案。

三、第一步:加载文档 → Document 对象

任何文档都要先变成 LangChain 统一的 Document 对象。
一个 Document 包含:

  • page_content:文档的文本内容(字符串)
  • metadata:元数据,比如来源、作者、页码等(字典)

示例:

from langchain_core.documents import Document

doc = Document(
    page_content="狗是人类的好朋友",
    metadata={"source": "pet_article.pdf", "page": 1}
)

四、加载 PDF 文件 —— PyPDFLoader

  • 安装:不用额外装,LangChain自带。
  • 用法:
from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("我的文档.pdf")
pages = loader.load()   # 每一页变成一个 Document 对象
  • pages[0].page_content 是第一页的文本。
  • pages[0].metadata 包含页码、创建者等信息。

五、加载 Markdown 文件 —— UnstructuredMarkdownLoader

  • 需要先装包:pip install "unstructured[md]" nltk
  • 两种模式:
    • mode="single"(默认):整个 Markdown 文件变成一个 Document
    • mode="elements":按标题、段落、列表等拆分成多个 Document(适合精细处理)。

示例:

from langchain_community.document_loaders import UnstructuredMarkdownLoader

# 整个文件作为一个 Document
loader = UnstructuredMarkdownLoader("README.md", mode="single")
docs = loader.load()

# 按元素拆分
loader = UnstructuredMarkdownLoader("README.md", mode="elements")
elements = loader.load()
print(len(elements))  # 可能是几百甚至上千个元素

elements 模式的好处:每个元素(标题、段落、列表项)都有自己的类型(category),比如 "Title""NarrativeText""ListItem"

  • 可以通过 parent_idelement_id 还原文档的层级结构(比如哪个标题下有哪些段落)。

六、重点提醒

  • 加载 PDF 时,每页一个 Document
  • 加载 Markdown 时,mode="elements" 会把文档拆得很细,方便你只取有用的部分(比如只要标题或只要正文)。
  • 元数据(metadata) 很重要:你可以用它来过滤文档(比如只查某个作者写的章节)。

七、完整的 RAG 流程(非常简化)

  1. PyPDFLoaderUnstructuredMarkdownLoader 加载你的本地文件 → 得到 Document 列表。
  2. Document 切块(后续章节会讲) → 转成向量 → 存入向量数据库。
  3. 用户提问 → 从向量数据库检索相关内容 → 把内容和问题一起给大模型 → 大模型给出答案。

总结一句话:文档加载器就是 RAG 的“第一步”——把你自己的文件变成 LangChain 能读懂的 Document 对象,为后面的检索打基础。

Logo

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

更多推荐