基于大语言模型的本地知识库问答(离线部署)

一、前言

知识库问答是一种应用广泛的系统,可以在许多领域发挥重要作用。不过以往的系统通常是基于固定规则、相似度检索或者seq2seq模型,这类系统开发成本较高、修改也较为麻烦,尤其在数据准备过程需要耗费大量精力。

而大语言模型(LLM)的出现打破了这种局面,在LLM的加持下,无论是系统编写还是数据准备上,工作量都大大减少,可以使用百行代码实现非常智能的知识库问答系统。

本文将分享使用开源LLM,完全离线部署知识库问答系统。

二、前置知识

2.1 大语言模型

语言模型是自然语言处理领域中一个非常重要的概念,语言模型是评估一串有序字符是句子的概率的一种模型。比较经典的有Bert系列、GPT系列等。而现在常说的大语言模型更多是指GPT系列的模型。

以往的GPT模型的训练方式非常简单,就是根据一个句子的前n个词,预测第n+1个词。这种看似非常简单的方式,却给GPT带来了非常大的自由度。

2.2 Prompt工程

比如我们可以直接把GPT应用在情感分析上,我们只需要设置一下输入的前n个词,比如:

"这部电影真烂"的情绪是

然后GPT就可以预测下一个词以及下下个词,最后得到结果"消极"。或者我们可以输入:

这部电影真烂, 消极
太好看了,

然后GPT就可以预测出"积极"。或者我们输入的内容还可以更长:

这部电影真烂, 消极
xxxx, 积极
xxxx, 消极
太好看了,

上面这种通过改变输入内容让GPT输出特定结果的方式就叫做prompt工程,通过修改prompt,我们可以让GPT完成更复杂的工作,比如问答、翻译等。下面以翻译为例,我们只需要输入prompt:

中文:不要温顺的走进那个良夜
英文:

这样就可以让GPT输出句子的英文。

2.3 模板

上面我们列举了一些Prompt示例,在实际使用时,输入的Prompt有部分内容是动态修改的,这部分可以用一些占位符来占位,比如:

这部电影真烂, 消极
{sentence},

我们只需要输入句子s,然后用s替换掉prompt中的{sentence}(字符串替换),就得到了最终输入GPT的Prompt,上面这种带有占位符的Prompt就是模板。

2.4 RLHF

早期的GPT是无监督学习,就是前面描述的根据前n个词预测下一个词。而现在主流的类GPT模型都是使用无监督学习+RLHF的方式。

首先无监督学习部分和早期GPT是一样的,做文字接龙或者类似句子接龙等任务。而RLHF则是基于人类反馈的强化学习,在这个过程中会有人类老师帮助GPT纠正输出,让GPT输出更符合人类的对话形式。也正是RLHF造就了ChatGPT。

三、实现原理

3.1 基于检索的问答

在Bert时代,还有一种基于向量相似度的问答系统。这种系统非常简单,但是前期数据的收集需要花费较多的时间,这种系统与本文要讨论的系统有许多相似的地方。

首先我们需要收集大量的问答对,比如:

江西是省会是哪?南昌
北京是南方还是北方?北方
...?xx

收集完成后,使用bert提取问题的特征向量,然后存储到向量数据库。在提问时,提取问题的特征向量,并检索出最相似的问题,并返回对应答案。

3.2 基于大语言模型知识库问答

基于检索的问答系统有几个问题,因为数据需要问答对的形式,因此数据收集需要消耗大量时间。另外回答的内容是固定的,因此输入同一个问题,会得到相同的结果。在某些系统中,这是个优点,但是如果是客服系统则过于机械。

基于大语言模型的知识库问答的也需要借助向量数据库。下面是具体实现步骤:

  1. 收集大量文档数据
  2. 对文档进行拆分,拆分成多个段
  3. 使用Bert等模型将每段文档进行embedding(提取特征向量)并存储到数据库
  4. 根据问题检索到k段最相关的文档
  5. 将文档注入Prompt,利用大语言模型回答问题。

首先是数据收集,我们不再需要问答对形式,只需要处理干净的文档形式,这样就减少了大量的工作。

因为文档通常比较大,而Bert模型(在这里也称为Embedding模型)有上下文长度的限制,因此需要将文档拆分成Embedding模型限制的大小。

而提取embedding和存储向量数据库部分则和3.1是一样的。因为文档有上下文大小的限制,同时某一个问题可能出现在文档的多个位置,因此检索时返回k个相关文档段。

最后利用Prompt工程+LLM完成最后的问答。下图是取自Llamindex的一张图:

四、代码实现

4.1 LLM

首先需要选择一个LLM,现在LLM百花齐放,可以选择的开源方案有很多,包括ChatGLM、Llama2等。这里选择Llama2作为LLM。而Llama2的部署方式也是多种多样,这里使用llamacpp部署,我们需要安装llama-cpp-python模块:

python 复制代码
pip install llama-cpp-python

另外还需要编译转换Llama2的模型文件。具体方式参考: www.bilibili.com/video/BV1m3...

然后只需要编写下面的代码就可以运行llama2:

python 复制代码
from llama_cpp import Llama  
  
model_path = "llama-2-13b-chat.Q4_0.gguf"  
llm = Llama(  
    model_path=model_path,  
    n_ctx=2048,  
    chat_format="llama-2"  
)  
response = llm('Human:你好啊\nAssistant:', stop=['Human:'])  
print(response['choices'][0]['text'])

4.2 文档处理

文档处理对应3.2中的前三步,我们可以使用langchain来完成这步操作,代码如下:

python 复制代码
import os  
from langchain.document_loaders import DirectoryLoader  
from langchain.text_splitter import CharacterTextSplitter  
from langchain.embeddings.huggingface import HuggingFaceEmbeddings  
from langchain.vectorstores import Chroma  
  
# 加载embedding  
embedding_model_dict = {  
    "ernie-tiny": "nghuyong/ernie-3.0-nano-zh",  
    "ernie-base": "nghuyong/ernie-3.0-base-zh",  
    "text2vec": "GanymedeNil/text2vec-large-chinese",  
    "text2vec2": "uer/sbert-base-chinese-nli",  
    "text2vec3": "shibing624/text2vec-base-chinese",  
}  
  
  
def load_documents(directory="documents"):  
    """  
    加载books下的文件,进行拆分  
    :param directory:  
    :return:  
    """  
    loader = DirectoryLoader(directory)  
    documents = loader.load()  
    text_spliter = CharacterTextSplitter(chunk_size=256, chunk_overlap=0)  
    split_docs = text_spliter.split_documents(documents)  
    return split_docs  
  
  
def load_embedding_model(model_name="text2vec3"):  
    """  
    加载embedding模型  
    :param model_name:  
    :return:  
    """  
    encode_kwargs = {"normalize_embeddings": False}  
    model_kwargs = {"device": "cuda:0"}  
    return HuggingFaceEmbeddings(  
        model_name=embedding_model_dict[model_name],  
        model_kwargs=model_kwargs,  
        encode_kwargs=encode_kwargs  
    )  
  
  
def store_chroma(docs, embeddings, persist_directory="VectorStore"):  
    """  
    讲文档向量化,存入向量数据库  
    :param docs:  
    :param embeddings:  
    :param persist_directory:  
    :return:  
    """  
    db = Chroma.from_documents(docs, embeddings, persist_directory=persist_directory)  
    db.persist()  
    return db  
  
  
# 加载embedding模型  
embeddings = load_embedding_model('text2vec3')  
# 加载数据库  
if not os.path.exists('VectorStore'):  
    documents = load_documents()  
    db = store_chroma(documents, embeddings)  
else:  
    db = Chroma(persist_directory='VectorStore', embedding_function=embeddings)  

我们在当前目录下准备一个documents文件夹,在里面放入我们的txt文档即可。在langchain里面内置了包括json、csv、PDF等文档处理的类,这里可以根据自己的需求修改load_documents函数。这里需要注意两个参数:

python 复制代码
text_spliter = CharacterTextSplitter(chunk_size=256, chunk_overlap=0)  

上面这句是拆分文档的代码。其中chunk_size是每段的长度,而chunk_overlap则是两个段之间重叠的大小。chunk_size可以根据电脑性能、Embedding模型上下文限制、LLM上下文限制来确定。而chunk_overlap可以选0.1或0.5*chunk_size。

另外上面加载的模型都是中文的Embedding,如果有其它语言需求,可以选择用多语言的Bert作为Embedding。具体参考sentence-transformers可用的模型。

4.3 将文档注入Prompt

在输入前,需要先检索相关的文档,比如我存储了"塞尔达王国之泪"的攻略,使用下面代码搜索"究极手"相关内容:

python 复制代码
docs = db.similarity_search('究极手是干啥用的', k=5)  
for doc in docs:  
    print(doc)

得到如下输出:

python 复制代码
page_content='尝试用右手打开神殿大门但以失败告终,这是一个自称"劳鲁"的灵体出现在了林克的身后。劳鲁告诉林克需要去岛上的神庙中回复手臂的力量才能打开大门。\n\n来到神殿旁的第一个神庙。\n\n【乌可乌侯神庙---创造之力】\n\n进入神庙与劳鲁对话,获得第一个能力---究极手。\n\n用究极手能力抓取板子搭在两个平台之间,然后再通过。\n\n来到第二个平台后,通过的方法大致相同,不过需要将旁边两块板子拼合成更长一点的长板。\n\n来到神龛前的平台,用一旁的铁钩和木板组合成一个简单移动平台,接着将移动平台挂在铁轨上,站在上面抵达对面。' metadata={'source': 'documents\\王国之泪游侠攻略.txt'}
page_content='还有一个神庙在雪山上。\n\n神庙附近有个呀哈哈,追上发光的物体即可抓到。\n\n接下来我们按照逆时针方向,把初始空岛探索一遍。(顺指针走也行,但是逆时针走对新手来说是更好的体验)\n\n继续往西走有两条路。\n\n可以在神庙下方直接用木板搭桥过去。\n\n也可以到附近的山头上,组合铁钩和木板,从铁轨滑过去。\n\n第3页:初始空岛\n\n西部\n\n初始空岛\n\n西部\n\n来到初始空岛西部,与劳鲁对话。对话木匠魔像学习砍树,旁边树桩上拿到斧子。\n\n用斧子砍树得到圆木,圆木可以组合起来作为立足处,也可以进一步劈开做成木柴。' metadata={'source': 'documents\\王国之泪游民攻略(上).txt'}
page_content='回湖边想办法渡水,因为风是向南刮的,把帆插在木筏上,就可以顺风过去,抵达第二个神庙。(有种孙悟空出海找寻菩提祖师的感觉哈哈)\n\n第4页:初始空岛\n\n伊恩伊萨神庙\n\n初始空岛\n\n伊恩伊萨神庙\n\n走上楼梯,进入神庙。\n\n拾起双手剑。\n\n使用余料建造能力,把石头安装到双手剑上,组合成石锤。(余料建造能力也有多种玩法,也可以给自己的盾上安装东西)\n\n用石锤就可以砸开挡路的石块了。\n\n看左边水池,高处有个宝箱,用石锤打碎石柱即可拿到宝箱。' metadata={'source': 'documents\\王国之泪游民攻略(上).txt'}
page_content='把立方块放到弹射器上,然后踩按钮,立方块就会被往上弹射。\n\n站在立方块上,用时光倒流上升,然后就能滑翔到终点了。\n\n神庙北面的路通往迷雾森林,暂时进不去以后再说。路上齐洛利森林有个深穴,通往地底。\n\n第153页:奥尔汀地区\n\n大妖精之泉(蒂拉)\n\n奥尔汀地区\n\n大妖精之泉(蒂拉)\n\n回到驿站,把两个轮子粘上,帮乐团修好车。\n\n给马装上牵引挽具,骑马拖着车带他们去大妖精之泉。\n\n乐团唤醒蒂拉后,她会为你标出另外三个大妖精之泉的位置。' metadata={'source': 'documents\\王国之泪游民攻略(上).txt'}
page_content='进入神庙发现黑漆漆的,可以把之前获得的采矿套装穿上照亮身旁。不过最好使的还是光亮花,建议多射几个把整个迷宫照亮。\n\n进入迷宫先右拐,用究极手拿出右侧墙壁上的石块,里面有个宝箱,用究极手把宝箱抓出来。\n\n继续往前走,岔路口右拐有个宝箱。\n\n继续往前走,岔路口右拐,把地板揭开有个宝箱,宝箱里拿到小钥匙。\n\n继续往前走,抬头看上面,用通天术上去有个宝箱。\n\n继续往前走,激光后面有个宝箱。\n\n回到入口,用小钥匙开门抵达终点。\n\n第142页:海拉鲁大森林\n\n奇乌悠悠乌神庙\n\n海拉鲁大森林\n\n奇乌悠悠乌神庙' metadata={'source': 'documents\\王国之泪游民攻略(上).txt'}

基于LLM的知识库问答需要一个特殊的Prompt来完成,具体如下如下:

根据下面的上下文(context)内容回答问题。
如果你不知道答案,就回答不知道,不要试图编造答案。
答案最多3句话,保持答案简介。
总是在答案结束时说"谢谢你的提问!"
{context}
问题:{question}

然后需要将文档注入到如下的Prompt里面,代码如下:

python 复制代码
template = """  
根据下面的上下文(context)内容回答问题。  
如果你不知道答案,就回答不知道,不要试图编造答案。  
答案最多3句话,保持答案简介。  
总是在答案结束时说"谢谢你的提问!"  
{context}  
问题:{question}"""  
query = "究极手是干啥用的"  
docs = db.similarity_search(query, k=5)  
context = "\n".join([f"{idx + 1}.{doc.page_content}" for (idx, doc) in enumerate(docs)])  
prompt = template.format(context=context, question=query)  
print(prompt)

最后只需要将这个Prompt输入给LLM即可:

python 复制代码
response = llm(f'Human:{prompt}\nAssistant:', stop=['Human:'])  
print(response['choices'][0]['text'])

最后输出结果如下:

python 复制代码
 嗯,究极手是一种非常实用的能力,可以用于装配和拆解各种物品,以及对盾牌和武器进行合成拆卸。你可以用它来组合材料、砍树、打碎石块等,也可以在各种情况下进行优化和适应。

可以看到回答结果部分是正确的。

五、总结

在LLM的加持下,知识库问答的实现变得非常简单。而且现在有诸如langchain、Llamaindex之类的框架集成了许多与LLM相关的操作。另外现在LLM的开源社区非常活跃,出现了诸如Llama2、mistral-7b等优秀的开源LLM。不管是参数量、资源消耗、训练速度等都降低到在消费级显卡上完成。

chatglm实现方案: github.com/IronSpiderM...

视觉版: github.com/IronSpiderM...

llama2模型下载地址: 链接:pan.baidu.com/s/1UFp-kY3u... 提取码:kxwa

相关推荐
pianmian18 分钟前
python数据结构基础(7)
数据结构·算法
好奇龙猫2 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
sp_fyf_20243 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
香菜大丸3 小时前
链表的归并排序
数据结构·算法·链表
jrrz08283 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time3 小时前
golang学习2
算法
南宫生4 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
懒惰才能让科技进步5 小时前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
Ni-Guvara5 小时前
函数对象笔记
c++·算法
泉崎6 小时前
11.7比赛总结
数据结构·算法