写在最前面:关于本文所使用的数据集以及代码可以参考这个链接获取

https://github.com/visionlmlm/LangChain_test_agent_rag

经过前面的I/O、Chain、Memory、Tool、Agent等的了解,现在该进入最后一个环节,也就是Retrieval的学习。他常常被用于构建一个“企业/私人的知识库”,提升大模型的整体能力。

1、Retrieval模块的设计意义

1.1 大模型的两大难题之一

在一些专业领域中,LLM无法获取所有的专业知识的细节,因此在用户的使用中就会无法给出准确的答案,甚至会杜撰一些虚假的信息,这种现象就是LLM的“幻觉”问题,这也是大模型存在的两个摆脱不掉的难题,当前还没有百分之百解决的方案

但是还是有方法能够缓解这种困境的,那就是RAG:

首先,为大模型提供一定的上下文信息,让其输出变得更加稳定;其次,利用RAG技术将检索出来的文档和提示词再输送给大模型,生成更加可靠的答案。

1.2 RAG的解决方案

当应用需求集中在利用大模型去回答特定私有领域的知识,并且知识库足够大,那么除了微调大模型之外,RAG就是非常有效的缓解大模型推理“幻觉”问题的解决方案。

当前已经出现了非常多的产品使用到了RAG,包括客服系统、基于大模型的数据分析。

1.3 RAG的优缺点

优点:

  • 相较提示词工程,RAG有更丰富的上下文和数据样本,可以不需要用户提供过多的背景描述,就能够生成比较符合用户预期的答案;
  • 相比于模型微调,RAG可以提升问答内容的时效性(一定程度上缓解“知识冻结”问题)以及可靠性;
  • 在一定程度上保护了业务数据的隐私性。

缺点:

  • 由于每次问答都会涉及外部系统数据检索,因此RAG的响应时延相对较高
  • 引用的外部知识数据会消耗大量的模型Token资源。

2、文档加载器 Document Loaders

2.0 总述

(1)不同的文档使用不同的文档加载器

txt文档:TextLoader

pdf文档:PyPDFLoader

csv文档:CSVLoader

json文档:JSONLoader

html文档:UnstructuredHTMLLoader

md文档:UnstructuredMarkdownLoader

文件目录:DirectoryLoader

(2)当创建好XXLoader的实例之后,都要调用load(),此时会在内存中返回一个list[Document]

2.1 加载Txt文档

from langchain_community.document_loaders import TextLoader, PythonLoader

# 指明txt文档路径
file_path = "./asset/load/01-langchain-utf-8.txt"
# 创建一个TextLoader的实例
text_loader = TextLoader(
    file_path = file_path,
    encoding = "utf-8" # 解码集要与存储时的编码集相同
)
# 调用load(),返回一个list[Document]
docs = text_loader.load()

在Document对象中有两个重要属性:

①page_content:真实的文档内容;②metadata:文档内容的元数据

print(docs[0].metadata) # 显示Document对象的元数据
print(docs[0].page_content) # 显示文档中的内容信息

2.2 加载Pdf文档

from langchain_community.document_loaders import PyPDFLoader

pdf_loader = PyPDFLoader(
    file_path = "./asset/load/02-load.pdf"

)
docs = pdf_loader.load()
print(docs)

如果将file_path换成在线网址就能够加载网络文件:

from langchain_community.document_loaders import PyPDFLoader

pdf_loader = PyPDFLoader(
    file_path = "https://arxiv.org/pdf/2302.03803"
)
docs = pdf_loader.load()
print(docs)
print(len(docs))

2.3 加载CSV文档

from langchain_community.document_loaders import CSVLoader

csv_loader = CSVLoader(
    file_path = "./asset/load/03-load.csv"
)
docs = csv_loader.load()

print(docs)
print(len(docs))

使用source_column参数指定文件加载的列,并且保存在source变量中:

from langchain_community.document_loaders import CSVLoader

csv_loader = CSVLoader(
    file_path = "./asset/load/03-load.csv",
    source_column="author"
)
docs = csv_loader.load()

for doc in docs:
    print(doc)

2.4 加载JSON文档

示例1:加载所有数据

from langchain_community.document_loaders import JSONLoader

json_loader = JSONLoader(
    file_path = "./asset/load/04-load.json",
    jq_schema = "." ,#表示加载所有的字段
    text_content = False # 将加载的json对象转换为json字符串
)
docs = json_loader.load()
print(docs)

示例2:加载content字段内容

from langchain_community.document_loaders import JSONLoader

json_loader = JSONLoader(
    file_path = "./asset/load/04-load.json",
    jq_schema = ".messages[].content" ,
    # text_content = False # content字段本身就是字符串格式,无需转换
)
docs = json_loader.load()
for doc in docs:
    print(doc.page_content)

示例3:提取04-response.json文件中嵌套在 data.items[].content 的文本

from langchain_community.document_loaders import JSONLoader
# 方式1
# json_loader = JSONLoader(
#     file_path = "./asset/load/04-response.json",
#     jq_schema = ".data.items[].content" ,
# )
# 方式2
json_loader = JSONLoader(
    file_path = "./asset/load/04-response.json",
    jq_schema = ".data.items[]" ,
    content_key=".content",
    is_content_key_jq_parsable=True #使用jq解析content_key
)
docs = json_loader.load()
for doc in docs:
    print(doc.page_content)

示例4:提取04-response.json文件中嵌套在 data.items[] 里的 title、content 和 其文本

# 1.导入相关依赖
from langchain_community.document_loaders import JSONLoader
from pprint import pprint
# 2.定义json文件的路径
file_path = 'asset/load/04-response.json'
# 3.定义JSONLoader对象
loader = JSONLoader(
    file_path=file_path,
    # jq_schema=".data.items[] | {id, author, text: (.title + '\n' + .content)}",
    jq_schema=".data.items[]",
    content_key='.title + "\\n\\n" + .content',
    is_content_key_jq_parsable=True  # 用jq解析content_key
)
# 4.加载
data = loader.load()
for doc in data:
    print(doc.page_content)

2.5 加载HTML文档

from langchain_community.document_loaders import UnstructuredHTMLLoader

html_loader = UnstructuredHTMLLoader(
    file_path = "./asset/load/05-load.html",
    mode = "elements",
    strategy="fast" # 加载策略
)

docs = html_loader.load()
print(len(docs))

for doc in docs:
    print(doc)

2.6 加载Markdown文档

from langchain_community.document_loaders import UnstructuredMarkdownLoader

markdown_loader = UnstructuredMarkdownLoader(
    file_path = "./asset/load/06-load.md",
    strategy="fast"
)
docs = markdown_loader.load()
print(len(docs))
for doc in docs:
    print(doc)

2.7 加载File Directory文档

批量加载一个文件夹内的所有文件:

from langchain_community.document_loaders import DirectoryLoader,PythonLoader

fd_load = DirectoryLoader(
    path="./asset/load",
    glob = "*.py",
    use_multithreading=True, # 是否使用多线程
    show_progress=True,
    loader_cls=PythonLoader
)
docs = fd_load.load()
print(len(docs))
for doc in docs:
    pprint(doc)

3、文档拆分器 Text Splitters

3.0 为什么要拆分文档

对于传入的文档,如果作为一个整体使用,会存在以下问题:

  1. 如果用户的问题Query的答案仅出现在某一个Document对象中,那么将检索到的所有Document对象直接放入prompt中并不是最优的选择,因为这一定会包含很多无关的信息,无关的信息越多,对大模型后续的推理影响越大;
  2. 任何一款大模型都存在最大输入的Token限制,如果一个Document很大,例如是几百兆的PDF文件,大模型肯定无法容纳如此多的信息。

基于上述信息,我们必须对完整的Document对象进行分块处理。无论是在存储还是检索的时候都将以(chunks)为基本单位,这样就会有效的避免内容不相关问题和超出最大输入限制问题。

3.1 Chunking拆分的策略

  1. 根据句子切分:按照自然句子边界进行切分,以保持语义的完整性;
  2. 按照固定字符数切分:根据特定的字符数量来划分文本,但可能会在不适合的位置切断句子;
  3. 固定字符数来切分,结合重叠窗口:在方法2的基础上通过重叠窗口技术避免切分关键内容,保证信息的连贯性;
  4. 递归字符切分方法:通过递归字符方法动态确定切分点,可以根据文档的复杂性与内容的密度来调整块的大小;
  5. 根据语义内容切分:属于(高级策略)依据文本的语义内容来划分块,旨在保持相关信息的集中和完整,适合于需要高度语义保持的应用场景。

以上的方法没有说是有绝对好的一种,各有优势和局限。

3.2 TextSplitter使用

内部定义的一些常用方法:

情况一:按照字符串进行切分

  • split_text():传入参数是字符串类型,返回值的类型是List[str];
  • create_documents():传入参数类型是List[str],返回值类型是List[Document];

情况二:按照Document对象进行切分

  • split_documents():传入参数类型是List[Document],返回值类型是List[Document]。

3.3 具体实现

3.3.1 具体的拆分器之 CharacterTextSplitter:Split by character

示例1:体会参数chunk_size以及chunk_overlap

# 1.导入相关依赖
from langchain_text_splitters import CharacterTextSplitter
# 2.示例文本
text = """
LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者能够更容易地构建复杂的应用程序。
"""
# 3.定义字符分割器
splitter = CharacterTextSplitter(
    chunk_size=50, # 每块大小
    chunk_overlap=5,# 块与块之间的重复字符数
    #length_function=len,
    separator="" # 设置为空字符串时,表示禁用分隔符优先
)
# 4.分割文本
texts = splitter.split_text(text)
# 5.打印结果
for i, chunk in enumerate(texts):
    print(f"块 {i+1}:长度:{len(chunk)}")
    print(chunk)
    print("-"* 50)

示例2:体会参数separator

# 1.导入相关依赖
from langchain_text_splitters import CharacterTextSplitter
# 2.定义要分割的文本
text = "这是一个示例文本啊。我们将使用CharacterTextSplitter将其分割成小块。分割基于字符数。"
# text = """
# LangChain 是一个用于开发由语言模型。驱动的应用程序的框架的。它提供了一套工具和抽象。使开发者能够更容易地构建复杂的应用程序。
# """
# 3.定义分割器实例
text_splitter = CharacterTextSplitter(
    chunk_size=30,   # 每个块的最大字符数
    chunk_overlap=5, # 块之间的重叠字符数
    separator="。",  # 按句号分割 分隔符优先
)
# 4.开始分割
chunks = text_splitter.split_text(text)
# 5.打印效果
for  i,chunk in enumerate(chunks):
    print(f"块 {i + 1}:长度:{len(chunk)}")
    print(chunk)
    print("-"*50)

3.3.2 具体的拆分器之RecursiveCharacterTextSplitter:最常用

这种方式在遇到特定字符时进行分割,默认情况下尝试切割的字符包括“\n\n”,“\n”,“ ”,“”

根据第一个字符进行切块,但如果任何切块太大,则会继续移动到下一个字符继续切块。

示例1:使用split_text()方法

# 1.导入相关依赖
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 2.定义RecursiveCharacterTextSplitter分割器对象
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True,
)
# 3.定义拆分的内容
text="LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
# 4.拆分器分割
paragraphs = text_splitter.split_text(text)
for para in paragraphs:
    print(para)
    print('-------')

示例2:使用create_documents()方法演示,传入字符串列表,返回Document对象列表

# 1.导入相关依赖
# 2.定义RecursiveCharacterTextSplitter分割器对象
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True,
)
# 3.定义分割的内容
# text="LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
list=["LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"]
# 4.分割器分割
# create_documents():形参是字符串列表,返回值是Document的列表
paragraphs = text_splitter.create_documents(list)
for para in paragraphs:
    print(para)
    print('-------')

示例3:使用create_documents()方法演示,将本地文件内容加载成字符串,进行拆分

# 1.导入相关依赖
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 2.打开.txt文件
with open(
        "asset/load/08-ai.txt",
        encoding="utf-8"
) as f:state_of_the_union = f.read()  #返回的是字符串
# 3.定义RecursiveCharacterTextSplitter(递归字符分割器)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,#chunk_overlap=0,
    length_function=len
)
# 4.分割文本
texts = text_splitter.create_documents([state_of_the_union])
# 5.打印分割文本
for text in texts:
    print(f"🔥{text.page_content}")

示例4:使用split_documents()方法演示,利用PDFLoader加载文档,对文档的内容用递归切割器切割

# 1.导入相关依赖
from langchain_community.document_loaders import PyPDFLoader
# 2.定义PyPDFLoader加载器
loader = PyPDFLoader("./asset/load/02-load.pdf")
# 3.加载和切割文档对象
docs = loader.load()
# 返回Document对象构成的list
# print(f"第0页:\n{docs[0]}")
# 4.定义切割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,#chunk_size=120,
    chunk_overlap=0,# chunk_overlap=100,
    length_function=len,
    add_start_index=True,
)
# 5.对pdf内容进行切割得到文档对象
paragraphs = text_splitter.split_documents(docs)
#paragraphs = text_splitter.create_documents([text])
for para in paragraphs:
    print(para.page_content)
    print('-------')

3.3.3 具体的拆分器之 TokenTextSplitter/CharacterTextSplitter:Split by tokens

示例1:使用TokenTextSplitter

# 1.导入相关依赖
from langchain_text_splitters import TokenTextSplitter
# 2.初始化 TokenTextSplitter
text_splitter = TokenTextSplitter(
    chunk_size=33,#最大 token 数为 32
    chunk_overlap=0,#重叠 token 数为 0
    encoding_name="cl100k_base",# 使用 OpenAI 的编码器,将文本转换为 token 序列
)
# 3.定义文本
text ="人工智能是一个强大的开发框架。它支持多种语言模型和工具链。人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50年代诞生以来,人工智能经历了多次起伏。"
# 4.开始切割
texts = text_splitter.split_text(text)

# 打印分割结果
print(f"原始文本被分割成了 {len(texts)} 个块:")
for i, chunk in enumerate(texts):
    print(f"块 {i+1}: 长度:{len(chunk)} 内容:{chunk}")
    print("-" * 50)

示例2:使用CharacterTextSplitter

# 1.导入相关依赖
from langchain_text_splitters import CharacterTextSplitter
import tiktoken
# 用于计算Token数量
# 2.定义通过Token切割器
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", # 使用 OpenAI 的编码器
    chunk_size=18,
    chunk_overlap=0,
    separator="。",  # 指定中文句号为分隔符
    keep_separator=False,  # chunk中是否保留分隔符
)
# 3.定义文本
text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。今天天气很好,想出去踏青。但是又比较懒不想出去,怎么办"
# 4.开始切割
texts = text_splitter.split_text(text)
print(f"分割后的块数: {len(texts)}")
# 5.初始化tiktoken编码器(用于Token计数)
encoder = tiktoken.get_encoding("cl100k_base")  # 确保与CharacterTextSplitter的encoding_name一致
# 6.打印每个块的Token数和内容
for i, chunk in enumerate(texts):
    tokens = encoder.encode(chunk)  # 现在encoder已定义
    print(f"块 {i + 1}: {len(tokens)} Token\n内容: {chunk}\n")

3.3.4 具体拆分器之SemanticChunker:语义分块

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
import os
import dotenv
dotenv.load_dotenv()
# 加载文本
with open("asset/load/09-ai1.txt", encoding="utf-8") as f:
    state_of_the_union = f.read()#返回字符串
# 获取嵌入模型
os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
embed_model = OpenAIEmbeddings(
    model="text-embedding-3-large" )
# 获取切割器
text_splitter = SemanticChunker(
    embeddings=embed_model,
    breakpoint_threshold_type="percentile",#断点阈值类型:字面值["百分位数", "标准差", "四分位距", "梯度"] 选其一
    breakpoint_threshold_amount=65.0 #断点阈值数量 (极低阈值 → 高分割敏感度)
)
# 切分文档
docs = text_splitter.create_documents(texts = [state_of_the_union])
print(len(docs))
for doc in docs:
    print(f"🔍 文档 {doc}:")

剩余的部分(嵌入模型、向量数据库、检索器)的内容在下次的博客进行说明。

Logo

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

更多推荐