背景
本文主要介绍一下,基于Langchain与Vicuna-13B的外挂OceanBase知识库项目实战以及QA使用,项目地址:
github.com/csunny/DB-G…
在开始之前,我们还是先看看效果~
自Meta发布LLaMA大模型以来, 围绕LLaMA微调的模型也是层出不穷。 从alpaca 到 vicuna,再到刚刚发布的中医垂直领域微调大模型华佗GPT, 可谓是风光无限。 但其中最出名、效果最好的当属vicuna-13B。如下图所示,当前在众多大模型当中,Vicuna-13B的效果非常接近ChatGPT,有其92%的效果。 这意味着什么呢? 意味着,我们基于开源的Vicuna-13B即可搞定决大多数的任务与需求。 当然什么外挂知识库QA这样的简单需求自然不在话下。
那Langchain又是什么呢?
毫无疑问,Langchain是目前大语言模型领域最炙手可热的LLM框架。
LangChain 是一个构建在LLM之上的应用开发框架。想让应用变得更强大,更加不同,单单通过调用大模型的API肯定是不够的, 还需要有以下特性:
- 数据思维: 连接大模型到其他的元数据上。
- 代理思维: 语言模型可以与环境交互。
以上就是Langchain的设计理念, It’s very simple, but enough nature. 是的,足够简单,但很贴近本质。我们也是被Langchain的理念深深的吸引。 所以,我们来了~
方案
既然是一个实战项目,那么在项目开始之前,我们有必要对项目整体的架构做一个清晰的梳理。
添加图片注释,不超过 140 字(可选)
如图所示,是我们整体的架构图。 从图中我们可以看到,左侧有一条线是知识库 -> Embedding -> 向量存储 -> 大模型(Vicuna-13B) -> Generate 的路径。 在我们本文中,就是依赖此路径外挂知识库进行推理、总结,以完成QA的工作。
所以我们整体将以上过程拆分为如下所示的四个步骤。
-
知识库准备: 如同所示中,因为我们是面向DB领域的GPT,所以我们准备了主流数据库的文档,并进行了分类。
-
Embedding: embedding这一步是需要将文本转换成向量进行存储,当然了,存储媒介是向量数据库,关于向量数据库的了解,大家可以从这里了解向量数据库
-
Embedding之后的知识,会存储在向量数据库当中,用于后面的检索。
-
利用大模型的能力,通过ICL(In-Context-Learning) 让大模型实现基于现有知识的推理、总结。
-
这样我们就可以实现一个基于现有知识库QA的项目了。
整个知识库的处理过程,也可以参考Langchain-ChatGLM项目中的一张图。
图片来自Langchain-ChatGLM
代码说明
既然是实战,那肯定少不了代码,毕竟我们一贯坚持的是:
Talk is cheap, show me the code.
模型加载
目前基本主流的模型都是基于HuggingFace的标准,所以模型加载代码其实就变得很简单了。 如下所示,为模型加载类,所需要的参数只需要传一个model_path, 在这个类当中,我们实现了一个方法,loader方法,通过这个方法我们可以获得两个对象。 1. tokenizer 2. model, 根据这两个对象,我们就得到一个模型了,后面的事情,关注使用就可以啦。
class ModelLoader:"""Model loader is a class for model loadArgs: model_path"""kwargs = {}def __init__(self, model_path) -> None:self.device = "cuda" if torch.cuda.is_available() else "cpu"self.model_path = model_path self.kwargs = {"torch_dtype": torch.float16,"device_map": "auto",}def loader(self, num_gpus, load_8bit=False, debug=False):if self.device == "cpu":kwargs = {}elif self.device == "cuda":kwargs = {"torch_dtype": torch.float16}if num_gpus == "auto":kwargs["device_map"] = "auto"else:num_gpus = int(num_gpus)if num_gpus != 1:kwargs.update({"device_map": "auto","max_memory": {i: "13GiB" for i in range(num_gpus)},})else:raise ValueError(f"Invalid device: {self.device}")if "chatglm" in self.model_path:tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True)model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).half().cuda()else:tokenizer = AutoTokenizer.from_pretrained(self.model_path, use_fast=False)model = AutoModelForCausalLM.from_pretrained(self.model_path,low_cpu_mem_usage=True, **kwargs)if load_8bit:compress_module(model, self.device)if (self.device == "cuda" and num_gpus == 1):model.to(self.device)if debug:print(model)return model, tokenizer
知识库准备
准备知识库,没什么特别需要讲的,可以是pdf、txt、md等等的吧。 在这里,我们准备的是一个md文档,知识库是基于开源的OceanBase官方文档。 预备好的知识库地址: OceanBase文档。
注:这里特别说明一下,为什么没有直接下载pdf。 两个原因 1. OB pdf文档有很多的格式,这些格式在向量处理的过程中也会保存下来, 默认处理后的知识没有压扁平,不利于后续的大模型使用。 2. pdf文档相对比较大,在本地跑,通过模型抽向量的过程会比较长,因此我们准备了一个简单的MarkDown文件来做演示。
知识转向量并存储到向量数据库
这里我们实现了一个Knownledge2Vector的类。这个类顾命思意,就是把知识库转换为向量。 当然我们转换成向量之后会持久化到数据库存储。 (问题1: 类名没有体现存数据库,是不是应该在斟酌一下?KnownLedge2VectorStore会更好? 🤔)
class KnownLedge2Vector:"""KnownLedge2Vector class is order to load document to vector and persist to vector store.Args: - model_nameUsage:k2v = KnownLedge2Vector()persist_dir = os.path.join(VECTORE_PATH, ".vectordb") print(persist_dir)for s, dc in k2v.query("what is oceanbase?"):print(s, dc.page_content, dc.metadata)"""embeddings: object = None model_name = LLM_MODEL_CONFIG["sentence-transforms"]top_k: int = VECTOR_SEARCH_TOP_Kdef __init__(self, model_name=None) -> None:if not model_name:# use default embedding modelself.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) def init_vector_store(self):persist_dir = os.path.join(VECTORE_PATH, ".vectordb")print("向量数据库持久化地址: ", persist_dir)if os.path.exists(persist_dir):# 从本地持久化文件中Loadprint("从本地向量加载数据...")vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings)# vector_store.add_documents(documents=documents)else:documents = self.load_knownlege()# 重新初始化vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings,persist_directory=persist_dir)vector_store.persist()return vector_store def load_knownlege(self):docments = []for root, _, files in os.walk(DATASETS_DIR, topdown=False):for file in files:filename = os.path.join(root, file)docs = self._load_file(filename)# 更新metadata数据new_docs = [] for doc in docs:doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} print("文档2向量初始化中, 请稍等...", doc.metadata)new_docs.append(doc)docments += new_docsreturn docmentsdef _load_file(self, filename):# 加载文件if filename.lower().endswith(".pdf"):loader = UnstructuredFileLoader(filename) text_splitor = CharacterTextSplitter()docs = loader.load_and_split(text_splitor)else:loader = UnstructuredFileLoader(filename, mode="elements")text_splitor = CharacterTextSplitter()docs = loader.load_and_split(text_splitor)return docsdef _load_from_url(self, url):"""Load data from url address"""passdef query(self, q):"""Query similar doc from Vector """vector_store = self.init_vector_store()docs = vector_store.similarity_search_with_score(q, k=self.top_k)for doc in docs:dc, s = docyield s, dc
这个类的使用也非常简单, 首先实例化,参数也是只有一个model_name, 需要注意的是,这里的model_name 是转向量的模型,跟我们前面的大模型不是同一个,当然这里能不能是同一个,当然也是可以的。(问题2: 可以思考一下,这里我们为什么没有选择LLM抽向量?)
这个类里面我们干的事情其实也不多,总结一下就那么3件。 1. 读文件(_load_file) 2. 转向量+持久化存储(init_vector_store) 3. 查询(query), 代码整体比较简单,在进一步的细节我这里就不解读了,还是相对容易看明白的。
注: 特别说明一下,我们这里用的抽向量的模型是Sentence-Transformer, 它是Bert的一个变种模型,Bert想必大家是知道的。如果有不太熟悉的同学,可以转到我这边文章,来了解Bert的来龙去脉。Magic:LLM-GPT原理介绍与本地(M1)微调实战
# persist_dir = os.path.join(VECTORE_PATH, ".vectordb")
# print(persist_dir)k2v = KnownLedge2Vector()
for s, dc in k2v.query("what is oceanbase?"):print(s, dc.page_content, dc.metadata)
知识查询
通过上面的步骤,我们轻轻松松将知识转换为了向量。 那么接下来,我们就是根据Query查询相关知识了。
我们定义了一个KnownLedgeBaseQA, 这个类只有短短十几行代码, 所以看起来也不费劲。 核心的方法就一个,get_similar_answer, 这个方法只接收一个query字符串,根据这个query字符串,我们就可以在我们之前准备好的知识库当中,查询到相关知识。
class KnownLedgeBaseQA:def __init__(self) -> None:k2v = KnownLedge2Vector()self.vector_store = k2v.init_vector_store()self.llm = VicunaLLM()def get_similar_answer(self, query):prompt = PromptTemplate(template=conv_qa_prompt_template,input_variables=["context", "question"])retriever = self.vector_store.as_retriever(search_kwargs={"k": VECTOR_SEARCH_TOP_K})docs = retriever.get_relevant_documents(query=query)context = [d.page_content for d in docs] result = prompt.format(context="\n".join(context), question=query)return result
推理&QA
知识都查出来了,剩下的就交给大模型吧。 我们这里使用的是vicuna-13b的模型,具体的示例代码如下,
是的,这里也没什么难的,就是构造一个参数,然后发一个POST,也没啥特别好讲的。
def generate(query):template_name = "conv_one_shot"state = conv_templates[template_name].copy()pt = PromptTemplate(template=conv_qa_prompt_template,input_variables=["context", "question"])result = pt.format(context="This page covers how to use the Chroma ecosystem within LangChain. It is broken into two parts: installation and setup, and then references to specific Chroma wrappers.",question=query)print(result)state.append_message(state.roles[0], result)state.append_message(state.roles[1], None)prompt = state.get_prompt()params = {"model": "vicuna-13b","prompt": prompt,"temperature": 0.7,"max_new_tokens": 1024,"stop": "###"}response = requests.post(url=urljoin(VICUNA_MODEL_SERVER, vicuna_stream_path), data=json.dumps(params))skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("</s>") * 3for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"):if chunk:data = json.loads(chunk.decode())if data["error_code"] == 0:output = data["text"][skip_echo_len:].strip()state.messages[-1][-1] = output + "▌"yield(output)
最后,让我们看看知识问答的效果吧。如果觉得效果好,为我们点个赞吧👍
小结
综上所属,我们讲了当前开源主流的两个扛把子强强联合的应用实战。 Vicuna-13B与Langchain在整个AI的生态里面,做的是完全不同的事情。 一个是定义框架做标准跟链接, 一个是深入核心做技术跟效果。很显然,这两条路都获得了重大的成功。 整个发展的思路,我相信很值得我们借鉴,通过本文的介绍,希望能对大家有一些帮助。
最后,如果你觉得本教程里面的内容对你有帮助,并且想持续关注我们的项目,请帮忙在GitHub给我们的项目点个赞吧❤️💗💗😊😊😊。 项目地址: github.com/csunny/DB-G…
当然了,如你开篇所见,这仅仅是我们项目里面很小的一部分,同时这也会是一个系列教程。 如果关心我们的项目,或者对我们的工作感兴趣,欢迎持续关注我们。