目录
一、概述
Retrieval Augmented Generation RAG 检索增强的内容生成。
从字面上来看检索只是一种手段途径,在人工智能领域中存在多种增强大语言模型输出的方式,而检索只是其中一种。不同于传统应用的检索,RAG中的检索更像是大语言模型能力的扩展,它的侧重点在于如何通过检索出来的信息,让大模型生成出更优质的内容。
为什么大语言模型的输出会需要增强呢?
现阶段大语言模型在训练时会接触到大量文本数据,这些文本数据可能包括书籍,网页,论文,新闻资讯等信息。这些数据构成了模型对现实世界的基础理解,模型信息中会特地标注训练时所使用的参数量(Billion),7B 代表着 70亿个训练参数。优质的内容和训练参数的数量级都会影响模型的能力和输出质量,但知识库的构建会基于某个时间点的数据,模型的训练也可能在之后的时间完成,这带来不可避免的滞后性和时效性。
通过检索最新的数据作为上下文提示,能在一定程度上弥补语言模型在知识更新方面的不足,可以提供更加准确的回答,但它的效果也依赖检索系统和知识库的质量。在私域场景下,大模型在理解私域文档后结合它自己知识库所产生的输出,要比传统私域文档中做检索要优质得多。
几十亿个训练参数听起来已经足够多了,LLAMA2 在训练时使用了2T大小的数据但它的上下文在4K左右,虽然训练参数和上下文随着时间的推移还在不断的增加,但对于模型应用来说还远远不够,对于整个人类世界来说还远远不够。而RAG就是为了在有限的上下文中让模型发挥更强大的能力。
二、过程概述
构建一个RAG大致能分为三个部分:
1.为私域数据建立索引(Embedding)
2.基于用户的输入,将相似度最高的内容的提取出来(向量搜索)
3.基于用户的问题,对回答的内容进行增强(提示词工程)。
下图来源于:https://scriv.ai/guides/retrieval-augmented-generation-overview
LangChain 三行代码构建一个简单的RAG示例:
fromlangchain.document_loaders importWebBaseLoader
fromlangchain.indexes importVectorstoreIndexCreator
loader = WebBaseLoader("http://www.paulgraham.com/greatwork.html")
index = VectorstoreIndexCreator().from_loaders([loader])
index.query("What should I work on?")
检索本质上是一种搜索操作,传统的搜索引擎可以根据用户的输入查找最相关的信息。在AI检索中的行为也是类似的,它由两个部分组成:
1.索引:将知识库变为可以搜索/查询的内容。
2.查询:从搜索词中提取最相关的知识。
任何搜索过程都可以用于检索,可以把检索当作一个外部扩展的工具,无论是通过Restful API 还是通过向量搜索,只要能检索出输出问题想匹配的信息,满足相关的上下文输出给大模型,那么这个检索过程就是符合要求的。
建立索引的过程也是Embedding的过程,因为模型会有上下文的限制,通常情况下建立私域知识库时会先将文档进行"切片"。切片对于RAG是一个很重要的部分,如果将一个大文档直接输入给大语言模型,根本没什么意义,这样检索出来的信息太泛化了。切的太小也会造成输出质量的下降------相关性内容会丢失。
开发者必须要考虑切成什么大小才能更接近平衡点,虽然LangChain 提供工具来简化这一过程,但不同的文档和内容不能一概而论,而是需要通过具体的场景再做权衡。索引是构建整个RAG最困难但又最重要的部分,整个索引部分可以分为两步:
1.加载:从通常存储的位置获取知识库的内容
2.分割:将知识库分割成适合嵌入搜索的片段大小的块。
"适合"这个词在软件工程中让人感觉格格不入,到底什么是适合?这句话和食谱中的"适量"有着相同的迷惑性,似乎存在各种不确定性。将用户输入的问题和一大堆的提示词结合在一起,将它们向量化后在另外一堆向量数据中进行匹配,最终输入给一个黑盒并企图它能输出优质内容,这更像是《计算机程序的构造和解释》前言中描述的魔法,因为它真的能运行。
三、如何优化提问?
怎样提一个好问题?从主观上来讲,如果别人问我们一个问题,比较理想的情况是:回答者只要稍加思索便能准确捕捉到其中的关键点,从而轻松回答出对方想要的答案。
上述描述肯定不是一个好的回答,因为太理想化,现实生活中因为问题质量差而导致得不到回答的例子比比皆是,那么又怎么能奢求大模型输出高质量的回答呢?提一个好的问题是有门槛的,否则互联网也不会存在关于"如何提一个好问题"的文章,市面上也不会存在指导人们如何提问的书。
反过来,通过现实生活的思考优化对大模型的提问似乎也是可行的。我们更希望问题中提供充足的相关背景,足够简单,具体的词句,目的明确。
但事实上应用无法决定用户输入什么,输入可以是一个简短的单词,它不存在上下文,或过于概括,也可能不存在目的性。在这种情况下,有一种思路是:将一个问题生成多个问题,以此来丰富整个问题。
输入:向量
生成:解释'向量'这个词?
生成:'向量'这一概念存在于哪些领域?
生成:请解释计算机领域中的向量
生成:请解释物理学中的向量
生成:请解释数学中的向量
...
在私域场景下,将一个问题生成多个问题的方式称为Multi Query ,在这种情况下不同的问题自然会生成各自相关的检索,也可能在同一份文档中得到相同的块。在Multi Query中可以做的相对简单一点,直接去重就好了。
RAG-Fusion中会对检索后的信息进行排序,这背后的意义在于:文档块在多次被检索后出现的频率可以用作被分析的指标,就像传统应用中的热点缓存,通过分析某些重合的文档片段相似度来进行一个统一的排序,提高多次命中的热点文档片段在上下文中的排序。
将问题具象
将问题具象化很符合人类的思考方式,现实生活中将一个大的问题拆分成多个子问题,小的问题更容易回答,只要回答这些小的问题,大的问题也迎刃而解。同时这些小的问题也存在一定的关联性,更像是一个逐步递进的过程:
通过提示词将一个抽象的问题拆分成具象化的子问题,并要求这些问题依次回答,然后将答案作为检索文档的一部分,为下一个问题提供信息依据。
输入:如何写作?
生成:写作的基本概念是什么?
答案:写作是使用文字来表达思想、情感和信息的一种方式。
生成:写作有哪些不同的类型和风格?
答案:写作类型包括但不限于小说、散文、诗歌、剧本、报告、学术论文等。风格则根据作者的个人特点和写作目的而有所不同。
生成:如何确定写作的主题或论点?
答案:确定主题或论点通常基于个人兴趣、专业领域、社会热点或读者需求。
...
答案:写作是一个复杂的过程,但可以分解为几个关键步骤来逐一掌握。以下是写作的基本指南:
1.确定写作目的:首先明确你写作的目的是什么,是为了传达信息、娱乐读者、还是表达个人观点。
2.选择主题:根据你的写作目的,选择一个你感兴趣且具有一定知识基础的主题。
3.进行研究:如果需要,进行必要的研究以收集信息和数据,确保你的内容准确无误。
4.构思大纲:创建一个大纲,概述你的文章结构,包括引言、主体段落和结论。
5.撰写初稿:根据大纲开始写作,不必担心初稿的完美,关键是将想法转化为文字。
可预见的是:如果一个问题本身就很具体,如果再进行子任务的拆分,那么不可避免的到一些无意义的信息,这样不仅不会提高输出的质量,反而会下降。这也意味着,这种处理方式只适合太过概括或抽象的问题。如果太抽象的问题可以将它们具体化,那么具体的问题能不能抽象化呢?
将问题抽象化可以拔高视角,再与原问题进行检索,以得到更全面的信息。
HyDE(Hypothetical Document Embeddings) 提供另外一种思路:如果用户输入的问题过于简短,那么可以基于LLM的能力生成一个假设性文档,再对这个文档进行检索。这也是一种将问题抽象化的方式,但显而易见的是:对于现阶段AI应用来说它的效果可能并不好------如今AI应用都在极力降低LLM的幻觉,这种生成假设性文档的内容方式显然会加剧幻觉的不可控。当然,这种思路至少看起来会让问题产生更多的创造性。
上述是从应用的角度在"如何提问"这个维度进行分析,从用户的角度有一个小技巧也可以提高输出质量,即:**用多种不同的语言提问,会得到完全不一样的答案。**LLM 在训练时的预料质量和数据量都会影响输出的质量,不同语言的语言,语法也不一样,这点会很直观地反映到输出内容中。
四、路由和高级查询
优化问题最简单方式就是增加问题的覆盖面,将一个问题变成多个问题,意图得到更多的信息并加以分析。但私域文档能否提供足够的信息满足这些问题?或者如何找到契合的文档?
路由(Routing)将这些衍生出来的问题通过分发,将不同的问题映射到契合度更高的模版中,在不同场景使用不同的处理方式。
Logincal
当一个问题产生后,需要思考的是这个问题使用什么处理方式才是最优解?是传统的SQL查询?是图查询?还是进行向量索引?
一个常见的场景是用户输入一个指向性十分具体的问题,比如查询某年月日发生的事件的内容,这种情况下不需要任何生成的"创新",使用传统检索的方式可能会更好,但如果依然使用向量检索,在向量数据库中本身就存有大量关联性的数据,在这种关联数据过于紧密的情况下反而会分散LLM的注意力,导致输出结果并不理想。
路由的作用就是在不同情况下使用不同的检索数据,但路由不是通过编程去实现的,在AI应用的构建中路由是通过LLM的相关能力完成的,这点虽然和传统的网络路由有相近的部分,但它的构建有着本质的区别。
传统应用在并发量较大时会将数据库分表分库,在构建AI应用时也可以沿用这样的思路,当私域文档过于庞杂后,通过多个向量数据库存放不同领域的文档,再通过路由分流以适用于各个场景。
Semantic routing
不是每个私域都存在大量文档,在某些轻量级的场景下使用多个向量数据库未免太过奢侈。模版也可以做语义化或近似度的匹配,Semantic routing的思路是将路由分到不同的提示词模版,在此之前直接将问题和提示词模版做向量化,当两者结合后在向量数据库中直接做近似度匹配。
使用向量数据库的原因是私域文档或文档内容信息太多,所以才需要使用向量索引提升检索效率,向量存储做非结构化很好,但是在AI领域中依然没有"银弹"。使用自然语言查询关系性数据库,或图数据库也是很常见的需求场景。
五、丰富索引结构
在过程概述这一小节中提到切片对于RAG是一个很重要的部分,将一个文档切分成若干个片段,比较简单的方式可以基于换行或段落来进行切片。如果这个文档中包含代码的片段(代码中的换行较多),那么切分出来的结果可能太过散乱,LLM并不能有效对这些信息加以分析。如果切得太大会导致上下文长度过长,切片中的噪音也会影响LLM的幻觉。
如果存在很多文档,在切片前可以使用一些特定函数将这些文档先进行聚合,合并成一个聚合文档,其中的特点是将整个过程聚合的文档都存入向量数据库中,在这个过程中可以形成一个阶梯状的多层级结构。这种方式的好处是,无论回溯还是检索在不同层面都能找到对应的检索点。
在[优化提问]中,将一个问题变成多个问题为的是丰富上下文的信息,将多个文档聚合的每个阶段都保留下来,也是为了有更好的召回率,而且细化了每个节点,增加了命中率。
文档中不仅仅只包含文本,也有可能包含大量的图片,图表等信息。这种情况下仅仅依靠切分文本信息是不够的,LangChain建议是使用unstructured框架将其中的复杂内容单独提取出来,然后通过LLM分别将它变为文本的摘要,最后再将这些信息分别向量化于原文档一同进行存储。在检索时根据源文件和检索信息相似度拟合,会有更好的召回率。
从技术理论上思考似乎潜力很大,在实际应用场景下会更复杂且多变,对于某些特定领域,使用通用的向量化可能也不太适用。在某些场景下建议使用专有的向量化模型进行enbeddings,比如ColBERT。
现有的开源大模型训练趋势是生产出通用能力的模型,这更像是一个基础的操作系统,AI应用在LLM的基础上扩展某一领域的能力,这些方式会有诸多限制,比如受限于上下文,受限于模型的能力,在复杂场景下虽然理论思路层出不穷,但构建AI应用时会受到很多挑战。
比较理想化的情况是:让各个领域训练专有模型,在此基础上构建AI应用会相对容易,也更容易发挥出模型的能力。但反过来思考,可能某些领域受限于高昂的算力和成本走向弱势化,最终趋同单一化。
六、重排序上下文
从[优化提问]开始,生成的问题本身可能就是经过某种关联关系衍生的,这个生成的过程本身就包含一定权重。对文档切片时,文档内容本身也是存在关联的,一般都是从简到繁。经过这些观察很容易想到,如果文档中的某一部分命中率较高,是不是能提高它的权重,让LLM更好的输出文档优质的内容?
但从排序来看通过权重来控制索引的排列顺序,这点类似于搜索引擎。但不同于权重算法来决定顺序,在RAG中重新排序是计算问题和文档中间的相关性,按照从高到低进行排序。因为LLM上下文的限制,需要对输入窗口TopK值进行调整。即使上下文可能很大,但过多的信息依然会分散LLM的注意力,毕竟可能引入了过多的低相关性的信息。
七、总结
本文简单概述RAG以及构建过程,讨论了如何优化提问,路由,索引切片和重新排序这几种方式提高输出质量。
RAG是一种简单而又强大的技术,可以仅靠三行代码构建出一个简易应用,相比Agent对LLM能力要求也较低,又可以显著提升在特定领域的应用能力。随着技术的爆发性进步,层出不穷优化RAG的思路在努力消除大语言模型带来的幻觉,提供更准确,更相关的信息,从而生成更高质量的内容。
作者:王凯| 后端开发工程师
学习资料:
1、从零开始学习RAG
2、How do domain-specific chatbots work? An Overview of Retrieval Augmented Generation (RAG)
3、https://rolen.wiki/talk-to-a-successful-american-with-seven-kids**