一:RAG vs Fine-tuning
(一)Fine-tuning(微调)
是用一定量的数据集对LLM进行局部参数的调整,以期望LLM更加理解我们的业务逻辑,有更好的zero-shot能力。
(二)RAG(检索增强生成)
是把企业内部的文档数据先进行embedding,借助检索先获得大致的知识范围答案,再结合prompt给到LLM,让LLM生成最终的答案。(三)对比
说实话两种方式都不简单,但是Fine-tuning 的成本似乎更大一些。 所以目前有一种趋势就是更倾向RAG方式,毕竟对于客户本身来说,操作空间会更大,他们可以通过管理文档来调整最终的检索和问答能力。 所以目前有一些预测已经旗帜鲜明地认为fine-tuning的需求一定会下降。二:RAG介绍
https://blog.csdn.net/zhuyingxiao/article/details/138796932 大型语言模型(LLM)会产生“幻觉”现象,检索增强生成(RAG)通过从外部知识库检索相关文档chunk并进行语义相似度计算,增强了LLM的功能,很大程度解决幻读。 通过引用外部知识,RAG有效地减少了生成事实不正确内容的问题。 RAG目前是基于LLM系统中最受欢迎的架构,有许多产品基于RAG构建,使RAG成为推动聊天机器人发展和增强LLM在现实世界应用适用性的关键技术。 RAG在问答系统中的一个典型应用主要包括三个步骤:Indexing(索引):将文档分割成chunk,编码成向量,并存储在向量数据库中。
- 文档加载与切分 在此阶段,各种格式的文档首先经过加载或处理,转化为大语言模型可解析的纯文本数据。例如,对于PDF文件,采用PDF提取器抽取文本;而对于图片则利用OCR技术识别并转换为文字信息。鉴于文档可能存在过长的问题,需要进行文档切片,将长篇文档分割成多个文本块,以便更高效地处理和检索信息。
- 文本嵌入模型编码(Text Embedding) 拆分后的文本块通过文本嵌入模型进行处理,将高维数据映射到低维向量空间。这使得在向量空间中相似的文本块具有相近的表示,从而有效捕捉文本之间的语义关系。这一过程的目的在于实现后续检索时的高效文本搜索,有助于提高后续搜索引擎的检索准确性。
- 存入向量数据库(Vector Database) 经过文本嵌入模型处理的拆分后的文本块进入向量数据库。这一阶段不仅仅包括简单的物理存储,还涉及对数据的结构化处理和索引算法的优化。该处理确保数据以一种有序、高效的方式存储,以便在检索阶段迅速且准确地访问。
Retrieval(检索):根据语义相似度检索与问题最相关的前k个chunk。
- “用户查询”文本嵌入编码 在此阶段,“用户查询”经由文本嵌入模型进行编码,该过程将用户查询映射为向量表示,以便在先前构建的索引中检索与用户提问相关度最高的文本块。
- 数据召回 数据召回是向LLM提问的关键步骤,根据用户提问从向量数据库中召回相关文档数据。这一步骤充分利用了先前存储的索引信息,实现了对源数据的快速、动态提取。
Generation(生成):将原始问题和检索到的chunk一起输入到LLM中,生成最终答案。
- 答案生成 获取的数据和聊天历史记忆将与问题一同提交至LLM。LLM进行推理并生成最终答案,整个过程考虑了用户查询、检索结果和模型内部记忆。这一生成过程是整个RAG架构中的关键环节,确保系统能够有效地应对用户的复杂查询,从而生成准确、上下文合理的答案。
三:RAG在线架构解析
通过将检索部分进行拆分(检索前处理、检索、检索后处理),我们可以把整个架构分为5个部分:
-
索引:是信息的结构化存储和组织方式,是将文本分解成可管理的chunk的过程,是组织系统的关键步骤。
-
检索前处理:RAG的一个主要挑战是它直接依赖用户的原始查询作为检索的基础,需要对query进行预处理。
-
检索:是根据用户需求从索引中获取相关信息的过程,利用预训练语言模型可以在潜在空间中(向量空间)有效地表示query和document,从而建立query和document之间的语义相似性,以支持检索。
-
检索后处理:直接检索整个文档chunk并将它们直接输入到LLM的上下文环境中并非最佳选择,对检索后的文档进行处理,可以帮助LLM更好地利用上下文信息。
-
生成:利用LLM根据用户的查询和检索到的上下文信息生成答案,需要对答案进行进行处理。
(一)索引
索引是将文本分解成可管理的chunk的过程,是组织系统的关键步骤,面临三个主要挑战:- 不明确的chunk大小选取:chunk的语义信息受到分割方法的影响,如果chunk太小或太大,可能会导致重要信息的丢失或隐藏。
- 不准确的chunk相似性搜索:随着数据量的增加,检索中的噪声增多,导致频繁与错误数据匹配,使检索系统变得脆弱和不可靠。
- 不明确的引用轨迹:检索到的chunk可能来源于任何文档,缺乏引用路径,可能导致存在来自多个不同文档的chunk,尽管这些chunk在语义上相似,但包含的内容完全不同的主题。
1.分块
较大的chunk可以捕获更多的上下文,但它们也会产生更多的噪音,需要更长的处理时间和更高的成本。
较小的chunk可能无法完全传达必要的上下文,但它们的噪音较少。
1.1 固定大小的分块
- 可以直接按字符切割
- 也可以在字符切割基础上,判断句子,比如:section.split('\n');尽可能保证每一个分块是由连续的语句组成
1.2 上下文丰富
- 递归分块:在固定分块的基础上,对每个chunk进行递归拆分成更小的块,这些子chunk和原始chunk是父子关系,当检索到子节点时,会递归检索到其父节点,然后再将父节点为检索结果提交给 LLM。(更加精准,并且上下文没有丢失)
#将每一个chunk切分成不同大小的子chunk
sub_chunk_sizes = [128, 256, 512]
sub_node_parsers = [
SentenceSplitter(chunk_size=c, chunk_overlap=20) for c in sub_chunk_sizes
]
#然后统一将所有子chunk和父chunk,放入all_nodes列表中,列表每个元素包含chunk和chunk的parent索引信息
#通过向量相似度,检索所有的nodes,找到相似度最高的chunk,返回其父chunk给llm,父chunk包含更多的上下文信息
- 滑动窗口:在固定分块时,使用窗口重叠方式进行分块,减少因为文档切割导致的上下文丢失问题。可以对字符数量、句子窗口K(句子数量)进行重叠来增强语义过渡。
1.3 元数据附加
- 元数据/摘要添加:在固定分块的基础下,根据chunk来生成元数据子节点,然后再将元数据子节点和原始节点一起传入检索索引。也需要对元数据节点加上parent信息,方便查询原始chunk,传递给llm
- 问答对添加:也是在固定分块的基础上,对每个chunk生成多个问答对,同上
- 上面两者可以结合使用
1.4 专门的分块
- Markdown: Markdown是一种轻量级的标记语言,通常用于格式化文本。通过识别Markdown语法(例如,标题、列表和代码块),您可以根据其结构和层次结构智能地划分内容,从而生成语义更连贯的块。
- LaTex: LaTeX是一种文档准备系统和标记语言,通常用于学术论文和技术文档。通过解析LaTeX命令和环境,您可以创建尊重内容逻辑组织的块(例如,节、子节和方程),从而产生更准确和上下文相关的结果。
- .......
2.向量化
这是将文本、图像、音频和视频等转化为向量矩阵的过程,也就是变成计算机可以理解的格式,embedding模型的好坏会直接影响到后面检索的质量,特别是相关度。一般我们现在可以选择的embedding模型有这些:- 中文embedding模型:BGE/M3E,具体选择看使用场景
- 文心千帆/通义千问的embedding模型:国内大厂的embedding模型;
- Text-embedding-ada-002:OpenAI的embedding模型,1536维,通用性很强,效果不错。初学使用的就是这个
- 自己训练embedding模型
3.搜索索引
3.1 相似度计算:包括欧式距离、余弦相似度、曼哈顿距离...
3.2 索引结构:参考向量数据库
- Flat Index:最简单的索引结构,将所有向量存储在一起,适用于小规模数据集。搜索时需遍历整个数据集,计算查询向量与每个数据向量的相似度。
- IVF (Inverted File Index) :基于聚类的思想,先将数据集划分为多个子集(聚类中心),再对每个子集内部使用其他索引结构(如Flat或Hierarchical Clustering)。搜索时先找到最相关的几个子集(近似搜索),再在子集中精确搜索。
- HNSW (Hierarchical Navigable Small World) :基于图的近似最近邻搜索算法(K-近邻算法),构建多层图结构,每一层节点代表一个向量,节点间边代表相似度。搜索时通过层次跳跃快速缩小搜索范围,最终找到近似最近邻。
3.3 简单的索引组合
对于大型数据库,一种有效的方法是创建两个索引,一个由摘要组成,另一个由文档chunk组成,并进行两步搜索,首先通过摘要筛选出相关文档,然后仅在这个相关组内进行搜索。(二)检索前处理
RAG的一个主要挑战是它直接依赖用户的原始查询作为检索的基础。一个不合适的query,会得到一个轻率的查询结果,导致检索效果不佳。面临的主要挑战包括:- 措辞不当的查询:语言组织不良,query过于复杂。
- 语言复杂性和歧义:语言模型在处理专业词汇或含义多义的模糊缩写时往往会遇到困难。
1.查询扩展(Query Expansion)
将单一查询扩展为多个查询可以丰富查询的内容,提供更多的上下文来解决缺乏特定细微差别的问题,从而确保生成答案的最佳相关性。1.1 多查询
通过Prompt工程来扩展查询(查询的多样性和覆盖范围),这些查询可以并行执行。 使用多个查询的一个挑战是可能稀释用户原始意图的风险。为了缓解这一问题,我们可以指导模型在Prompt工程中给予原始查询更大的权重。2.查询转换(Query Transformation)
2.1 查询重写
原始查询并不总是最适合LLM检索的,特别是在现实世界的场景中。因此,我们可以提示LLM重写查询(可以传入背景让LLM重写,也可以自己拼接),使得查询内容符合当前查询场景:- 在没有查询重写策略时,如果用户输入
"如何部署"
,召回的文档的相关性分数都小于0.5
,会都被过滤掉,最后GPT无法获得足够的上下文信息,无法回答。 - 增加查询重写策略时,如果用户输入"如何部署",query会被改写为"如何部署XXX",此时召回的5篇文档的相关性分数都是大于0.5的,可以作为上下文传给GPT,最终GPT给出响应的答案。
2.2 退后式提示
使用LLM生成一个更一般的查询,为其检索我们获得的更一般或高层次的上下文,有助于支撑我们对原始查询的回答。也会对原始查询执行检索,两种上下文都会在最终答案生成步骤中输入到LLM中。2.3 子查询(上面的退后式也是子查询的一种)
如果查询很复杂,LLM可以将其分解为几个子查询。子问题规划过程代表了生成必要的子问题,当结合起来时,这些子问题可以帮助完全回答原始问题。从原理上讲,这个过程与查询扩展类似。 具体来说,一个复杂的问题可以使用从简到繁的提示方法分解为一系列更简单的子问题。 这些查询将并行执行,然后将检索到的上下文合并在一个提示中,供LLM合成对初始查询的最终答案。3.查询构造(Query Construction)
将用户查询转换成其他查询语言以访问替代数据源。常见的方法包括:- 文本转Cypher
- 文本转SQL
4.查询路由(Query Routing)
查询路由是一个基于LLM的决策制定步骤,针对用户的查询决定接下来要做什么。通过让llm判断query的类别,根据不同类别执行不同的方法,实现路由。 可以用于选择一种索引,来供用户进行查询。无论是你拥有多个数据源,例如经典的向量存储、图数据库或关系数据库,还是你拥有一个索引层级。对于多文档存储来说,一个典型的案例可能是一个摘要索引和另一个文档chunk向量索引。(三)检索
检索是根据用户需求从索引中获取相关信息的过程,需要考虑以下三个主要因素:- 检索效率:检索过程应该高效,能够快速地返回相关的文档或答案。这要求在检索过程中采用高效的算法和数据结构,以及优化的索引和查询方法。
- 嵌入质量:嵌入向量应该能够准确地捕捉文本的语义信息。这要求使用高质量的预训练语言模型,以确保生成的嵌入向量能够在潜在空间中准确地表示文本的语义相似性。
- 任务、数据和模型的对齐:检索过程需要根据具体的任务需求和可用的数据来选择合适的模型和方法。任务、数据和模型之间的对齐是关键,可以通过合理的数据预处理、模型选择和训练来提高检索的效果。
1.稀疏检索器
稀疏检索通常基于某种形式的离散表示,如关键词或短语,来索引和检索数据。这种方法强调从文档集中选择少量但高度相关的特征(如词汇或标签)进行索引。虽然稀疏编码模型可能被认为是一种有些过时的技术,通常基于统计方法,如词频统计,但由于其更高的编码效率和稳定性,它们仍然占有一席之地。常见的稀疏编码模型包括BM25和TF-IDF。2.密集检索器
密集检索则使用连续的向量空间,通常基于深度学习模型来表示文档和查询。每个文档和查询被嵌入到一个密集的向量中,向量的每个维度并不直接对应于具体的单词,而是捕捉文档的语义特征。 基于神经网络的密集编码模型包括几种类型:- 基于BERT架构构建的编码器-解码器语言模型,如
ColBERT
。 - 综合多任务微调模型,如
BGE文本嵌入
。 - 基于云API的模型,如
OpenAI-Ada-002
和Cohere Embedding
。
3.混合检索
两种嵌入方法捕获不同的相关性特征,并且通过利用互补的相关性信息,可以相互受益。- 稀疏检索模型可以用来提供训练密集检索模型的初步搜索结果。
- 预训练语言模型可以用来学习术语权重以增强稀疏检索。具体来说,它还表明稀疏检索模型可以增强密集检索模型的零样本检索能力,并帮助密集检索器处理包含罕见实体的查询,从而提高鲁棒性。
(四)检索后处理
直接检索整个文档chunk并将它们直接输入到LLM的上下文环境中并非最佳选择。后处理文档可以帮助LLM更好地利用上下文信息。 主要挑战包括:- 丢失中间部分:像人类一样,LLM倾向于只记住长文本的开始和结束部分,而忘记中间部分。
- 噪声/反事实chunk:检索到的噪声多或事实上矛盾的文档可能会影响最终的检索生成。
- 上下文窗口:尽管检索到了大量相关内容,但大型模型中对上下文信息长度的限制阻止了包含所有这些内容。
1.重排Reranking
很多时候我们的检索结果并不理想,原因是chunks在系统内数量很多,我们检索的维度不一定是最优的,一次检索的结果可能就会在相关度上面没有那么理想。这时候我们需要有一些策略来对检索的结果做重排序,比如使用planB重排序,或者把组合相关度、匹配度等因素做一些重新调整,得到更符合我们业务场景的排序。因为在这一步之后,我们就会把结果送给LLM进行最终处理了,所以这一部分的结果很重要。这里面还会有一个内部的判断器来评审相关度,触发重排序。 不改变内容或长度的情况下,对检索到的文档chunk进行重新排序,以增强对LLM更为关键的文档chunk的可见性。1.1 基于规则的重新排序
根据特定规则,计算度量来重新排序chunk,常见的度量包括:- 多样性
- 相关性
- 最大边际相关性(Maximal Marginal Relevance):优化了对查询的相似性和选定文档之间的多样性。在保持与查询相关性的同时,增加结果之间的多样性。通过首先从一个较大的候选集中选择,然后从中挑选出
k
个最终结果。这种方法在许多搜索和推荐系统中非常有用,尤其是当你希望避免向用户展示非常相似的结果时。
1.2 基于模型的重新排序
使用语言模型对文档chunk进行重新排序,可选方案包括:- 来自BERT系列的编解码器模型,如
SpanBERT
。 - 专门的重新排序模型,如
Cohere rerank
或bge-reranker-large
。 - 通用大型语言模型,如
GPT-4
。
2.过滤
在RAG过程中,一个常见的误解是认为检索尽可能多的相关文档并将它们连接起来形成一个冗长的检索提示是有益的。然而,过多的上下文可能会引入更多噪声,削弱LLM对关键信息的感知,导致如“中间丢失”等问题。我们可以基于相似度分数、关键词、元数据过滤结果(模型api提供相关的搜索结果返回),将一些低质的文档过来到,不传递到LLM的上下文中。这是在将我们检索到的上下文输入LLM以获取最终答案之前的最后一步。3.参考引用(更像是一种工具,而不是检索改进技术)
如果我们使用了多个来源来生成答案,无论是由于初始查询的复杂性(我们不得不执行多个子查询,然后将检索到的上下文合并成一个答案),或者因为我们在不同的文档中找到了单个查询的相关上下文,那么就会出现一个问题,即我们是否能准确地反向引用我们的来源。如果能够准确的反向引用来源,就可以根据结果的不同来源,分别进行处理,并标识结果来源。 有几种方法可以做到这一点:- 将这个引用任务插入到我们的提示中,并要求LLM提及所使用来源的ID。
- 将生成的答案部分与索引中的原始文本块匹配——LlamaIndex为这种情况提供了一种高效的模糊匹配解决方案(fuzzy matching based solution)
4.会话历史
处理在检索到答案后,还需要提供多轮对话的上下文历史,需要通过查询压缩技术来解决,同时考虑聊天上下文和用户查询。 而关于上述上下文压缩主要包括下面两种:4.1 ContextChatEngine
将用户查询检索出来的上下文,和聊天历史记录,一起发送给llm,以便LLM在生成下一个答案时了解之前的上下文。4.2 CondensePlusContextMode
在每次互动中,聊天历史和最后一条消息被压缩成新的查询,然后这个查询进入索引,检索到的上下文连同原始用户消息一起传递给LLM以生成答案。(五)生成
利用LLM根据用户的查询和检索到的上下文信息生成答案。1.LLM模式
根据场景的不同,LLM的选择可以分为以下两种类型:1.1 云API基础生成器
通过调用第三方LLM的API来使用,如OpenAI的gpt-3.5-turbo、gpt-4-turbo和百度的ERNIE-Bot-turbo等。 优点包括: 没有服务器压力 高并发性 能够使用更强大的模型 缺点包括: 数据通过第三方,可能引起数据隐私问题 无法调整模型(在绝大多数情况下)1.2 本地部署
本地部署开源或自行开发的LLM,如Llama系列、GLM等。 其优点和缺点与基于云API的模型相反。本地部署的模型提供了更大的灵活性和更好的隐私保护,但需要更高的计算资源2. 响应合成器
这是RAG流程的最后一步,根据检索得到的所有上下文和用户初始查询生成答案。 响应合成的主要方法有:第五期 LangChain学习- 最简单的方法就是将所有获取的上下文与查询一起连接起来,并一次性输入到LLM中,上下文信息更全,并且只调用一次语言模型,但是受到token限制。
- 通过将检索到的上下文逐chunk送入LLM,迭代地完善答案,多次调用llm,并且会弱化前面答案
- 将检索到的上下文进行总结,以适应prompt,但是上下文可能弱化。
- 基于不同的上下文chunk生成多个答案,然后将它们连接或进行总结。
3.后处理响应(Postprocess Response)
如果不是采用流式输出,在获取到LLM生成的结果后,我们可以根据具体业务场景,进行最后干预。如果用户咨询其它问题或者LLM给出了无关的答案,都需要进行结果干预。四:Graph RAG
GraphRAG 本质上就是 RAG,只不过与一般 RAG 相比,其检索路径上多了一个知识图谱。(一)知识图谱
1. 知识图谱概念
Graph RAG 中的 Graph 指的是知识图谱 —— Knowledge Graph:一个用来表示实体及其相互关系的结构化图形数据模型。在知识图谱中,节点(Nodes) 代表实体如人、地点、事件等;边(Edges)则代表这些实体之间的关系,(如人物关系、地理位置等)。 比如《天龙八部》中人物的关系图谱如下: 比如关于《蒙娜丽莎》的知识图谱: GraphRAG 中,实体和关系以图的形式存储在图数据库(graph database) 中, 作为 RAG pipeline 的一部分。2. 向量和知识图谱的区别
从人的视角,向量的视角,以及 Graph 的视角,来看一个“苹果: 向量“苹果”的表示则是一组数字,这组数字以编码的形式表征了相应文本的一部分意义。在 RAG 过程中,这组数字通过一次计算,识别其与另一组向量的相似度。但是人类无法理解这组向量内部每个数字所代表的内容,无法从这组数字尝试理解其上下文,或将其融入更长的文本中显然也无能为力。 知识图谱“苹果”的表示则是“declarative”(声明式的),用 AI 的术语来讲,是 symbolic(符号化)的。对人类来说, 知识图谱的表示方式直观,使用自然语言标签和关系,人类可以轻松理解其中的内容,比如我们上边的几张图。对于机器来说, 符号化表示的形式化和标准化特性,易于机器进行解析,并能进行逻辑和算法推理。(二)Graph RAG 运行模式
GraphRAG 本质上就是 RAG,只不过与一般 RAG 相比,其检索路径上多了一个知识图谱。GraphRAG 与 RAG 的基本架构也相同,区别在于其数据库中,同时存储了结构化的知识图谱数据和文本 Embedding 后的向量数据。 上图是一个由用户提问触发的图查询过程。实际应用中可以将图数据和向量分别存储在两个不同的数据库中,或者使用 Neo4j 这样支持向量搜索的图数据库。Graph RAG的核心链路分如下三个阶段:
- 索引(三元组抽取):通过LLM服务实现文档的三元组提取,写入图数据库。
- 检索(子图召回):通过LLM服务实现查询的关键词提取和泛化(大小写、别称、同义词等),并基于关键词实现子图遍历(DFS/BFS),搜索N跳以内的局部子图。
- 生成(子图上下文):将局部子图数据格式化为文本,作为上下文和问题一起提交给大模型处理。
(三)Graph RAG生命周期
使用 GraphRAG 的生成式 AI 应用与任何 RAG 应用的模式基本相同(上面的核心链路和传统RAG类似),只是在开始时增加了一个“Create Graph-创建图”的步骤:- 图的可迭代性很高:可以从一个“minimum viable graph”开始逐渐扩展。数据纳入知识图谱后,数据的进化变得容易了许多。通过向知识图谱中添加更多类型的数据,可以充分利用数据网络效应带来的益处,同时还能通过提升数据质量增强应用价值。
- 创建图的技术栈发展飞快,随着工具的进步和完善,创建图会变得更加容易。