基于 InternLM 和 LangChain 搭建你的知识库 环境配置 InternLM 环境 开发环境除了 pytorch
等库以外,还需要安装以下库
1 2 3 4 5 6 7 8 python -m pip install --upgrade pip pip install modelscope==1.9.5 pip install transformers==4.35.2 pip install streamlit==1.24.0 pip install sentencepiece==0.1.99 pip install accelerate==0.24.1
模型下载 1 2 3 4 import torch from modelscope import snapshot_download, AutoModel, AutoTokenizer import os model_dir = snapshot_download('Shanghai_AI_Laboratory/internlm-chat-7b' , cache_dir='/root/data/model' , revision='v1.0.3' )
配置 Langchain 除了配置大模型的运行环境以外,还需要配置 Langchain 运行环境。
1 2 3 4 5 6 pip install langchain==0.0.292 pip install gradio==4.4.0 pip install chromadb==0.4.15 pip install sentence-transformers==2.2.2 pip install unstructured==0.10.30 pip install markdown==3.3.7
安装依赖
下载 Embedding 模型 同时,我们需要使用到开源词向量模型 Sentence Transformer - paraphrase-multilingual-MiniLM-L12-v2 :(我们也可以选用别的开源词向量模型来进行 Embedding,教程中选用这个模型是相对轻量、支持中文且效果较好的,我这里选择使用了更为好用的 bge 系列的 Embedding 模型 BAAI/bge-large-zh-v1.5 ))
首先需要使用 huggingface
官方提供的 huggingface-cli
命令行工具。安装依赖:
1 pip install -U huggingface_hub
然后在和 /root/data
目录下新建python文件 download_hf.py
,填入以下代码:
resume-download:断点续下
local-dir:本地存储路径。(linux环境下需要填写绝对路径)
1 2 3 4 import os os.system('huggingface-cli download --resume-download BAAI/bge-large-zh-v1.5 --local-dir /root/data/model/bge-large-zh-v1.5' )
但是,使用 huggingface 下载可能速度较慢,我们可以使用 huggingface 镜像下载。与使用hugginge face下载相同,只需要填入镜像地址即可。
将 download_hf.py
中的代码修改为以下代码:
1 2 3 4 5 6 7 import os os.environ['HF_ENDPOINT' ] = 'https://hf-mirror.com' os.system('huggingface-cli download --resume-download BAAI/bge-large-zh-v1.5 --local-dir /root/data/model/bge-large-zh-v1.5' )
然后,在 /root/data
目录下执行该脚本即可自动开始下载:
下载bge模型
下载 NLTK 相关资源 我们在使用开源词向量模型构建开源词向量的时候,需要用到第三方库 nltk
的一些资源。正常情况下,其会自动从互联网上下载,但可能由于网络原因会导致下载中断,此处我们可以从国内仓库镜像地址下载相关资源,保存到服务器上。
我们用以下命令下载 nltk 资源并解压到服务器上:
1 2 3 4 5 6 7 8 cd /root git clone https://gitee.com/yzy0612/nltk_data.git --branch gh-pagescd nltk_datamv packages/* ./cd tokenizers unzip punkt.zipcd ../taggers unzip averaged_perceptron_tagger.zip
之后使用时服务器即会自动使用已有资源,无需再次下载。
下载教程代码 我们在仓库中同步提供了所有脚本,可以查看该教程文件的同级目录的 demo
文件夹。
建议通过以下目录将仓库 clone 到本地,可以直接在本地运行相关代码:
1 2 cd /root/data git clone https://github.com/InternLM/tutorial
通过上述命令,可以将本仓库 clone 到本地 root/data/tutorial
目录下,在之后的过程中可以对照仓库中的脚本来完成自己的代码,也可以直接使用仓库中的脚本。
知识库搭建 数据收集 教程选择了由上海人工智能实验室开源的一系列大模型工具开源仓库作为语料库来源,包括:
首先我们需要将上述远程开源仓库 Clone 到本地,可以使用以下命令:
1 2 3 4 5 6 7 8 9 cd /root/ data git clone https://gi tee.com/open-compass/ opencompass.git git clone https://gi tee.com/InternLM/ lmdeploy.git git clone https://gi tee.com/InternLM/ xtuner.git git clone https://gi tee.com/InternLM/ InternLM-XComposer.git git clone https://gi tee.com/InternLM/ lagent.git git clone https://gi tee.com/InternLM/ InternLM.git
接着,为语料处理方便,我们将选用上述仓库中所有的 markdown、txt 文件作为示例语料库。注意,也可以选用其中的代码文件加入到知识库中,但需要针对代码文件格式进行额外处理(因为代码文件对逻辑联系要求较高,且规范性较强,在分割时最好基于代码模块进行分割再加入向量数据库)。
我们首先将上述仓库中所有满足条件的文件路径找出来,我们定义一个函数,该函数将递归指定文件夹路径,返回其中所有满足条件(即后缀名为 .md 或者 .txt 的文件)的文件路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import os def get_files (dir_path ): 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
加载数据 得到所有目标文件路径之后,我们可以使用 LangChain 提供的 FileLoader 对象来加载目标文件,得到由目标文件解析出的纯文本内容。由于不同类型的文件需要对应不同的 FileLoader,我们判断目标文件类型,并针对性调用对应类型的 FileLoader,同时,调用 FileLoader 对象的 load 方法来得到加载之后的纯文本对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from tqdm import tqdmfrom langchain.document_loaders import UnstructuredFileLoaderfrom langchain.document_loaders import UnstructuredMarkdownLoaderdef get_text (dir_path ): 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
使用上文函数,我们得到的 docs
为一个纯文本对象对应的列表。
构建向量数据库 得到该列表之后,我们就可以将它引入到 LangChain 框架中构建向量数据库。由纯文本对象构建向量数据库,我们需要先对文本进行分块,接着对文本块进行向量化。
LangChain 提供了多种文本分块工具,此处我们使用字符串递归分割器,并选择分块大小为 500,块重叠长度为 150(由于篇幅限制,此处没有展示切割效果,学习者可以自行尝试一下,想要深入学习 LangChain 文本分块可以参考教程 《LangChain - Chat With Your Data》 :
1 2 3 4 5 from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500 , chunk_overlap=150 ) split_docs = text_splitter.split_documents(docs)
接着我们选用开源词向量模型 Sentence Transformer 来进行文本向量化。LangChain 提供了直接引入 HuggingFace 开源社区中的模型进行向量化的接口:
1 2 3 from langchain.embeddings.huggingface import HuggingFaceEmbeddings embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/bge-large-zh-v1.5" )
同时,考虑到 Chroma 是目前最常用的入门数据库,我们选择 Chroma 作为向量数据库,基于上文分块后的文档以及加载的开源向量化模型,将语料加载到指定路径下的向量数据库:
1 2 3 4 5 6 7 8 9 10 11 12 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 ) vectordb.persist()
整体脚本 将上述代码整合在一起为知识库搭建的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 from langchain.document_loaders import UnstructuredFileLoaderfrom langchain.document_loaders import UnstructuredMarkdownLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain.vectorstores import Chromafrom langchain.embeddings.huggingface import HuggingFaceEmbeddingsfrom tqdm import tqdmimport osdef get_files (dir_path ): 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_listdef get_text (dir_path ): 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 tar_dir = [ "/root/data/InternLM" , "/root/data/InternLM-XComposer" , "/root/data/lagent" , "/root/data/lmdeploy" , "/root/data/opencompass" , "/root/data/xtuner" ] docs = []for dir_path in tar_dir: docs.extend(get_text(dir_path)) text_splitter = RecursiveCharacterTextSplitter( chunk_size=500 , chunk_overlap=150 ) split_docs = text_splitter.split_documents(docs) embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/bge-large-zh-v1.5" ) persist_directory = 'data_base/vector_db/chroma' vectordb = Chroma.from_documents( documents=split_docs, embedding=embeddings, persist_directory=persist_directory ) vectordb.persist()
可以在 /root/data
下新建一个 demo
目录,将该脚本和后续脚本均放在该目录下运行。运行上述脚本,即可在本地构建已持久化的向量数据库,后续直接导入该数据库即可,无需重复构建。
InternLM 接入 LangChain 为便捷构建 LLM 应用,我们需要基于本地部署的 InternLM,继承 LangChain 的 LLM 类自定义一个 InternLM LLM 子类,从而实现将 InternLM 接入到 LangChain 框架中。完成 LangChain 的自定义 LLM 子类之后,可以以完全一致的方式调用 LangChain 的接口,而无需考虑底层模型调用的不一致。
基于本地部署的 InternLM 自定义 LLM 类并不复杂,我们只需从 LangChain.llms.base.LLM 类继承一个子类,并重写构造函数与 _call
函数即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from langchain.llms.base import LLMfrom typing import Any , List , Optional from langchain.callbacks.manager import CallbackManagerForLLMRunfrom transformers import AutoTokenizer, AutoModelForCausalLMimport torchclass InternLM_LLM (LLM ): tokenizer : AutoTokenizer = None model: AutoModelForCausalLM = None def __init__ (self, model_path :str ): super ().__init__() print ("正在从本地加载模型..." ) 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 ("完成本地模型的加载" ) 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"
在上述类定义中,重写了构造函数和 _call
函数:对于构造函数,我们在对象实例化的一开始加载本地部署的 InternLM 模型,从而避免每一次调用都需要重新加载模型带来的时间过长;_call
函数是 LLM 类的核心函数,LangChain 会调用该函数来调用 LLM,在该函数中,我们调用已实例化模型的 chat 方法,从而实现对模型的调用并返回调用结果。
在整体项目中,我们将上述代码封装为 LLM.py,后续将直接从该文件中引入自定义的 LLM 类。
构建检索问答链 LangChain 通过提供检索问答链对象来实现对于 RAG 全流程的封装。所谓检索问答链,即通过一个对象完成检索增强问答(即RAG)的全流程,针对 RAG 的更多概念,我们会在视频内容中讲解,也欢迎读者查阅该教程来进一步了解:《LLM Universe》 。我们可以调用一个 LangChain 提供的 RetrievalQA
对象,通过初始化时填入已构建的数据库和自定义 LLM 作为参数,来简便地完成检索增强问答的全流程,LangChain 会自动完成基于用户提问进行检索、获取相关文档、拼接为合适的 Prompt 并交给 LLM 问答的全部流程。
加载向量数据库 首先我们需要将上文构建的向量数据库导入进来,我们可以直接通过 Chroma 以及上文定义的词向量模型来加载已构建的数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from langchain.vectorstores import Chromafrom langchain.embeddings.huggingface import HuggingFaceEmbeddingsimport os embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/bge-large-zh-v1.5" ) persist_directory = 'data_base/vector_db/chroma' vectordb = Chroma( persist_directory=persist_directory, embedding_function=embeddings )
上述代码得到的 vectordb
对象即为我们已构建的向量数据库对象,该对象可以针对用户的 query
进行语义向量检索,得到与用户提问相关的知识片段。
实例化自定义 LLM 与 Prompt Template 接着,我们实例化一个基于 InternLM 自定义的 LLM 对象:
1 2 3 from LLM import InternLM_LLM llm = InternLM_LLM(model_path = "/root/data/model/Shanghai_AI_Laboratory/internlm-chat-7b" ) llm.predict("你是谁" )
构建检索问答链,还需要构建一个 Prompt Template,该 Template 其实基于一个带变量的字符串,在检索之后,LangChain 会将检索到的相关文档片段填入到 Template 的变量中,从而实现带知识的 Prompt 构建。我们可以基于 LangChain 的 Template 基类来实例化这样一个 Template 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from langchain.prompts import PromptTemplate template = """使用以下上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 问题: {question} 可参考的上下文: ··· {context} ··· 如果给定的上下文无法让你做出回答,请回答你不知道。 有用的回答:""" QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context" ,"question" ],template=template)
构建检索问答链 最后,可以调用 LangChain 提供的检索问答链构造函数,基于我们的自定义 LLM、Prompt Template 和向量知识库来构建一个基于 InternLM 的检索问答链:
1 2 3 from langchain.chains import RetrievalQA qa_chain = RetrievalQA.from_chain_type(llm,retriever=vectordb.as_retriever(),return_source_documents=True ,chain_type_kwargs={"prompt" :QA_CHAIN_PROMPT})
得到的 qa_chain
对象即可以实现我们的核心功能,即基于 InternLM 模型的专业知识库助手。我们可以对比该检索问答链和纯 LLM 的问答效果:
1 2 3 4 5 6 7 8 9 10 question = "什么是InternLM" result = qa_chain({"query" : question})print ("检索问答链回答 question 的结果:" )print (result["result" ]) result_2 = llm(question)print ("大模型回答 question 的结果:" )print (result_2)
部署一个 Web Demo 之后我们可以基于 Gradio 框架将其部署到 Web 网页,从而搭建一个小型 Demo,便于测试与使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 import gradio as grfrom langchain.vectorstores import Chromafrom langchain.embeddings.huggingface import HuggingFaceEmbeddingsimport osfrom LLM import InternLM_LLMfrom langchain.prompts import PromptTemplatedef load_chain (): embeddings = HuggingFaceEmbeddings(model_name="/root/data/model/bge-large-zh-v1.5" ) persist_directory = 'data_base/vector_db/chroma' vectordb = Chroma( persist_directory=persist_directory, embedding_function=embeddings ) llm = InternLM_LLM(model_path = "/root/data/model/Shanghai_AI_Laboratory/internlm-chat-7b" ) template = """使用以下上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。 问题: {question} 可参考的上下文: ··· {context} ··· 如果给定的上下文无法让你做出回答,请回答你不知道。 有用的回答:""" QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context" ,"question" ], template=template) from langchain.chains import RetrievalQA qa_chain = RetrievalQA.from_chain_type(llm, retriever=vectordb.as_retriever(), return_source_documents=True , chain_type_kwargs={"prompt" :QA_CHAIN_PROMPT}) return qa_chainclass Model_center (): """ 存储问答 Chain 的对象 """ def __init__ (self ): self.chain = load_chain() 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 model_center = Model_center() block = gr.Blocks()with block as demo: with gr.Row(equal_height=True ): with gr.Column(scale=15 ): gr.Markdown("""<h1><center>InternLM</center></h1> <center>书生浦语</center> """ ) with gr.Row(): with gr.Column(scale=4 ): chatbot = gr.Chatbot(height=450 , show_copy_button=True ) msg = gr.Textbox(label="Prompt/问题" ) with gr.Row(): db_wo_his_btn = gr.Button("Chat" ) with gr.Row(): clear = gr.ClearButton( components=[chatbot], value="Clear console" ) db_wo_his_btn.click(model_center.qa_chain_self_answer, inputs=[ msg, chatbot], outputs=[msg, chatbot]) gr.Markdown("""提醒:<br> 1. 初始化数据库时间可能较长,请耐心等待。 2. 使用中如果出现异常,将会在文本输入框进行展示,请不要惊慌。 <br> """ ) gr.close_all() demo.launch()
运行截图如下:
运行gradio
Langchain+InternLM问答
如图,能够正确地回答知识库中的知识。
问题解决以及 Langchain 调试 我们在遇到奇怪问题的时候,想要调试 Langchain,这个时候可以借助 Langchain 的全局设置设置调试模式,设置方式如下所示:
Debugging | 🦜️🔗 Langchain
1 2 3 from langchain.globals import set_verbose set_verbose(True )
langchain 的调试输出
将应用部署在 OpenXLab 上 计算机网络问答机器人
Sqlite 问题1 OpenXLab 上的 sqlite3 版本低于我们项目用的 Chroma 要求。可参考 Troubleshooting | Chroma (trychroma.com) ,在 requirements.txt
中添加 pysqlite3-binary
,之后加载 sqlite3 库来绕过这个问题。否则就要写脚本在运行时自己安装上更新版本的sqlite3了。下面是修改加载 sqlite3 库的 trick 命令:
1 2 3 __import__ ('pysqlite3' )import sys sys.modules['sqlite3' ] = sys.modules.pop('pysqlite3' )
运行截图 openxlab-deploy
加载模型
部署
运行日志
参考内容
1 . 书生・浦语大模型实战营第三课作业(基础+进阶) - 知乎 (zhihu.com) ↩