赞
踩
通用的大语言模型(Large Language Model,LLM)往往具备比较好的语义理解能力,以此理解能力为基础,可以开发出众多基于LLM的智能应用。在两个月以前,OpenAI宣布GPTs,可以直接使用对话的方式构建一个个性化的GPT,近日OpenAI开放了GPT商店,用户可以分享自己的经过个性化调试以后的GPT,将GPT应用到了多种场景里面。1
LLM的训练过程极其漫长,所以在应用时往往存在知识时效性受限、专业能力有限、个人化定制化成本高的缺点,如何继续扩展已经训练好的模型的能力显得极为重要。目前基于大模型进行应用开发有两种核心范式:检索增强生成(RAG)和模型微调(Finetune)。
RAG是给模型外挂一个知识库,在用户提问时,将知识库和用户提问同时输入给大模型,大模型可以基于知识库完成本次回答;Finetune是使用新数据集对模型参数进行微调,使模型在新的数据集上具备理解能力。RAG优势在于成本低,可以实时更新,但是能力上限由基座模型所决定,外挂知识库会占用大量上下文;Finetune的个性化微调数据覆盖面更广,对于非可见知识,如回答风格的模拟效果非常好,但是需要重新训练,依旧对GPU资源的需求量大,而且无法做到实时更新知识,更新时需要重新微调模型参数。
LangChain是主流的RAG开发框架之一,本次课程介绍如何使用LangChain实现RAG应用。可能内容包括RAG开发的常规流程:使用个人私域数据构建向量数据库;基于LLM大语言模型搭建知识库助手;使用Gradio将知识库助手部署为网页交互界面。
本次课程所展示的知识库助手基于InternLM模型,该模型是上海人工智能实验室开源的大语言模型,能够理解用户自然语言的指令,具有强大的知识储备和逻辑推理能力,可以作为应用开发的基座模型。2
RAG的原理是:对于每个用户输入,使用向量模型将其转换为向量,并在向量数据库中匹配相似的文本段(被认为与输入请求相关,包含对应答案),将用户输入和相似文本段一起嵌入到Prompt中,将Prompt传递给LLM,得到模型的回答。
LangChain是一个为各种LLM提供通用接口,简化应用开发流程的开源工具,LangChain包含各种组件,组件之间的组合可以开发出各种LLM应用,例如将自己的私域数据嵌入到LangChain的组件中,构建适用于相关场景的RAG应用。LangChain最核心的组件是链(Chains):将组件组合实现端到端应用,例如检索问答链覆盖实现RAG的全部流程,可以直接将自己的数据库和大模型引入到检索问答链中,高效搭建自己的RAG应用。
下图是知识助手的检索问答链工作流程,LangChain需要使用SentenceTransformers分别对用户的输入和附加数据库向量化,在向量数据库中可以匹配向量之间的相似度,基于用户输入和相似文本生成Prompt交给InternLM获得回答结果。图片中虚线框内的流程已经封装在LangChain中,基于LangChain构建自己的RAG应用变得更加简单。
实现RAG应用的第一步是构建自己的知识数据库,构建流程包括加载源文件、文档分块和文档向量化。源文件加载的实现过程需要确定文档类型,针对不同的类型选择相对应的加载器,将源数据转化为无格式的字符串。然后对字符串进行分块,避免单个文档超过大模型上下文的上限,一般根据字符串长度进行分割,可以手动控制分割长度和重叠长度,后续的检索将以每个分割块为基本单元。使用Embedding模型将本文数据向量化以支持文本之间的语义匹配,本次课程使用SentenceTransformers作为本文向量化的模型。向量化以后的数据可以组成向量数据库,本次课程使用轻量级的Chroma来组织向量数据库,Chroma是支持语义检索的向量数据库框架。
LangChain框架支持自定义LLM,部署在本地的InternLM可以被封装为自定义LLM类,然后由LangChain框架调用来构建检索问答链。通过将自定义向量数据库和自定义LLM接入到检索问答链,用户只需要输入问题,即可获得LLM基于知识库的回答,实现知识库助手的核心功能。
这种方法无需修改模型的参数,构建起来方便快捷,为了进一步提高RAG知识库助手的能力,未来工作中可以考虑在文本分割步骤时于文本的语义进行分割,或者对每个分割块生成概括性索引,保证更优的检索精度,亦可在Prompt的生成阶段选择更优的策略,让LLM可以给出更加符合要求的回到。
基于RGA构建的LLM知识库助手可以部署在Web端,目前有很多支持简易Web应用的工具,例如Gradio、Streamlit等。本次课程将使用Gradio部署知识库助手的Web界面,实现更加直观的交互方式。
本次课程的知识库助手需要用到多种开发工具,以下是对相关开发工具的简单介绍。
SentenceTransformers是一个可以将句子、文本等数据提取为向量特征的方法,该方法最早公开于Sentence-BERT论文中3,方法支持超过100种语言的本文嵌入,本文之间的嵌入特征能够用Cosine相似度来进行语义度量。4
SentenceTransformers提供基于Pytorch和Transformers的多种预训练模型,本次课程将要用到的预训练模型为下图所示模型。
Chroma是向量数据库框架,在LLM应用开发阶段,可以更好地把需要用到的文本数据整合起来,特别契合RAG应用的开发过程,如下图所示,Chroma可以将知识库的文本和嵌入保存为数据库,当用户提出一个请求时,使用Embedding模型提取请求的嵌入特征,然后从数据库中检索相似的文本数据,用户请求和相似文本数据一同生成Prompt输入给LLM。
Gradio是一个可以帮助开发者快速部署深度学习网页应用的框架,本次作业使用该框架完成网页端的模型部署,使用Chatbot
类在网页上显示整个对话过程中的用户输入和模型输出信息。5
Chatbot
可以接收Markdown的部分语法,而且可以在聊天过程中直接显示图片、音频和视频。当Chatbot
作为输入使用时,可以将当前的每一轮对话的输入输出作为一个列表,用于LLM的上下文判断;当Chatbot
作为输出使用时,其接受一个对话列表,列表中每个元素是一个[user message, response message]
的对话,然后将所有的对话显示在网页上。
以下Python代码使用Chatbot
的简单样例,可以看到chatbot
变量分别作为respond
函数的输入(给出内容)和输出(接收内容),在该代码中,respond
函数中的chat_history.append((message, bot_message))
语句是用来更新Chatbot
的内容。
import gradio as gr import random import time with gr.Blocks() as demo: chatbot = gr.Chatbot(height=500) msg = gr.Textbox() clear = gr.ClearButton([msg, chatbot]) def respond(message, chat_history): bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"]) chat_history.append((message, bot_message)) time.sleep(2) return "", chat_history msg.submit(respond, [msg, chatbot], [msg, chatbot]) if __name__ == "__main__": demo.launch()
安装好Gradio以后,运行以上Python代码,得到的结果如下图所示,模型可以接受每次用户的收入,随机从["How are you?", "I love you", "I'm very hungry"]
中返回一条信息。
为了进一步探索Chatbot
的显示效果,将上述代码中的chat_history
更新语句后天添加一句chat_history.append((message, bot_message) )
,其他部分保持不变,重新运行代码,此时的对话效果如下图所示,说明chatbot只会检查返回的列表格式是否符合要求,而不会检查此次调用返回了多少轮对话。
作业:复现课程知识库助手搭建过程 (截图)
本次作业同样使用InternStudio开发机来实现,使用LangChain开放框架,完成InternLM项目文件的知识库助手。以下是进行开发前的准备工作:
Internlm-demo
开发环境,完成以下配置:
langchain, gradio, chromadb, sentence-transformers, unstructured, markdown
nltk
库的相关资源,为防止使用时从互联网下载出现问题,可以将其下载到本地备用。/root/model/Shanghai_AI_Laboratory
文件夹内。HF_ENDPOINT='https://hf-mirror.com'
是说本次命令运行时的环境变量HF_ENDPOINT
的值为'https://hf-mirror.com'
,即使用HuggingFace镜像站下载模型。HF_ENDPOINT='https://hf-mirror.com' huggingface-cli download \
--resume-download sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 \
--local-dir /root/model/sentence-transformer
/root/data/InternLM_corpus
目录中。经过以上步骤,我们已经完成程序运行环境的配置,和模型与数据的下载。接下来开始知识库助手的相关编程。
在/root/code/
中创建RAG_demo
文件夹用来存放用到的程序文件,使用VSCode打开该文件夹开始编程。
首先编写Python脚本整理数据,该Python脚本定义两个函数:文件遍历和文件读取。通过调用这两个函数,可以将仓库中的.md
文件和.txt
文件加载为无格式字符串。然后使用列表docs
将所有文本文件的字符串连接起来。
import os from tqdm import tqdm from langchain.document_loaders import UnstructuredFileLoader from langchain.document_loaders import UnstructuredMarkdownLoader def get_files(dir_path): # args: dir_path, corpus folder file_list = [] for filepath, dirnames, filenames in os.walk(dir_path): 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 def get_text(dir_path): # args:dir_path,corpus folder file_lst = get_files(dir_path) 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: continue docs.extend(loader.load()) return docs
我们可以使用显示get_text('/root/data/InternLM_corpus/InternLM')[0]
的数据类型和内容,展示程序读取第一个文件的结果,如下图所示,文件被读取以后是langchain.schema.document.Document
类型的数据,该数据类型的page_content
就是文件内的无格式文本内容。
得到无格式本文字符串以后,使用LangChain的RecursiveCharacterTextSplitter(chunk_size=..., chunk_overlap=...)
组件指定分块的参数并对docs
分块。
from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_text(docs):
# args:docs,text string
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=150)
split_docs = text_splitter.split_documents(docs)
return split_docs
利用LangChain的HuggingFaceEmbedding构建sentence-transformer,并使用Chroma构建向量数据库以满足向量数据之间的检索。
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
def create_db(split_docs, model_path, db_path):
# args: model_path, sentence-transformers path
embeddings = HuggingFaceEmbeddings(model_path)
persist_directory = db_path
vectordb = Chroma.from_documents(
documents=split_docs,
embedding=embeddings,
persist_directory=persist_directory
)
return vectordb
编写主程序,读取所有的文件,并将向量数据库存储在磁盘上,供后续代码直接调用。
def main(corpus_paths, model_path, db_path): docs = [] for corpus_path in corpus_paths: docs.extend(get_text(corpus_path)) split_docs = split_text(docs) vectordb = create_db(split_docs, model_path, db_path) vectordb.persist() if __name__ == '__main__': corpus_paths = [ "/root/data/InternLM_corpus/InternLM", "/root/data/InternLM_corpus/InternLM-XComposer", "/root/data/InternLM_corpus/lagent", "/root/data/InternLM_corpus/lmdeploy", "/root/data/InternLM_corpus/opencompass", "/root/data/InternLM_corpus/xtuner" ] model_path = '/root/model/sentence-transformer' db_path = '/root/data/InternLM_corpus/chroma' main(corpus_paths, model_path, db_path)
以上代码保存为create_vdb.py
文件,运行代码以后可以在/root/data/InternLM_corpus/chroma
路径内看到创建好的向量数据库:
这一部分需要将InternLM封装为可供LangChain调用的LLM类,命名为InternLM_LLM
类,需要修改__inti__()
和_call()
两个函数,前者用来加载InternLM,后者使用InternLM模型对Prompt做出响应,需要注意的是,在该InternLM_LLM
类中,模型不会介绍History,也就是无法获取多轮对话的上下文,唯一设置的上下文是书生·浦语模型的初始化。
此外还需要在代码中定义_llm_type()
方法,重新设置类的标识。
from langchain.llms.base import LLM from typing import Any, List, Optional from langchain.callbacks.manager import CallbackManagerForLLMRun from transformers import AutoTokenizer, AutoModelForCausalLM import torch class InternLM_LLM(LLM): tokenizer: AutoTokenizer = None model: AutoModelForCausalLM = None def __init__(self, model_path: str): super.__init__() print("Load Model...") self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) self.model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True).to(torch.bfloat16).cuda() self.model = self.model.eval() print("End...") def _call(self, prompt: str, stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any): system_prompt = """You are an AI assistant whose name is InternLM (书生·浦语). - InternLM (书生·浦语) is a conversational language model that is developed by Shanghai AI Laboratory (上海人工智能实验室). It is designed to be helpful, honest, and harmless. - InternLM (书生·浦语) can understand and communicate fluently in the language chosen by the user such as English and 中文. """ messages = [(system_prompt, '')] response, history = self.model.chat(self.tokenizer, prompt, history=messages) return response @property def _llm_type(self) -> str: return "InternLM"
我们实例化该模型,并运行以下代码,可以得到模型的回复。
if __name__ == '__main__':
InternLM_path = '/root/model/Shanghai_AI_Laboratory/internlm-chat-7b'
llm = InternLM_LLM(InternLM_path)
print(llm.predict("你是?"))
有了知识向量库和LLM以后,就可以使用LangChain构建检索问答链,完成RAG应用的开发。
在检索问答链中,定义load_chain()
函数,用于加载词向量模型、向量数据库和LLM。因为知识库助手的LLM每次接收的上下文包含了用户输入和检索出来的知识库片段,这一过程需要使用LangChain工具的PromptTemplate
组件定义Prompt模板,该Prompt模板就是LLM将会看到的内容,就像上面内容所说的,修改Prompt模板会影响模型的性能。然后LangChain的RetrievalQA
构建检索问答链,需要将LLM、。具体代码如下所示:
from langchain.vectorstores import Chroma from langchain.embeddings.huggingface import HuggingFaceEmbeddings from create_LLM import InternLM_LLM from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA def load_chain(embedding_path, vdb_path, llm_path): embeddings = HuggingFaceEmbeddings(model_name = embedding_path) vectordb = Chroma( persist_directory=vdb_path, embedding_function=embeddings, ) template = """使用以下上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 问题: {question} 可参考的上下文: ··· {context} ··· 如果给定的上下文无法让你做出回答,请回答你不知道。 有用的回答:""" QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"], template=template) llm = InternLM_LLM(llm_path) qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectordb.as_retriever(), return_source_documents=True, chain_type_kwargs={'prompt': QA_CHAIN_PROMPT}) return qa_chain
使用以下代码对比检索问答链和单一LLM的回答结果:
if __name__ == '__main__': embedding_path = '/root/model/sentence-transformer' vdb_path = '/root/data/InternLM_corpus/chroma' llm_path = '/root/model/Shanghai_AI_Laboratory/internlm-chat-7b' llm = InternLM_LLM(llm_path) qa_chain = load_chain(embedding_path, vdb_path, llm_path) # 检索问答链回答效果 question = "什么是InternLM-XComposer" result = qa_chain({"query": question}) print("检索问答链回答 question 的结果:") print(result["result"]) # 仅 LLM 回答效果 result_2 = llm(question) print("大模型回答 question 的结果:") print(result_2)
检索问答链无法回答该问题,而InternLM可以直接回答,按说不应该这样,所以猜测可能是语料库分割的时候每500个字符还是有点短,导致无法检索到比较准确的信息。
重新设置语料库分割长度为1000,重叠区域为400,将新的知识库保存在/root/data/InternLM_corpus/chroma2
文件夹内,运行检索问答链和InternLM的对比代码,结果表明知识库助手依旧无法回答该问题,说明该RAG应用还存在某些可以改进的地方。
为了方便用户和知识库助手之间的交互,本次课程使用Gradio编写网页端交互界面。首先定义Model_center()
类,用以对检索问答链进行封装,主要包括问答链的加载和问答链的核心工作逻辑设计,实现代码如下所示:
from create_chain import load_chain class Model_center(): def __init__(self, embedding_path, vdb_path, llm_path): self.chain = load_chain(embedding_path, vdb_path, llm_path) def qa_chain_self_answer(self, question: str, chat_history: list = []): if question == None or len(question) < 1: return "", chat_history try: chat_history.append( (question, self.chain({'query': question})['result']) ) return "", chat_history except Exception as e: return e, chat_history
然后编写Gradio的Chatbot
,将检索问答链整合到网页的消息处理函数中。这一部分的代码可见教程中的示例文件,本次作业改动了聊天按钮和清除按钮的排列,将他们放置在了同一行。
以下是网页示例对话展示:
为了更直观地查看知识库助手和原始InternLM的回答差异,将两个模型同时部署在Web端,对于每个用户的问题,显示两个模型的回答结果,改动的地方仅仅是Model_center
中的chat_history
更新策略后面再添加一句:
chat_history.append(
("", "InternLM回答:" + self.llm(question))
)
下图是网页上的回答结果,可以看到虽然检索问答链用的就是InternLM作为其LLM,但是两者在同一问题上的回答是不同的。而且因为检索问答链中限制模型需要参考搜索出来语料上下文进行回答,在一定程度上限制了模型的语言组织能力,就算模型知道某些问题的答案,但是因为和知识库的内容无关,模型还是会先回答不知道。模型也具备了更多的知识,例如对OpenCompass和浦语灵笔的回答。可能是语料库中虽然没有格式,但是存在一些英文字符,例如原先的表格、数字等,导致模型的语言组织能力出现一定偏差,回答“你是谁”问题时,小助手出现了乱码情况。
本次项目的目录结构如下,分别对应上述四个步骤。
RAG_demo/
|-- __pycache__
|-- create_LLM.py
|-- create_chain.py
|-- create_vdb.py
`-- web_demo.py
1 directory, 4 files
作业:选择一个垂直领域,收集该领域的专业资料构建专业知识库,并搭建专业问答助手,并在 OpenXLab 上成功部署(截图,并提供应用地址)
留到下篇文章…
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。