本文将体系分享从零开始搭建本地大模型问答知识库过程中所碰到的问题及其解决方案。
1 概述
如今,搭建大语言问答知识库能接纳的方法重要包罗微调模型、再次练习模型以及增强检索生成(RAG,Retrieval Augmented Generation)三种方案。
而我们的目标是盼望能搭建一个低成本、快速响应的问答知识库。由于微调/练习大语言模型需要泯灭大量资源和时间,因此我们选择利用开源的本地大型语言模型联合RAG方案。颠末测试,我们发现llama3:8b和qwen2:7b这种体量的大语言模型能快速响应用户的提问,比较符合我们搭建问答知识库的需求。
我们花了一段时间,对RAG各个步调的原理细节和改进方案进行了一些研究,下面将按照RAG的步调进行讲解。
1.1 简述RAG原理
首先,我们来讲解一下RAG的原理。假设我们有三个文本和三个问题,如下所示:
- context = [
- "北京,上海,杭州",
- "苹果,橘子,桃子",
- "太阳,月亮,星星"
- ]
- questions = ["城市", "水果", "天体"]
复制代码 接下来利用ollama的mxbai-embed-large模型,把三个文本和三个问题都转化为向量的表现情势,代码如下所示:
- vector = []
- model = "mxbai-embed-large"
- for c in context:
- r = self.engine.embeddings(c, model=model)
- vector += [r]
- print("r =", r)
- '''
- r = [-0.4238928556442261, -0.037000998854637146, ......
- '''
- qVector = []
- for q in questions:
- r = self.engine.embeddings(q, model=model)
- qVector += [r]
- print("r =", r)
- '''
- q = [-0.3943982422351837,
- '''
复制代码 接下来利用numpy模块来编写一个函数,盘算向量的相似度,代码如下所示:
- def cosine_similarity(self, a, b):
- return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
- for i in range(3):
- for j in range(3):
- similar = self.engine.cosine_similarity(qVector[i], vector[j])
- print(f"{questions[i]}和{context[j]}的相似度为:{similar}")
- '''
- 城市和北京,上海,杭州的相似度为:0.6192201604133171
- 城市和苹果,橘子,桃子的相似度为:0.6163859899608889
- 城市和太阳,月亮,星星的相似度为:0.5885895914816769
-
- 水果和北京,上海,杭州的相似度为:0.6260800765574224
- 水果和苹果,橘子,桃子的相似度为:0.6598514846105531
- 水果和太阳,月亮,星星的相似度为:0.619382129127254
-
- 天体和北京,上海,杭州的相似度为:0.5648588692973202
- 天体和苹果,橘子,桃子的相似度为:0.6756043740633552
- 天体和太阳,月亮,星星的相似度为:0.75651740246562
- '''
复制代码 从上面的示例可以看出,盘算出的效果值越大,表现两个文本之间的相似度越高。上述过程是RAG原理的简化版。
有一定基础的都知道,大语言模型本质上就是盘算概率,因此它只能回答练习数据中包含的内容。如果要搭建一个问答知识库,而且该知识库的内容是公开的,那么这些内容很可能已经包含在大模型的练习数据会合。在这种情况下,不需要进行任何额外操作,可以直接利用大模型进行问答,这也是如今大多数人利用大语言模型的普遍方式。
然而,我们要搭建的本地知识库大部分包含私有数据,这些内容并不存在于当前大语言模型的练习数据会合。在这种情况下,可以考虑对大模型进行微调或重新练习,但这种方案既费时又费钱。
因此,更常用的方案是将知识库的内容放入到prompt中,让大语言模型通过我们提供的内容来进行回答。但是该方案存在一个问题,那就是token的长度限制(上下文长度限制)。比如,本地大语言模型中的qwen2:7b,token的最大长度为128k,表现输入的上下文通过AutoTokenizer盘算出的长度不能超过128k,但是正常情况下,知识库的大小超过128k是很正常的。我们不可能把所有的知识库内容都放入到prompt中,因此就有了RAG方案。
RAG的方案步调如下所示:
- 对知识库根据一定的大小进行分片处理。
- 利用embedding大模型对分片后的知识库进行向量化处理。
- 利用embedding大模型对用户的问题也进行向量化处理。
- 把用户问题的向量和知识库的向量数据进行相似度匹配,找出相似度最高的k个效果。
- 把这k个效果作为上下文放入到prompt当中,跟着用户的问题进行提问。
在开头的例子中,context变量相当于知识库的内容,"城市"就相当于用户提出的问题,接着通过相似度盘算,"北京,上海"是最接近的信息,所以接着把该信息作为提问的上下文提供给GPT进行问答。
一个简单的prompt示例如下所示:
- prompt = [
- {
- "role": "user",
- "content": f"""当你收到用户的问题时,请编写清晰、简洁、准确的回答。
- 你会收到一组与问题相关的上下文,请使用这些上下文,请使用中文回答用户的提问。不允许在答案中添加编造成分,如果给定的上下文没有提供足够的信息,就回答"##no##"。
- 不要提供与问题无关的信息,也不要重复。
- > 上下文:
- >>>
- {context}
- >>>
- > 问题:{question}
- """
- }
- ]
复制代码 1.2 利用redis-search盘算相似度
在上述的例子中,是自行编写了一个cosine_similarity函数来盘算相似度,但是在实际的应用场景中,知识库的数据会非常大,这会导致盘算相似度的速率非常慢。
颠末研究发现,能对向量进行存储而且快速盘算相似度的工具有:
- redis-search
- chroma
- elasticsearch
- opensearch
- lancedb
- pinecone
- qdrant
- weaviate
- zilliz
下面分享一个利用redis-search的KNN算法来快速找到最相似的k个内容的方案。
首先搭建redis-search情况,可以利用docker一键搭建,如下所示:
- $ docker pull redis/redis-stack
- $ docker run --name redis -p6379:6379 -itd redis/redis-stack
复制代码 接着编写相关python代码,如下所示:
- # 首先定义一个redis相关操作的类
- import redis
- from redis.commands.search.query import Query
- from redis.commands.search.field import TextField, VectorField
-
- class RedisCache:
- def __init__(self, host: str = "localhost", port: int = 6379):
- self.cache = redis.Redis(host=host, port=port)
-
- def set(self, key: str, value: str):
- self.cache.set(key, value)
-
- def get(self, key: str):
- return self.cache.get(key)
-
- def TextField(self, name: str):
- return TextField(name=name)
-
- def VectorField(self, name: str, algorithm: str, attributes: dict):
- return VectorField(name=name, algorithm=algorithm, attributes=attributes)
-
- def getSchema(self, name: str):
- return self.cache.ft(name)
-
- def createIndex(self, index, schema):
- try:
- index.info()
- except redis.exceptions.ResponseError:
- index.create_index(schema)
-
- def dropIndex(self, index):
- index.dropindex(delete_documents=True)
-
- def hset(self, name: str, map: dict):
- self.cache.hset(name=name, mapping=map)
-
- def query(self, index, base_query: str, return_fields: list, params_dict: dict, num: int, startnum: int = 0):
- query = (
- Query(base_query)
- .return_fields(*return_fields)
- .sort_by("similarity")
- .paging(startnum, num)
- .dialect(2)
- )
- return index.search(query, params_dict)
-
- # 初始化类
- redis = RedisCache()
- # 定义一个文本类型,用来储存知识库内容
- info = TextField("info")
- # 建立一个向量类型,使用HNSW算法
- name="embedding"
- algorithm="HNSW"
- # DIM计算方法
- r = self.engine.embeddings(q, model=model)
- DIM = len(r)
- attributes={
- "TYPE": "FLOAT32",
- "DIM": DIM,
- "DISTANCE_METRIC": "COSINE"
- }
- embed = VectorField(
- name=name, algorithm=algorithm, attributes=attributes
- )
- # 创建索引
- scheme = (info, embed)
- index = redis.getSchema(self.redisIndexName)
- redis.createIndex(index, scheme)
-
- # 接着把所有知识库内容进行向量化后储存进redis中
- def insertData(self, model):
- j = 0
- for file in self.filesPath:
- for i in file.content:
- # i就是分片后的知识库内容
- embed = self.engine.embeddings(i, model = model)
- emb = numpy.array(embed, dtype=numpy.float32).tobytes()
- im = {
- "info": i,
- "embedding": emb,
- }
- name = f"{self.redisIndexName}-{j}"
- j += 1
- self.redis.hset(name, im)
-
- # 查询
- # 取最接近的10个结果
- k = 10
- base_query = f"* => [KNN {k} @embedding $query_embedding AS similarity]"
- return_fields = ["info", "similarity"]
- # question为用户的问题
- qr = self.engine.embeddings(question, model = model)
- params_dict = {"query_embedding": np.array(qr, dtype=np.float32).tobytes()}
- index = self.redis.getSchema(self.redisIndexName)
- result = self.redis.query(index, base_query, return_fields, params_dict, k)
- for _, doc in enumerate(result.docs):
- # 查看相似度值和上下文内容
- print(doc.info, doc.similarity)
复制代码 2 RAG知识库难点
在相识了RAG的原理后,我们就可以尝试编写相关代码,搭建一个本地问答知识库。但是在我们开始举措后,就会发现事变并不会按照我们所预料的发展。
2.1 难点一:大语言模型能力不敷
在我们的假想中,问答知识库是如许工作的:
- 根据用户的提问,在知识库中找到最相似k个的内容。
- 把这k个内容作为上下文,提供给大模型。
- 大模型根据用户提问的上下文,给出正确的答案。
但是在实际应用的过程中,程序却无法按照我们的意愿运行,首先是问答的大语言模型能力不敷,究竟我们利用的是qwen2:7b这种小体积的大模型,无法和GPT4这类的大模型相比较。但是修改一下提问的方式大概上下文和问题之间的顺序,照旧能比较好的到达我们预期的效果。
但是更重要的是embed大模型的能力同样存在不敷,这里用开头例子进行阐明,我们把context的内容简单修改一下,如下所示:
- context = [
- "北京,上海,杭州",
- "苹果,橘子,梨",
- "太阳,月亮,星星"
- ]
复制代码 然后再盘算一次相似度,如下所示:
- 城市和北京,上海,杭州的相似度为:0.6192201604133171
- 城市和苹果,橘子,梨的相似度为:0.6401285511286077
- 城市和太阳,月亮,星星的相似度为:0.5885895914816769
-
- 水果和北京,上海,杭州的相似度为:0.6260800765574224
- 水果和苹果,橘子,梨的相似度为:0.6977096659034031
- 水果和太阳,月亮,星星的相似度为:0.619382129127254
-
- 天体和北京,上海,杭州的相似度为:0.5648588692973202
- 天体和苹果,橘子,梨的相似度为:0.7067548826946035
- 天体和太阳,月亮,星星的相似度为:0.75651740246562
复制代码 我们发现,"城市"竟然和"苹果,橘子,梨"是最相似的。固然这只是一个简单的例子,但是在实际的应用中经常会碰到这类的情况,通过用户提问的内容找到的知识库中最相似的内容有可能跟问题并不相关。
这是否是本地embed大模型的问题?OpenAI的embed大模型效果如何?接着我们找了一些本地embed大模型和OpenAI的text-embedding-3-small、text-embedding-ada-002、text-embedding-3-large进行一个简单的测试,判断通过问题找到的最相似的知识库上下文,是否是预期的内容。
最终的效果是,不管是本地的大模型照旧OpenAI的大模型,成功率都在50%-60%之间。embed大模型的能力差是普遍存在的问题,并不仅仅是本地embed大模型的问题。
该问题颠末我们一段时间的研究,找到了“将就”能用的解决方案,将会在后文细说。
2.2 难点二:提问的复杂性
理想中的问答知识库是能处理复杂问题,而且能进行多轮对话,而不是一轮提问就竣事。
以Seebug Paper作为知识库,提出的问题具体到某一篇文章,而且多轮对话之间的问题是相互独立的,这种情况是最轻易实现的。比如:
- user: 帮我总结一下CVE-2024-22222漏洞。
- assistant: ......
- user: CVE-2023-1111漏洞的危害如何?
- assistant: ......
- ......
复制代码 但是想要让问答知识库成为一个好用的产物,不可能把目标仅限于此,在实际的应用中还会产生多种复杂的问题。
1.范围搜索性提问
参考以下几种问题:
2024年的文章有哪些?
CTF相关的文章有哪些?
跟libc有关的文章有哪些?
…
拿第一个问题举例,问题为:2024年的文章有哪些?。
接着问答知识库的流程为:
- 通过embed模型对问题进行向量化。
- 通过redis搜索出问题向量数据最接近的k个知识库内容。(假设k=10)
- 然后把这10个知识库内容作为上下文,让GPT进行回答。
如果按照上面的逻辑来进行处理,最优的情况就是,问答大模型根据提供的10个知识库上下文成功的回答了10篇2024年的文章。
如许问题就产生了,如果2024年的文章有20篇呢?大概我们可以提高k值的大小,但是k值提高的同时也会增长运算时间。固然看似暂时解决了问题,但是却产生了新问题,k值提高到多少?提高到20?那如果2024年的文章有50篇。提高到100?那如果2024年的文章如果只有1篇,该方案就浪费了大量运算时间,同时可能会超过大语言模型的token限制,如果利用的是商业GPT(比如OpenAI),那么token就是款项,这就浪费了大量资金。如果利用的是开源大语言模型,token同样有限制,而且在token增长的同时,大语言模型的能力也会相应的降落。
2.多轮对话
参考以下多轮提问:
user: 2024年的文章有哪些?
assistant: …
user: 还有吗?
问答知识库在处理第二个提问的流程为:
- 通过embed模型对"还有吗?"问题进行向量化。
- 通过redis搜索出问题向量数据最接近的k个知识库内容。(假设k=10)
- …
问题产生了,根据"还有吗?"搜索出的相似上下文会是我们需要的上下文吗?根本上不可能是。
该问题颠末我们一段时间的研究,同样是找到了“将就”能用,但是并不优雅的解决方案,将会在后文细说。
2.3 难点三:文本的处理
在利用Seebug Paper搭建问答知识库的过程中,发现两个问题:
- 文章中的图片怎么处理?
- 文章长度一般在几k到几十k之间,因此是需要分片处理的,那么如何分片?
关于图片处理委曲还有一些解决方案:
- 利用OCR辨认将图片转换为笔墨。(效果不太好,因为有些图片重要的不是笔墨。)
- 利用llava这类的大模型来对图片进行形貌和概括(llava效果不太好,不外gpt4的效果会好很多)。
- 直接加上如下图所示,而且在prompt中告诉GPT,让其需要用到图片时,直接返回图片的链接。
但是分片的问题却不是很利益理,如果要进行分片,那么如何分片呢?研究了llama_index和langchain框架,根本都是根据长度来进行分片。
比如在llama_index中,对数据进行分片的代码如下所示:
- from llama_index import SimpleDirectoryReader
- from llama_index.node_parser import SimpleNodeParser
-
- documents = SimpleDirectoryReader(input_dir="./Documents").load_data()
- node_parser = SimpleNodeParser.from_defaults(chunk_size=514, chunk_overlap=80)
- nodes = node_parser.get_nodes_from_documents(documents)
复制代码 在上面的示例代码中,设置了chunk_size的值为514,但是在Paper中,经常会有内嵌的代码段长度大于514,如许就把一段相关联的上下文分在了多个差别的chunk当中。
假设一个相关联的代码段被分成了chunk1和chunk2,但是根据用户问题搜索出相似度前10的内容中只有chunk2并没有chunk1,最终GPT只能获取到chunk2的上下文内容,缺失了chunk1的上下文内容,如许就无法做出精确的回答。
不仅仅是代码段,如果只是通过长度进行分片,那么有可能相关联的内容被分成了多个chunk。
3 将就能用的解决方案
下面分享一些针对上面提出的难点研究出的解决方案,但是仅仅只是将就能用的方案,都是通过时间换正确率,暂时没找到完美的解决方案。
3.1 rerank模型
通过研究QAnything项目,发现可以利用rerank模型来提拔embed模型的正确率。
举个例子,比如你想取前10相似的内容作为提问的上下文。那么可以通过redis获取到前20相似的内容,接着通过rerank模型对这20个内容进行重打分,最后获取前10分数最高的内容。相关代码示例如下所示:
- from BCEmbedding import RerankerModel
-
- # 初始化rerank模型
- rerankModel = RerankerModel(model_name_or_path="maidalun1020/bce-reranker-base_v1", local_files_only=True)
-
- ......
- # 搜索出知识库中与用户问题相似度最高的前20个内容
- k = 20
- result = self.redis.query(index, base_query, return_fields, params_dict, k)
- passages = []
- for _, doc in enumerate(result.docs):
- passages += [doc.info]
- # 根据用户提问,对这20个内容进行重打分
- rerank_results = rerankModel.rerank(question, passages)
- info = rerank_results["rerank_passages"]
- last_result = info[:10]
复制代码 rerank模型本质上就是练习出了一个专门打分的大语言模型,让该模型对问题和答案进行打分。该方案一定程度上可以提拔搜索出内容的正确度,但是仍旧无法完美解决难点一的问题。
3.2 上下文压缩
通过研究LLMLingua和langchain项目,发现了上下文压缩方案。
在上面的例子中,都是以k=10来举例的,那么k的值便是多少才合适呢,这需要根据知识库分片的大小和大语言模型的能力来调整。
首先,要求上下文的长度加上prompt的内容不能超过大语言模型token长度的限制。其次,根本所有的大语言模型都会随着上下文的增长导致能力降落。所以需要根据利用的大语言模型找到一个长度阙值,来设置k的大小。
在实际应用中,会发现前k个上下文中可能大部分内容都跟用户的提问无关,因此可以利用上下文压缩技术,去除无用的内容,减小上下文体积,增长k值大小。
由于LLMLingua项目利用的模型有点大,跑起来费时费电(跑不起来),所以根据其原理,实现了一个低级版本的压缩代码,如下所示:
- def compress(self, question: str, context: list[str], maxToken: int = 1024) -> list[str]:
- template = f"下面将会提供问题和上下文,请判断上下文信息是否和问题相关,如果不相关,请回复##no##,如果相关,请提取出和上下文相关的内容。*注意*:请直接提取出上下文的关键内容,请*不要*自行发挥,*不要*进行任何修改或者压缩删减操作。\n\n> 问题:{question}\n> 上下文:\n>>>\n%s\n>>>"
- result = []
- for c in context:
- qs = template%c
- answer = self.engine.chat(qs)
- # print(c, answer)
- # input()
- if "##no##" not in answer:
- result += [answer]
- newContent = "\n".join(result)
- question = f"你是一个去重机器人,下面将会提供一组上下文,请你对上下文进行去重处理。*注意*,请*不要*自行发挥,*不要*进行任何添加修改,请直接在上下文内容中进行去重。\n上下文:>>>\n{newContent}\n>>>"
- answer = self.engine.chat(question)
- return answer
复制代码 由于有了上下文压缩方案,我们可以考虑设置k的值为一个非常大的值,然后分析盘算出的相似度的值,比如我发如今我的案例中,redis搜索效果相似度大于0.4的内容就完全跟提问无关,rerank重打分分数小于0.5的内容完全跟提问无关,所以可以做出以下修改:
- k = 1000
- ......
- passages = []
- for _, doc in enumerate(result.docs):
- # 只取相似度小于0.4的内容
- if float(doc.similarity) > 0.4:
- break
- passages += [doc.info]
- rerank_results = rerankModel.rerank(questionHistory, passages)
- info = rerank_results["rerank_passages"]
- score = rerank_results["rerank_scores"]
- contexts_list = []
- for i in range(len(info)):
- # 只取重打分后分数大于0.5的
- if float(score[i]) > 0.5:
- contexts_list += [info[i]]
- else:
- break
- # 上下文压缩
- contexts = self.compress(question, contexts_list)
复制代码 通过以上方案,一定程度上解决了范围性搜索提问的困难。同样,我们可以尝试探求一个相似度分数的阙值,来解决多轮对话的困难,因为"还有吗?"这类的多轮对话问题和知识库的相关性非常低,得到的分数都会非常低。如许当我们最终获取到的上下文内容为空时,表明当前为多轮对话,再按照对轮对话的逻辑进行处理。
3.3 知识库分片处理
颠末研究,如今没找到完美的分片方案,我以为针对差别格式的知识库设计针对性的分片方案会比较好。
针对Seebug Paper的情况,我们考虑根据一级标题来进行分片,每个chunk中还需要包含当前文章的基础信息,比如文章名称。如果有代码段,则需要根据token的大小来进一步分片。
在一些框架中,差别chunk之前会有一部分重叠内容,但是我们研究后发现这种处理方案不会让最终的效果有较大的提拔。
颠末研究,我们发现固定格式的文档是最佳的知识库素材,例如漏洞应急简报,每篇简报的内容大小适中,而且接纳Markdown格式便于匹配和处理。我们能根据漏洞概述、漏洞复现、漏洞影响范围、防护方案、相关链接来进行分片,每部分的相关性都不大。
4 总结
我们期望的问答知识库是大语言模型能根据我们提供的知识库快速、正确的回答用户的提问。如今来看是照旧不存在理想中的问答知识库,一方面是由于大语言模型能力的限制,在当前的大语言模型中,快速响应和精准响应照旧一对反义词。
利用embed大语言模型探求相关文档的正确率太低,大部分的优化方案都是通过时间调换正确率。所以照旧寄盼望于生成式大语言模型将来的发展,是否能告竣真人工智能。
如今的问答知识库和RAG类的框架效果相差不大,都是属于先把框架建好,把大语言模型分割开来,能随意替换各类大语言模型,因此这类框架的能力取决于利用的是什么大语言模型。相当于制作一个机器人,把身段都给搭建好了,但是还缺少一颗良好的脑子。
如何学习大模型
如今社会上大模型越来越遍及了,已经有很多人都想往这里面扎,但是却找不到得当的方法去学习。
作为一名资深码农,初入大模型时也吃了很多亏,踩了无数坑。如今我想把我的经验和知识分享给你们,资助你们学习AI大模型,能够解决你们学习中的困难。
下面这些都是我当初辛苦整理和花钱购买的资料,如今我已将重要的AI大模型资料包罗市面上AI大模型各大白皮书、AGI大模型体系学习路线、AI大模型视频教程、实战学习,等录播视频免费分享出来,需要的小同伴可以扫取。
一、AGI大模型体系学习路线
很多人学习大模型的时候没有方向,东学一点西学一点,像只无头苍蝇乱撞,我下面分享的这个学习路线盼望能够资助到你们学习AI大模型。
二、AI大模型视频教程
三、AI大模型各大学习册本
四、AI大模型各大场景实战案例
五、竣事语
学习AI大模型是当前科技发展的趋势,它不仅能够为我们提供更多的机会和挑战,还能够让我们更好地理解和应用人工智能技术。通过学习AI大模型,我们可以深入相识深度学习、神经网络等焦点概念,并将其应用于自然语言处理、盘算机视觉、语音辨认等领域。同时,掌握AI大模型还能够为我们的职业发展增添竞争力,成为将来技术领域的领导者。
再者,学习AI大模型也能为我们自己创造更多的价值,提供更多的岗位以及副业创收,让自己的生存更上一层楼。
因此,学习AI大模型是一项有远景且值得投入的时间和精力的重要选择。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |