整体介绍
RAG技术介绍
RAG,即检索增强生成(Retrieval-Augmented Generation),是一种结合了信息检索和文本生成的自然语言处理模型架构。它的目的是利用外部知识库来增强大型语言模型(LLM)的回答能力和准确性。以下是一些关于RAG的基础知识:
- 操作流程:RAG的操作流程通常包括文本切分、向量化、索引建立、提示词设计等步骤。首先,将文本切分成小段,然后使用Transformer编码器模型将这些文本段转换成向量形式,并汇集到一个索引中。最后,为LLM制定一个提示词,指导模型根据找到的上下文信息回答用户查询。
- 核心组成:RAG模型通常由两部分组成:信息检索模块和文本生成模块。信息检索模块负责从大量文本数据中检索相关信息,而文本生成模块则利用这些信息生成答案或响应。
- 技术细节:RAG的实施需要使用矢量数据库等技术,这些技术能够快速编码新数据并进行高效检索。数据准备阶段通常是一个离线过程,涉及数据提取、文本分割、向量化以及数据入库。在应用阶段,用户提问后,系统会进行数据检索、注入Prompt,并由LLM生成答案。
- 应用领域:RAG模型可以应用于问答系统、文本摘要、对话系统等多个领域,通过允许LLM利用额外的数据资源来提高生成式AI的质量。
- 优势与挑战:RAG的优势在于能够提供及时的上下文答案,同时存储库可以不断更新以适应新的信息。然而,实施RAG也面临挑战,如如何确保检索到的信息是准确和相关的,以及如何处理和整合来自不同来源的数据。
RAG与LLM紧密相关,旨在提升LLM的性能和准确性。以下是它们之间的关系概述:
- 功能互补:RAG技术通过利用外部知识库来补充LLM的上下文信息,帮助LLM生成更准确和相关的响应。这种结合使得LLM能够在回答问题时访问最新的、特定领域的数据,从而提高了回答的质量。
- 解决挑战:传统的LLM可能会产生不准确或不相关的内容,这被称为“幻觉”现象。RAG框架通过提供额外的特定领域数据来帮助缓解这些挑战,使LLM的决策过程更加透明。
- 提高效率:与对LLM进行完整微调相比,RAG通常具有训练时间短、成本低的优势。同时,它在处理长文本时也提供了更快的响应速度和较低的推理成本。
- 技术发展:RAG的发展可以分为朴素RAG、高级RAG和模块化RAG三个阶段,每个阶段都有其特点和方法,不断优化LLM的应用效果。
- 实际应用:在实际应用中,RAG可以通过索引相关文本语料库来定制适应特定领域的需求,从而为特定行业或任务提供定制化的解决方案。
- 安全性和管理:RAG允许更好地管理企业现有和新增的知识,解决知识依赖问题。它还提供了访问权限控制和数据管理的功能,这对于保护私有数据非常重要。
实现步骤
向量化
向量化模块是RAG系统中的关键组成部分,它负责将文本数据转换为高维的数学表示形式,即向量。这些向量能够捕捉文本的语义信息,使得计算机能够理解和处理自然语言数据。
要创建一个向量化的类,首先需要确定类的数据成员和功能,然后实现这些功能。以下是创建向量类的基本步骤:
- 确定类的数据成员:向量通常有两个组成部分,即x和y坐标。在极坐标系中,这可以是幅度(mag)和角度(ang)。因此,类应该包含这些数据成员。类还需要一个数据成员来存储向量的表示形式,即直角坐标或极坐标。可以使用枚举类型来表示这两种模式。
- 设计构造函数:构造函数应该能够根据输入参数初始化向量的坐标和表示形式。例如,可以提供一个构造数,它接受直角坐标作为参数,并将表示形式设置为RECT。也可以提供另一个构造函数,它接受极坐标作为参数,并将表示形式设置为POL。
- 实现类的功能:根据需要,可以添加方法来执行向量运算,如向量加法、点积等。如果需要,可以添加方法来转换向量的表示形式,即从直角坐标转换为极坐标,反之亦然。
在实现向量类时,应该考虑包括以下几种重要的计算量:
- 距离度量:这些度量方法可以衡量两个向量之间的相对位置差异。具体如下: 1.闵可夫斯基距离:是一组距离的定义,它包括了欧氏距离、曼哈顿距离和切比雪夫距离等特殊情况。它适用于n维空间中两点间的距离测量。 2.曼哈顿距离:又称为城市街区距离,是沿着坐标轴测量两点之间的距离,适用于网格结构的数据。 3.欧氏距离:是在n维空间中两点之间的直线距离,适用于连续数据。 4.马氏距离:是欧氏距离的扩展,它考虑了数据的协方差,适用于特征之间存在相关性的情况。
- 相似度度量:这些度量方法直接刻画两个向量之间的相似程度。具体如下: 1.余弦相似度:通过测量两个向量夹角的余弦值来衡量它们之间的相似性,与向量的大小无关,只与方向有关,适用于文本数据的比较。 2.相关系数:衡量两个变量之间的线性关系强度和方向,适用于统计分析中的相似性评估。
下面给出计算两个向量之间的余弦相似度的代码:
def cosine_similarity(cls, vector1: List[float], vector2: List[float]) -> float:
"""
calculate cosine similarity between two vectors
"""
dot_product = np.dot(vector1, vector2)
magnitude = np.linalg.norm(vector1) * np.linalg.norm(vector2)
if not magnitude:
return 0
return dot_product / magnitude
文档加载和切分
文档加载是指从不同的源(如本地文件系统、数据库、网络等)读取文档内容的过程。这通常涉及到多种文件格式的支持,例如PDF、Markdown(md)、纯文本(txt)等。加载过程可能需要利用专门的库来解析这些格式,确保信息的准确提取。例如,对于PDF文件,可能会使用像PyMuPDF
(也称作fitz
)或PDFMiner
这样的库来解析内容,而对于Markdown或文本文件,则可能直接读取并处理。
文档切分则是将加载后的文档内容按照某种策略分成较小的单元,便于后续的信息检索和处理。有效的切分策略可以提高检索的准确性和效率,同时也便于模型理解和利用文档内容。常见的文档切分方法包括:
- 基于字符分块:按照固定字符数目或特定字符进行切分,简单但可能破坏语义完整性。
- 固定大小分块:每个块包含固定数量的令牌(token),可能有重叠以保持语义连贯性。
- 基于token的分块:按照固定数量的单词或语素切分,使用与目标语言模型相同的分词器。
- 内容感知分块:利用自然语言处理工具(如NLTK、spaCy)进行基于内容的切分,考虑句子边界、段落、标题等。
- 递归分块:先按段落或标题切分,如果块仍然过大,则在这些块上递归进行切分,直至满足大小要求。
这里我们采用按 Token 的长度来切分文档。我们可以设置一个最大的 Token 长度,然后根据这个最大的 Token 长度来切分文档。这样切分出来的文档片段就是一个一个的差不多相同长度的文档片段了。不过在切分的时候要注意,片段与片段之间最好要有一些重叠的内容,这样才能保证检索的时候能够检索到相关的文档片段。还有就是切分文档的时候最好以句子为单位,也就是按 \n
进行粗切分,这样可以基本保证句子内容是完整的。代码如下:
def get_chunk(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 line_len > max_token_len:
print('warning line_len = ', line_len)
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
数据库和向量检索
向量数据库是一种专门用于存储和检索高维向量数据的数据库系统,它在RAG框架中扮演着桥梁的角色,连接着原始文档内容和语言模型。传统的关系型数据库或键值存储并不适用于高维空间中的相似性检索,而向量数据库通过优化的索引结构和算法,如倒排索引、近似最近邻(Approximate Nearest Neighbor, ANN)搜索算法等,能够高效地完成大规模文本向量的查询。
常用的向量数据库有FAISS、Annoy、Pinecone、Milvus等,它们提供了对海量文本向量的快速相似性检索支持。这些数据库能够接收文本特征向量作为输入,并在接收到查询请求时迅速找到最相似的向量集合,进而找到对应的文档片段或信息。
一个数据库对于最小RAG架构来说,需要实现以下功能:
persist
:数据库持久化,本地保存load_vector
:从本地加载数据库get_vector
:获得文档的向量表示query
:根据问题检索相关的文档片段
向量检索的优势:
向量检索在处理高维向量数据和进行快速相似性搜索方面具有显著优势。它能够更好地理解查询的语义内容,因为它利用了深度学习模型的能力来编码文本的含义,不仅仅是关键字匹配。
随着AI模型的发展,向量检索背后的语义准确度也在稳步提升。通过用向量的距离相似度来表示语义相似度,已经成为了自然语言处理(NLP)的主流形态。
在RAG系统中,检索的任务是快速且精确地找出与输入查询语义上最匹配的信息。这一步骤对于整个系统的性能至关重要,因为它直接影响到最终答案的相关性和准确性。
向量检索步骤大致如下:
文本向量化:使用预训练好的嵌入模型(如BERT、Sentence-BERT、TfidfVectorizer等)将文档内容转换成高维向量。这个过程通常在文档加载和切分之后进行,将每个文档片段映射为一个固定长度的向量表示。
向量存储:将生成的向量及其对应文档的元数据(如文档ID、段落位置等)存储到向量数据库中。这样,每个文档片段都有了一个易于比较和检索的数学表示。
查询向量化:用户输入的查询文本同样被转换成向量形式,这通常与文档向量化使用相同的技术,确保查询和文档在相同的向量空间中比较。
相似性检索:利用向量数据库的搜索功能,输入查询向量,找出数据库中最相似的文档向量。这一步通常涉及近似最近邻搜索算法,它在保证一定精度的同时,显著提高了检索速度。
结果排序与过滤:检索到的向量根据与查询向量的相似度进行排序,可能还会应用一些业务逻辑或过滤条件进一步优化结果。
生成阶段准备:检索到的相关文档片段作为额外的上下文信息,与原始查询一起输入到语言生成模型(如GPT系列、T5等),以生成最终的输出。
实现如下:
def query(self, query: str, EmbeddingModel: BaseEmbeddings, k: int = 1) -> List[str]:
query_vector = EmbeddingModel.get_embedding(query)
result = np.array([self.get_similarity(query_vector, vector)
for vector in self.vectors])
return np.array(self.document)[result.argsort()[-k:][::-1]].tolist()
大模型
大模型模块是RAG实现步骤中的最后一步,它负责生成最终的回答。
系统会将查询阶段检索出的段落与用户的原始问题一起整合到一个Prompt模板中,形成一个完成的提示(Prompt)。这个提示会被发送给大语言模型,由模型基于这些信息生成回答。
此外,大模型模块的作用不仅仅是生成回答,它还结合了搜索技术和大语言模型的提示词功能,使得模型能够在更丰富的上下文信息的基础上给出更加准确和相关的回答。这种架构的优势在于,它能够充分利用大语言模型的理解能力,同时通过检索增强的方式提供更具体的上下文信息,从而提高答案的质量。
以 InternLM2-chat-7B 模型为例,给出示例代码:(采用加载本地开源大模型的方式)
class InternLMChat(BaseModel):
def __init__(self, path: str = '') -> None:
super().__init__(path)
self.load_model()
def chat(self, prompt: str, history: List = [], content: str='') -> str:
prompt = PROMPT_TEMPLATE['InternLM_PROMPT_TEMPALTE'].format(question=prompt, context=content)
response, history = self.model.chat(self.tokenizer, prompt, history)
return response
def load_model(self):
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
self.tokenizer = AutoTokenizer.from_pretrained(self.path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(self.path, torch_dtype=torch.float16, trust_remote_code=True).cuda()
字典维护prompt
使用字典来维护Prompt是一种有效组织和管理不同场景或用途下的提示词的方法。Prompt本质上是用于指导AI语言模型生成特定类型响应的文本指令。一个精心设计的字典可以帮助你快速查找、更新和重用Prompt,提高工作效率。
首先,定义一个字典,其中键(key)代表Prompt的类别或应用场景,值(value)则包含实际的Prompt文本。例如:
prompts_dict = {
"weather_query": "今天北京的天气怎么样?",
"news_summary": "请简要总结一下今天的国际新闻。",
"joke_telling": "讲一个关于程序员的笑话。",
# 更多Prompt...
}
随着使用需求的变化,随时在字典中添加新的Prompt或者修改已有的Prompt。例如,如果发现某个Prompt不够有效,可以直接修改其值:
prompts_dict["weather_query"] = "查询当前北京的实时天气状况。"
在需要时,通过键来提取Prompt并传递给AI模型。这种方式使得代码更清晰且便于维护:
model_input = prompts_dict["news_summary"]
response = ai_model.generate(model_input)
最后,可以通过api调用大模型或者是加载本地开源的大模型进行使用了。
本节学习内容最关键的是掌握RAG实现的几个步骤,并且对每个步骤有一定的实践能力。