暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

LangChain---RAG基础:数据连接——加载、切分与向量化

AI云枢 2025-06-03
73
【本期目标】
  • 理解 Document 对象的结构及其在LangChain中的重要性。
  • 掌握 LangChain 中各种 Document Loaders 的使用,从不同来源(如文本文件、网页、PDF)加载数据。
  • 学习 Text Splitters 的原理和最佳实践,尤其是 RecursiveCharacterTextSplitter,实现高效智能的文本切分。
  • 理解 Embedding Models 的作用,并学会如何将文本内容转换为数值向量。
  • 通过LCEL将数据加载、切分、向量化流程串联起来,为RAG应用奠定数据基础。

引言:RAG为何需要“数据连接”?
在前面的内容中,我们学会了如何构建基于大语言模型(LLM)的智能链条。然而,LLM本身的知识是有限且有“截止日期”的。当我们需要LLM回答关于最新信息、企业内部文档或特定领域知识的问题时,仅仅依靠LLM的“记忆”是远远不够的。
这就是 RAG(Retrieval-Augmented Generation,检索增强生成) 技术的用武之地。RAG 的核心思想是:在LLM生成答案之前,先从一个外部知识库中检索出与用户问题最相关的事实依据,然后将这些依据作为上下文提供给LLM,让LLM基于这些事实来生成更准确、更可靠的回答。
而“数据连接”正是RAG的第一步:如何把你的私有数据或特定领域数据,转化为LLM可以“理解”并“检索”的格式? 这就涉及到数据加载、切分和向量化。

核心概念:Document——LangChain的数据载体
在 LangChain 中,所有外部数据(无论是文本文件、网页、PDF还是数据库记录)在被处理和传递时,都会被封装成 Document 对象。
一个 Document 对象非常简单,主要包含两个核心字段:
  1. page_content (字符串): 这是文档的主要文本内容。
  2. metadata (字典): 这是一个可选的字典,用于存储关于文档的附加信息,如源文件路径、作者、创建日期、页码等。这些元数据在后续的检索和过滤中非常有用。

手动创建一个 Document 对象:
    from langchain_core.documents import Document


    my_document = Document(
        page_content="LangChain是一个用于开发大语言模型应用的框架。",
        metadata={"source""tutorial_intro""date""2025-06-02"}
    )


    print(my_document)
    print(my_document.page_content)
    print(my_document.metadata)

    一、Document Loaders——数据源的桥梁
    Document Loaders 的任务是:从各种数据源(文件、网页、数据库、SaaS应用等)读取数据,并将其转换为 LangChain Document 对象的列表。
    LangChain langchain-community 包提供了海量的 Document Loaders。你需要根据数据来源安装对应的额外依赖。

    常用 Document Loaders 示例:
    1. TextLoader:加载普通文本文件
      • 最简单直接的加载器。
      • 通常会把文件路径作为 metadata
      # 1. 准备一个文本文件 (先手动创建一个 example.txt)
      # 文件内容:
      # LangChain是一个开源框架。
      # 它的主要目标是帮助开发者构建大语言模型应用。
      # RAG是LangChain的重要应用之一。
      with open("example.txt""w", encoding="utf-8"as f:
          f.write("LangChain是一个开源框架。\n")
          f.write("它的主要目标是帮助开发者构建大语言模型应用。\n")
          f.write("RAG是LangChain的重要应用之一。")


      from langchain_community.document_loaders import TextLoader


      loader = TextLoader("example.txt", encoding="utf-8")
      documents = loader.load() # load() 返回一个 Document 列表


      print("\n--- TextLoader 示例 ---")
      print(f"加载的文档数量: {len(documents)}")
      print(f"第一个文档内容:\n{documents[0].page_content}")
      print(f"第一个文档元数据:\n{documents[0].metadata}")
      2. WebBaseLoader:加载网页内容
        • 需要安装 bs4 (BeautifulSoup) 库:pip install beautifulsoup4
        • 从指定的URL抓取网页内容。
        from langchain_community.document_loaders import WebBaseLoader


        loader = WebBaseLoader("https://www.langchain.com/langsmith"# LangChain官方博客页
        web_documents = loader.load()
        print("\n--- WebBaseLoader 示例 ---")
        print(f"加载的文档数量: {len(web_documents)}")


        if web_documents:
            print(f"第一个网页文档内容 (部分):\n{web_documents[0].page_content[:200]}...")
            print(f"第一个网页文档元数据:\n{web_documents[0].metadata}")
        3. PyPDFLoader:加载PDF文件
          • 需要安装 pypdf 库:pip install pypdf
          • 每个PDF页面通常会作为一个独立的 Document
          from langchain_community.document_loaders import PyPDFLoader


          try:
              pdf_loader = PyPDFLoader("sample.pdf"# 替换为你的PDF文件路径
              pdf_documents = pdf_loader.load()
              print("\n--- PyPDFLoader 示例 ---")
              print(f"加载的PDF文档数量 (按页分): {len(pdf_documents)}")
              if pdf_documents:
                  print(f"第一个PDF页面内容 (部分):\n{pdf_documents[0].page_content[:200]}...")
                  print(f"第一个PDF页面元数据:\n{pdf_documents[0].metadata}")
          except FileNotFoundError:
              print("\n--- PyPDFLoader 示例 (跳过): 请放置一个 'sample.pdf' 文件在当前目录 ---")
          except Exception as e:
              print(f"\n--- PyPDFLoader 示例 (错误): {e} ---")
          4. lazy_load() 方法 (推荐)
          • load() 会一次性加载所有内容到内存。对于非常大的文件或大量文件,这可能导致内存问题。
          • lazy_load() 返回一个生成器(generator)。它不会立即加载所有数据,而是在你需要时逐个生成 Document 对象。这对于处理大规模数据非常高效。
            from langchain_community.document_loaders import TextLoader


            # 假设 example.txt 很大
            loader = TextLoader("example.txt", encoding="utf-8")
            print("\n--- lazy_load() 示例 ---")


            for i, doc in enumerate(loader.lazy_load()):
                print(f"正在处理第 {i+1} 个文档 (内容部分: {doc.page_content[:50]}...)")
                if i >= 1# 仅处理前2个作为示例
                    break
            print("懒加载完成。\n")

            小结:Document Loaders 是将你原始数据转化为 LangChain Document 的第一步。选择合适的加载器,并考虑使用 lazy_load() 来优化性能。

            二、Text Splitters——智能切分,优化检索
            将整个文档直接提供给LLM有几个问题:
            1. 上下文窗口限制: LLM有最大输入Token限制,长文档会超出。
            2. 检索不精确: 整个大文档进行检索,可能返回大量无关内容,稀释了相关性。

            Text Splitters 的作用就是将一个大的 Document 或文本字符串切分成更小、更易于管理、且语义完整的“块”(chunks)。
            1. RecursiveCharacterTextSplitter (推荐)
              • 这是最常用且推荐的文本切分器。它的核心思想是:递归地尝试不同的分隔符来切分文本,直到满足 chunk_size 要求。
              • 它会优先尝试大粒度的分隔符(如 \n\n 段落),如果不行再尝试小粒度的(如 \n 换行符),最后是单个字符。这有助于尽可能保持语义完整性。
              • 参数:
                • chunk_size:每个块的最大字符数(不是Token数!)。
                • chunk_overlap:相邻块之间重叠的字符数。这有助于确保上下文不会在块的边界处被切断,提高检索的鲁棒性。
              from langchain.text_splitter import RecursiveCharacterTextSplitter


              long_text = """


              LangChain 是一个强大的框架,用于开发由大型语言模型驱动的应用程序。
              作为一名LangChain教程架构师,我负责设计一套全面、深入且易于理解的LangChain系列教程,
              旨在帮助读者从入门到精通,掌握LangChain的核心技术和应用。
              RAG(检索增强生成)是LangChain中的一个关键应用场景。
              通过RAG,我们可以将LLM与外部知识库相结合,从而让LLM能够回答其训练数据之外的问题。
              这大大扩展了LLM的应用范围,解决了幻觉和知识过时的问题。
              """


              text_splitter = RecursiveCharacterTextSplitter(
                  chunk_size=100# 每个块的最大字符数
                  chunk_overlap=20# 相邻块之间的重叠字符数
                  length_function=len# 使用 Python 的 len() 函数来计算长度
                  is_separator_regex=False# 分隔符不是正则表达式
              )


              # 可以直接切分字符串
              chunks_from_str = text_splitter.split_text(long_text)
              print("\n--- RecursiveCharacterTextSplitter 示例 (切分字符串) ---")


              for i, chunk in enumerate(chunks_from_str):
                  print(f"块 {i+1} (长度 {len(chunk)}):\n'{chunk}'\n")


              # 也可以切分 Document 对象列表 (这是更常见的用法)
              # 首先创建一个 Document
              doc_to_split = Document(page_content=long_text, metadata={"source""example_doc"})
              chunks_from_doc = text_splitter.split_documents([doc_to_split])
              print("\n--- RecursiveCharacterTextSplitter 示例 (切分 Document) ---")


              for i, chunk_doc in enumerate(chunks_from_doc):
                  print(f"块 {i+1} 内容 (长度 {len(chunk_doc.page_content)}):\n'{chunk_doc.page_content}'")
                  print(f"块 {i+1} 元数据:\n{chunk_doc.metadata}\n")
              注意: 当切分 Document 对象时,原始文档的 metadata 会自动复制到每个切分出的 Document 块中。这是非常重要的,因为你需要知道每个块的来源。

              2. 其他常用 Text Splitters (简要提及)
                • CharacterTextSplitter:最简单的,只根据一个或一组字符进行切分(例如,每 100 个字符或每个换行符)。
                • MarkdownTextSplitterHTMLHeaderTextSplitter: 结构感知型切分器。它们会识别Markdown或HTML的结构(如标题、代码块),尽量不破坏这些结构来切分,确保语义完整性。
                • TokenTextSplitter:基于Token数量而不是字符数量进行切分,更精确地控制LLM的上下文。

              小结: 文本切分是RAG中至关重要的一步。RecursiveCharacterTextSplitter 是你的首选,并根据需求调整 chunk_size 和 chunk_overlap

              三、Embedding Models——从文本到向量
              人类理解文本的意义,而计算机理解数字。Embedding Models 的任务就是将文本(单词、句子、段落或整个文档)转换成一个固定长度的数值列表,这个列表就叫做向量(Vector)或嵌入(Embedding)。
              这些向量的奇妙之处在于:语义相似的文本,它们对应的向量在多维空间中的距离也更近。这使得我们能够通过计算向量距离(如余弦相似度)来判断两段文本的相似性。

              常用 Embedding Models 示例:
              1. OpenAIEmbeddings (推荐)
                • 由OpenAI提供,通常是 text-embedding-3-large模型。性能强大且稳定。
                • 需要 OpenAI API Key。
                from langchain_openai import OpenAIEmbeddings
                embeddings_model = OpenAIEmbeddings(model="text-embedding-3-large")


                # 将单个文本转换为向量
                text1 = "苹果是一种水果"
                embedding1 = embeddings_model.embed_query(text1) # embed_query 用于单个文本
                print("\n--- OpenAIEmbeddings 示例 ---")
                print(f"'{text1}' 的向量长度: {len(embedding1)}")


                # print(f"向量:\n{embedding1[:10]}...") # 打印部分向量值
                text2 = "香蕉是一种水果"
                text3 = "苹果手机没国产的好用"
                embedding2 = embeddings_model.embed_query(text2)
                embedding3 = embeddings_model.embed_query(text3)


                # 简单计算相似度 (这里只是示意,实际会用向量数据库的相似度计算)
                from sklearn.metrics.pairwise import cosine_similarity
                import numpy as np


                # 将列表转换为 numpy 数组进行计算
                sim1_2 = cosine_similarity(np.array(embedding1).reshape(1, -1), np.array(embedding2).reshape(1, -1))[0][0]
                sim1_3 = cosine_similarity(np.array(embedding1).reshape(1, -1), np.array(embedding3).reshape(1, -1))[0][0]
                print(f"'{text1}' 和 '{text2}' 的相似度: {sim1_2:.4f} (语义相似)")
                print(f"'{text1}' 和 '{text3}' 的相似度: {sim1_3:.4f} (语义不相似,但词语重合)\n")
                # 你会发现 sim1_2 远高于 sim1_3,因为“苹果”和“香蕉”都是水果,语义上更近。
                2. HuggingFaceEmbeddings (本地/开源选项)
                  • 允许你使用 Hugging Face 上托管的各种开源Embedding模型,甚至可以在本地运行。
                  • 需要安装 sentence-transformers 库:pip install sentence-transformers
                  • 模型下载可能需要时间。

                重要方法:embed_documents() 和 embed_query()
                • embed_documents(texts: List[str]) -> List[List[float]]: 接受一个字符串列表(通常是切分后的文档块),返回每个字符串对应的向量列表。
                • embed_query(text: str) -> List[float]: 接受单个字符串(通常是用户查询),返回其向量。在RAG中,我们会把用户查询也向量化,然后去向量数据库中找相似的文档块。

                小结: Embedding 模型是 RAG 的核心。选择一个适合你语言和应用场景的模型,将文本转化为高维向量,为下一步的相似度检索做好准备。

                四、LCEL整合:数据准备流水线
                现在,我们将数据加载、切分和向量化这三个步骤用LCEL串联起来,形成一个完整的RAG数据准备流水线。
                  from dotenv import load_dotenv
                  import os
                  from langchain_community.document_loaders import TextLoader
                  from langchain.text_splitter import RecursiveCharacterTextSplitter
                  from langchain_openai import OpenAIEmbeddings
                  from langchain_core.runnables import RunnableLambda
                  load_dotenv()


                  # --- 1. 定义数据加载器 ---
                  loader = TextLoader("example.txt", encoding="utf-8"# 使用我们之前创建的 example.txt


                  # --- 2. 定义文本切分器 ---
                  text_splitter = RecursiveCharacterTextSplitter(
                      chunk_size=100,
                      chunk_overlap=20,
                      length_function=len
                  )


                  # --- 3. 定义Embedding模型 ---
                  embeddings_model = OpenAIEmbeddings(model="text-embedding-3-large")


                  # --- 4. 构建LCEL数据准备流水线 ---
                  # 步骤 A: 加载文档 (loader.load() 返回 List[Document])
                  # 步骤 B: 切分文档 (text_splitter.split_documents() 接受 List[Document],返回 List[Document])
                  # 步骤 C: 提取每个 Document 的 page_content,形成 List[str]
                  # 步骤 D: 将 List[str] 转换为 List[List[float]] (Embedding 向量)
                  data_preparation_pipeline = (
                      RunnableLambda(lambda x: loader.load()) # A: 加载文档
                      | RunnableLambda(lambda docs: text_splitter.split_documents(docs)) # B: 切分文档
                      | RunnableLambda(lambda chunks: [chunk.page_content for chunk in chunks]) # C: 提取文本内容
                      | RunnableLambda(lambda texts: embeddings_model.embed_documents(texts)) # D: 生成向量
                  )
                  print("\n--- LCEL 数据准备流水线示例 ---")


                  # 运行流水线
                  # 注意:这里 invoke() 的输入可以为空字典 {},因为 loader.load() 不依赖外部输入
                  all_embeddings = data_preparation_pipeline.invoke({})
                  print(f"生成的块数量: {len(all_embeddings)}")
                  if all_embeddings:
                      print(f"第一个块的向量长度: {len(all_embeddings[0])}")
                      # print(f"第一个块的向量 (部分):\n{all_embeddings[0][:10]}...")
                  代码解析:
                  • 我们用 RunnableLambda 将普通的函数调用封装成 Runnable,使其能够通过 | 连接。
                  • 流水线清晰地展示了数据如何从文件加载,经过切分,最终被向量化。
                  • invoke({}) 用于启动流水线,因为 loader.load() 内部不依赖用户输入。

                  小结: 至此,我们已经成功地将原始文本数据转化为了一系列带有元数据的、语义上更紧凑的、数值化的向量块。这些向量块就是我们构建 RAG 知识库的基础!

                  本期小结
                  • 数据加载: 掌握了 Document 对象和各种 Document Loaders 的使用,尤其强调了 lazy_load() 的高效性。
                  • 文本切分: 理解了 Text Splitters 的重要性,并学会了使用 RecursiveCharacterTextSplitter 进行智能切分。
                  • 向量化: 了解了 Embedding Models 的原理,并实践了如何将文本转换为数值向量。

                  现在,你手中的数据不再是散乱的文本,而是一系列高维向量,它们是理解语义的数字指纹。在下一期教程中,我们将学习如何存储这些向量(向量数据库),以及如何利用它们进行高效的检索与召回,为RAG的核心功能打上坚实的基础!敬请期待!


                  往期链接:
                  Langchain 入门:用结构化思维构建 LLM 应用!
                  LangChain---LCEL深度解析:构建模块化与可组合的智能链条

                  文章转载自AI云枢,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论