一、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)
- 檢索內容太多不準該怎么辦?
- 等等 明天再寫把