0.介绍
0.1 什么是RAG?
检索增强生成(Retrieval Augmented Generation),简称 RAG,已经成为当前最火热的LLM应用方案。经历今年年初那一波大模型潮,想必大家对大模型的能力有了一定的了解,但是当我们将大模型应用于实际业务场景时会发现,通用的基础大模型基本无法满足我们的实际业务需求,主要有以下几方面原因:
- 知识的局限性:模型自身的知识完全源于它的训练数据,而现有的主流大模型(ChatGPT、文心一言、通义千问...)的训练集基本都是构建于网络公开的数据,对于一些实时性的、非公开的或离线的数据是无法获取到的,这部分知识也就无从具备。
- 幻觉问题:所有的AI模型的底层原理都是基于数学概率,其模型输出实质上是一系列数值运算,大模型也不例外,所以它有时候会一本正经地胡说八道,尤其是在大模型自身不具备某一方面的知识或不擅长的场景。而这种幻觉问题的区分是比较困难的,因为它要求使用者自身具备相应领域的知识。
- 数据安全性:对于企业来说,数据安全至关重要,没有企业愿意承担数据泄露的风险,将自身的私域数据上传第三方平台进行训练。这也导致完全依赖通用大模型自身能力的应用方案不得不在数据安全和效果方面进行取舍。
RAG的架构如图中所示,简单来讲,RAG就是通过检索获取相关的知识并将其融入Prompt,让大模型能够参考相应的知识从而给出合理回答。因此,可以将RAG的核心理解为"检索+生成",前者主要是利用向量数据库的高效存储和检索能力,召回目标知识;后者则是利用大模型和Prompt工程,将召回的知识合理利用,生成目标答案。
0.2 RAG应用流程
-
数据准备阶段:数据提取------>文本分割------>向量化(embedding)------>数据入库
-
应用阶段:用户提问------>数据检索(召回)------>注入Prompt------>LLM生成答案
-
数据提取
-
数据加载:包括多格式数据加载、不同数据源获取等,根据数据自身情况,将数据处理为同一个范式。
-
数据处理:包括数据过滤、压缩、格式化等。
-
元数据获取:提取数据中关键信息,例如文件名、Title、时间等 。
-
文本分割 :
文本分割主要考虑两个因素:1)embedding模型的Tokens限制情况;2)语义完整性对整体的检索效果的影响。一些常见的文本分割方式如下:
-
句分割:以"句"的粒度进行切分,保留一个句子的完整语义。常见切分符包括:句号、感叹号、问号、换行符等。
-
固定长度分割:根据embedding模型的token长度限制,将文本分割为固定长度(例如256/512个tokens),这种切分方式会损失很多语义信息,一般通过在头尾增加一定冗余量来缓解。
-
向量化(embedding):
向量化是一个将文本数据转化为向量矩阵的过程,该过程会直接影响到后续检索的效果。目前常见的embedding模型如表中所示,这些embedding模型基本能满足大部分需求,但对于特殊场景(例如涉及一些罕见专有词或字等)或者想进一步优化效果,则可以选择开源Embedding模型微调或直接训练适合自己场景的Embedding模型。
模型名称 | 描述 | 获取地址 |
---|---|---|
ChatGPT-Embedding | ChatGPT-Embedding由OpenAI公司提供,以接口形式调用。 | platform.openai.com/docs/guides... |
ERNIE-Embedding V1 | ERNIE-Embedding V1由百度公司提供,依赖于文心大模型能力,以接口形式调用。 | cloud.baidu.com/doc/WENXINW... |
M3E | M3E是一款功能强大的开源Embedding模型,包含m3e-small、m3e-base、m3e-large等多个版本,支持微调和本地部署。 | huggingface.co/moka-ai/m3e... |
BGE | BGE由北京智源人工智能研究院发布,同样是一款功能强大的开源Embedding模型,包含了支持中文和英文的多个版本,同样支持微调和本地部署。 | huggingface.co/BAAI/bge-ba... |
1.导入所需库
- FAISS是Faceboo
FAISS (Facebook AI Similarity Search )是Facebook AI团队开源的针对聚类和相似性搜索的库,为稠密向量提供高效相似度搜索和聚类,支持十亿级别向量的搜索,是目前最为成熟的近似近邻搜索库。它包含多种搜索任意大小向量集的算法,以及用于算法评估和参数调整的支持代码。FAISS用C++编写,并提供与Numpy完美衔接的Python接口。除此以外,对一些核心算法提供了GPU实现。
- langchain
LangChain是一个由语言模型LLMs驱动的应用程序框架,它允许用户围绕大型语言模型快速构建应用程序和管道。可以直接与OpenAI的ChatGPT模型以及Hugging Face集成。通过LangChain可快速构建聊天机器人、生成式问答(GQA)、本文摘要等应用场景。LangChain目前有python和nodejs两种版本实现,python支持的能力较多。
LangChain是一个强大的框架,旨在帮助开发人员使用语言模型构建端到端的应用程序。它提供了一套工具、组件和接口,可简化创建由大型语言模型LLM和聊天模型提供支持的应用程序的过程。简单来说,可以理解LangChain相当于开源版的GPT插件,它提供了丰富的大语言模型工具,支持在开源模型的基础上快速增强模型的。
- ERNIE Bot
ERNIE Bot是百度研发的知识增强大语言模型,其中文名为文心一言。ERNIE Bot能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。1。
python
!pip install langchain
!pip install erniebot
!pip install faiss-cpu
bash
Collecting faiss-cpu
Downloading https://mirrors.aliyun.com/pypi/packages/98/a6/4caf215afd86e3b365f3ba0d9c01800d46bc8e42e65fe3667dd9dd3a3213/faiss_cpu-1.7.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.6 MB)
[2K [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m17.6/17.6 MB[0m [31m224.9 kB/s[0m eta [36m0:00:00[0m00:01[0m00:03[0m
[?25hInstalling collected packages: faiss-cpu
Successfully installed faiss-cpu-1.7.4
python
import os
import time
# 序列化和反序列化库
import pickle
# 文心一言库
import erniebot
# 进度条库
from tqdm import tqdm
# 文本加载器
from langchain.document_loaders import TextLoader
# 目录加载器
from langchain.document_loaders import DirectoryLoader
# Langchain默认使用的文本切割器,也是Langchain推荐使用的文本切割器
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 相似性搜索库
from langchain.vectorstores.faiss import FAISS
# 泛型库List
from typing import List
# 向量类
from langchain.schema.embeddings import Embeddings
2.设置对应的密钥
python
# 设置文心一言鉴权
erniebot.ak = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
erniebot.sk = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
erniebot.type = "qianfan"
4.构建向量库
python
!mkdir index
arduino
mkdir: cannot create directory 'index': File exists
python
# 文库目录
docs_dir = 'docs'
# 索引目录
data_dir = 'index'
# 文库文件扩展名
exts = ['md', 'rst', 'txt']
batch_size = 16
# 块长度
chunk_size = 384
# 加载器
loader_cls = TextLoader
# 文档库
docs = []
# 向量库
embeddings = []
texts = []
metadatas = []
# 加载文库
for ext in exts:
loader = DirectoryLoader(
docs_dir, # 目录
glob='*.%s' % ext, # 索引
recursive=True, # 是否递归目录
show_progress=True, #显示进度
# silent_errors=True, #是否显示错误信息
loader_cls=loader_cls # 加载器
)
docs += loader.load()
# 切分文本
splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size)
docs = splitter.split_documents(docs)
# 分batch
batch_docs = [docs[i:i + batch_size] for i in range(0, len(docs), batch_size)]
for batch_doc in tqdm(batch_docs):
try:
response = erniebot.Embedding.create(
model='ernie-text-embedding',
input=[item.page_content for item in batch_doc]
)
embeddings += [item['embedding'] for item in response['data']]
texts += [item.page_content for item in batch_doc]
metadatas += [item.metadata for item in batch_doc]
time.sleep(1)
except:
for text in tqdm(batch_doc):
try:
response = erniebot.Embedding.create(
model='ernie-text-embedding',
input=[text.page_content]
)
embeddings.append(response['data'][0]['embedding'])
texts.append(text.page_content)
metadatas.append(text.metadata)
time.sleep(1)
except:
continue
# 组件向量库
data = {
"embeddings": embeddings,
"texts": texts,
'metadatas': metadatas,
}
# 保存向量库
with open(os.path.join(data_dir, 'data.pkl'), 'wb') as f:
pickle.dump(data, f)
bash
0it [00:00, ?it/s]
0it [00:00, ?it/s]
50%|█████ | 1/2 [00:00<00:00, 1204.91it/s]
100%|██████████| 1/1 [00:01<00:00, 1.15s/it]
python
!dir index
kotlin
data.pkl
5.构建知识库
python
class ErniebotEmbeddings(Embeddings):
def __init__(self, model: str = 'ernie-text-embedding',
batch_size: int = 16, embedding_size: int = 384):
self.model = model
self.batch_size = batch_size
self.embedding_size = embedding_size
def embed_documents(self, texts: List[str]) -> List[List[float]]:
batch_texts = [texts[i: i + self.batch_size]
for i in range(0, len(texts), self.batch_size)]
embeddings = []
for batch_text in tqdm(batch_texts):
try:
response = erniebot.Embedding.create(
model=self.model,
input=batch_text
)
embeddings += [item['embedding'] for item in response['data']]
except:
for text in tqdm(batch_text):
try:
response = erniebot.Embedding.create(
model=self.model,
input=[text]
)
embeddings.append(response['data'][0]['embedding'])
except:
embeddings.append([0.0] * self.embedding_size)
continue
return embeddings
def embed_query(self, text: str) -> List[float]:
response = erniebot.Embedding.create(
model=self.model,
input=[text]
)
return response['data'][0]['embedding']
python
# 加载向量库
embedding = ErniebotEmbeddings()
with open(os.path.join(data_dir, 'data.pkl'), 'rb') as f:
data = pickle.load(f)
index = FAISS._FAISS__from(
texts=data['texts'], embeddings=data['embeddings'], embedding=embedding, metadatas=data['metadatas']
)
# 保存知识库
index.save_local(data_dir)
6.文档问答
python
embedding = ErniebotEmbeddings()
index = FAISS.load_local(data_dir, embedding)
query = input('> ')
k = 10
search_type = 'similarity' # mmr
prompt = '请根据如下问题和文档给出回答。\n问题:%s\n参考:\n' % query
references = []
for i, doc in enumerate(index.search(query, k=k, search_type=search_type)):
prompt += '[%d] %s\n%s\n\n' % (i + 1, doc.metadata['source'], doc.page_content)
reference = doc.metadata['source']
if reference not in references:
references.append(reference)
prompt += '\n回答:\n'
response = erniebot.ChatCompletion.create(
model='ernie-bot-4',
messages=[{
'role': 'user',
'content': prompt
}],
stream=True
)
for chunk in response:
print(chunk['result'], end='')
print('\n\n\n## 参考文档\n' + '\n'.join(
['%d. [%s](%s)' % (i + 1, item.strip(), item.replace(" ", '%20')) for i, item in enumerate(references)]))
# 毛泽东哪年出生?
bash
> 毛泽东哪年出生?
毛泽东于1893年12月26日(清光绪十九年十一月十九日)出生。
## 参考文档
1. [docs/abc.txt](docs/abc.txt)
1.FAISS github.com/facebookres...