LangChain快速筑基(带代码)P4-文档加载Document Loaders

93 阅读6分钟

怎么让大模型基于自己提供的数据做出回答呢? LangChain提供了文档加载器Document Loaders用于从各种来源(PDF文件、TXT文件、网页、数据库等)加载数据到LangChain中。 数据加载之后,LangChain就可以使用其他组件(如文本分割器 Text Splitter、向量存储 Vector Store、检索起 Retriever)来实现基于数据的问答啦。

概念补充——RAG RAG(Retrieval-Augmented Generation,检索增强生成),是一种结合外部知识检索和LLM生成,提升LLM在回答问题时的准确性和针对性的技术。 核心思想是检索+生成。

  • 检索(Retrieval) 从外部知识库(如文档、数据库)中检索与用户问题相关的内容。
  • 生成(Generation) 讲检索到的内容作为上下文,与用户问题一起传给LLM,让其生成更准确、基于事实的的回答。

文档加载是RAG流程的起点。

image.png

文档加载器 Document Loaders

Document Loaders可以讲数据加载成标准的LangChain Document对象格式。 每个 Document 对象通常包含:

  • page_content: 一个包含来自数据源的实际文本内容的字符串。
  • metadata: 一个包含关于内容来源信息的字典(例如,文件名、页码、URL)。这些元数据在后续引用来源或进行过滤时非常有用。

另外,不同加载器还需要不同的库以支持加载,比如PDF还需要pypdf,网页加载则需要beautifulsoup4

import os

# LLM 和 LangChain 基础 (保留上下文,尽管在加载器示例中不直接使用)
from langchain_deepseek import ChatDeepSeek
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains import LLMChain  # 如果我们想使用加载的文档,可以用于一个简单的链
from langchain.memory import ConversationBufferMemory

# --- 文档加载器 ---
from langchain_community.document_loaders import TextLoader  # 用于 .txt 文件
from langchain_community.document_loaders import PyPDFLoader  # 用于 .pdf 文件
from langchain_community.document_loaders import WebBaseLoader  # 用于加载网页


# 环境key
os.environ["DEEPSEEK_API_KEY"] = 'sk-xxx'


def get_deepseek_key():
    key = os.getenv('DEEPSEEK_API_KEY')
    if key is None:
        raise ValueError(
            "DEEPSEEK_API_KEY not found in environment variables.")
    return key

# --- LLM 初始化 ---


def create_deepseek_llm():
    api_key = get_deepseek_key()
    if not api_key:
        raise ValueError("没有ds key")
    return ChatDeepSeek(
        model="deepseek-chat",
        temperature=0.1,  # 温度,更具备确定性
        max_tokens=1024,
        timeout=None,
        max_retries=2,
        api_key=api_key
    )


# --- 示例 1: TextLoader ---
def load_from_text_file():
    print("\n--- 示例 1: 从 .txt 文件加载 ---")
    # 为此示例创建一个虚拟文本文件
    file_path = "my_sample_document.txt"
    with open(file_path, "w", encoding="utf-8") as f:
        f.write("这是我的示例文档的第一行。\n")
        f.write("LangChain的文档加载器非常有用。\n")
        f.write("它可以帮助我们从不同来源加载数据。\n")
        f.write("比如文本文件、PDF,甚至是网页。\n")

    # 使用文件路径初始化 TextLoader
    loader = TextLoader(file_path, encoding="utf-8")  # 如果不是默认编码,请指定

    # 加载文档
    # .load() 方法返回一个 Document 对象列表。
    # 对于 TextLoader,它通常为整个文件返回单个 Document 对象。
    documents = loader.load()

    print(f"加载了 {len(documents)} 个文档。")
    for i, doc in enumerate(documents):
        print(f"\n文档 {i+1}:")
        print(f"  内容 (前 100 个字符): {doc.page_content[:100]}...")
        print(f"  元数据: {doc.metadata}")  # 元数据将包含源文件路径

    # 清理虚拟文件
    os.remove(file_path)
    return documents  # 返回以便将来使用

# --- 示例 2: PyPDFLoader ---


def load_from_pdf_file():
    print("\n--- 示例 2: 从 .pdf 文件加载 ---")
    # 对于此示例,你需要一个 PDF 文件。
    # 假设你在同一目录中有一个名为 "sample.pdf" 的 PDF 文件。
    # 如果你没有,可以在线下载一个示例 PDF。
    # 例如,搜索 "dummy pdf" 或 "sample pdf"。
    pdf_file_path = "sample.pdf"  # 替换为你的 PDF 文件路径

    if not os.path.exists(pdf_file_path):
        print(f"PDF 文件 '{pdf_file_path}' 未找到。跳过 PyPDFLoader 示例。")
        print("请创建或下载一个 'sample.pdf' 文件并将其放置在脚本的目录中。")
        return []

    # 初始化 PyPDFLoader
    loader = PyPDFLoader(pdf_file_path)

    # 加载文档
    # PyPDFLoader 通常为 PDF 中的每一页创建一个 Document 对象。
    # 对于大型 PDF,.load() 可能会比较慢。
    # 也可以使用 .load_and_split(),它会先加载然后按页分割。
    pages = loader.load()  # 将每一页加载为一个单独的 Document

    print(f"从 PDF 加载了 {len(pages)} 页。")
    if pages:
        print(f"\n第 1 页内容 (前 150 个字符): {pages[0].page_content[:150]}...")
        print(f"第 1 页元数据: {pages[0].metadata}")  # 将包括来源和页码

        if len(pages) > 1:
            print(f"\n第 2 页内容 (前 150 个字符): {pages[1].page_content[:150]}...")
            print(f"第 2 页元数据: {pages[1].metadata}")
    return pages

# --- 示例 3: WebBaseLoader ---
# 需要: pip install beautifulsoup4


def load_from_webpage():
    print("\n--- 示例 3: 从网页加载 ---")
    # target_url = "https://blog.langchain.dev/langchain-expression-language/" # 一篇 LangChain 博客文章
    # 一篇关于 LLM Agent 的知名博客文章
    target_url = "https://lilianweng.github.io/posts/2023-06-23-agent/"

    # 初始化 WebBaseLoader
    # 它使用 urllib 获取页面并使用 BeautifulSoup 解析 HTML。
    loader = WebBaseLoader(target_url)

    # 你也可以传递多个 URL:
    # loader = WebBaseLoader(["url1", "url2"])

    print(f"正在从 {target_url} 加载内容...")
    try:
        documents = loader.load()  # 返回一个列表 (通常每个 URL 一个 Document)
        print(f"从网页加载了 {len(documents)} 个文档。")
        if documents:
            print(
                f"\n文档 1 内容 (前 200 个字符): {documents[0].page_content[:200]}...")
            print(f"文档 1 元数据: {documents[0].metadata}")  # 包括来源 URL 和标题
        return documents
    except Exception as e:
        print(f"加载网页时出错: {e}")
        print("这可能是由于网络问题、网站结构或缺少依赖项(如 'html2text')造成的。")
        print("如果你看到 'No module named html2text',请运行: pip install html2text")
        return []


# --- 如何使用加载的文档 (非常基础的示例) ---
def use_loaded_document_in_chain(documents):
    if not documents:
        print("\n未加载任何文档,跳过链示例。")
        return

    # 为简单起见,我们只取第一个文档的内容
    # 在一个真正的 RAG 系统中,你会做更多的事情 (分割、嵌入、检索)
    context_text = documents[0].page_content

    llm = create_deepseek_llm()
    prompt = ChatPromptTemplate.from_messages([
        ("system", "你是一个仅根据提供的上下文回答问题的助手。如果答案不在上下文中,请说“根据提供的文本我不知道”。"),
        ("human", "上下文:\n{context}\n\n问题: {question}")
    ])

    chain = LLMChain(llm=llm, prompt=prompt)

    # 示例问题 - 这将高度依赖于你文档的内容
    # 对于 "my_sample_document.txt":
    # question = "文档加载器有什么用?"
    # 对于 PDF 或网页,你会问一个与其内容相关的问题。
    # 对于 Lilian Weng 博客文章示例:
    question = "一个由LLM驱动的自主代理系统的关键组成部分是什么?"

    print(f"\n--- 在简单链中使用加载的文档 ---")
    print(f"上下文 (前 100 个字符): {context_text[:100]}...")
    print(f"问题: {question}")

    # 流式传输响应
    response_content = ""
    print("\nAI 回答:")
    # chain.stream 会产生包含 'text' 键的字典(对于 LLMChain)
    for chunk in chain.stream({"context": context_text, "question": question}):
        if 'text' in chunk:
            print(chunk['text'], end="", flush=True)
            response_content += chunk['text']

    if not response_content:  # 如果流式传输没有产生预期的结构,则回退
        result = chain.invoke({"context": context_text, "question": question})
        print(result['text'])
    print("\n--- 链示例结束 ---")


if __name__ == '__main__':
    # 确保 DEEPSEEK_API_KEY 环境变量已设置或在代码中直接提供
    if "DEEPSEEK_API_KEY" not in os.environ:
        print("警告:DEEPSEEK_API_KEY 环境变量未设置。LLM 调用可能会失败。")
        # 你可以在这里设置一个默认的 key,或者提示用户输入
        # os.environ["DEEPSEEK_API_KEY"] = "sk-your-actual-key"

    # 测试 TextLoader
    txt_docs = load_from_text_file()
    # 如果你加载了一个包含相关信息的文本文件,你可以将 txt_docs 传递给 use_loaded_document_in_chain

    # 测试 PyPDFLoader
    # 确保有一个 sample.pdf 文件或更改路径
    pdf_pages = load_from_pdf_file()
    # if pdf_pages:
    #     use_loaded_document_in_chain(pdf_pages) # 示例:问一个关于你的 PDF 的问题

    # 测试 WebBaseLoader
    web_docs = load_from_webpage()
    if web_docs:
        use_loaded_document_in_chain(web_docs)  # 示例:问一个关于网页内容的问题