RAG(检索增强的生成式模型)项目的搭建

1. LLM的局限性

1)LLM的知识不是实时的;

2)LLM可能不知道你的私有领域/业务知识。

2. 检索增强

RAG(Retrieval Augmented Generation),通过检索的方式增强生成模型的能力。

类似于开卷考试,让LLM先翻书,再回答问题。同时可以减少LLM胡编乱造的情况。

3. RAG基本项目的搭建流程

3.1 项目需要达到的效果

上传本地知识,加载业务数据。

图中左侧是对话的结果,右侧是检索的结果。

3.2 文本关键字搜索的搭建过程

3.2.1 文档加载,并按一定条件切割成片段

安装pdf解析库:

pip install pdfminer.six -i https://mirrors.aliyun.com/pypi/simple 

from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer


def extract_text_from_pdf(filename, page_numbers=None, min_line_length=1):
    #从 PDF 文件中(按指定页码)提取文字
    paragraphs = []
    buffer = ''
    full_text = ''
    # 提取全部文本
    for i, page_layout in enumerate(extract_pages(filename)):
    # 如果指定了页码范围,跳过范围外的页
        if page_numbers is not None and i not in page_numbers:
            continue
        for element in page_layout:
            if isinstance(element, LTTextContainer):
                full_text +=element.get_text()+'\n'
    # 按空行分隔,将文本重新组织成段落
    lines = full_text.split('\n')
    for text in lines:
        if len(text)>= min_line_length:
            buffer += (''+text) if not text.endswith('-') else text.strip('-')
        elif buffer:
            paragraphs.append(buffer)
            buffer = ''
    if buffer:
        paragraphs.append(buffer)

    return paragraphs


if __name__ == "__main__":
    file = f"2404.16130v1.pdf"
    paragraphs = extract_text_from_pdf(file, [1, 2], min_line_length=20)
    for para in paragraphs[:3]:
        print(para)
3.2.2 将切割的文本片段灌入检索引擎

安装搜索引擎ES客户端

pip3 install elasticsearch7 -i https://mirrors.aliyun.com/pypi/simple 

安装NLTK 文本方法处理库

pip install nltk -i https://mirrors.aliyun.com/pypi/simple 

from elasticsearch7 import helpers, Elasticsearch
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
import nltk
import re

import warnings
warnings.simplefilter("ignore")   #屏蔽es的一些warning
# 英文切词、词根、切句等方法
nltk.download("punkt")
# 英文停用词库
nltk.download("stopwords")

def to_keywords(input_string):
    # 使用正则将所有非字母数字的字符替换为空格
    no_symbols = re.sub(r'[^a-zA-Z0-9\s]', '', input_string)
    word_tokens = word_tokenize(no_symbols)
    # 加载停用词表
    stop_words = set(stopwords.words('english'))
    ps = PorterStemmer()
    filtered_sentence = [ps.stem(w) for w in word_tokens if not w.lower() in stop_words]
    return ' '.join(filtered_sentence)

中文实现:

import re
import jieba
import nltk
from nltk.corpus import stopwords

nltk.download('stopwords')


def to_keywords(input_string):
    """ 将句子转成检索关键词序列 """
    word_tokens = jieba.cut_for_search(input_string)
    stop_words = set(stopwords.words('chinese'))
    # 去除停用词
    filtered_sentence = [w for w in word_tokens if not w in stop_words]
    return " ".join(filtered_sentence)


def sent_tokenize(input_string):
    """  按标点断句  """
    sentences = re.split(r'(?<=[。!?;?!])', input_string)
    return [sentence for sentence in sentences if sentence.strip()]

if __name__ == "__main__":
    a = to_keywords("我毕业于北京信息科技大学计算机专业,后来在华东师范大学攻读硕士学位!")
    b = sent_tokenize("这是,第一句。这事第二句嘛?是的!啊")
    print(a)
    print(b)

将文本灌入搜索引擎

# 创建Elasticsearch连接
username = ''
password = ''
es = Elasticsearch(
    hosts=['http://localhost:9200'],
    http_auth=(username, password),
)
# 定义索引名称
index_name = "demo_index"
# 如果索引存在,删除
if es.indices.exists(index=index_name):
    es.indices.delete(index=index_name)
# 创建索引
es.indices.create(index=index_name)
# 灌库指令
paragraphs = ''
actions = [
    {
        "_index": index_name,
        "_source": {
            "keywords": to_keywords(para),
            "text": para
        }
    }
    for para in paragraphs
]
helpers.bulk(es, actions)
3.2.3 封装检索接口
from openai import OpenAI
import os
# 加载环境变量
from dotenv import load_dotenv,find_dotenv
_ = load_dotenv(find_dotenv())
# 读取本地.env文件,里面定义了OPEN_API_KEY
client = OpenAI()

def get_completion(prompt, model='gpt-3.5-turbo'):
    messages = [{"role": "user", "content": prompt}]
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0
    )
    return response.choices[0].message.content


def build_prompt(prompt_template, **kwargs):
    prompt = prompt_template
    for k,v in kwargs.items():
        if isinstance(v, str):
            val = v
        elif isinstance(v, list) and all(isinstance(element, str) for element in v):
            val = "\n".join(v)
        else:
            val = str(v)
        prompt = prompt.replace(f"__{k.upper()}__", val)
    return prompt


prompt_template = """
你是一个问答机器人。
你的任务是根据下面给定的一致信息回答用户问题。
确保你的回复完全依据下述已知的信息。不要编造答案。
如果下述已知信息不足以回答问题时,请直接回复“我无法回答您的问题”。

已知信息:
__INFO__

用户问题:
__QUERY__

请用中文回答用户问题
"""

其中,前三部是离线操作的,需要再系统上线前做好。

3.2.4 构建调用流程

query--> 检索-->prompt-->LLM-->回复

rag pipeline

from elasticsearch import search
from llm_wrapper import build_prompt, prompt_template, get_completion

user_query = "how many parameters does llama2 have?"
# 1 检索
search_result = search(user_query, 2)
# 构造prompt
prompt = build_prompt(prompt_template, info=search_result, query=user_query)
# 调用LLM
response = get_completion(prompt)

3.2.5 关键字检索的局限性

同一个语义,用词不同,可以导致检索不到有效的信息。

# 关键字检索的局限性
user_query = "Does llama2 have a chat version?"
# 用词不一样,导致检索不到有效信息
#user_query = "Does llama2 have a conversational variant?"
search_result = search(user_query)
for res in search_result:
    print(res+"\n")

3.3 向量检索

3.3.1 文本向量的表示
import numpy as np
from numpy import dot
from numpy.linalg import norm

from openai import OpenAI
import os
# 加载环境变量
from dotenv import load_dotenv,find_dotenv
_ = load_dotenv(find_dotenv())
# 读取本地.env文件,里面定义了OPEN_API_KEY
client = OpenAI()

def cos_sim(a, b):
    # 计算两个向量的余弦相似度
    # norm是求向量的二范数,即向量的长度,norm(a)=sqrt(a1**2 + a2**2 + ... + an**2)
    return dot(a, b)/(norm(a) * (norm(b)))

def l2(a, b):
    "计算两个向量的欧式距离"
    return norm(np.asarray(a)-np.asarray(b))


def get_embeddings(texts, model="text-embedding-ada-002"):
    data = client.embeddings.create(input=texts, model=model)
    return [x.embedding for x in data]


if __name__ == "__main__":
    query = "国际争端"
    
    documents = [
        "联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
        "土耳其、芬兰、瑞典与北约代表将继续就瑞典入约使劲按进行谈判",
        "我国首次在空间站开展舱外辐射生物学暴露实验"
    ]
    query_vec = get_embeddings([query])[0]
    doc_vec = get_embeddings(documents)

    print("Cosine distance:")
    print(cos_sim(query_vec, query_vec))
    for vec in doc_vec:
        print(cos_sim(query_vec, vec))

    print("Euclidean distance:")
    print(l2(query_vec, query_vec))
    for vec in doc_vec:
        print(l2(query_vec, vec))



且OpenAI的向量模型是支持跨语言的。 

3.3.2 向量数据库

专门为向量检索做的中间件。

安装向量数据库中间件。

pip install chromadb -i https://mirrors.aliyun.com/pypi/simple
3.3.2.1 下面是将向量数据库建在内存中
from rag_test import paragraphs
from embedding import get_embeddings
import chromadb
from chromadb.config import Settings

# 此方法将向量数据库建在内存中
class MyVectorDBConector:
    def __init__(self, collection_name, embeddig_fn):
        chroma_client = chromadb.Client(Settings(allow_reset=True))
        # 为了演示,实际不需要每次reset
        chroma_client.reset()
        # 创建一个collection
        self.collection = chroma_client.get_or_create_collection(name=collection_name)
        self.embedding_fn = embeddig_fn   # 传入向量模型

    def add_document(self, documents, metadata={}):
        " 向collection中添加文档与向量 "
        self.collection.add(
            embeddings=self.embedding_fn(documents), # 每个文档的向量
            documents=documents, # 文档的原文
            ids=[f"id{i}" for i in range(len(documents))]
        )

    def search(self, query, top_n):
        ''' 检索向量数据库 '''
        results = self.collection.query(
            query_embeddings=self.embedding_fn([query]),
            n_results=top_n
        )
        return results


if __name__ == "__main__":
    # 创建一个向量数据库
    vector_db = MyVectorDBConector("demo", get_embeddings)
    # 向向量数据库总添加文档
    vector_db.add_document(paragraphs)
    user_query = "llama2有多少个参数"
    results = vector_db.search(user_query, 2)

    for para in results['documents'][0]:
        print(para + "\n")
3.3.2.2 搭建向量数据库服务

在server端:

chroma run --path /db_path

在client端:

import chromadb
chorma_client = chromadb.HttpClient(host='localhost', port=8000)

3.3.3 主流向量数据库对比

3.3.4 基于向量检索的RAG

搭建一个rag机器人

from llm_wrapper import build_prompt, prompt_template
from chroma_demo import vector_db
from llm_wrapper import get_completion

class RAG_Bot:
    def __init__(self, vector_db, llm_api, n_results=2):
        self.vector_db = vector_db
        self.llm_api = llm_api
        self.n_results = n_results

    def chat(self, query):
        # 检索
        search_results = self.vector_db.search(query, self.n_results)
        # 构建prompt
        prompt = build_prompt(
            prompt_template,
            info=search_results['documents'][0],
            query=query
        )
        # 调用LLM
        response = self.llm_api(prompt)
        return response


if __name__ == "__main__":
    # 创建一个rag机器人
    bot = RAG_Bot(
        vector_db,
        llm_api=get_completion
    )
    user_query = "llama2有多少个参数"
    response = bot.chat(user_query)

3.4 RAG系统性能提升

3.4.1 文本粒度

文本粒度太大->检索精度不准确,文本粒度太小->信息不全面,问题的答案可能跨越两个片段。

改进思路:

1)按一定粒度,部分重叠式地切割文本,使上下文更加完整。

3.4.2 检索后排序

有时候最合适的答案不一定排在最前面

改进思路:

1)检索时,过召回一部分文本;

2)通过一个排序模型对query和document重新打分排序。

安装cross-encoder模块

pip install sentence_transformers -i https://mirrors.aliyun.com/pypi/simple
from sentence_transformers import CrossEncoder
from chroma_demo import results

model = CrossEncoder('cross_encoder/ms-marco-MiniLM-L-6-v2', max_length=512)
user_query = "how safe is llama 2"
scores = model.predict([(user_query, doc)
                    for doc in results['documents'][0]
                        ])
sorted_list = sorted(
    zip(scores, results['documents'][0]),
    key=lambda x: x[0],
    reverse=True
)
for score, doc in sorted_list:
    print(f"{score}\t{doc}")
    

  • 26
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值