用RAG技术让大模型开卷考试,建立私人数据库

最近一段时间在研究大模型Agent的项目,其中包含一个技术叫做RAG,如果你不是很清楚原理,此文可带你从零理解RAG基本原理,并且构造一个自己的知识数据库。

ps:注意此文的所有代码都只适用于学习原理,切忌部署于生产环

什么是RAG?

检索增强生成(RAG) 是 一种使用来自私有或专有数据源的信息来辅助文本生成的技术 。

它将检索模型(设计用于搜索大型数据集或知识库)和生成模型(例如大型语言模型(LLM),此类模型会使用检索到的信息生成可供阅读的文本回复)结合在一起。

为什么需要RAG?

LLM会产生误导性的 “幻觉”,依赖的信息可能过时,处理特定知识时效率不高,缺乏专业领域的深度洞察,同时在推理能力上也有所欠缺。

正是在这样的背景下,检索增强生成技术(Retrieval-Augmented Generation,RAG)应时而生,成为 AI 时代的一大趋势。

代码原理:

tinyRAG  
├─ build.ipynb  
├─ component  
│  ├─ chain.py  
│  ├─ databases.py  
│  ├─ data_chunker.py  
│  ├─ embedding.py  
│  └─ llms.py  
├─ data  
│  ├─ dpcq.txt  
│  ├─ README.md  
│  └─ 中华人民共和国消费者权益保护法.pdf  
├─ database  
├─ image  
│  └─ 5386440326a2c9c5a06b5758484d375.png  
├─ README.md  
├─ requirements.txt  
└─ webdemo_by_gradio.ipynb  

component目录是RAG的组件,分为五大部分(数据切分,数据向量化,数据向量存储,大模型,链)

data目录用于存放需要嵌入的文件(兼容Pdf TXT,md文件)

database目录用于存放向量化后的数据,也是数据库的加载路径

build.ipynb构建向量数据库

webdemo_by_gradio使用gradio基于嵌入的文件调用OpenAI的回答助手

GITHUB链接:

https://github.com/phbst/tinyRAG

数据切分

这段代码的主要功能是将文档进行切分和编码,然后存储为一个字块列表。这个列表将作为我们后续构建知识库的基础。这里的字块长度我们设置为600,重复片段长度设置为150。

设置重复片段长度是为了确保在切分文档时,关键信息不会被隔断。当一个字块达到最大长度时,我们会从这个字块的后面开始新的一个字块,但是 新增的字块会包含前一个字块的末尾部分 ,这个部分的长度就是我们设定的重复长度。这样做的目的是为了防止字块切分时,关键信息被切分在两个字块的边界,从而导致在检索时无法完整地获取到这部分信息。

import os  
import PyPDF2  
import tiktoken  
  
# 用于数据切分时,判断字块的token长度,速度比较快  
enc = tiktoken.get_encoding("cl100k_base")  
  
class ReadFile:  
  
    #传入文件夹路径  
    def __init__(self, path):  
        self.path = path  
  
    #读取初始化类时传入的文件夹路径,返回该文件夹路径下所有文件的路径  
    def readlist(self):  
        file_list = []    
        for filepath, dirnames, filenames in os.walk(self.path):  
            # os.walk 函数将递归遍历指定文件夹  
            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))  
                    elif filename.endswith(".pdf"):  
                        file_list.append(os.path.join(filepath, filename))      
  
        return file_list  
  
    # 切分数据,传入一个字符串,返回一个字块列表,这里最大长度设置了600,为了避免关键信息被切分  
    @classmethod  
    def chunk_content(cls, text: str, max_token_len: int = 600, cover_content: int = 150):  
        chunk_text = []  
        curr_len = 0  
        curr_chunk = ''  
        lines = text.split('\n')  
        for line in lines:  
            line = line.replace(' ', '')  
            line_len = len(enc.encode(line))  
            if curr_len + line_len <= max_token_len:  
                curr_chunk += line  
                curr_chunk += '\n'  
                curr_len += line_len  
                curr_len += 1  
            else:  
                chunk_text.append(curr_chunk)  
                curr_chunk = curr_chunk[-cover_content:]+line  
                curr_len = line_len + cover_content  
        if curr_chunk:  
            chunk_text.append(curr_chunk)  
        return chunk_text  
  
    #读取文件内容,传入一个文件路径,返回该文件内容字符串  
    @classmethod  
    def read_file_content(cls, file_path: str):  
        if file_path.endswith('.pdf'):  
            return cls.read_pdf_content(file_path)  
        elif file_path.endswith('.md'):  
            return cls.read_md_content(file_path)  
        elif file_path.endswith('.txt'):  
            return cls.read_txt_content(file_path)  
  
    @classmethod  
    def read_md_content(cls, file_path: str):  
        with open(file_path, 'r', encoding='utf-8') as f:  
            return f.read()  
  
    @classmethod  
    def read_pdf_content(cls, file_path: str):  
        text=""  
        with open(file_path, 'rb') as f:  
            reader=PyPDF2.PdfReader(f)  
            for num_page in range(len(reader.pages)):  
                text+=reader.pages[num_page].extract_text()  
        return text  
  
    @classmethod  
    def read_txt_content(self, file_path: str):  
        with open(file_path, 'r', encoding='utf-8') as f:  
            return f.read()  
  
  
    #该类的整合函数,根据初始化类时的文件夹路径,读取所有内容进行切分,返回一个字块列表  
    def get_all_chunk_content(self,max_len:int=600,cover_len:int=150):  
        docs=[]  
        for file in self.readlist():  
  
            content=self.read_file_content(file)  
  
            chunk_content=self.chunk_content(content,max_len,cover_len)  
  
            docs.extend(chunk_content)  
  
        return docs  

数据向量化

下面的代码实现了数据的向量化。它包含四个类,每个类代表一种不同的向量化方法:

HuggingFace的模型(HFembedding)、OpenAI的模型(OpenAIembedding)、ZhipuAI的模型(Zhipuembedding)以及JinaAI的模型(Jinaembedding)。

每个类中都包含以下方法: 初始化模型 、 将字符串编码成向量 、 求取两个字符串的相似度 、 以及求取两个向量的相似度(余弦相似度) 。其中,字符串的相似度是通过将字符串转化为向量,再计算这两个向量的余弦相似度实现的。

## 这个组件,能下载的embedding模型都使用离线的embedding,不能下载的就使用api  
  
import numpy as np  
from transformers import AutoModel  
from numpy.linalg import norm  
from langchain.embeddings.openai import OpenAIEmbeddings  
from zhipuai import ZhipuAI  
from langchain.embeddings.huggingface import HuggingFaceEmbeddings  
import os  
from typing import List  
  
#Embedding类  
class HFembedding:  
  
#初始化embedding,如果是离线模型就下载下来,传入模型路径  
    def __init__(self, path:str=''):  
        self.path = path  
        self.embedding=HuggingFaceEmbeddings(model_name=path)  
  
#对字符串进行编码,传入字符串,输出一个向量  
    def get_embedding(self,content:str=''):  
        return self.embedding.embed_query(content)  
  
#对两个字符串求相似度,使用embedding模型进行编码,再使用编码后的向量进行余弦相似度求值   
    def compare(self, text1: str, temxt2: str):  
        embed1=self.embedding.embed_query(text1)   
        embed2=self.embedding.embed_query(text2)  
        return np.dot(embed1, embed2) / (np.linalg.norm(embed1) * np.linalg.norm(embed2))  
  
#对两个向量进行相似度求值,余弦相似度求值   
    def compare_v(cls, vector1: List[float], vector2: List[float]) -> float:  
        dot_product = np.dot(vector1, vector2)  
        magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)  
        if not magnitude:  
            return 0  
        return dot_product / magnitude  
  
class OpenAIembedding:  
  
    def __init__(self, path:str=''):  
        self.path = path  
        self.embedding=OpenAIEmbeddings()  
  
    def get_embedding(self,content:str=''):  
        content = content.replace("\n", " ")  
        return self.embedding.embed_query(content)  
  
    def compare(self, text1: str, text2: str):  
        embed1=self.embedding.embed_query(text1)   
        embed2=self.embedding.embed_query(text2)  
        return np.dot(embed1, embed2) / (np.linalg.norm(embed1) * np.linalg.norm(embed2))  
  
    def compare_v(cls, vector1: List[float], vector2: List[float]) -> float:  
        dot_product = np.dot(vector1, vector2)  
        magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)  
        if not magnitude:  
            return 0  
        return dot_product / magnitude  
  
class Zhipuembedding:  
  
    def __init__(self, path:str=''):  
  
  
        client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))   
        self.embedding_model=client  
  
    def get_embedding(self,content:str=''):  
        response =self.embedding_model.embeddings.create(  
            model="embedding-2", #填写需要调用的模型名称  
            input=content #填写需要计算的文本内容,  
        )  
        return response.data[0].embedding  
  
    def compare_v(cls, vector1: List[float], vector2: List[float]) -> float:  
        dot_product = np.dot(vector1, vector2)  
        magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)  
        if not magnitude:  
            return 0  
        return dot_product / magnitude  
  
    def compare(self, text1: str, text2: str):  
        embed1=self.embedding_model.embeddings.create(  
            model="embedding-2", #填写需要调用的模型名称  
            input=text1 #填写需要计算的文本内容,  
        ).data[0].embedding  
  
        embed2=self.embedding_model.embeddings.create(  
            model="embedding-2", #填写需要调用的模型名称  
            input=text2 #填写需要计算的文本内容,  
        ).data[0].embedding  
  
        return np.dot(embed1, embed2) / (np.linalg.norm(embed1) * np.linalg.norm(embed2))  
  
  
class Jinaembedding:  
  
    def __init__(self, path:str='jinaai/jina-embeddings-v2-base-zh'):  
        self.path = path  
        self.embedding_model=AutoModel.from_pretrained('jinaai/jina-embeddings-v2-base-zh', trust_remote_code=True)   
  
    def get_embedding(self,content:str=''):  
        return self.embedding_model.encode([content])[0]  
  
    def compare(self, text1: str, text2: str):  
  
        cos_sim = lambda a,b: (a @ b.T) / (norm(a)*norm(b))  
        embeddings = self.embedding_model.encode([text1, text2])  
        return cos_sim(embeddings[0], embeddings[1])  
  
    def compare_v(cls, vector1: List[float], vector2: List[float]) -> float:  
        dot_product = np.dot(vector1, vector2)  
        magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)  
        if not magnitude:  
            return 0  
        return dot_product / magnitude  

这里使用质谱的embedding举个例子。我们RAG 实际上也就是这样

emd.compare(**你的问题**,**数据库相关信息**)  
通过寻找相似度最大的字块信息,给到大模型做参考  



emd=Zhipuembedding()  
  
emd.compare('我喜欢你','我爱你')  
0.825826818166129  
  
emd.compare('我喜欢你','我讨厌你')  
0.6838215984597642  

数据向量存储

在数据向量存储部分,定义了一个名为Vectordatabase的类,主要包括以下几个功能:

  • • 对字块列表进行批量的嵌入式编码
  • • 将得到的向量列表存储到json文件中
  • • 从json文件中加载向量和字块
  • • 求取两个向量的余弦相似度
  • • 求取一个字符串与向量列表中所有向量的相似度并返回相似度最高的前k个字块。

最关键的函数

query 函数的作用是对输入的查询进行搜索,返回与其最相关的k个文本片段。下面是这个函数的具体步骤:

  1. 1. query_vector = EmbeddingModel.get_embedding(query) : 这一步将查询字符串通过EmbeddingModel转换为一个向量,这个向量能够代表查询的语义信息。
  2. 2. result = np.array([self.get_similarity(query_vector, vector,EmbeddingModel) for vector in self.vectors]) : 这一步将查询向量与数据库中的所有向量进行比较,得到一个相似度的列表。列表中的每一个元素都是查询向量与数据库中的一个向量的相似度。
  3. 3. return np.array(self.document)[result.argsort()[-k:][::-1]].tolist() : 这一步首先对相似度列表进行排序,然后取出相似度最高的k个元素的索引。然后通过这些索引在数据库中找到对应的文本片段并返回。

这个函数的作用就是找出与查询最相关的k个文本片段,以便于后续的处理和回答。

from tqdm import tqdm  
import numpy as np   
from component.embedding import HFembedding,OpenAIembedding,Zhipuembedding,Jinaembedding  
import os  
import json  
from typing import List  
  
class Vectordatabase:  
  
    #初始化方法,传入一个字块列表  
    def __init__(self,docs:List=[]) -> None:  
        self.docs = docs  
  
    #对字块列表进行,批量的embedded编码,传入embedding模型,返回一个向量列表  
    def get_vector(self,EmbeddingModel)->List[List[float]]:  
        self.vectors = []  
        for doc in tqdm(self.docs):  
            self.vectors.append(EmbeddingModel.get_embedding(doc))  
        return self.vectors  
  
    #把向量列表存储到json文件中,把子块列表也存储到json文件,默认路径为'database'  
    def persist(self,path:str='database')->None:  
        if not os.path.exists(path):  
            os.makedirs(path)  
        with open(f"{path}/doecment.json", 'w', encoding='utf-8') as f:  
            json.dump(self.docs, f, ensure_ascii=False)  
        with open(f"{path}/vectors.json", 'w', encoding='utf-8') as f:  
                json.dump(self.vectors, f)  
  
    #加载json文件中的向量和字块,得到向量列表、字块列表,默认路径为'database'  
    def load_vector(self,path:str='database')->None:  
        with open(f"{path}/vectors.json", 'r', encoding='utf-8') as f:  
            self.vectors = json.load(f)  
        with open(f"{path}/doecment.json", 'r', encoding='utf-8') as f:  
            self.document = json.load(f)  
  
    #求向量的余弦相似度,传入两个向量和一个embedding模型,返回一个相似度  
    def get_similarity(self, vector1: List[float], vector2: List[float],embedding_model) -> float:  
        return embedding_model.compare_v(vector1, vector2)  
  
    #求一个字符串和向量列表里的所有向量的相似度,表进行排序,返回相似度前k个的子块列表  
    def query(self, query: str, EmbeddingModel, k: int = 1) -> List[str]:  
        query_vector = EmbeddingModel.get_embedding(query)  
        result = np.array([self.get_similarity(query_vector, vector,EmbeddingModel)  
                          for vector in self.vectors])  
        return np.array(self.document)[result.argsort()[-k:][::-1]].tolist()  

建立数据库

这里建议使用ipynb运行

from   component.embedding import Zhipuembedding,OpenAIembedding,HFembedding,Jinaembedding  
from component.data_chunker import ReadFile  
from component.databases import Vectordatabase  
import os  
import json  
from typing import Dict, List, Optional, Tuple, Union  
import PyPDF2  
  
#读取文件内容  
filter=ReadFile('./data')  
  
#进行数据切分,返回一个docs的字块列表  
docs=filter.get_all_chunk_content(200,150)  
  
#初始化embedding模型  
embedding_model=Zhipuembedding()  
  
#初始化database,对docs的字块列表进行向量化  
database=Vectordatabase(docs)  
Vectors=database.get_vector(embedding_model)  
  
#把向量化的子块和子块本身存储到json文件  
database.persist()  

不出意外的话,你的database下会出现两个json文件了,这就是你的数据库

Untitled

加载数据库

from   component.embedding import Zhipuembedding,OpenAIembedding,HFembedding,Jinaembedding  
from component.data_chunker import ReadFile  
from component.databases import Vectordatabase  
import os  
import json  
from typing import Dict, List, Optional, Tuple, Union  
import PyPDF2  
  
#查找与 "项目结构" 相似度最高的子块  
text="项目结构"  
  
embedding_model=Zhipuembedding()  
db=Vectordatabase()  
db.load_vector()  
result=db.query(text,embedding_model,1)  
print(result)  

结果还不错,匹配到一个好的子块,把这个子块给到大模型回答就行

['讲解(以下之构建了一个简单的RAG结构,深入可自行了解)\n\n---\n\n总览:\n\n项目结构如下\n\ncomponent是RAG的组件,分为五大部分(数据切分,向量化,向量存储,大模型,链)\ndata用于存放需要嵌入的文件(兼容PdfTXT,md文件)\ndb用于存放向量化后的数据,也是数据库的加载路径\n\nbuild.ipynb构建向量数据库\nwebdemo_by_gradio使用gradio基于嵌入的文件调用OpenAI的回答助手\n\n```markdown\n\ntinyRAG\n']  

LLM大模型类

from langchain.schema import HumanMessage,SystemMessage  
from langchain_openai import ChatOpenAI,OpenAI  
from langchain.prompts import  PromptTemplate,ChatPromptTemplate,HumanMessagePromptTemplate,SystemMessagePromptTemplate  
from   component.embedding import Zhipuembedding,OpenAIembedding,HFembedding,Jinaembedding  
from component.data_chunker import ReadFile  
from component.databases import Vectordatabase  
import os  
import json  
from typing import Dict, List, Optional, Tuple, Union  
import PyPDF2  
  
#把api_key放在环境变量中,可以在系统环境变量中设置,也可以在代码中设置  
# import os  
# os.environ['OPENAI_API_KEY'] = ''  
  
class Openai_model:  
    def __init__(self,model_name:str='gpt-3.5-turbo-instruct',temperature:float=0.9) -> None:  
  
        #初始化大模型  
        self.model_name=model_name  
        self.temperature=temperature  
        self.model=OpenAI(model=model_name,temperature=temperature)  
  
        #加载向量数据库,embedding模型  
        self.db=Vectordatabase()  
        self.db.load_vector()  
        self.embedding_model=Zhipuembedding()  
  
    #定义chat方法  
    def chat(self,question:str):  
  
        #这里利用输入的问题与向量数据库里的相似度来匹配最相关的信息,填充到输入的提示词中  
  
        template="""使用以上下文来回答用户的问题。如果你不知道答案,就说你不知道。总是使用中文回答。  
        问题: {question}  
        可参考的上下文:  
        ···  
        {info}  
        ···  
        如果给定的上下文无法让你做出回答,请回答数据库中没有这个内容,你不知道。  
        有用的回答:"""  
  
        info=self.db.query(question,self.embedding_model,1)  
  
        prompt=PromptTemplate(template=template,input_variables=["question","info"]).format(question=question,info=info)  
  
        res=self.model.invoke(prompt)  
  
        return  res  

demo演示

这段代码是创建一个 Gradio 的聊天界面,它使用了一个名为 echo 的函数作为其后端。echo 函数中,OpenAI模型被用于生成回复。

import gradio as gr  
from component.llms import Openai_model  
import time  
  
model=Openai_model()  
  
def echo(message, history):  
    result=model.chat(message)  
    for i in range(len(result)):  
        time.sleep(0.02)  
        yield result[: i+1]  
  
# 自定义的流式输出  
  
demo = gr.ChatInterface(fn=echo,  
                        examples=["中华人民共和国消费者权益保护法什么时候,在哪个会议上通过的?", "中华人民共和国消费者权益保护的目录是什么?","RinyRAG的项目结构是怎么样的"],  
                        title="Echo Bot",  
                        theme="soft")  
demo.launch()  
  • • model=Openai_model() :初始化一个OpenAI模型的实例。
  • • def echo(message, history): :定义一个名为 echo 的函数,它接收两个参数:message 和 history。其中,message 是用户输入的消息,history 是之前的对话历史。
  • • result=model.chat(message) :调用模型的 chat 方法,将用户的消息作为输入,返回模型生成的回复。
  • • for i in range(len(result)): 和 yield result[: i+1] :这是一个生成器,它会逐渐产生回复的每一部分,每产生一部分就暂停,等待下一次调用。这样做的目的是为了实现流式输出,即模拟聊天机器人逐字打出回复的效果。
  • • demo = gr.ChatInterface(fn=echo,…) :创建一个 Gradio 聊天界面,使用 echo 函数作为后端。
  • • demo.launch() :启动聊天界面。

在Gradio聊天界面中,用户可以输入问题,系统会调用OpenAI模型生成回答,并在聊天界面中显示出来。

result:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值