心法利器
本栏目主要和大家一起讨论近期自己学习的心得和体会。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有。
2023年新的文章合集已经发布,获取方式看这里:又添十万字-CS的陋室2023年文章合集来袭,更有历史文章合集,欢迎下载。
往期回顾
心法利器[105] 基础RAG-大模型和中控模块代码(含代码)
心法利器[106] 基础RAG-调优方案
心法利器[107] onnx和tensorRT的bert加速方案记录
心法利器[108] | 微调与RAG的优缺点分析
心法利器[109] | RAG效果评估经验
RAG强调的是通过知识检索,为大模型提供足够的知识支撑,从而缓解大模型出现幻觉的问题,但这里有一个大前提,就是“有知识”,只是从哪来,怎么处理,如何放入检索库,如何被检索、使用的,本文主要给大家讲清楚这个整体流程以及内部常见的方案。
知识的使用流程
在讲方法之前,先了解一下知识全流程的流动路线和具体的使用方法,毕竟只有知道这个数据怎么用,我们才知道什么形态适合,能更快地进行调整。
知识从原始材料到最终使用,主要会经历离线和在线两个阶段。离线阶段是指,是把原始材料存入数据库的流程,这个阶段是离线处理的,所以被称为离线阶段;在现阶段是指通过query被检索出来最终被作为prompt的一部分输入大模型的过程,这就是在线阶段。
离线阶段会经历这几个过程:
基础文档解析——将各种形式的文档,转化可以容易处理模式,例如都转为文本。
内容理解——从文档中提取各种重要信息,可以是关键词、实体、标签、摘要、切片等显式信息,也可以是embedding这种隐式信息。
入库——存到数据库中,做成方便检索的索引,供在线的搜索使用。
在线阶段会经历这几个过程:
检索出库——被搜索出来,从数据库中被取出。
排序、判别、过滤——对检索出来的知识进行进一步的筛选,选出最合适的备选知识。
拼接prompt——知识拼接成prompt,形成回复结果。
离线阶段——知识处理
基础文档解析
在现实应用中,我们可能会遇到各种各样不同的知识材料。从内容类型,可以有文本、音频、视频、图片,格式上,文本有word、pdf、txt甚至excel、ppt等,音频有wav、mp3,视频有mp4、rmvb等,进一步的即使是文本,有政策文档,表格、代码、富文本等多种格式,还有可能是文本的截图,输入的内容如此混乱,我们需要对各种内容进行针对性处理。
目前比较常见的,是对文本的处理,对于结构化的形式,类似json、csv、tsv格式的不需要赘述,读取方法都比较简单,更多需要关注的是pdf、word以及html。
以PDF为例,python下其实已经有大量的工具能够读取pdf了,大家可以参考这里面的内容,另外也有很多别的工具也可以自己进一步去查。
Python:PDF文件处理(数据处理):https://blog.csdn.net/Big_Data_Legend/article/details/129091548
PyPDF2: 一款操作操作PDF非常丝滑的Python库:https://zhuanlan.zhihu.com/p/674154847
解密PymuPDF:Python秘籍轻松操控PDF文件!https://zhuanlan.zhihu.com/p/669887252
在对基础文档进行解析后所得到的,是更方便常规处理的各种模态内容,如常规意义的文本、音频、图片等,以便进行进一步处理和入库工作。
内容理解
内容理解本身是推搜领域的说法,但在这里,我还是想直接挪用过来,他应该是相比基础文档解析更特别的部分,需要拆解出来。他是在基础文档解析完成后进行的进一步处理,按照我的实践经验,常见的会有如下工作(可能还会结合实际情况加减):
向量化,即embedding,通过多种方式,例如多个不同结构的模型、多种不同的特征或训练方法得到的模型,可以得到多种不同类型的向量。
特定字段或特征的提取,例如对文档进行分类得到类目标签,对实体进行提取得到实体标签。
对长文本进行摘要或者切片,然后再进行向量化(这里可以看到,各个工作是可以自由组合的)。
内容的重新结构化,把很多特征,从列表形式或者表格形式转化为更适合进行检索的json形式,对应到库里的字段,以便后续存入各种检索库里。
可见,内容理解的所要做的以及用到的技术是完全不同的,这一步其实可能有大量的NLU工作,通过提取更多不同维度的信息,以便后续更好地检索,这里的工作远不是文档切片和向量化两件事,不是向量化存入库,在线一查就好了的,为了让内容更好地被查出来,从离线的内容理解开始,就要做好各种准备。
此处以文档切片为例简单说一下里面所涉及的技术和关注点。文档切片是针对文档而言的,由于匹配对空间信息的要求较高(太长容易出现内容稀释),且后续大模型使用这些信息时对长度有要求(现阶段看,前者问题更大),因此对长文档而言,切片是最简单便捷的方案。常见的方案基本可以在“langchain.text_splitter”中找到,例如RecursiveCharacterTextSplitter可以递归尝试不同字符来切割文本,CharacterTextSplitter可以按照字符来切割文本等,通用地,这些方案可以直接拿来使用,但为了最终效果,这种生硬的方式可能不能达到理想水平,大家需要根据实际情况自己写或者是继承下来对某些小部分进行小幅度调整。
另外,在之前我给出的开源项目中,也有比较简单的内容理解模块的代码,是以向量化为例的(心法利器[104] | 基础RAG-向量检索模块(含代码))。我再把这块相关的代码片段放出来
vec_model = VectorizeModel(VEC_MODEL_PATH, DEVICE)
index_dim = len(VectorizeModel(VEC_MODEL_PATH, DEVICE).predict_vec("你好啊")[0])
source_index_data = []
with open(SOURCE_INDEX_DATA_PATH, encoding="utf8") as f:
for line in f:
ll = json.loads(line.strip())
if len(ll["title"]) >= 2:
source_index_data.append([ll["title"], ll])
if len(ll["desc"]) >= 2:
source_index_data.append([ll["desc"], ll])
# if len(source_index_data) > 2000:
# break
logger.info("load data done: {}".format(len(source_index_data)))
# 推理向量
vectorize_result = []
for q in tqdm(source_index_data):
vec = vec_model.predict_vec(q[0]).cpu().numpy()
tmp_result = copy.deepcopy(q)
tmp_result.append(vec)
vectorize_result.append(copy.deepcopy(tmp_result))
此处就是把处理好的数据从本地打开,然后进行向量推理的过程,看起来是比较简单的,但实际应用中,背后可能还有很多工作要做:
向量模型的调优,就是这个模型
VectorizeModel
。原始数据的处理,这里可以看到,目前的数据是已经被结构化的,是json结构,但实际上并不一定是,还需要进一步的抽取挖掘。
在实际情况,这块的工作的坑可以很深,还是得耐着性子去做,配合后续RAG效果的bad case反馈,逐步迭代完善优化。
入库
在对各种文档进行了内容理解后,一般我们能够得到待入库的数据物料,然后,我们就批量地灌入到检索库中去。在之前我给出的开源项目中,就已经给了这个模块的脚本demo(心法利器[104] | 基础RAG-向量检索模块(含代码)),我直接放出来。
vec_searcher = VecSearcher()
vec_searcher.build(index_dim, VEC_INDEX_DATA)
# 开始存入
for idx in tqdm(range(len(vectorize_result))):
vec_searcher.insert(vectorize_result[idx][2], vectorize_result[idx][:2])
# 保存
vec_searcher.save()
这里只是demo,数据量有大有小,使用的工具各异,接口也可能不同,大家需要根据实际情况整,可能的情况有这些:
数据字段类型不同,可能是关键词、数字啥的,那要考虑用关键字索引、数字索引之类的。
检索库的选择,单机的分布式的之类的。
向量索引里面可能也有一些参数需要依赖,例如hnsw索引中要权衡内存占用和召回率的关系。
在线阶段——知识检索
在现阶段大家应该会比较了解了,其实就是知识的检索和使用。
检索出库
检索出库是指,给定query去库里面检索和他最接近的doc,最简单能想到的,依旧是向量,就是根据接口查就行,这里就不赘述了。这里可以给大家打开多个思路:
召回链路是可以多路的,具体选哪个交给下一步就行。可以是不同模型的向量,可以是不同特征、训练方法构造的向量;可以是字面的召回或者有关特征的召回,例如“适合1岁以下宝宝的奶粉”,类似这种问题交给向量、字面其实都不合适,肯定是数字索引,一定要注意因地制宜。
检索要记得截断,简单的思路就是TOPN,即只取前N个,但有些时候,可能知识库里就没有合适的,即使TOPN,前面几个也并不能很好地回答问题,此时更多情况是宁可不召也比召错强,没答案的话后续弄兜底方案就行。
排序、判别、过滤
这些操作其实都是原来老的推荐、搜索领域一脉相承了,对召回回来的内容做进一步的筛选,主要思路就是排序、判别和过滤。
排序是指,对给定的知识进行重新的排序,召回的多路在融合后,谁在前面谁在后面,往往需要一个统一的标准进行融合,且排序层因为面对的数据较少,所以逐个和query进行相似度计算,也不会很难,此时可以大胆的使用准确度、灵活性更高的交互式深度学习模型、基于特征的机器学习模型或者是规则打分。这里也可以看到,排序是可以使用交互式深度学习模型、基于特征的机器学习模型或者是规则打分的,可以结合目前已有的数据、具体效果需求、召回链路情况来进行综合判断。
判别是指直接评价特定的一个召回的doc和query的关系,本质也是一个分类,只有两者的关系足够紧密,召回结果才能被用于后续的大模型推理中,类似的思路在CRAG论文里面可以用到,之前我有对这篇论文做详细的讨论(前沿重器[43] | 谷歌中科院新文:CRAG-可矫正的检索增强生成),在这篇论文里,用的是大模型来判别,这是一个好方法,但不绝对,很多时候其实小模型也能达到不错的效果,需要根据实际情况进行选择。
过滤是对一定不对的内容进行过滤,常规的阈值和TOPN过滤是比较直接的,但有些情况可能会有一些变化,可能还要添加别的标准,例如某些字段的限定,像人物百科下要对人进行限制(这些在向量召回中,可能会不好约束,得召回回来后再进行约束),这个根据具体业务需求来做就行,得有一个意识存在这个方案。
拼接prompt
拼接prompt是一个大家应该都知道的活了,简单的就是这个模式:
RAG_PROMPT = """请根据用户提问和参考资料进行回复。
用户提问:{}
参考材料:
{}"""
复杂的可以考虑多个方面给出更多具体信息,可以参考这些:
描述场景和角色,让大模型对场景有一定的认识,配合场景的回复能更精准。
字数、长度限制,避免无限制写。
进一步约束不匹配情况,对不匹配的给出拒绝回复。
补充——实际情况选择
今天的文章我在很多地方都提到了根据实际情况选择,这里有两个重点,其一是必要的知识储备,其二是对实际情况的把握。
必要的知识储备要求的是我们得有足够的“招数”以免对不同的实际情况,以前面基础文件解析为例,我们需要面对的可能是各种各样的文档,都需要做解析,pdf、excel等等,我们只有对类似的文档类型有了解才能够去处理。我们要提前有一定的知识储备才能够做。扎心的,遇到问题多想想,手里是不是只有大模型这一招,别的就不会了。
实际情况的把握来自对业务、数据的理解,对目前面对的问题的了解。我们要经常看数据,带着思考解决方案的目标去看,此时我们手里有很多方案,但是具体要用哪个,是要根据问题来确定的,例如大部分知识问答是很适合用faq的,但是一些结构化的知识,可能用query理解+字面检索的方式可能更加适合,此时早期全链路的设计就有天壤之别,很多成熟的算法工程师甚至可能会花好几天甚至一周的时间来看数据和分析方案,原因就在此,磨刀不误砍柴工,觉得看数据无趣,直接想当然地要动手干可能要吃亏了。
后记
本文是计划是解释文档知识处理的主要方法,但是提纲写下来就改为知识的整体使用流程了,文档处理的方案已经能从多个平台上找到很多大佬的讲解,然而挺多讲解都缺少一个全局视野,只讲了具体的方法,虽然很完整,但是没讲好怎么选择,所以我想着要把全局和大家说明白,这样更有利于方案选择和理解,只有有关的细节技术,大家可以结合我列举的技术,在网上都能找到类似的方案,此处我也就略写了。