基于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的实现原理简述如下:
- 依次使用分隔符列表
["\n\n", "\n", " ", ""]
进行文本拆分。文本会按照这些分隔符的优先级依次拆分,首先尝试使用 “\n\n”(两个换行符)进行拆分。如果未能有效拆分,则尝试使用 “\n”(单个换行符),然后是 " "(空格),最后使用 “”(空字符串,按字符拆分)。 - 在每次拆分时,如果分割出的文本块的长度小于
chunk_size
,则将这些文本块合并为一个块。如果分割出的文本块的长度大于chunk_size
,则会递归地使用下一个分隔符进一步拆分这些文本块,直到每个块的长度小于等于chunk_size
。 - 当某个文本块的长度大于
chunk_size
时,不是直接合并,而是会调用split_text
函数递归地使用剩余的分隔符继续对文本块进行拆分,直到块的大小满足要求。 - 如果拆分出的块较小(即小于
chunk_size
),它们会被合并为一个大的文本块。否则,递归继续拆分直到满足要求。 - 最终,算法返回若干个文本块,每个块的字符数 小于或等于
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 文本分割和块重叠示意图
向量化和向量数据库
构建OpenAIEmbeddings
对象,将分割的文本向量化,然后存储至向量数据库Chroma
中。向量化是将文本转为一组高维的数值坐标,在高维空间中,每个点(嵌入)的位置反映了其对应文本的含义。通过将文本简化为数值表示,我们可以使用简单的数学运算来快速测量两段文本的相似程度,而无论它们的原始长度或结构如何。一些常见的相似性指标包括:
- 余弦相似度:测量两个向量之间夹角的余弦值。
- 欧氏距离:测量两点之间的直线距离。
- 点积:测量一个向量在另一个向量上的投影。
embed_model = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents=docs, embedding=embed_model, collection_name='OpenAI')
其中OpenAI提供embed_query
和embed_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。其目的是替换上述使用的OpenAIEmbeddings
和ChatOpenAI
。在此处,我们使用HuggingFaceEmbeddings
和Qwen3-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/