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}")