之前体验OpenAI GPT-4o模型的时候,感觉到大语言模型进化的太快,基于AI应用做出的努力可能很快就被新一代模型降维打击,变得没有价值了,“人生苦短,终归尘土”,最终都化为虚无,还有什么意义呢?
不过余华说,“活着得意义就是活着本身”,那么“学习的意义也可以是学习本身”,结果没有意义,但“过程”本身是有意义的。算了,再装下去,要露出“哲学”根基不稳的马脚了。废话不多说,这篇文章我们来看大语言模型RAG应用有哪些优化手段可以提高性能。
前面《RAG-最热门的大语言模型应用方向》文章中,简单介绍过RAG应用通常需要的各个模块及其功能,并用10行python代码实现了一个最基本的RAG应用。刚开始接触RAG时心情激动,感觉这东西很惊艳,肯定会有很多应用场景。但经过一段时间的试用,火热的心快要被一杯一杯凉水慢慢地浇灭了。因为在实际应用场景中,当各式各样的的文档装进知识库之后,RAG应用的准确性就变的差强人意,提高RAG的准确性和稳定性就变得很迫切了。
过去一段时间我也持续在网上查找一些提升RAG性能(主要是准确性和稳定性)的方法,看了不少道友们分享的文章和视频讲解,这里基于个人体会做一些总结。
RAG性能提升方法介绍
影响RAG应用性能的因素中,大模型本身的能力相对不是最关键的。当然更快更好更强的大模型肯定会带来更好的RAG性能,只是很多时候我们没的选择,又没有能力自己搞出一个吊打一切的大模型来,所以提升RAG应用性能的主要工作都是在想办法提升知识库的质量和检索质量。
著名AI大模型博主Greg Kamradt说“All you need is retrieval”这句话,甚得我心。想要RAG性能好,关键在于如何从具有大量内容的知识库中找到跟用户提问最相关最准确的那部分内容,而且没有其它不相干的内容。把这样的内容和用户问题一起提给大模型,自然就能得到更稳定更准确的回答。
下面这个图标识出了RAG应用中提升性能的几个关键环节所在。
以下对这几个优化环节分别展开介绍。
知识库制作
针对不同的RAG应用场景,制作一个内容准确全面而且不带不相干内容的知识库,就成功了一半。
RAG应用的知识库通常是向量库和文档库,制作这样的知识库都会涉及到文档内容提取、切片,以及内容预处理等。以下是一些优化方法和思路介绍,
一)文档切片优化
文档切片的目标是把整个文档切割成主题完整且单一的多个内容切片。比如Langchain中常用的 RecursiveCharacterTextSplitter 是通过一个预设的字符列表把文档按“段落”优先,再“句子”,再“短句”等顺序结合设定长度进行文本切割, Llamaindex中常用的 SentenceSplitter也是类似的方法。
但是这些通用的方法不一定对每个文档都最佳,因此Langchain和Llamaindex框架还提供多种其它的切片方法,方便对不同形式的文档进行切片处理。
即便如此,仍需要我们针对不同文档,测试不同长度的文档切片,以期得到较好的检索效果。同时这些分割器也无法在语意层面按主题完整且单一的切割文档,因此Greg Kamredt在他的个人网站和油管视频中分享了他本人设计开发的按语意切割文档的方法,目前该方法已经被Langchain和Llamaindex框架收录,名为SemanticSplitter。
我看过Greg这个关于SemanticSplitter的讲解视频,只看懂了个大概。这里简单说下,各位道友姑且听之,有感觉不对的可以看原视频获取作者观点。
SemanticSplitter语意分隔器方法是先按“句子”把整个文档初步分隔,然后按顺序默认每连续3句成一组,下一组往后移动1个句子,比如第一组是 [1,2,3],第二组是 [2,3,4],依此类推直到结束,再把每一组内容通过大模型的Embedding服务进行向量化;接着按顺序从头到尾分别比较相邻两组的语意,通过计算两组内容向量之间的余弦相似度,得到相似度值similarity value。再通过numpy数学包计算得到相似度差异超过95百分位以上的那些值(这些值就表示相应的两组句子之间语意差异相对较大),这些值所在的位置就成了文档按语意分隔的点。
举个例子,假如100句话的文档,按[1,2,3],[2,3,4],[3,4,5]的分组方式分成97组,计算相邻组的语意相似度,得到97个相似度值;再找到代表差异最大的超过95百分位的那5个值,产生这5个值的5对分组间的位置就是整个文档的分割点。可以看出这个方法相对会运行比较慢,也更费钱,但仍然是很有价值的一种分隔方式。毕竟这些开销都是制作知识库时的一次性开销。
Greg在视频中提到他是从一个中国AI团队发表的一篇论文中得到的灵感,在视频中他也介绍了这篇论文。我领悟下来,这篇论文主要思想是说可以通过遍历文档中的每个句子,然后通过大模型理解每句话的语意,按“命题/推论”(原文中用的是proposition这个词)把句子分组,每个组内再按需按长度分割,以此进行文档处理,听起来感觉像是做知识图谱的一种方法。这样处理完的文档内容不丢失,而只是按“命题”分组了,可能会丢失一些可读性,但不影响大模型“读”。
基于此,Greg在视频最后还提出一个思路,就是把文档切片当作一个独立的agent-like系统看待,通过各种智能化手段把文档切割处理成适合大模型使用的向量库和文档库,他用Agentic Splitting来代指这样的系统,不过目前还没看到有成熟的实现。
按我理解,Semanticsplitter可能对佛教、道经的释义文档效果一般,因此目前没有在“大雨滴”小程序中应用,当然成本也是一个因素。
二)索引方式优化
在RAG应用中,文档切片的长度存在这样一个矛盾:如果切片较大,每个切片可能包含较多内容,这样内容完整性有保障,但主题聚焦不够,和用户提问相关的内容在切片中比重太低,造成用户提问和该切片的相似度太低而无法查询出来;如果切片较小,用户提问的相似度查询准确性更好,但因为切片较小而很可能丢失相关的上下文内容,造成大模型回答时不够全面和准确。
为了缓解这个矛盾,除前面提到的切片方法优化,还有一种思路就是想办法同时照顾到 “相似度查询的准确性” 和 “查询结果的上下文完整性”。主要有以下几种实现方法。
● 多向量索引法(对应MultiVectorRetriever)。这种方法是在创建向量索引时,使用较大的切片,但是对切片内容向量化时,还对文档切片进行内容总结,然后把总结内容也向量化,以提高查询匹配度。此外,还可以依据文档切片用大模型逆向生成各种对应的查询问题,把这些问题本身也添加到向量库并关联到原始文档切片。
● 父文档索引法(对应ParentDocumentRetriever)。这种方法是对文档进行多级分割,先分割成较大的切片存入文档库docstore,再对每个切片继续切割成更小的切片,并用更小的切片进行向量化索引,当通过用户提问查询到较小的文档切片时,找到该切片关联的父文档切片,并用父文档切片给到大模型来回答问题。
● 语句窗口检索法(对应Llamaindex的SentenceWindowRetriever)。此方法更偏向检索方法优化,就是在创建向量索引时,以每个句子为单位进行切片,当检索到一个句子时,把该句子在原始文档中前后各N个句子一起取出,给到大模型回答问题。
除以上之外,还有其它很多向量索引优化方法,但上面几种比较通用也更容易实现一些。
三)针对应用场景编写知识库文档
除了切片和索引方式优化外,文档内容质量的提升也很有帮助。比如IT系统的智能助手(或智能客服),通常需要对用户使用过程中可能遇到的各种问题进行回答,提供帮助。那就找苦力罗列出具体的各种各样的问题,并编写问题的答案,以Q&A的方式编写出文档。只要问题够多,内容够全面,这样的知识库经过大模型加持后,效果会相当好。只是这种方式就真的是依靠“人工”智能了,尽管很多时候为了提高性能,不得不这么做。
四)对特殊格式的文档进行内容转换处理
比如很多时候会遇到excel表格文档,而结构化的内容通常都只有表头带元数据信息,如果只是简单读取文本内容并按设定长度切割,那么除了含表头的那个切片之外,其它切片都会丢失表头元数据信息,在内容检索时效果会很差。目前langchain和llamaindex开发框架都提供excel文件读取方法,但是文档切割仍然没有好的方法。因此我们可以编写脚本,把excel按每行都转换成含表头元数据的一段一段的文本内容,再加载进向量库,效果会好很多。
查询转换/查询重写
很多场景下,用户的提问都不是语意清晰完整的,有些甚至是有歧义的。这时重写用户提问,使其成为一个内容清晰完整的提问,再基于这个重写后的提问检索出相关内容并提交给大模型,会对大模型回答地准确性有巨大帮助。
在之前做“大雨滴”小程序应用时,使用的Langchain框架的ConversationalRetrievalChain 就有考虑到查询重写的问题,主要是结合对话历史,用大模型把用户的新的提问重写成一个独立的,语意完整的提问,再用改写的提问去检索知识库,并最终提交给大模型进行回答。
除此之外,还有一种“多查询Multi-Query”的方法,是用大模型把原始提问从不同的角度改写成不同用词但含义相近的多个提问,用这些提问分别检索出Top K个文档切片,然后通过排序算法进行必要的合并,重新筛选出Top K个文档切片提交给大模型回答。当原始提问的用词或表达方式跟知识库中文档的表达方式不一致时,多查询可以缓解检索遗漏或得分偏低而被过滤掉的情况。
Langchain和Llamaindex框架中目前都提供了multi-query检索能力。
检索方法
依据前面提到的不同的索引优化方法,就需要相应的文档检索方法,不再赘述,这里先简单说一下多检索器(Multi-Retriever)优化方法。
多数场景中,我们都优先选择语意相似度搜索算法检索知识库。而传统的关键字搜索算法比如BM25Retriever使用TF-IDF搜索算法,在有些场景中仍然非常有效,因此如果我们能结合多种搜索算法对相同的知识库进行检索,势必能提高检索的准确度和完整性,这就是多检索器思路。
最近我在“大雨滴”小程序中增加了BM25Retriever,同时使用了相似度搜索和关键字搜索,后面会详细介绍。
检索结果处理和转换
无论是Multi-Query,Multi-Retriever还是其它的优化方法,都会产生较多的检索结果,可能会超过大模型的token数量限制,此时就需要对检索结果进行再处理。
通常我们使用一些排序算法比如RRF(Reciprocal Rank Fusion)倒数排序融合,或商业服务比如CohereAI重排序器服务,对检索结果进行合并筛选。
此外,在很多场景中,检索出的内容对大语言模型的来说仍然过多,对大模型的回答造成干扰。此时可以考虑对检索结果进行上下文压缩-Contextual Compression,尽可能给到大模型精炼且完整的内容来回答用户提问。
提示词工程
提示词优化对大模型回答问题也影响很大,在前面的提示词文章中已经介绍过,不再赘述。
多检索器实战
以上介绍的检索优化方法都容易理解,在Langchain和Llamaindex文档中也能够找到相应的示例,依照这些示例来进行测试,都比较容易实现。不过,想要把这些方法集成到现有的RAG系统中,和其他的功能一起使用,还是有一些难度的,也可能会遇到一些坑。
目前大雨滴小程序已经有了查询重写功能,考虑到集成的优化方法越多,运行和响应时间越慢,因此计划只把多检索器、多查询、父文档检索器等优化方法应用到这个小程序。本篇后半部分就来介绍一下“多检索器”方法的具体实现。
Llamaindex框架简介
在开始之前,先简单说一下Llamaindex框架。Llamaindex框架和LangChain一样,是另外一个比较流行的大模型开发设计框架,跟LangChain的架构也比较相似。但是Llamaindex在知识库存储方面的功能相对更丰富一些。
Llamaindex框架中的index类似于elasticsearch中的索引,可以用来存储向量数据,也可以用来存储文档,还可以用作其他目的,比如管理知识图谱类的数据。而且index物理存储形式可以是本地存储,也可以使用像mongoDB、Redis这样的数据库,或者其它数据库系统。而LangChain中的index总是和向量库一起的,最新的LangChain框架也引入了文档库,但目前只有一种InMemoryStore类型。
之所以提到文档库DocStore,是因为以上提到的很多优化方式都需要用到它,比如多检索器方法集成的BM25Retriever就依赖DocStore。因为TF-IDF算法是关键字搜索算法,无法基于现有的向量库运行,而需要使用文档库。Llamaindex框架中index可以管理多文档,每个文档切片后用Node对象来管理,Node和LangChain中的Document对象相似,都是用来管理文档切片的,但是Node对象功能更强,可以像B+树结构一样分级管理,并且Node之间通过index组成双向链表,支持各种管理和使用需求。
多检索器框架选择考虑
集成多选择器用LlamaIndex还是继续用Langchain让我纠结了一段时间,一方面Llamaindex对文档库的支持更强,而且支持MongoDB和Redis持久化文档库,LangChain目前只有一个InMemoryStore,一般来说,RAG应用的知识库需要同时带持久化的向量库和文档库才是长久之道,方便后续的功能拓展;另一方面大雨滴小程序目前都是基于Langchain实现的,再集成另外一个框架有点儿不伦不类的感觉,工作量也会多很多。
后来在测试BM25Retriever的时候,发现即使用Llamaindex的持久化文档库,运行时也会把文档库全量加载进内存。原因在于目前Llamaindex和LangChain框架的BM25Retriever都是基于rank_bm25这个包来执行算法程序进行关键字搜索的,MongoDB、Redis都无法支持在数据库本地进行TF-IDF搜索,因此需要把全量文档都读到RAG应用的本地内存再进行搜索。小型文档库问题不大,而大型文档库虽然可以在系统初始化时一次性加载文档库进内存,但是系统运行期间文档库的增删改维护就复杂了,当然内存资源是否足够也是要考虑的问题。
鉴于“大雨滴”小程序的文档库很小,就暂时放弃了Llamaindex。未来有机会用Llamaindex重新做一个知识库模块,应该会是个不错的选择。
集成BM25Retriever
既然LangChain框架只有InMemoryStore一种docstore,要维护一个和向量库内容一致的docstore供BM25检索器使用,就稍微麻烦一些。我的做法,是在用户开始首次对话时,临时从向量库按当前用户的权限查询出该用户当前模式下有权访问的全部文档切片,并做成一个session级的docstore供该用户使用,这样可以保证不同检索器面向的文档集合一致,用户的文档权限功能仍然正常有效。
因为当前使用的是Chroma向量库,生成docstore的关键代码如下,
代码写完一测试,发现bm25_retriever不起作用,根据用户提问查出的文档切片完全不含查询关键字。经过一番调查,发现原来BM25Retriever类使用的默认文本预处理函数很简单,只是通过空格对文本进行分词。这样一来,英文文本没有问题,但中文就不行了,毕竟中文不会在每个词前后加上空格。
怎么解决呢?我在github的Llamaindex论坛找到了答案(Llamaindex的BM25Retriever有相同的问题)。其实也很简单,加一个中文自然语言处理(NLP),比如 python的 jieba包,感兴趣的道友可以参考github上分享的完整代码。想图简单,也可以直接修改以上截图的原代码中
return text.split()
改成以下代码即可,
return list(jieba.lcut(text.strip()))
细心的道友可能已经发现,以上只要用自定义预处理函数作为BM25Retriever类的初始化参数即可,没必要另做一个子类。
确实是可以的。但在测试过程中,还发现BM25Retriever类调用的rank_bm25包中BM25Okapi类也有个小问题,涉及TF-IDF算法本身的具体实现这里就不展开,只说一下问题点和结论:算法中计算IDF得分是计算对数的,而且对那些同时出现在多数文档中的词会降低它们的得分,BM25Okapi中默认是给出现在超过一半文档中的词一个很小的得分,但是刚好出现在一半文档中的词却给0分。
所以又弄了个BM25Okapi的子类小改一下代码,因为BM25Retriever类写死了BM25Okapi类的引用,只好再给BM25Retriever也弄个子类改写一下。
关于LangChain的EnsembleRetriever,它不仅是简单的合并多个检索器,同时也对检索结果做倒数排序融合RRF合并处理,但不会进一步筛选结果集。因此使用时需要自己控制一下每个检索器的TopK参数,避免最后的结果集超出大模型的最大token限制。
因为没有进行严格的量化测试,我无法给出增加多检索器后“大雨滴”小程序的性能提升了多少比例。只是在测试过程中的确看到BM25关键字搜索出的文档切片对之前的相似度搜索有很好的补充作用,尤其是对佛教、道经中大量古文或半白话文的短句检索效果明显。
如何学习AI大模型?
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。
四、AI大模型商业化落地方案
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量。