赞
踩
基于RAG的大模型应用作为大模型基础建设的必要课题之一,本方案为需要使用大模型知识库辅助推理模式的人员提供可参考的标准化流程和实现形式,增加大模型的领域能力,加速大模型相关课题的落地。
大模型改变了我们与信息的交互方式,但对于我们提出的要求,这个交互过程也存在许多限制:
对于第一个限制,开源基础模型的理解能力不断提升,例如已开源的qwen-72B刷榜各评测榜单,对使用者的提示能力要求进一步降低,且已有能力已满足多样性任务需求,适合直接部署使用。对于第二个限制,使用检索增强生成技术(RAG,Retrieval Augmented Generation)是目前一种经济可行的方案。
RAG通过大语言模型(LLM)+知识召回(Knowledge Retrieval)的方式, 解决通用大语言模型在专业领域回答缺乏依据、存在幻觉的问题,是各类基于大模型的知识问答应用的常用技术。RAG的知识召回基于各类信息检索算法,主要有以下几种类型:
针对领域RAG的应用以向量检索为主,其它检索方式辅助强化,其基本思路是把私域知识文档进行切片然后向量化,后续通过向量检索进行召回文档片段,再作为上下文注入到大语言模型进行归纳总结。另外,使用搜索引擎检索增强生成的方式已经成为目前AI Agent框架的重要组成部分。
RAG包括五个关键阶段,它们将成为构建的任何大型应用程序的一部分。分别为:
在完成数据源加载、数据预处理、索引构建并存入向量数据库后,能够启动RAG应用执行流程,一个通用的完整流程如下图:
上图中每一步所进行的操作内容:
应用场景 | 描述 |
---|---|
智能客服和支持 | 使用知识库+大模型为客户提供实时支持,回答问题,自动化常见客户服务任务。 |
市场调研和分析 | 利用大模型处理和分析大量市场数据,洞察趋势,识别机会,预测市场变化。 |
自然语言处理 | 提高文档管理、语义分析和情感分析的效率,从文本数据中提取有价值的信息。 |
产品推荐和个性化体验 | 基于用户行为和偏好,提供个性化产品推荐、内容推送和购物建议,提高用户满意度。 |
财务分析和风险管理 | 运用大模型预测财务趋势,监控风险,制定投资策略,提高决策的准确性。 |
供应链优化 | 优化供应链管理,预测需求,提高库存效率,减少成本,确保供应链的稳定性。 |
人力资源管理 | 帮助招聘、培训和绩效管理,自动筛选简历,分析员工满意度,提供职业发展建议。 |
风险识别和安全 | 识别潜在风险,监控网络安全,检测欺诈活动,维护企业的信息和数据安全。 |
市场营销和广告 | 通过分析市场趋势、用户行为和社交媒体数据,制定精确的市场营销策略和广告定位。 |
知识管理和文档检索 | 建立知识库,帮助员工更容易地查找和分享信息,提高工作效率和决策质量 |
构建向量数据库是在构建RAG应用程序之前需要完成的工作。向量数据库作为数据中心,为上游的索引构建提供存储支撑,为下游的检索算法提供向量数据来源和平台服务。
向量数据库普遍支持各类搜索算法,提供了在大量数据中的高效检索。目前Langchain已经提供了对超过15种向量数据库的支持,下表是常用向量数据库介绍:
数据库 | 简介 |
---|---|
Faiss | Facebook AI 相似度搜索(Faiss)(opens in a new tab)是一种用于稠密向量的高效相似度搜索和聚类的库。它包含了能够搜索任意大小的向量集合的算法,甚至包括可能不适合内存的向量集合。 |
Annoy | Annoy是由Spotify开发的一种高效的向量搜索库,它可以在内存中存储大量的向量,并且可以快速地进行向量搜索。Annoy的一个主要优点是它的内存使用效率非常高,这使得它在处理大规模的数据时非常有优势。Annoy的缺点是它不支持在线的数据更新,这意味着如果我们需要添加或删除数据,我们可能需要重新构建整个索引。 |
Chroma | Chroma 是 Chroma 公司的矢量存储/矢量数据库。 Chroma DB 与许多其他矢量存储一样,用于存储和检索矢量嵌入。 Chroma 是一个免费开源项目,优势在于接口简洁,易于使用。 |
Redis | redis 通过RedisSearch 模块,也原生支持向量检索。 RedisSearch 是一个Redis模块,提供了查询、二级索引,全文检索以及向量检索等能力。 |
Weaviate | Weaviate 是一个开源的向量数据库,可以存储对象、向量,支持将矢量搜索与结构化过滤与云原生数据库容错和可拓展性等能力相结合。 |
Milvus | Milvus是一种开源的向量数据库,它支持在线的数据更新和实时的向量搜索。Milvus的一个主要优点是它的灵活性,它支持多种类型的向量搜索算法,并且可以根据用户的需求进行定制。然而,Milvus的一个缺点是它的内存使用效率相对较低,这可能会在处理大规模的数据时成为一个问题。 |
Qdrant | 具有扩展过滤支持的向量相似度引擎。Qdrant 完全使用Rust语言开发,实现了动态查询计划和有效负载数据索引。向量负载支持多种数据类型和查询条件,包括字符串匹配、数值范围、地理位置等。 |
Pinecone | Pinecone是一种全托管的向量搜索服务,它可以处理大规模的数据,并且可以在云端进行高效的计算。Pinecone的一个主要优点是它的易用性,用户无需关心底层的实现细节,只需要通过API就可以进行向量搜索。缺点是属于付费服务。 |
lanceDB | LanceDB是一款新型开发者友好型无服务器向量数据库,专为AI应用而设计。它可嵌入应用程序中,无需管理服务器,其扩展性依赖于磁盘而非内存,具有低延迟性。LanceDB支持向量搜索、全文搜索和SQL,并针对多模态数据进行了优化。 |
向量数据库的选择中,主流方案有以下几种:
向量数据库是储存经Embedding模型向量化后的文本/图像/音视频数据索引,从数据源转换到可储存向量的过程包括下图所示阶段:
数据提取是RAG数据库建设的第一步,其核心目的是从各种数据源中获取相关信息。数据提取过程需要保证信息的完整性和时效性,以下是各指标的具体描述
完整性:完整性指的是在数据提取过程中确保收集的信息全面,覆盖所有目标任务知识领域及话题。
时效性:时效性是指确保数据的更新,特别是对于快速变化的信息。
在数据提取阶段,主要任务包括:
数据提取工作主要涉及源文档的获取以及对源文档进行解析。源文档通过接口调用、脚本采集或爬虫的方式获取。下面以5G需求体系中功能体系为例获取和加载icenter数据:
- class UrlAPI:
- def __init__(self, account="", token=""):
- self.content_url = ""
- self.sub_tree_url = ""
- self.uac_server = ""
- self.history_url = ""
- self.history_detail_url = ""
- self.get_comments_url = ""
- self.add_comments_url = ""
- self.modify_comment_url = ""
- self.delete_comment_url = ""
- self.template_url = ""
- self.all_tree_url = ""
- self.authority = ""
- self.put_authority = ""
- self.X_Emp_No = '' #账号
- self.password = '' # 密码
- self.X_Auth_Value = ''
- self.loginsyscode = 'Portal'
- self.originsyscode = ''
- self.token_status = 0
- self.space_id = 'b3406ddb052f4c5da716ff9485814817'
- if account == "":
- token_dic = self.get_token()
- self.account = token_dic['other']['account']
- self.token = token_dic['other']['token']
- self.headers = {'Content-type': 'application/json', 'X-Emp-No': self.account, 'X-Auth-Value': self.token, 'X-Lang-Id': 'zh_CN'}
- else:
- self.account = account
- self.token = token
- self.headers = {'Content-type': 'application/json', 'X-Emp-No': self.account, 'X-Auth-Value': self.token, 'X-Lang-Id': 'zh_CN'}
- self.auth = requests.auth.HTTPDigestAuth(self.account, self.token)
-
- self.proxies = {"http": "", "https": ""}
-
- def get_host_ip(self):
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.connect(('8.8.8.8', 80))
- ip = s.getsockname()[0]
- finally:
- s.close()
- return ip
-
- def get_token(self):
- url = ""
- client_ip = self.get_host_ip()
- text = \
- {
- "account": self.X_Emp_No,
- "passWord": self.password,
- "loginClientIp": client_ip,
- "loginSystemCode": self.loginsyscode,
- "originSystemCode": self.originsyscode,
- "other": {
- "networkArea": '1',
- "networkAccessType": '1'
- },
- "verifyCode": hashlib.md5(str(self.X_Emp_No + self.password + client_ip + self.loginsyscode + self.originsyscode).encode(encoding='utf-8')).hexdigest()
- }
- headers = {'Content-type': 'application/json'}
- content = requests.post(url, data=json.dumps(text), headers=headers)
- return content.json()
-
- def get_page_info(self, content_id):
- url = self.content_url % (content_id, self.space_id)
- resp = requests.get(url, auth=self.auth, headers=self.headers, proxies=self.proxies)
- reps_json_data = json.loads(resp.content)
- try:
- if resp.status_code != 200 or reps_json_data["code"]["msgId"] != 'RetCode.Success':
- time.sleep(1)
- resp = requests.get(url, auth=self.auth, headers=self.headers, proxies=self.proxies)
- reps_json_data = json.loads(resp.content)
- except Exception as e:
- return None, None
- if "bo" not in reps_json_data.keys():
- return None, None
- page_txt = reps_json_data['bo']['contentBody']
- title = reps_json_data['bo']['title']
- return page_txt, title
对提取的文本数据按照markdown格式分章节处理,得到初步的原始数据。
原始数据包含大量上下文无关的噪音,下一步需要对原始数据进行清洗,提升召回数据的质量。
数据清洗是提高RAG系统性能的关键,数据清洗旨在保证信息的规范性、必要性、准确性以及一致性,以下是各指标的具体描述:
在数据清洗阶段,主要任务包括:
结合目标任务应用的情况,使用同义词、释义或其他自然语言描述方式来增强语料库的多样性。
下表中的指标范围为参考值,评估实施时,指标范围需要结合数据具体情况进行设定。
指标名称 | 指标范围 | 计算方法 | 备注 |
---|---|---|---|
中英文字符的占比 | >80% | 通过正则表达式过滤出中英文字符,然后计算占比 | 文本中中英文字符占比较低,表明可能存在乱码、无意义符号等,知识含量不高 |
可疑字符:&、#、<、>、{、}、[、]等占比 | <1% | 通过正则表达式过滤出可疑字符,然后计算占比 | 这些可疑字符一般在文本中比例不会太高,如果比例过高,说明文本中存在干扰信息,比如html标签。这类指标不能用于代码类型的语料或者代码类型的语料指标范围要进行调整 |
整个文本的大小 | >1024Byte>15个单词/字 | 1、计算整个文本的size2、通过正则计算中英文字词数 | 如果整个文本的大小或者字词数较少,说明知识含量较低 |
短句子的占比 | <10% | 这里的句子可以先按照行处理先将文档按照换行符分割再计算每行的字符数,每行字符数不应该小于5 | 短句子出现较多的情况包括网页中导航栏、跟帖中大量出现“点赞"等词汇、书本中页眉页脚等 |
平均语句长度 | >20 | 1、将文本分割成句子,计算所有句子的平均长度 | 大模型语料期望更多的长句子或者说信息含量较多的句子。计算平均语句长度可以评估文本的整体信息含量 |
缩略词的占比 | <1% | 1、缩略词占总词数的百分比(Stanford Named Entity Recognizer:https://nlp.stanford.edu/software/CRF-NER.shtml) | 缩略词较多用于聊天或者论坛中。正式文档例如学术论文中也有,但是一般比例不会太高 |
未知词的百分比 | <1% | 1、未知词汇占总词数的百分比(通过将NLTK中的标准词性标注器应用于文本来计算,该标注器对未知词有单独的“X”类。) | 未知词汇一般不太会出现在正常的知识文本中,一般出现在网络论坛等非严谨的情况或者语句是病句。 |
错误单词占比 | <1% | 1、拼写错误单词占总单词的比例(PyEnchant) | |
病句的百分比 | <1% | 1、使用nltk进行语义判断2、使用深度学习模型(如BERT、GPT等)进行更深入的语义理解 | |
词汇丰富度 | >80% | 1、nltk分词后不同词的个数除以总词数 | 词汇丰富度较低,说明文本中有大量的重复词 |
困惑度 | 待定 | 参照各模型的使用 | 1、不同模型的得分不同,需要根据实际情况确定指标阈值2、困惑度值越低越好 |
敏感词占比 | 0 | 使用人工收集的敏感词进行计算 | |
字词重复率 | <5% | 相同字词占整个文本的比例 | |
句子(行)重复率 | <5% | 相同句子(行)占整个文本的比例 | |
文本重复率 | <80% | 单个文本和语料库中其他文本的相似度应小于80% |
在语料质量检查中,人工验证效率比较低下,只能通过抽样对部分语料进行检查。但是人工检查是不可缺失的一环,因为目前的技术代码不能够非常准确的判断文本的流畅性、上下文的相关性等
参考论文《A Large-scale Chinese Short-Text Conversation Dataset and Chinese pre-training dialog models》和《Evaluation of Text Generation: A Survey》,按固定采样比例(如0.1%)进行采样后,从以下几个方面人工评估。
具体来说,不符合语法性或相关性的语句,给予0分;语句流畅、符合相关性但信息量不足的语句,给予1分;语句流畅、符合相关性且信息量充足的语句,给予2分。
人工验证流畅性、相关性的同时,也能顺便检查下文本是否存在乱码、是否有大段空白、是否存在隐私信息等
RAG应用中需要对知识库的数据内容进行分块储存,以达到段落召回的目标。
分块(chunking)是将大块文本分解为多个小段落的过程,当我们使用Embedding模型编码内容时,这是一项必要的技术,分块可以帮助我们减少Embedding编码内容的噪音,优化从向量数据库中被召回内容的准确性,提高RAG系统效率。
各种分块策略的主要参数有以下两点:
选择合适的分块参数很重要,合理的参数能够决定召回结果与query的相关性,直接影响模型回答效果,同时也能够控制token数量,保证不超过模型本身的限制。
Langchain中提供了多种文本分割器,适用于不同场景,应该充分理解各方法的优缺点,按照具体情况选择合适的方式。
字符文本分割是最常用的方式,只需要决定每一个块的字符或token的数量,以及各块之间的重叠度,以确保语义上下文不会在块之间丢失。
在多数情况下,固定大小的分块是最佳方式,在计算上更加经济且易于使用。
NLTK(Natural Language Toolkit)是一个python自然语言工具包,包含已经训练好的模型,它能够将一个段落或一篇文章分割成独立的句子。
Langchain中提供了代码分割器,PythonCodeTextSplitter可以将文本按Python类和方法定义进行拆分,它是RecursiveCharacterSplitter的一个简单子类,具有Python特定的分隔符。
递归文本分割器它由字符列表参数化。它尝试按顺序在它们上进行分割,直到块足够小。默认列表为[" ", "\n", " ", ""]。这样做的效果是尽可能地保持所有段落(然后是句子,然后是单词)在一起,因为它们通常看起来是最强的语义相关的文本片段。
我们选择典型的字符文本分割器来进行实验,之后为了评估不同分割器对RAG系统的影响,在后面的章节将会尝试使用更多的文本分割器。
- def read_files_in_directory(directory):
- file_contents = []
- file_paths = []
- metadatas = []
-
- # 遍历指定路径下的所有文件和文件夹
- for root, dirs, files in os.walk(directory):
- for file_name in files:
- file_path = os.path.join(root, file_name) # 获取文件的绝对路径
- file_paths.append(file_path)
-
- with open(file_path, 'r') as file:
- content = file.read()
- file_contents.append(content)
- # print(file_path)
- metadatas.append(file_path.split("/")[-1].split(".md")[0])
-
- return file_contents, file_paths, metadatas
-
-
- def split_character(directory_path):
-
- contents, _, _ = read_files_in_directory(directory_path)
- text_splitter = CharacterTextSplitter(
- separator = " ",
- chunk_size = 650,
- chunk_overlap = 200,
- length_function = len,
- )
- content_str = ""
- for content in contents:
- content_str += content
- chunks = text_splitter.create_documents([content_str], metadatas=[{"source": ''}])
- # print(len(chunks))
- # print (chunks[:5])
- return [{"text": chunk.page_content, "source": chunk.metadata["source"]} for chunk in chunks]
设定metadata一般为对应片段内容的来源路径source,能够快速定位原文段。设定为该文档的名称或片段概述等关键词能够在索引过程更加高效。
文本嵌入(Text Embedding)是指将自然格式的信息(如单词、字母等)通过编码模型转换为包含语义信息的向量,这些带有语义信息的向量经过更多NLP任务的训练能够适应不同的需求,例如分类任务或生成任务。选择好的Embedding模型直接决定了待处理的文本意义能否被理解,那么显然向量召回具有最主要的两个优点:
大模型时代的Text Embedding模型均是Transformer结构的仅编码器模型,也就是类bert结构。
在选用适合于自己RAG系统的Embedding模型时,需要考虑以下方面:
确定Embedding模型选用可以参考MTEB-海量文本嵌入基准的官方榜单(https://huggingface.co/spaces/mteb/leaderboard),MTEB包含8个语义向量任务,涵盖58个数据集和112种语言。
与MTEB对应的中文评估指标C-MTEB也可以参考,包含6种任务类型,35个数据集,较中肯的评估各模型在不同任务的效果。
以上两类benchmark具有类似的评估任务,可以综合二者结果进行模型选取。
涉及到的任务包含多个类型:
示例选用m3e-base模型进行向量化,使用 Langchain 的 Embedding 相关模块(HuggingFaceEmbeddings和OpenAIEmbeddings)加载模型并编码我们的文档块。
- class EmbedChunks:
- def __init__(self, model_name):
- if model_name == "text-embedding-ada-002":
- self.embedding_model = OpenAIEmbeddings(
- model=model_name,
- openai_api_base=os.environ["OPENAI_API_BASE"],
- openai_api_key=os.environ["OPENAI_API_KEY"])
- else:
- self.embedding_model = HuggingFaceEmbeddings(
- model_name=model_name,
- model_kwargs={"device": "cuda"},
- encode_kwargs={"device": "cuda", "batch_size": 100})
-
- def __call__(self, batch):
- embeddings = self.embedding_model.embed_documents(batch["text"])
- return {"text": batch["text"], "source": batch["source"], "embeddings": embeddings}
-
-
- def embedding(embedding_model_name):
-
- chunks_ds = split_character()
- embedded_chunks = ray.data.from_items(chunks_ds).map_batches(
- EmbedChunks,
- fn_constructor_kwargs={"model_name": embedding_model_name},
- batch_size=100,
- num_gpus=1,
- compute=ActorPoolStrategy(size=2))
- # print(embedded_chunks)
- embedded_chunks.show(1)
调用m3e-base模型,通过embedding函数对每个文段编码,embed_documents即Embedding模型的推理过程,计算过程采用ray数据集的map_batches模块,计算使用两个工作线程,每个工作线程有 1 个 GPU。第一文本块的Embedding结果如下:
- {
- 'text': '# PTRS管理\n![](https://icenterapi.zte.com.cn/group3/M00/16/43/CjYTEWGd3MWAQPCXAAAJr9xIJws671.png)\n\n\n# PTRS管理的功能设计\n## PTRS管理的功能设计分析\n### PTRS管理的业务能力\n 相位噪声指射频器件在各种噪声的作用下引起的系统输出信号相位的随机变化。相位噪声会恶化接收端的信噪比或误差向量幅度,造成大量的误码,这样就限制了高阶调制的使用,会严重影响系统的容量...',
- 'source': '',
- 'embeddings': [0.6715492010116577, 1.1172834634780884, 0.7916168570518494, -0.11632557958364487, -0.09606651216745377, -0.19460099935531616, 0.574505627155304, -0.36584919691085815, -0.4636607766151428, 0.5073410272598267, 0.495039701461792, 0.3359251022338867, 0.660287618637085, -0.6332092881202698, -0.48762381076812744, -0.3404944837093353, 0.035247091203927994, 0.6208893060684204, ...]
-
- }
获得了完整的所有内容的Embedding编码块后,需要将其索引(存储)起来,以便于能够快速检索和推理。有许多可选的向量数据库,如2.1.1节所示。将(text, source, embeddings)三元组保存在可进行向量检索的数据库中,通过相似度算法(余弦相似度等)计算Embedding和query,如果使用Langchain进行索引生成,那么需要调整三元组为Langchain适用形式,这里以Langchain中集成的Faiss为例进行索引生成:
- def index(directory_path):
-
- contents, _, _ = read_files_in_directory(directory_path)
- text_splitter = CharacterTextSplitter(
- separator = " ",
- chunk_size = 650,
- chunk_overlap = 200,
- length_function = len,
- )
- content_str = ""
- for content in contents:
- content_str += content
- chunks = text_splitter.create_documents([content_str], metadatas=[{"source": ''}])
-
- model_name = "model/m3e-base"
- model_kwargs = {"device": "cuda"}
- encode_kwargs = {"device": "cuda", "batch_size": 100}
- embedding = HuggingFaceBgeEmbeddings(
- model_name=model_name,
- model_kwargs=model_kwargs,
- encode_kwargs=encode_kwargs,
- query_instruction="为文本生成向量表示用于文本检索"
- )
- db = FAISS.from_documents(chunks, embedding)
- db.save_local("faiss_index")
将faiss_index索引文件保存在本地,方便进行关联查询,对于示例知识库,构建索引和保存过程大约需要3min,文件内容如下。对于较大的知识库,部分向量数据库也提供云服务等。
faiss索引保存的内容
查询生成阶段包括查询检索和响应生成。查询检索时将问题通过与构建索引时相同的Embedding模型进行编码,然后对查询向量和知识库编码向量之间进行相似度计算(欧氏距离、余弦相似度、点积相似度),当检索到最相关的几个片段,便可以使用各种采样方式选择文本,进而将其作为大模型的上下文来生成响应。
Langchain中提供相似度计算的接口,当构件好索引,便可以传入query直接运行相似度匹配算法,获取查询结果。
- def similarity_search(query):
- model_name = "model/m3e-base"
- model_kwargs = {"device": "cuda"}
- encode_kwargs = {"device": "cuda", "batch_size": 100}
- embedding = HuggingFaceBgeEmbeddings(
- model_name=model_name,
- model_kwargs=model_kwargs,
- encode_kwargs=encode_kwargs,
- query_instruction="为文本生成向量表示用于文本检索"
- )
- new_db = FAISS.load_local("faiss_index", embedding)
-
- # 第一种搜索方式
- # docs = new_db.similarity_search(query)
-
- # 第二种:构建检索器
- retriever = new_db.as_retriever(search_type="mmr", search_kwargs={"k":8})
- docs = retriever.get_relevant_documents(query)
- return docs
在Langchain中可以构建检索器来指定search_type,search_type主要包括两种:query- Similarity Search 和 Max Marginal Relevance Search (MMR Search),其中MMR Search旨在提高搜索结果的多样性,常用于推荐系统。如果想检索更准确则推荐选择query- Similarity Search,默认的search_type也是query- Similarity Search。同时,配置search_kwargs的k参数可以选择召回文段数量,提高系统召回率。
将召回结果组织为大模型的上下文,进行推理服务,这样就实现了典型的RAG系统,修改知识库内容使得模型具有领域知识,其中各模块包括模型均是可插拔式设计,系统可塑性强,具有很高的优化价值。
RAG应用构建过程并不复杂,但由于 RAG 流程较长,包含不同的组件,要使 RAG 系统的性能调整到令人满意的状态是非常困难的。一个RAG应用主要包含以下两个组件:
评估 RAG 流程时,必须单独评估两个组件,并综合两个组件的评估结果来判断RAG 流程是否在哪些方面仍需要改进。此外,要了解 RAG 应用程序的性能是否有所改善,必须对其进行定量评估。为此,需要两个要素:评估指标和评估数据集。
RAGAs(Retrieval-Augmented Generation Assessment,检索增强生成评估)是一个框架(GitHub,文档),可提供必要的工具,帮助在组件层面评估 RAG 流程。
RAGAs 不需要依赖评估数据集中人工标注的标准答案,而是利用底层的大语言模型进行评估。为了对 RAG 流程进行评估,RAGAs 需要以下几种信息:
question
:RAG 流程的输入,即用户的查询问题。answer
:RAG 流程的输出,由RAG流程生成的答案。contexts
:为解答 question
而从外部知识源检索到的相关上下文。ground_truths
:question
的标准答案,这是唯一需要人工标注的信息。这个信息仅在评估 context_recall
这一指标时才必须(详见 评估指标)。RAGA 提供了一些指标来按组件和端到端评估 RAG 系统。 在组件级别,RAGAs 提供了评价检索组件(包括context_relevancy和context_recall)和生成组件(涉及faithfulness和answer_relevancy)的专门指标。
上下文精度 :测量检索到的上下文与问题的相关程度。评估所有在上下文(contexts)中呈现的与基本事实(ground-truth)相关的条目是否排名较高。理想情况下,所有相关文档块(chunks)必须出现在顶层。值范围在 0 到 1 之间,其中分数越高表示精度越高。 公式解释
上下文召回率: 衡量是否检索到回答问题所需的所有相关信息。该指标是根据 ground_truth(这是框架中唯一依赖人工注释的指标)和 contexts 计算的。为了根据真实答案(ground truth)估算上下文召回率(Context recall),分析真实答案中的每个句子以确定它是否可以归因于检索到的Context。 在理想情况下,真实答案中的所有句子都应归因于检索到的Context.
忠实性:衡量生成答案的事实准确性。给定上下文中正确陈述的数量除以生成答案中的陈述总数。如果答案(answer)中提出的所有基本事实(claims)都可以从给定的上下文(context)中推断出来,则生成的答案被认为是忠实的。
答案相关性:衡量生成的答案与问题的相关程度。该指标是使用 question 和 answer 计算的。
例如,答案“法国位于西欧”。对于问题“法国在哪里,它的首都是什么?”的答案相关性较低,因为它只回答了问题的一半。当答案直接且适当地解决原始问题时,该答案被视为相关。重要的是,我们对答案相关性的评估不考虑真实情况,而是对答案缺乏完整性或包含冗余细节的情况进行惩罚。为了计算这个分数,LLM会被提示多次为生成的答案生成适当的问题,并测量这些生成的问题与原始问题之间的平均余弦相似度。基本思想是,如果生成的答案准确地解决了最初的问题,LLM应该能够从答案中生成与原始问题相符的问题。
上下文精度和召回率是对检索器组件的评估,忠实性和答案相关性是对生成结果质量的评价,通过对各组件指标进行均值归一化,得到两个评价标准:
使用ragas包评估RAG应用效果,首先需要安装ragas包
pip install ragas
安装完成后,可以使用以下代码测试安装的包是否可用
from ragas import evaluate
此出引入后如没有报错,则表明安装的包可以用,如出现以下情况的报错则需要处理
1. 错误场景1:
cannot import name 'AzureOpenAIEmbeddings' from 'langchain.embeddings'
此类属于ragas包版本和langchain版本不匹配问题,需要写在langchain、openai、ragas包,重新安装ragas
2. 错误场景2:
ValueError: Unknown scheme for proxy URL URL('socks://xxxx.com:80')
此类属于代理问题引起,在终端上执行:unset all_proxy; unset ALL_PROXY
测试数据包含以下信息:
question
:用户的查询问题。answer
:RAG 流程的输出,由RAG流程生成的答案。contexts
:为解答 question
而从外部知识源检索到的相关上下文。ground_truths
:question
的标准答案,这是唯一需要人工标注的信息。构建数据集过程中, question和ground_truths是需要人工标注的。 context为检索知识库后获取到的上下文内容,answer是下游应用结合context对question的回答,下面给出了一个自动化构建的测试数据的代码:
- questions = []
-
- ground_truths = []
-
- answers = []
-
- contexts = []
- def load_dataset():
- # 加载数据集(question和ground_truths)
- data = pd.read_csv("rag/test_dataset.csv")
-
- for i, row in data.iterrows():
- print(i, row["question"], row["ground_truths"])
- questions.append(row["question"])
- ground_truths.append([row["ground_truths"]])
- # 检索知识库获取检索到的上下文内容
- context = retrieve_context("vector", "gte-large-zh", "feature_gte_large_zh", row["question"], row["question"], 5, 0.5)
-
- contexts.append(context)
- # 将上下文内容与用户问题注入prompt模板,输入给下游模型,获取模型分析的结果
- answer = retrieve_answer(row["question"], context)
- answers.append(answer)
- evaluate_model()
将构建完成的评估数据注入给评估模型内,选择评估指标。此时将调用openai的模型开发对数据集进行评估,评估结果拿到后可保存在csv文件中。
- from ragas import evaluate
- from ragas.metrics import (
- faithfulness,
- answer_relevancy,
- context_recall,
- context_precision,
- )
-
- def evaluate_model():
- # 构建评估数据
- data = {
- "question": questions,
- "answer": answers,
- "contexts": contexts,
- "ground_truths": ground_truths
- }
- dataset = Dataset.from_dict(data)
-
- #选择评估指标,进行评估
- result = evaluate(
- dataset = dataset,
- metrics=[
- context_precision,
- context_recall,
- faithfulness,
- answer_relevancy,
- ],
- )
- df = result.to_pandas()
- df.to_csv("gte.csv", index=False)
- print(df)
直接使用evaluate方法,会默认使用openai的GPT3.5模型进行评估,评估过程受限于apikey的使用次数限制,因此建议使用自己的大模型进行评估
- from ragas.llms import RagasLLM
- from langchain.schema import LLMResult
- from langchain.schema import Generation
- from langchain.callbacks.base import Callbacks
- from langchain.schema.embeddings import Embeddings
- from FlagEmbedding import FlagModel
- from ragas.metrics import AnswerRelevancy
-
- class MyLLM(RagasLLM):
-
- def __init__(self, llm):
- self.base_llm = llm
-
- @property
- def llm(self):
- return self.base_llm
-
- def generate(
- self,
- prompts: List[str],
- n: int = 1,
- temperature: float = 0,
- callbacks: t.Optional[Callbacks] = None,
- ) -> LLMResult:
- generations = []
- llm_output = {}
- token_total = 0
- for prompt in prompts:
- content = prompt.messages[0].content
- text = self.retrieve_answer(content) # 调用模型的接口获取输出
- generations.append([Generation(text=text)])
- token_total += len(text)
- llm_output['token_total'] = token_total
-
- return LLMResult(generations=generations, llm_output=llm_output)
-
- async def agenerate(
- self,
- prompts: List[str],
- n: int = 1,
- temperature: float = 0,
- callbacks: t.Optional[Callbacks] = None,
- ) -> LLMResult:
- generations = []
- llm_output = {}
- token_total = 0
- for prompt in prompts:
- content = prompt.messages[0].content
- text = self.retrieve_answer(content)
- generations.append([Generation(text=text)])
- token_total += len(text)
- llm_output['token_total'] = token_total
-
- return LLMResult(generations=generations, llm_output=llm_output)
-
-
- def retrieve_answer(self, prompt):
- url = ""
- payload = json.dumps({
- "model": "",
- "messages": [
- {
- "role": "user",
- "content": prompt
- }
- ],
- "max_tokens": 4096,
- "temperature": 0.1,
- "stream": False
- }, ensure_ascii=False).encode("utf-8")
- headers = {
- 'Authorization': 'TEST-46542881-54d4-4096-b93d-6d5a3db326ac',
- 'Content-Type': 'application/json'
- }
-
- response = requests.request("POST", url, headers=headers, data=payload)
- res = json.loads(response.text)
- return res['choices'][0]['message']['content']
-
-
- class BaaiEmbedding(Embeddings):
-
- def __init__(self,model_path, max_length=512, batch_size=256):
- self.model = FlagModel(model_path, query_instruction_for_retrieval="为这个句子生成表示以用于检索相关文章:")
- self.max_length = max_length
- self.batch_size = batch_size
-
- def embed_documents(self, texts: List[str]) -> List[List[float]]:
- return self.model.encode_corpus(texts, self.batch_size, self.max_length).tolist()
-
- def embed_query(self, text: str) -> List[float]:
- return self.model.encode_queries(text, self.batch_size, self.max_length).tolist()
-
- # 使用阶段
- def direct_evaluate():
- data = pd.read_csv("gte_raw.csv")
- for i, row in data.iterrows():
- print(row["contexts"])
- questions.append(row["question"])
- answers.append(row["answer"])
- contexts.append(eval(row["contexts"]))
- ground_truths.append(eval(row["ground_truths"]))
- data = {
- "question": questions,
- "answer": answers,
- "contexts": contexts,
- "ground_truths": ground_truths
- }
- dataset = Dataset.from_dict(data)
- my_llm = MyLLM("")
- ans_relevancy = AnswerRelevancy(embeddings=BaaiEmbedding(""))
- faithfulness.llm = my_llm
- context_recall.llm = my_llm
- context_precision.llm = my_llm
- ans_relevancy.llm = my_llm
- result = evaluate(
- dataset = dataset,
- metrics=[
- context_precision,
- context_recall,
- faithfulness,
- ans_relevancy,
- ],
- )
- df = result.to_pandas()
- df.to_csv("gte.csv", index=False)
- print(df)
为了验证RAG系统在召回相关文段上下文对模型回复效果确实有提升,我们可以通过设置num_chunks=0(无上下文)将其与num_chunks=5(chunk_size=500,m3e-base)来进行比较。
分块的大小影响每个上下文段中的概念完备性,更大的块可能更加能够封装完整的模块化知识,但会受到更多噪声的影响,分块策略上可以考虑:
为了验证分块大小对RAG系统的影响程度,采用300-900的分块大小构建索引,并采用retrieval_score和generate_score指标进行评估,评估中使用m3e-base模型。
chunk_size | retrieval_score | generate_score |
---|---|---|
300 | 0.6638 | 0.6762 |
500 | 0.7232 | 0.6951 |
700 | 0.7534 | 0.7653 |
900 | 0.7623 | 0.7456 |
随着分块大小的增加,检索质量也在提升,并且可能会继续增加,而模型回复质量先增后减,说明块大小一定程度能够带来更多有利的信息,提升回答质量,但相应的会引入更多噪音,一味的增加chunk_size不可行,需要按需选择最佳分块大小。
模型对召回效果影响明显,为了验证不同Embedding模型在我们RAG系统的表现,将采用以下三种开源模型进行评估:
三种模型评估得分如下(chunk_size=700):
Embedding模型 | retrieval_score | generate_score |
---|---|---|
m3e-base | 0.7534 | 0.7653 |
acge-large-zh | 0.6966 | 0.7168 |
gte-large-zh | 0.7792 | 0.7678 |
整体上来看检索得分与生成得分是成正相关的,说明模型输出较为稳定(temperature=0.1),且Embedding模型的选择对检索质量有明显提升,但检索质量达到一定程度,模型的生成质量则受模型基础能力影响更大。
仅关注数据层面的优化对相关文档的召回率有促进作用,但直接探索使用实际场景中的数据微调Embedding模型,有助于更好的数据表示,提高RAG系统的检索质量和得分。学习比默认的Embedding更符合上下文的token是一个直观的优化点,因为默认的tokenization可能存在问题:
Embedding模型已经经过自监督学习任务(word2vec)的训练,微调的任务是使模型确认数据集中的哪些部分能够最匹配输入,使得嵌入模型能够学习到数据集中更准确的向量表示。
微调的关键需要构造合理的领域数据集,可以使用模型selfQA合成数据或人工标注。
{'train_runtime': 274.6573, 'train_samples_per_second': 22.421, 'train_steps_per_second': 1.398, 'train_loss': 0.21397568702620143, 'epoch': 2.0}
当我们描述问题时,有时并不清楚我们的提问是否与提供的文本表达存在歧义,或者当我们只有模糊的记忆,只知道一两个词时,向量库的语义搜索往往无法满足我们的需求。此时可以结合关键词和语义进行同时检索,能够进一步提高我们的RAG的准确度。这种混合检索方法可以在保证搜索效率的同时,提高搜索的准确性和全面性。
传统的关键词搜索一定程度弥补的语义搜索的不足,传统关键词搜索擅长:
“混合检索”没有明确的定义,如果我们使用其他搜索算法的组合,也可以被称为“混合检索”。比如,我们可以将用于检索实体关系的知识图谱技术与向量检索技术结合。不同的检索系统各自擅长寻找文本(段落、语句、词汇)之间不同的细微联系,这包括了精确关系、语义关系、主题关系、结构关系、实体关系、时间关系、事件关系等。没有任何一种检索模式能够适用全部的情景。混合检索通过多个检索系统的组合,实现了多个检索技术之间的互补。
6 总结
本方案主要介绍了RAG系统的概念、组成结构、执行流程,并分别对流程的每个部分进行展开,详述了各阶段的实现流程,使用实际案例进行分析。介绍了RAG系统的两个评估指标和计算方式,并针对基础优化点开展了实验,对复杂优化点提供了方案思路,说明了RAG系统优化的必要性。
以大模型为中心的RAG是高度动态化的系统,同时系统中各组件间分工明确且几乎无相互波及,真正实现“可插拔式”。RAG系统也是基于大模型的Agent形式的重要组成部分,而RAG中的“增强”的过程也可以看做Agent中的超级工具之一,为大模型提供上下文参考信息查询的服务,因此对RAG的深入探索也是Agent应用探索的重要部分。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。