一、TL;DR
- 从0-1使用qwen chat model+ langchain的链式架构搭建一套rag系统
- 详细介绍了Langchain的工具链的调用流程
- 简单介绍了可能会出现什么问题
二、方法
参考开源链接:https://github.com/Aphasia0515/self_llm/
2.1 硬件和软件依赖
类型 | 需求 | 备注 |
硬件 |
| 实测:
|
软件 |
|
|
2.2 模型下载和准备
注意:模型下载记得从mirror-huggingface上下载,国内打不开huggingface
模型 | 作用 | 备注 |
Qwen | Rag系统的生成器、chat模型 |
|
Sentence Transformer | 对文本进行向量化 | 无 |
2.3 Rag的文本库准备
这一节就不仔细讲了,将所有你需要的参考文件知识库放在某个指定目录下,本文只做txt和markdown的文档拆分(用于后续的建库)
三、知识库提取和向量化
3.1 得到所有文档的纯本文内容
先遍历2.3节的指定目录,得到所有的markdown和txt文件,并存成list:
import os
def get_files(dir_path):# args:dir_path,目标文件夹路径file_list = []for filepath, dirnames, filenames in os.walk(dir_path):# os.walk 函数将递归遍历指定文件夹for filename in filenames:# 通过后缀名判断文件类型是否满足要求if filename.endswith(".md"):# 如果满足要求,将其绝对路径加入到结果列表file_list.append(os.path.join(filepath, filename))elif filename.endswith(".txt"):file_list.append(os.path.join(filepath, filename))return file_list
接下来对list里面的内容进行读取和加载,此处使用LangChain提供的FileLoader进行加载:
- 注意:此处得到的其实还是纯文本loader,而非真正的文本块
from tqdm import tqdm
from langchain.document_loaders import UnstructuredFileLoader
from langchain.document_loaders import UnstructuredMarkdownLoaderdef get_text(dir_path):# args:dir_path,目标文件夹路径# 首先调用上文定义的函数得到目标文件路径列表file_lst = get_files(dir_path)# docs 存放加载之后的纯文本对象docs = []# 遍历所有目标文件for one_file in tqdm(file_lst):file_type = one_file.split('.')[-1]if file_type == 'md':loader = UnstructuredMarkdownLoader(one_file)elif file_type == 'txt':loader = UnstructuredFileLoader(one_file)else:# 如果是不符合条件的文件,直接跳过continuedocs.extend(loader.load())return docs
3.2 对所有的文本进行分块并向量化
LangChain使用多种文本分块工具,示例代码使用的时字符串递归分割器,并选择分块大小时500,块重叠长度是150,分块代码如下所示:
注意:我上一篇博客所说的,分块的大小不同的模型有不同的选择,只有最合适的,没有固定的
from langchain.text_splitter import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=150)
split_docs = text_splitter.split_documents(docs)
分块完后,使用2.2节的下载好的模型(sentence Transformer)此时进一步进行向量化,LangChain 提供了直接引入 HuggingFace 开源社区中的模型进行向量化的接口::
注意:这个是直接加载本地路径
from langchain.embeddings.huggingface import HuggingFaceEmbeddingsembeddings = HuggingFaceEmbeddings(model_name="/root/autodl-tmp/embedding_model")
加载玩模型后,使用 Chroma 作为向量数据库,基于上文分块后的文档以及加载的开源向量化模型,将语料加载到指定路径下的向量数据库:
from langchain.vectorstores import Chroma# 定义持久化路径
persist_directory = 'data_base/vector_db/chroma'
# 加载数据库
vectordb = Chroma.from_documents(documents=split_docs,embedding=embeddings,persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
# 将加载的向量数据库持久化到磁盘上
vectordb.persist()
运行上述脚本,就得到一个本地构建已持久化的向量数据库,后续直接导入该数据库即可,无需重复构建。
四、接入LLM-Chat Model
注意:本步骤接入任何llm模型其实都是可以的,主要还是看这个检索框架的整体实现。
4.1 QwenLM接入LangChain
本地部署Qwen llm模型,然后将QwenLM接入到Langchain框架里面,完成自定义 LLM 类之后,可以以完全一致的方式调用 LangChain 的接口,而无需考虑底层模型调用的不一致。
- 从 LangChain.llms.base.LLM 类继承一个子类,并重写构造函数与 _call 函数
from langchain.llms.base import LLM
from typing import Any, List, Optional
from langchain.callbacks.manager import CallbackManagerForLLMRun
from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfigclass QwenLM(LLM):# 基于本地 Qwen 自定义 LLM 类tokenizer : AutoTokenizer = Nonemodel: AutoModelForCausalLM = Nonedef __init__(self, model_path :str):# model_path: Qwen 模型路径# 从本地初始化模型super().__init__()print("正在从本地加载模型...")model_dir = '/root/autodl-tmp/qwen/Qwen-7B-Chat'self.tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)self.model = AutoModelForCausalLM.from_pretrained(model_dir, device_map="auto", trust_remote_code=True).eval()# Specify hyperparameters for generationself.model.generation_config = GenerationConfig.from_pretrained(model_dir, trust_remote_code=True) # 可指定不同的生成长度、top_p等相关超参print("完成本地模型的加载")def _call(self, prompt : str, stop: Optional[List[str]] = None,run_manager: Optional[CallbackManagerForLLMRun] = None,**kwargs: Any):# 重写调用函数response, history = self.model.chat(self.tokenizer, prompt , history=[])return response@propertydef _llm_type(self) -> str:return "QwenLM"
上述代码在构造函数里直接加载了qwen模型,如果有其他的llm model也可以一并加载进去, _call 函数是 LLM 类的核心函数,LangChain 会调用该函数来调用 LLM来使用qwen的chat能力。
开源仓库里面将上述代码封装为 LLM.py,后续将直接从该文件中引入自定义的 LLM 类,直接使用from LLM import QwenLM。
五、构建检索问答链
LangChain 通过提供检索问答链对象来实现对于 RAG 全流程的封装:
- 我们可以调用一个 LangChain 提供的 RetrievalQA 对象,通过初始化时填入已构建的数据库和自定义 LLM 作为参数,来简便地完成检索增强问答的全流程,LangChain 会自动完成基于用户提问进行检索、获取相关文档、拼接为合适的 Prompt 并交给 LLM 问答的全部流程。
5.1 导入向量数据库
from langchain.vectorstores import Chroma
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
import os# 定义 Embeddings
embeddings = HuggingFaceEmbeddings(model_name="/root/autodl-tmp/embedding_model")# 向量数据库持久化路径
persist_directory = 'data_base/vector_db/chroma'# 加载数据库
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embeddings
)
注意:向量数据库里面存储的是分块的文档和对应的向量,是embedding和index的关系
5.2 实例化自定义的QwenLM对象
from LLM import QwenLM
llm = QwenLM(model_path = "/root/autodl-tmp/qwen")
llm.predict("你是谁")
5.3 构建Prompt Template
prompt Template的作用是通过将用户input、rag检索得到的知识片段组合成input:
- 是一个带变量的字符串
- 在检索之后,LangChain 会将检索到的相关文档片段填入到 Template 的变量中,从而实现带知识的 Prompt 构建。
from langchain.prompts import PromptTemplate# 我们所构造的 Prompt 模板
template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答案。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
有用的回答:"""# 调用 LangChain 的方法来实例化一个 Template 对象,该对象包含了 context 和 question 两个变量,在实际调用时,这两个变量会被检索到的文档片段和用户提问填充
QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],template=template)
5.4 直接检索问答
最后,可以调用 LangChain 提供的检索问答链构造函数,基于我们的自定义 LLM、Prompt Template 和向量知识库来构建一个基于 Qwen 的检索问答链:
question = "什么是QwenLM"
result = qa_chain({"query": question})
print("检索问答链回答 question 的结果:")
print(result["result"])# 仅 LLM 回答效果
result_2 = llm(question)
print("大模型回答 question 的结果:")
print(result_2)
可以看到,使用检索问答链生成的答案更接近知识库里的内容。
六、部署WebDemo
略
七、可优化的点
上述用的是开源代码,如果实际工程中可以从哪里优化呢?
- 检索的文本是大文本或者超大文本(>2K文本):
- summary,对大文本进行 map-reduce summary
- k-means(BRV steps)
- 检索内容太多不准该怎么办?
- 等等 明天再写把