来自工业界的知识库 RAG 服务(三),FinGLM 竞赛获奖项目详解

背景介绍

前面介绍过工业界的 RAG 服务 QAnything 和 RagFlow 的详细设计,也介绍过来自学术界的 一些优化手段。

前一阵子刚好看到智谱组织的一个金融大模型比赛 FinGLM,主要做就是 RAG 服务的竞赛,深入研究了其中的几个获奖作品,感觉还是有不少亮点。整理一些获奖项目的设计方案,希望对大家有所启发。

FinGLM 比赛

FinGLM 比赛介绍

FinGLM 是基于一定数量的上市公司财报构建知识库,使用 ChatGLM-6B 作为大模型完成知识库问答。需要回答的问题包含三类:

  1. 初级问题,可以直接从原文中获得信息进行回答,比如直接问特定公司某一年的研发费用,考察的是能否正确检索到内容的能力;
  2. 中级问题,需要对原文中内容进行统计分析和关联,比如问某公司某一年研发费用的增长率,考虑的是能否检索到内容并进行二次加工得到结果的能力;
  3. 高级问题,安全开放的问题,比如问研发项目是否涉及国家战略,考察的是检索到内容并综合处理的能力;

可以看到使用一个相对小的大模型 ChatGLM-6B,需要能准确回答上面的这些问题,对于内容的检索以及架构精细设计要求还是很高的,直接使用 最原始的 RAG 框架 肯定是不够的, 发挥不稳定的向量检索大概率是无法帮你获奖的。

比赛难点

我根据决赛答辩过程中的一些反馈,整理此比赛中的一些难点:

  1. 财报中包含大量的数据,掺杂文本内容与表格数据,很多精细的问题都需要依赖表格进行回答,如何进行精细的处理,保证原始文档中的内容可以正确检索到;
  2. 不同类型的问题需要不同的处理方案,如何区分不同的问题进行有针对性的解决;
  3. ChatGLM-6B 模型较弱,稍微复杂的情况就无法正确处理,甚至模型的输出就不可控了,如果保证稳定输出正确的答案;
  4. 用户问题与文档使用中词汇可能不是完全一致的,这会导致精确的检索更加困难;
获奖项目介绍

在天池的 决赛文章 可以看到最终获奖的队伍,本文主要想介绍的是项目是决赛获得第三名的 “ChatGLM反卷总局” 团队的项目,为什么没有选择前面的团队的项目,主要原因是:“ChatGLM反卷总局” 是获奖作品中性价比最高的实现方案:

  1. 其他团队或多或少都做了模型的微调,甚至获得第一名团队微调了 2 到 3 个模型,“ChatGLM反卷总局” 没有做任何的微调就取得了不错的成绩;
  2. 在原始 chatGLM 做关键词提取效果很差的情况下,其他团队都是靠微调解决,“ChatGLM反卷总局” 基于原始模型 + 正则表达式就实现了不错的效果;
  3. 实现方案比较轻量,没有引入太多额外的服务,生产环境可以方便应用;

项目方案详解

方案设计

项目设计的整体流程如下所示:

请添加图片描述

整理流程主要包含三个核心模块:

  1. 问题分类,不同类型的问题需要采取的方案完全不同,在选择处理方案时需要先确定是什么类型的问题,方便进行有针对性的处理;
  2. 关键词抽取,因为内容的检索主要使用关键词检索,因此需要从原始问题中提取对应的关键词;
  3. 表格与文档内容的检索与回答,根据问题类型的不同从不同来源获取数据,并执行必要的外部处理(主要是额外的数学计算),处理后提供给大模型得出最终结果;
问题分类

不同类型的问题存在不同的处理方案,因此在回答前需要先进行分类,分类的正确性对结果影响很大。分类一般是根据实际的测试问题进行归纳总结得出类型和判断依据, 之后就可以自动化分类。这部分不少团队是通过微调 chatGLM 模型实现线上分类的。

此项目是基于简单的规则判断,这个只能算是一个取巧方案,没有太多可说的东西。可以简单看看对应的实现:

if '保留' in q['question'] and com_match != '':question_type = 'calculate'
elif com_match != '' and len(year_match) > 0 and ('分析' in q['question'] or '介绍' in q['question'] or '如何' in q['question'] or '描述' in q['question'] or '是否' in q['question'] or '原因' in q['question'] or '哪些' in q['question'] or len(com_normal_keywords) > 0) and '元' not in q['question'] and len(com_info_keywords) == 0:question_type = 'com_normal'
elif com_match != '' and len(year_match) > 0:question_type = 'com_info'
elif com_match == '' and judge_tongji(q['question']):question_type = 'com_statis'
else:question_type = 'normal'q['question_type'] = question_type
if '增长率' in q['question']:q['question_type'] = 'com_info'

代码有点糙,应该是根据实际问题总结出来的。但是在实际生产环境中,如果问题分类可以通过简单规则直接区分出来,确实没必要做大模型的微调或复杂的意图识别了。

关键词抽取

常规的 RAG 服务一般是基于向量进行检索,但是此项目主要使用的是关键词进行检索,支持了关键词 + 向量的混合检索,貌似决赛还因为镜像过大最终没有使用向量检索,因此主要依赖的就是关键词检索。

而关键词抽取的效果直接影响关键词检索的效果,因此这一步相当重要,此项目使用的两种方案:

  1. 正则关键词抽取;
  2. 基于 LLM 的关键词抽取

同一内容在问题中的使用的关键词可能与文档中使用的关键词存在一些差异,因此还需要进行关键词的泛化。

正则关键词提取

正则关键词的提取完全是通过已有的关键词表进行匹配得到的,效果优劣与原始的关键词表的覆盖范围有很大关系。具体的实现简化如下:

# 各个来源的关键词列表汇总find_keywords = list(attr_mapping_title.keys() | gongshi_mapping.keys() | com_normal_attr_mapping_title.keys())
find_keywords.sort(key=len, reverse=True)# 构建正则表达式,可以匹配关键词表中存在的关键词attr_regex_pattern = r'(?:' + '|'.join(find_keywords) + r')'
attr_regex = re.compile(attr_regex_pattern, re.IGNORECASE)# 从原始问题 q 中提取关键词,并实施去重attr_match = attr_regex.findall(q['question'])
attr_match.extend(attr_regex.findall(q['question'].replace('的','')))
keywords = list(set(attr_match))

这个是通过简单的匹配覆盖常规的关键词提取,主打的就是快,即使无法命中也有 LLM 提取关键词兜底。

基于 LLM 的关键词提取

常规的 ChatGLM-6B 模型较小,抽取关键词的效果很可能不佳。常规方案是构造训练数据进行微调,但是此项目用了一种有意思的方案:

项目使用 Few Shot 去提升 ChatGLM-6B 的提取关键词的效果,与常规 Few Shot 不同在于,构造的 Few Shot 是放在历史聊天记录中的,而不是拼接在 prompt 中的。

对应的实现简化后如下所示:

cls_history = [("现在你需要帮我完成信息抽取的任务,你需要帮我抽取出句子中三元组,如果没找到对应的值,则设为空,并按照JSON的格式输出", '好的,请输入您的句子。'),("<year><company>电子信箱是什么?\n\n提取上述句子中的关键词,并按照json输出。", '{"关键词":["电子信箱"]}'),("根据<year>的年报数据,<company>的公允价值变动收益是多少元?\n\n提取上述句子中的关键词,并按照json输出。",'{"关键词":["公允价值变动收益"]}'),("<company>在<year>的博士及以上人员数量是多少?\n\n提取上述句子中的关键词,并按照json输出。",'{"关键词":["博士及以上人员数量"]}'),("<company><year>年销售费用和管理费用分别是多少元?\n\n提取上述句子中的关键词,并按照json输出。",'{"关键词":["销售费用","管理费用"]}'),("<company><year>的衍生金融资产和其他非流动金融资产分别是多少元?\n\n提取上述句子中的关键词,并按照json输出。",'{"关键词":["衍生金融资产","其他非流动金融资产"]}'),...
]prompt = f'{question}\n\n提取上述句子中的关键词,并按照json输出。'response, history = model.chat(tokenizer, prompt, history=cls_history, top_p=0.7, temperature=1.0)

通过这种方案,最终大模型提取关键词的表现更稳定,下面的团队给出的对比情况:

在这里插入图片描述
在这里插入图片描述

关键词泛化

因为原始问题中的关键词与实际文档中的关键词不是完全一致的,此时就无法正确匹配到文档中内容,因此需要执行必要的泛化,从而提升匹配的概率。

项目是通过 fuzzywuzzy 实现的,此项目目前已经迁移至 thefuzz 了,思路是通过 Levenshtein_distance 实现模糊的字符串匹配,其实就是基于编辑距离确定字符串的相似度。

项目中的关键词泛化的实现如下所示:

from fuzzywuzzy import processdef find_best_match_new(question, mapping_list, threshold=40):# 与系统中关键词列表通过编辑距离进行匹配matches = process.extract(question, mapping_list)# 使用阈值进行过滤,并按照相似度进行排序best_matches = [match for match in matches if match[1] >= threshold]best_matches = sorted(best_matches, key=lambda x: (-x[1], -len(x[0])))# 返回最匹配的内容和得分match_score = 0total_q = ""if len(best_matches) > 0:total_q = best_matches[0][0]match_score = best_matches[0][1]return total_q, match_score
数据库与文档内容的检索与回答

表格数据检索与回答

表格数据是在预处理阶段提取出来,保存在 excel 文件中,初始化时转换为 pandas 对象进行检索。

通过前面的关键词提取与泛化后,得到的关键词与文档中的关键词就保持一致了,此时通过关键词就可以准确地匹配所需的内容。这部分就可以看到精确匹配的优势所在,处理得当的情况下结果相对准确。

除了简单直接匹配表格数据的情况,实际问题中还存在需要通过公式计算最终结果的情况,是否能直接提供原始数据给 chatGLM-6B 进行计算呢,这个就有点强 GPT 所难了,实际测试往往容易出错。

目前是通过外部提取所需的原始后直接调用 Python 进行计算,具体的实现如下所示:


financial_formulas = {"研发经费与利润比值": (["研发费用", "净利润"], "研发费用 / 净利润"),"企业研发经费占费用": (["销售费用", "财务费用", "管理费用", "研发费用"], "研发费用 / (研发费用+管理费用+财务费用+销售费用)"),"研发人员占职工": (["研发人员", "职工人数"], "研发人员 / 职工人数"),"硕士及以上学历人员占职工": (["研发人员", "职工人数"], "研发人员 / 职工人数"),"流动比率": (["流动资产合计", "流动负债合计"], "流动资产合计 / 流动负债合计"),"速动比率": (["流动资产合计", "存货", "流动负债合计"], "(流动资产合计 - 存货) / 流动负债合计"),"硕士及以上人员占职工": (["硕士以上人数", "职工人数"], "硕士以上人数 / 职工人数"),"研发经费与营业收入比值": (["研发费用", "营业收入"], "研发费用 / 营业收入"),...
}# 根据关键词获得所需的公式项formula_data = financial_formulas[index_name]
data_values = {}# 获取计算所需的原始数据finance_data = dq.get_financial_data(year_, stock_name)# 将原始数据与公式所需的元素组合为键值对for field in formula_data[0]:value = finance_data.get(field)data_values[field] = value# 获取对应的公式计算字符串formula_str = formula_data[1]
calculation_str = formula_str.replace(" ", "")
for key, value in data_values.items():calculation_str = calculation_str.replace(key, str(value))# 调用 Python 执行计算result_value = eval(calculation_str)

文本数据检索与回答

对于文本检索,大量的信息会存在文本的各级标题中,而常规的文件分片中除了与标题直接相连的分片,其他分片中标题的信息是缺失的,效果类似如下所似:
在这里插入图片描述

为了解决这个问题,项目会通过正则表达式识别标题行,之后通过堆栈记录标题层级结构,在各个分片中增加标题信息,实现流程如下所示:

在这里插入图片描述
通过上面的修复,最终各个分片中都会包含各级标题的信息,检索更容易命中。修复后分片效果如下所示:

在这里插入图片描述
分片的数据是保存在 ES 中的,这样就可以比较方便的实现关键词检索,实际检索时关键词命中标题或文本内容都能被正确召回。效果如下所示:
在这里插入图片描述
实际的文本召回实现就相对简单了,只是 ES 客户端的简单调用:

def get_context(company, final_year, query, size=3, es_index="tianchi", keyword="", recall_titles={}, title_keyword=""):force_search_body = {"size": size,"_source": ["texts"],"query": {"bool": {"filter": [{"term": {"companys": company}},{"terms": {"year": final_year}},]}},}# 构造 query 检索force_search_body["query"]["bool"]["should"] = [{"match": {"texts": {"query": query}}}]# 额外的关键词检索增强if len(keyword) > 0:force_search_body["query"]["bool"]["should"].append({"match_phrase": {"texts": {"query": keyword}}})force_search_body["query"]["bool"]["filter"].append({"terms": {"titles_cut.keyword": recall_titles[keyword]}})if len(title_keyword) > 0:force_search_body["query"]["bool"]["should"].append({"match": {"titles_cut": {"query": title_keyword}}})# ES 检索调用search_result = es.search(index=es_index, body=force_search_body)hits = search_result["hits"]["hits"]recall_texts = [hit["_source"]["texts"] for hit in hits]return recall_texts

总结

本文对 “ChatGLM反卷总局” 在 FinGLM 比赛的项目进行了详细解读。比赛中获奖的项目往往会采取各种奇技淫巧的,毫无疑问大部分手段在当前项目都是有效的(要不然就无法获奖了),但是往往过于定制化,无法应用在常规的生产环境中,这个在 FinGLM 的很多获奖项目中都有看到。

在实际了解 “ChatGLM反卷总局” 项目过程中,有两个亮点可能对 RAG 的生产环境有不错的借鉴意义:

  1. 特殊的 Few Shot 设计,将 Few Shot 内容构造至聊天历史中,相对直接放入 prompt 中存在明显提升;
  2. 层次化的文件解析,将各级文件标题拼接至文件分片中,可以大幅提升文档召回率;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/24028.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pyramid Vision Transformer, PVT(ICCV 2021)原理与代码解读

paper&#xff1a;Pyramid Vision Transformer: A Versatile Backbone for Dense Prediction without Convolutions official implementation&#xff1a;GitHub - whai362/PVT: Official implementation of PVT series 存在的问题 现有的 Vision Transformer (ViT) 主要设计…

C++结合ffmpeg获取声音的分贝值

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、分贝是什么&#xff1f;1.功率量2.场量 二、实际操作1.分析wav文件2.读取麦克风 总结 前言 最近面对一个需求&#xff0c;就是需要传递声音文件到模型里推…

链表的回文结构OJ

链表的回文结构_牛客题霸_牛客网对于一个链表&#xff0c;请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法&#xff0c;判断其是否为。题目来自【牛客题霸】https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId49&&tqId29370&rp1&a…

CodeMeter助力Hilscher,推动实现全球智能制造连接解决方案

Hilscher的旗舰店为开放工业4.0联盟&#xff08;OI4&#xff09;社区提供了应用商店的便捷和开放性&#xff0c;将这一概念引入工业领域。该商店依托CodeMeter的许可证管理和加密保护&#xff0c;为工业用户提供了丰富的应用和解决方案库&#xff0c;满足他们在车间自动化和连接…

WPF中读取Excel文件的内容

演示效果 实现方案 1.首先导入需要的Dll(这部分可能需要你自己搜一下) Epplus.dll Excel.dll ICSharpCode.SharpZipLib.dll 2.在你的解决方案的的依赖项->添加引用->浏览->选择1中的这几个Dll点击确定。(添加依赖) 3.然后看代码内容 附上源码 using Excel; usi…

计网复习资料

一、选择题&#xff08;每题2分&#xff0c;共40分&#xff09; 1. Internet 网络本质上属于&#xff08; &#xff09;网络。 A.电路交换 B.报文交换 C.分组交换 D.虚电路 2.在 OSI 参考模型中,自下而上第一个提供端到端服务的是( )。 A.数据链路层 B.传输…

Thinkphp使用Elasticsearch查询

在Thinkphp中调用ES&#xff0c;如果自己手写json格式的query肯定是很麻烦的。我这里使用的是ONGR ElasticsearchDSL 构建 ES 查询。ongr ElasticsearchDSL 的开源项目地址&#xff1a;GitHub - ongr-io/ElasticsearchDSL: Query DSL library for Elasticsearch。ONGR Elastics…

100V 15A TO-252 N沟道MOS管 HC070N10L 惠海

MOS管的工作原理是基于在P型半导体与N型半导体之间形成的PN结&#xff0c;通过改变栅极电压来调整沟道内载流子的数量&#xff0c;从而改变沟道电阻和源极与漏极之间的电流大小。由于MOS管具有输入电阻高、噪声小、功耗低等优点&#xff0c;它们在大规模和超大规模集成电路中得…

package.json中resolutions的使用场景

文章目录 用途配置示例使用方法注意事项和peerDependencies有什么不同peerDependenciesresolutions 总结 ✍创作者&#xff1a;全栈弄潮儿 &#x1f3e1; 个人主页&#xff1a; 全栈弄潮儿的个人主页 &#x1f3d9;️ 个人社区&#xff0c;欢迎你的加入&#xff1a;全栈弄潮儿的…

git【工具软件】分布式版本控制工具软件

一、Git 的介绍 git软件的作用&#xff1a;管理软件开发项目中的源代码文件。 常用功能&#xff1a; 仓库管理、文件管理、分支管理、标签管理、远程操作 功能指令&#xff1a; add&#xff0c;commit&#xff0c;log&#xff0c;branch&#xff0c;tag&#xff0c;remote…

华为端云一体化开发 (起步1.0)(HarmonyOS学习第七课)

官方文献&#xff1a; 为丰富HarmonyOS对云端开发的支持、实现端云联动&#xff0c;DevEco Studio推出了云开发功能&#xff0c;开发者在创建工程时选择云开发模板&#xff0c;即可在DevEco Studio内同时完成HarmonyOS应用/元服务的端侧与云侧开发&#xff0c;体验端云一体化协…

论文代码解读STPGNN

1.前言 本次代码文章来自于《2024-AAAI-Spatio-Temporal Pivotal Graph Neural Networks for Traffic Flow Forecasting》&#xff0c;基本模型结构如下图所示&#xff1a; 文章讲解视频链接 代码开源链接 接下来就开始代码解读了。 2.代码解读 class nconv(nn.Module):de…

NDIS Filter开发-网络数据的传输

和NIC小端口驱动不同的是&#xff0c;无需考虑网络数据具体是如何传输的&#xff0c;只需要针对NBL进行处理即可。Filter驱动程序可以启动发送请求和接收指示&#xff0c;或“过滤”其他驱动程序的请求和指示。Filter模块堆叠在微型端口适配器上。 驱动程序堆栈中的Filter模块…

谷粒商城实战(033 业务-秒杀功能4-高并发问题解决方案sentinel 1)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第326p-第p331的内容 关注的问题 sentinel&#xff08;哨兵&#xff09; sentinel来实现熔断、降级、限流等操作 腾讯开源的tendis&#xff0c…

ctfshow web

【nl】难了 <?php show_source(__FILE__); error_reporting(0); if(strlen($_GET[1])<4){echo shell_exec($_GET[1]); } else{echo "hack!!!"; } ?> //by Firebasky //by Firebasky ?1>nl //先写个文件 ?1*>b //这样子会把所有文件名写在b里…

JSON 无法序列化

JSON 无法序列化通常出现在尝试将某些类型的数据转换为 JSON 字符串时&#xff0c;这些数据类型可能包含不可序列化的内容。 JSON 序列化器通常无法处理特定类型的数据&#xff0c;例如日期时间对象、自定义类实例等。在将数据转换为 JSON 字符串之前&#xff0c;确保所有数据都…

「动态规划」如何求地下城游戏中,最低初始健康点数是多少?

174. 地下城游戏https://leetcode.cn/problems/dungeon-game/description/ 恶魔们抓住了公主并将她关在了地下城dungeon的右下角。地下城是由m x n个房间组成的二维网格。我们英勇的骑士最初被安置在左上角的房间里&#xff0c;他必须穿过地下城并通过对抗恶魔来拯救公主。骑士…

【Text2SQL 论文】C3:使用 ChatGPT 实现 zero-shot Text2SQL

论文&#xff1a;C3: Zero-shot Text-to-SQL with ChatGPT ⭐⭐⭐⭐ arXiv:2307.07306&#xff0c;浙大 Code&#xff1a;C3SQL | GitHub 一、论文速读 使用 ChatGPT 来解决 Text2SQL 任务时&#xff0c;few-shots ICL 的 setting 需要输入大量的 tokens&#xff0c;这有点昂贵…

MacOS M系列芯片一键配置多个不同版本的JDK

第一步&#xff1a;下载JDK。 官网下载地址&#xff1a;Java Archive | Oracle 选择自己想要下载的版本&#xff0c;一般来说下载一个jdk8和一个jdk11就够用了。 M系列芯片选择这两个&#xff0c;第一个是压缩包&#xff0c;第二个是dmg可以安装的。 第二步&#xff1a;编辑…

eclipse插件开发(二)RCP第三方库的引入方式

RCP第三方库的引入 最近在RCP开发过程中遇到JSON串与对象互转的问题&#xff0c;如何像spring开发模式一样引入第三方库呢&#xff1f;eclipse插件开发中用到p2库&#xff0c;但也支持maven库的引入。关键在于.target这个关键文件。 .target 文件用于定义一个目标平台&#x…