基于LangChain构建一个简单的RAG应用

前言

本项目旨在构建一个简单的RAG应用,不考虑性能因素,仅作为学习记录。跑通整个流程为主,以初步了解RAG的应用构建过程。

基于OpenAI 构建RAG

前置

导入依赖

import os
from langchain_community.document_loaders import PyPDFLoader
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.schema import (SystemMessage, HumanMessage, AIMessage)

配置Open AI API KEY,配置此API KEY需要花钱购买额度才能使用,在下一章节将会讲述一种其他的方法,以完成整个流程。

os.environ["OPENAI_API_KEY"] = "your key"
chat = ChatOpenAI(
    openai_api_key=os.environ["OPENAI_API_KEY"],
    model='gpt-3.5-turbo'
)

文本加载和拆块

通过PyPDFLoader可以加载本地的文件,其中extract_images表示提取文件中的图片。

# 默认按页分段
loader = PyPDFLoader(
    file_path='../datasets/PatchTST.pdf',
    extract_images=True
)

默认按页划分文件,例如上述文件为24页,则len(pages)为24

pages = loader.load()

load_and_split() 方法和loader.load() 存在差异,load_and_split() 在加载 PDF 内容的基础上,进一步按逻辑分割内容,通过识别段落、行或空白区域来完成的。它将每一页的内容拆分成多个 Document 对象,这些对象可能代表每个段落、每行或每个逻辑区块。因此len(pages)不一定为24

pages = loader.load_and_split()

定义文本分块策略,本文使用递归方式划分文本块。默认通过四个分隔符进行切割,其中,["\n\n", "\n", " ", ""]分别代表["段与段之间的间隔","句与句之间的间隔","单词与单词之间的间隔","无间隔"]chunk_size表示每个块的大小、chunk_overlap表示块与块之间重叠个数。具体可参考Fig .1
RecursiveCharacterTextSplitter的实现原理简述如下:

  1. 依次使用分隔符列表 ["\n\n", "\n", " ", ""] 进行文本拆分。文本会按照这些分隔符的优先级依次拆分,首先尝试使用 “\n\n”(两个换行符)进行拆分。如果未能有效拆分,则尝试使用 “\n”(单个换行符),然后是 " "(空格),最后使用 “”(空字符串,按字符拆分)。
  2. 在每次拆分时,如果分割出的文本块的长度小于 chunk_size,则将这些文本块合并为一个块。如果分割出的文本块的长度大于 chunk_size,则会递归地使用下一个分隔符进一步拆分这些文本块,直到每个块的长度小于等于chunk_size
  3. 当某个文本块的长度大于 chunk_size 时,不是直接合并,而是会调用 split_text函数递归地使用剩余的分隔符继续对文本块进行拆分,直到块的大小满足要求。
  4. 如果拆分出的块较小(即小于 chunk_size),它们会被合并为一个大的文本块。否则,递归继续拆分直到满足要求。
  5. 最终,算法返回若干个文本块,每个块的字符数 小于或等于 chunk_size
text_splitter = RecursiveCharacterTextSplitter(
	 separators=["\n\n", "\n", " ", ""],
    chunk_size=500,
    chunk_overlap=50
)

最后,将文档均匀地分割为多个块。len(docs)为209

docs = text_splitter.split_documents(pages)

Fig .1
Fig .1 文本分割和块重叠示意图

向量化和向量数据库

构建OpenAIEmbeddings对象,将分割的文本向量化,然后存储至向量数据库Chroma中。向量化是将文本转为一组高维的数值坐标,在高维空间中,每个点(嵌入)的位置反映了其对应文本的含义。通过将文本简化为数值表示,我们可以使用简单的数学运算来快速测量两段文本的相似程度,而无论它们的原始长度或结构如何。一些常见的相似性指标包括:

  • 余弦相似度:测量两个向量之间夹角的余弦值。
  • 欧氏距离:测量两点之间的直线距离。
  • 点积:测量一个向量在另一个向量上的投影。
embed_model = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents=docs, embedding=embed_model, collection_name='OpenAI')

其中OpenAI提供embed_queryembed_documents两个方法,分别代表对单个文本和多个文本进行向量化,下面给出一段向量化的数值坐标。

[-0.019260549917817116, 0.0037612367887049913, -0.03291035071015358, 0.003757466096431017, 0.0082049
[-0.010181212797760963, 0.023419594392180443, -0.04215526953339577, -0.001532090245746076, -0.023573

在这里插入图片描述
Fig .2 向量化示意图

Prompt

根据相似度,检索与问题最相近的top-k个答案,然后将其嵌入一个简单的提示词中。此处给出一个简单的提示词模版,可以根据自己的需求对其进行改变。Prompt非常重要,会直接影响模型的准确率!

def augment_prompt(quire):
    # 获取相似度最高的3个回答
    results = vectorstore.similarity_search(quire=quire, k=3)
    # 拼接三个回答
    source_knowledge = '\n'.join([x.page_content for x in results])
    # construct prompt
    augmented_source_knowledge = f"""
    Using the contexts below, answer the quire.

    contexts:{source_knowledge}

    quire:{quire}
    """
    return augmented_source_knowledge

Texr Generation LLM

最后,通过ChatOpenAI模型回答返回的Prompt。

messages = [
    SystemMessage('You are a helpful assistant.')
]

# 通过向量相似度检索和问题最相关的k个文档
quire = 'What types of time series forecasting are divided into according to the forecast range?'
result = augment_prompt(quire)

prompt = HumanMessage(content=augment_prompt(quire))
messages.append(prompt)
res = chat(messages)
print(res)

基于非Open AI 构建RAG

前置

因为前一结使用的Open AI需要用钱购买额度,才能使用。因为本文的目标是跑通整个RAG流程,故此,在本小结提供一种免费的方式构建RAG。其目的是替换上述使用的OpenAIEmbeddingsChatOpenAI。在此处,我们使用HuggingFaceEmbeddingsQwen3-0.6B代替上述两个Open AI的产品。其次,因为频频无法在线连接Hugging Face模型,因此,本文提前下载相关模型,然后使用本地模型的方式导入。

模型下载

提供两种下载方式,基本没有差距。

  • 魔法版:如果拥有魔法,可以直接从Hugging Face下载相关模型
  • 遵纪守法版:如果没有魔法,则可以从魔搭社区下载

因为模型大多存在大文件。因此,在使用git下载时,需要注意电脑是否安装了lfs。具体可以参考https://git-lfs.com/

git lfs install

使用all-MiniLM-L6-v2替换OpenAIEmbeddings

本文使用all-MiniLM-L6-v2替换OpenAIEmbeddings。从上一小结下载模型之后,保存到项目路径中。其中'device': 'mps',是因为本机为MacBook。可以对应替换为cuda或者cpu

EMBEDDING_MODEL_NAME = "../local_models/sentence-transformers/all-MiniLM-L6-v2"
embed_model = HuggingFaceEmbeddings(
    model_name=EMBEDDING_MODEL_NAME,
    model_kwargs={'device': 'mps'},
    encode_kwargs={'normalize_embeddings': False, 'device': 'mps'}
)
vectorstore = Chroma.from_documents(documents=docs, embedding=embed_model, collection_name='HuggingFace-embedding')

使用Qwen3-0.6B替换ChatOpenAI

同理,下载的路径和运行的文件使用相对路径即可。其中,pipeline的作用是将一个特定的 NLP 任务(例如文本生成、情感分析等)封装起来,使得用户能够简单地调用它,而不需要手动处理模型加载、预处理和后处理等步骤。

model_dir = "../local_models/chat_models/Qwen3-0.6B"
tokenizer = AutoTokenizer.from_pretrained(model_dir)
model = AutoModelForCausalLM.from_pretrained(
    model_dir,
    device_map="auto",
    torch_dtype="auto"
)

generator = pipeline(
    "text-generation",  # 指定任务类型为文本生成
    model=model,
    tokenizer=tokenizer,
    max_length=1024,  # 指定生成文本的最大长度
    pad_token_id=tokenizer.eos_token_id
)

因为我们使用pipeline加载Text Generation,因此不需要System Message、Human Message等。直接对Prompt之后的文本进行推理。

query = 'What are the current challenges in time series forecasting?'

result = augment_prompt(query)

res = generator(result)

Github Repository

因为模型中部分文件太大,无法上传至Github仓库。因此,在仓库中省略了本地模型。小伙伴们可以根据上述章节自行下载模型,并按照路径导入到项目文件中,实现项目复现。
https://github.com/FranzLiszt-1847/LLM/tree/main

引用内容

[1] https://space.bilibili.com/256673804
[2] https://huggingface.co/
[3] https://python.langchain.com/docs/introduction/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FranzLiszt1847

嘟嘟嘟嘟嘟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值