一文读懂 RAG 全貌

更多新鲜技术资讯,欢迎关注公众号: 深度极客

术语

缩写全称描述
RAGRetrieval Augmented Generation检索增强生成
Token模型理解和处理的基本单位
Context Window上下文窗口上下文窗口指的是 AI 模型在生成回答时考虑的 Token 数量。它决定了模型能够捕捉信息的范围。上下文窗口越大,模型能够考虑的信息就越多,生成的回答也就越相关和连贯。例如,GPT-4 Turbo 拥有 128k 个 Token 的上下文窗口,相当于超过 300 页的文本
Context Length上下文长度上下文长度是 AI 模型一次能够处理的最大 Token 数量。它决定了模型处理能力的上限。例如,ChatGPT 3.5 的上下文长度为 4096 个 Token。这意味着 ChatGPT 3.5 无法接受超过 4096 个 Token 的输入,也无法一次生成超过 4096 个 Token 的输出。
Temperature温度温度是控制 AI 模型生成输出随机性的参数。
Embedding嵌入向量化的过程。在代码库索引的上下文中,使用embeddings的目的是为了更有效地检索和排序与给定查询最相关的代码片段或文档
Ranking/Reranking根据某种相关性或重要性度量对检索到的结果进行排序的过程,重新排序步骤提高了最终选择结果的质量,在检索速度和结果质量之间取得平衡
LLMlarge language model大语言模型
OllamaLLM(大型语言模型)服务工具,用于简化在本地运行大语言模型,降低使用大语言模型的门槛,使得大模型的开发者、研究人员和爱好者能够在本地环境快速实验、管理和部署最新大语言模型,包括如Llama 3、Phi 3、Mistral、Gemma等开源的大型语言模型。
Lancedb一个用于人工智能的开源矢量数据库

背景

在 AI 辅助编程领域,已经有较多成熟的 AI 辅助编程插件,比如 Copilot、CodeGeex 等。通过这些插件可以实现智能聊天、代码补全、内联聊天等功能。
不过随着 AI 技术发展,现有的 AI 辅助编程的准确性已经无法满足需求,如何丰富 prompt 的上下文并提升准确性,成为 AI 辅助编程的重点研究方向。
在 RAG 未出现之前,通常是以编辑器内当前光标之前和之后的内容作为上下文,虽然丰富了 prompt,但是局限性也较大。比如只能获取当前文件的内容作为上下文,无法获取工程中的其它文件,一些关联性非常强的代码无法作为上下文传递给 LLM,造成 LLM 回答的准确性不足。
在引入 RAG 技术后,以上问题得到了很好的解决。RAG的原理是将本地代码进行向量化,在提问时将“用户选中代码+提问”输入到向量模型中进行查询,根据向量相似度匹配度对应代码片段,以此作为 prompt 的上下文,最后将“提问+代码+context”作为模型的输入,得到更准确的代码生成。
本文主要介绍 RAG 的基础原理,以及在 AI 辅助编程领域如何使用 RAG 技术,目标是在 IDE 中得到实际应用。

RAG 技术简介

RAG 即“检索增强生成”(Retrieval Augmented Generation),这项技术通过将大量外部数据与基础模型相结合,显著增强了语言模型(LLM)的能力,使得AI的回应更加真实、个性化和可靠。

RAG 技术的核心在于结合了检索(Retrieval)和生成(Generation)两大核心技术。在处理复杂的查询和生成任务时,RAG 首先通过检索模块从大量数据中找到与查询最相关的信息,然后生成模块会利用这些检索到的信息来构建回答或生成文本。

自2020年提出以来,从最初的朴素RAG(Naive Rag),到高级RAG(Advance Rag),再到模块化RAG(Modular Rag),RAG系统不断优化和迭代,以解决实际应用中遇到的问题,如索引环节中的核心知识淹没问题、检索环节中的用户意图理解不准确问题,以及生成环节中的冗余信息干扰问题等。

在这里插入图片描述

RAG 流程应用于问答的代表性实例。它主要包括 3 个步骤。1)索引。将文档拆分成块,编码成向量,并存储在向量数据库中。2)检索。根据语义相似性检索与问题最相关的 Top k 个块。3)生成。将原始问题和检索到的块一起输入 LLM 以生成最终答案。

以下是 RAG 发展的几个版本:
在这里插入图片描述
RAG 三种范式对比。(左)Naive RAG 主要由索引、检索和生成三部分组成。(中)Advanced RAG 围绕预检索和后检索提出了多种优化策略,流程与 Naive RAG 类似,但仍然遵循链式结构。(右)模块化 RAG 继承并发展了上一范式,整体上表现出更大的灵活性,具体表现在引入多个具体的功能模块和对已有模块的替换,整体流程不局限于顺序检索和生成,还包括迭代检索、自适应检索等方法。

预检索阶段。此阶段主要关注优化索引结构和原始查询。优化索引的目标是提升被索引内容的质量。这涉及的策略包括:增强数据粒度、优化索引结构、添加元数据、对齐优化和混合检索。而查询优化的目标是使用户的原始问题更清晰,更适合检索任务。常用方法包括查询重写、查询转换、查询扩展等技术。

后检索过程。检索到相关上下文后,将其与查询有效地整合起来至关重要。后检索过程的主要方法包括重新排序块和上下文压缩。对检索到的信息重新排序,将最相关的内容重新定位到提示的边缘,这是一项关键策略。这一概念已在 LlamaIndex 2、LangChain 3 和 HayStack 等框架中实现。将所有相关文档直接输入 LLM 可能会导致信息过载,用不相关的内容稀释对关键细节的关注。为了缓解这种情况,后检索工作集中于选择必要的信息、强调关键部分和缩短要处理的上下文。

模块化 RAG。它采用了多种策略来改进其组件,例如添加用于相似性搜索的搜索模块以及通过微调来优化检索器。为了应对特定挑战,引入了重组 RAG 模块和重新排列的 RAG 管道等创新。向模块化 RAG 方法的转变正在变得普遍,它支持其组件之间的顺序处理和集成的端到端训练。尽管模块化 RAG 具有独特性,但它建立在高级和简单 RAG 的基本原理之上,体现了 RAG 家族的进步和完善

在这里插入图片描述
RAG 在“需要外部知识”和“需要模型自适应”方面与其他模型优化方法相比,Prompt Engineering 对模型和外部知识的修改较少,专注于利用 LLM 本身的能力。而 Fine-tuning 则涉及进一步训练模型。在 RAG(Naive RAG)的早期阶段,对模型修改的需求较低。随着研究的进展,Modular RAG 与 Fine-tuning 技术更加融合。

【RAG 生态系统概述】
在这里插入图片描述

方案

技术框架

![[Excalidraw/Drawing 2024-09-13 17.55.25.excalidraw.md]]

RAG 实现原理

上图是 RAG 技术在编程领域的实现原理,更接近高级 RAG 范式。

  • Chunking
    • 文档分割:将代码仓库中的文件或代码块按照特定大小(根据函数、类定义等逻辑单元)进行分割。目的是更精确地控制检索的粒度,同时减少检索时的噪声。
    • 保留元数据:每个代码块需要附带其来源文件的路径、起始行号等元数据,以便于后续检索和定位。将这些元数据存储到 sqlite 数据库中。
  • Embeding
    • 向量嵌入:使用预训练的代码嵌入模型(如CodeBERT、GraphCodeBERT等)将分割后的代码块转换为向量表示。
    • 建立索引:使用向量数据库(如 Faiss、Annoy 等)存储代码块的向量。这里使用专用的 lance 数据库,用于后续的相似度计算和检索。
  • Query
    • 查询构建:用户输入查询时,可以是基于关键字、代码片段、功能描述等的查询。
    • 查询转换:使用查询重写、查询扩展等技术,让用户输入的查询转换为更精确、适合检索的格式。
  • Retrieval
    • 使用用户提交的查询,在索引库中进行检索,找到与查询最相关的文档块。
    • 这通常涉及计算查询向量与索引库中所有向量之间的相似度。
  • Reranking
    • 对检索到的文档块进行重排,以进一步提高结果的准确性。
    • 这可能基于多种因素,如相关性、多样性、权威性等。
  • Combine Query
    • 将检索到 Chunks 和用户的 Query 作为 LLM 的输入,形成更丰富的 Prompt。
  • Generation
    • 使用大型语言模型(LLM)根据检索到的信息生成响应或答案。

AI 相关的功能是基于 Python 实现,而大部分 Python 库对 Python 版本有要求,基本都在 3.9 以上。
但 UOS 系统上的 Python 版本默认是 3.7,为了在部署过程中不破坏系统原有的 Python 环境,本方案用虚拟环境实现运行依赖部署。

实现

Chunking

![[Drawing 2024-09-18 14.04.48.excalidraw]]

文档分割:将代码仓库中的文件或代码块按照特定大小(根据函数、类定义等逻辑单元)进行分割。目的是更精确地控制检索的粒度,同时减少检索时的噪声。
保留元数据:每个代码块需要附带其来源文件的路径、起始行号等元数据,以便于后续检索和定位。将这些元数据存储到 sqlite 数据库中。
以下是对一个 python 文件进行语法分析并 Chunking 后演示。
【文件源码】

class Logger:
# debug levels.
LEVELS = {'NOTSET':logging.NOTSET,
'DEBUG':logging.DEBUG,
'INFO':logging.INFO,
'WARNING':logging.WARNING,
'ERROR':logging.ERROR}

@classmethod
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Chunk1 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def config(cls, filepath, logger_name='', logfile_level='DEBUG', console_level='INFO'):
cls.logs_dir = filepath
cls.logger_name = logger_name
cls.logs_file_level = logfile_level
cls.logs_console_level = console_level

# create logger dirctory.
cls.logger = logging.getLogger(cls.logger_name)
if os.path.exists(cls.logs_dir) and os.path.isdir(cls.logs_dir):
pass
else:
os.mkdir(cls.logs_dir)

# name log file.
timestamp = time.strftime("%Y-%m-%d",time.localtime())
logfilename = '%s.txt' % timestamp
logfilepath = os.path.join(cls.logs_dir, logfilename)
rotatingFileHandler = logging.handlers.RotatingFileHandler(filename=logfilepath,
maxBytes=1024*1024*50, backupCount=2)

# set log formate

formatter = logging.Formatter('[%(asctime)s] [%(filename)s] [%(lineno)s] [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S')

rotatingFileHandler.setFormatter(formatter)

consoleHandler = logging.StreamHandler()
consoleHandler.setLevel(cls.logs_console_level)
consoleHandler.setFormatter(formatter) 

# put handler to logger.
cls.logger.addHandler(rotatingFileHandler)
cls.logger.addHandler(consoleHandler)
cls.logger.setLevel(cls.logs_file_level)  
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Chunk2 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def getlogger(cls):
return logging.getLogger(ToolConfig.log_name)
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

# do test
if __name__ == '__main__':
Logger.config('.', 'test')
LOGGER = Logger().getlogger()
LOGGER.info("info")
LOGGER.debug("debug")
LOGGER.error("error")
LOGGER.warning("warning")

【表结构】
在这里插入图片描述
【存储内容】
在这里插入图片描述

Embeding

向量嵌入:使用预训练的代码嵌入模型(如 CodeBERT、GraphCodeBERT 等)将分割后的代码块转换为向量表示。在 RAG 中,检索是通过计算问题与 chunks 二者 Embedding 之间的相似度 (如余弦距离) 来实现的,而嵌入模型的语义表示能力在其中起着关键作用。

在这里插入图片描述

建立索引:使用向量数据库(如 Faiss、Annoy 等)存储代码块的向量。这里使用专用的 lance 数据库,用于后续的相似度计算和检索。LanceDB 是一个使用持久存储构建的用于向量搜索的开源数据库,可大大简化嵌入的检索、筛选和管理。
在这里插入图片描述

Query

在朴素 Rag 中,往往直接使用原始 query 进行检索,可能会存在三个问题。第一,原始 query 的措辞不当,尤其是涉及到很多专业词汇时,query 可能存在概念使用错误的问题;第二,往往知识库内的数据无法直接回答,需要组合知识才能找到答案;第三,当 query 涉及比较多的细节时,由于检索效率有限,大模型往往无法进行高质量的回答。

在高级 Rag 中,提出了 Query 改写的解决思路,模块化 Rag 对于这一思路做了细化,主要从三个方向进行优化,查询扩展、查询转换和查询构建。

查询扩展
查询扩展就是将单个查询拓展为多个查询,这样可以丰富查询内容,为潜在的Query内容缺失提供更多上下文,从而确保生成答案的最佳相关性。

多查询(Multi-Query):借助提示工程通过大型语言模型来扩展查询,将原始Query扩展成多个相似的Query,然后并行执行。目前有一种叫Rag-fusion的有效方案,首先对用户的原始query进行扩充,即使用 LLM 模型对用户的初始查询,进行改写生成多个查询;然后对每个生成的查询进行基于向量的搜索,形成多路搜索召回;接着应用倒数排名融合算法,根据文档在多个查询中的相关性重新排列文档,生成最终输出。

在这里插入图片描述

查询转换
查询转换是将用户的原始查询转换成一种新的查询内容后,再进行检索和生成,相比前面讲的查询扩展,并没有增加查询的数量。

查询重写:即直接利用LLM大模型重新表述问题。在进行多轮对话时,用户提问中的某些内容可能会指代上文中的部分信息,可以将历史信息和用户提问一并交给LLM大模型进行重新表述。

HYDE:全称是Hypothetical Document Embeddings,用LLM生成一个“假设”答案,将其和问题一起进行检索。HyDE的核心思想是接收用户提问后,先让LLM在没有外部知识的情况下生成一个假设性的回复。然后,将这个假设性回复和原始查询一起用于向量检索。假设回复可能包含虚假信息,但蕴含着LLM认为相关的信息和文档模式,有助于在知识库中寻找类似的文档。

Step-Back Prompting:如果果原始查询太复杂或返回的信息太广泛,我们可以选择生成一个抽象层次更高的“退后”问题,与原始问题一起用于检索,以增加返回结果的数量。例如,对于问题“勒布朗詹姆斯在2005年至2010年在哪些球队?”这个问题因为有时间范围的详细限制,比较难直接解决,可以提出一个后退问题“勒布朗詹姆斯的职业生涯是怎么样的?”,从这个回答的召回结果中再检索上一个问题的答案。

查询构建
和查询扩展与查询转化不一样,查询构建主要是为了将自然语言的 Query,转化为某种特定机器或软件能理解的语言。因为随着大模型在各行各业的渗透,除文本数据外,诸如表格和图形数据等越来越多的结构化数据正被融入 RAG 系统。

比如在一些 ChatBI 的场景下,就需要将用户的 Query 内容,转化为 SQL 语句,进行数据库查询,这就是 Text-to-SQL。

再比如工业设计场景下,可能需要将用户的 Query 转化为设计指令,或者设备控制指令,这就是 Text-to-Cypher。

Retrieval

检索器本质上是一种计算Query和内容块相似性的算子,这种算子最原始的形态就是基 Embedding模型的向量相似性,但可以通过转化算子的方式进行优化。

稀疏检索器:用统计方法将查询和文档转化为稀疏向量。其优势在于处理大型数据集时效率高,只关注非零元素。但在捕捉复杂语义方面,可能不如密集向量有效

密集检索器:使用预训练的语言模型(PLMs)为查询和文档提供密集表示。尽管计算和存储成本较高,却能提供更复杂的语义表示可以构建这两种检索器,然后通过编排模块,在特定Query的时候采用最合适的检索器。

【检索器微调】
在上下文可能与预训练语料库有差异的情况下,尤其是在存在大量专有术语的高度专业化领域,需要对检索器进行微调。
具体方法包括:监督微调、LM 监督检索器、适配器。
在代码辅助领域,检索器微调需求不是太明显,可以忽略。

【实现】
某些数据库可以直接实现相似度匹配,比如 lancedb。

Reranking

在朴素 Rag 中,系统会将所有检索到的块直接输入到 LLM 生成回答,导致出现中间内容丢失、噪声占比过高、上下文长度限制等问题。

高级 Rag 针对这个问题,提出了提示压缩和重新排序的解决思路,模块化 Rag 也同样继承了这类方法,并将其分成了三个模块,重排序、压缩和选择。

【重排序(Rerank)】
对于检索到的内容块,使用专门的排序模型,重新计算上下文的相关性得分。这些排序模型会考虑更多的特征,如查询意图、词汇的多重语义、用户的历史行为和上下文信息等,比如 Cohere 模型。

【压缩(Compression)】
对于检索到的内容块,不要直接输入大模型,而是先删除无关内容并突出重要上下文,从而减少整体提示长度,降低冗余信息对大模型的干扰。

【选择(Selection)】
与压缩文档内容不同,选择是直接移除无关的文档块。通过识别并剔除输入上下文中的冗余内容,可以精炼输入,从而提升语言模型的推理效率。有一种有效的选择方法叫做 LLM-Critique,是在生成最终答案前,通过 LLM 批评机制,过滤掉相关性不高的文档。

在辅助编程领域,使用比较多的是 Reranking。首先通过 Retrieval 获得相似性较高的数条记录,然后再使用 Reranking 对上述记录进行二次过滤,找出相关性最高的几条记录作为提示词。

Construct Query

将检索到 Chunks 和用户的 Query 作为 LLM 的输入,形成更丰富的 Prompt。

Generation

使用大型语言模型(LLM)根据检索到的信息生成响应或答案。

更多新鲜技术资讯,欢迎关注公众号: 深度极客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值