LangChain+LLM大模型问答能力搭建与思考

1. 背景

最近,大模型(LLMs,Large Language Models)可谓是NLP领域,甚至整个科技领域最火热的技术了。凑巧的是,我本人恰好就是NLP算法工程师,面临着被LLMs浪潮淘汰的窘境,决定在焦虑之余,试着用新家伙,去解决解决老问题(缓解一下焦虑...)

所以是什么老问题呢?------那就是Bert解决了过、但问题依旧存在的那个------垂直领域问答

2. 原思路简介

由于目前的确已经有许多类似的、写得很优秀的大佬写的文章了,所以我本文不会过多涉及具体的代码。我更倾向于探讨一下我们怎么用这项**"新技术"**,来解决之前就有的、而且颇有价值的"老问题"

首先,最基础的架构我们还是要说一下(免得一段时间以后回顾的时候一脸懵逼,只剩吹牛了)

我们先回顾一下,Bert时代的垂直领域问答系统是怎么构建的?

如上图所示,这就是个问答系统的大概面貌了。当然了,还有些其他辅助模块没在这里体现,这里只拿出了最关键的几个部分:

  1. query理解:充分理解query含义,挖掘query信号,给下一轮的doc(文档)"粉碎机"提供弹药!
  2. 文档召回:这里的召回方式大概可分为2大类,分别是基于query理解得到的信号召回,以及向量召回。
    1. 信号召回:基于ES或其他什么类ES自研框架(为什么要自研,还不是为了体现价值orz...)实现,可以简单理解为关键词召回,即从query和doc中分别找到关键词,然后进行匹配、打分;
    2. 向量召回:信号召回显然是基于"字面"的,而向量召回则是基于语义的。将doc提前向量化建库,然后query再向量化后进行向量检索。
    3. 最后,基于以上两路召回的文档得分,再结合一些其他trick(如质量打分)进行综合排序,取topK进入下一轮大逃杀!
  3. 答案抽取:使用预训练好的模型,分别对topK文档里的答案进行抽取。对于上一代Berter(哦不NLPer)而言,主模型都是Bert或其徒子徒孙。现在,则是最时髦的LLM
  4. 答案处理:后处理模块,可将得到的答案再次排序、可做答案融合、也可根据业务需要,进行合理的后处理,最后输出。

3. LLM出现以后的世界

3.1 LLM时代和Bert时代的差异

那么,在现如今ChatGPT、ChatGLM、Moss、LAMMA等大模型满天飞的世界,上述那套系统有什么变化了呢?

其实,这套系统整体流程变化并不大,只不过是将"答案抽取"模块的Bert时代模型替换成了LLMs时代的模型。而由于LLM在模型能力、允许输入长度等方面都比Bert强不少,所以一定程度上,LLMs时代这套系统对其他几个模块的要求也降低了

举个例子,在Bert时代,query理解是个要做得很重、异常重要的模块,毕竟如果这里能力不足,那下游模型就"巧妇难为无米之炊了",我上面那张图里query理解部分每个子模块都能撑得起一大块工作。而大家都知道,端到端的模型是大家一直追求的(是吗?毕竟这样要我们NLPer还能做啥?),而且能力足够的端到端模型可以最大程度减少信息损失,所以能力更强的LLMs直接把这个模块的工作包揽了下来,原本的query分词、分类、纠错、关键词等工作变成了制定合适的Prompt然后调试,具体的query理解,就由LLM来搞定了。

下面,我再简介一下结合LLM时,垂直领域问答的架构的样子

3.2 LLM时代垂直领域知识库问答常用架构

架构异常简单,暂时能力所限,仅为博君一笑~

如图所示,能看得出,整体架构比Bert时代的简单很多。我们对照着之前的架构来说:

  1. query理解 VS提问模块:前面举过例子,目前提问模块在当前架构下已经几乎没有什么任务了,但如果你想增加一路关键词召回,那么这里也可以借助大模型的能力来解决,用大模型(当然,也可以是传统nlp模型或规则词表)帮你完成关键词提取,然后到下一步中进行使用ES做关键词召回

  2. 两版文档召回模块:这一部分差别其实不大,但随着LLMs的模型能力提升,对于召回质量的忍耐度也在提升。由于Bert时代模型往往有512 tokens的长度限制,所以我们要更加小心地做好召回文段排序和截断、文段之间overlap等事情。但随着大模型目前普遍将文本可输入长度扩到了8K、甚至是16K/32K(当然这里指的是字符,汉字字数会少一些),这就意味着如果需要,我们可将更多的文段喂给LLM。也正因为此,我们现如今往往只用向量召回(LangChain)即可达到要求。当然了,为了进一步提升召回文章的丰富度,开发额外的ES关键词召回、甚至更复杂的召回链路也是可行的。笔者之前有幸在某大厂负责过电商搜索引擎的召回模块,各路召回之复杂实在让人欲仙欲死,然而它们也确实是有用的,有些召回路径满足的是搜索的精准,有的召回路径满足的是物料的丰富,有的召回路径从用户历史兴趣角度考量,也有的召回路径从品类、产地、功能等多角度试图猜到用户所需。不过,由于知识库问答场景往往没有这么多目标,所以我们就尽可能做的轻一些------less is more

  3. 答案抽取模块VS大模型问答模块:虽然核心的模型从Bert变成了本地的微调过的ChatGLM、或ChatGPT-api,但整体思路是一致的。Bert时代首先需要将处理后的query和若干召回的doc拼接成类似[CLS]query[SEP]doc1, [CLS]query[SEP]doc2, ...类似的样子,然后分别送给Bert进行答案抽取、输出;LLMs时代,则是将召回的doc和用户query装配成最终Prompt,类似的样子如下:

    python 复制代码
    """已知信息:
    {context} 
    
    根据上述已知信息,简洁和专业地回答用户问题。如果无法从中得到答案,请说 "根据已知信息无法回答该问题" 或 "没有提供足够的相关信息",不允许在答案中添加编造成分,答案请使用中文。 问题是:{question}
    """

    以上Prompt模板参考链接。最后,LLM会将答案输出。

  4. 答案处理模块在LLM版本架构中,不再需要这一模块,因为LLM已经将输入资料整合并完成回答了。但Bert时代,由于多段资料分别和query拼接进行答案抽取,所以结果还要对多个答案进行排序。

4. LangChain+LLM实战心得体会

具体LangChain+LLM的代码,笔者参考该repo,并在上面进行了些细微的二次开发。由于该项目已经非常完备了,所以不再赘述。我在这里,主要说一下我在项目开发中遇到的问题和一点思考:

4.1 数据处理和清洗方面

  1. 关于目录:目录部分应提前识别并删除

    这部分关键词巨多(误命中)、各种标点符号巨多(误分段)、大多不是自然语言(用来做目录索引的,没实际的文本含义),引入目录,会在文档召回阶段通过向量或关键词产生大量误召回,建议直接咔嚓掉

  2. 文档分段长度:模型允许的情况下,能长则长;可根据数据酌情向后扩展

    在大模型可接受文本总字数有限的情况下,增加召回数量和增加单文档长度的权衡

    • 首先,由于我所处理的数据是相对干货的政策法规和企业规章,所以文章质量较高;其次,相似的内容较少出现在多个出处、而且有些政策文本会有不少1.xxx, 2.xxx, ..., 10.xxx这种不适合切割的超长内容,所以对我的数据而言,与其召回更多个短文本段,还不如召回更少的长文本段,以减少低相关杂质引入,也防止有用内容被截断。

    • 不过之前我也处理过各种政策解读、财经类新闻的语料,这种语料往往良莠不齐且同一件事有多个相似文章在描述,为了确保结果的全面性,则是召回更多个短文本段更好。

    关于分段截断的问题

    • 一般通用分段方式,是在固定max_length的基础上,对出现。/;/?/....../\n等等地方进行切割。

    • 但这种方式显然比较武断,面对特殊情况需要进一步优化。比如前面的那个1.xxx, 2.xxx, ..., 10.xxx超长内容的情况,直接按这种方法切割就会导致潜在的内容遗漏。

    • 对于这种候选语料"内聚性"很强的情况,容易想到,我们要么在切割语料时动手脚 (不把连续数字符号所引领的多段文本切开);要么在切割时照常切割、但在召回时动手脚 (若命中了带连续数字符号的这种长文本的开头 ,那么就一并把后面连续数字符号引领的多段文本一起召回)。笔者目前只想到了这两种方法且还没具体做实验,只是凭空想来,前者方案有较明显瑕疵,因为这样会------

      1. 相对于更短文段而言,长文段的语义更丰富,每个单独的语义点更容易被淹没,所以在有明确语义query的召回下这种长文段可能会吃亏;
      2. 长文段一旦被召回,只要不是针对整段文本的提问,那么也是引入了更多的噪声(不过鉴于LLM的能力,这可能也无伤大雅,就是费点显存or接口费用了)

      但后者就显得更灵活些,不过确实也不够聪明。暂时没想到其他办法,有好想法的人可以来交流一下~

    • 此外,有研究表明,长文本作为输入LLM输入时,LLM倾向于更关注长文本的开头、结尾处,然而中间部分的语义可能会被忽略。这里挖个小坑,下次再来关注一下这方面问题

4.2 模型方面

  1. 调整temperature参数规避模型"知识幻觉"

    • 对于知识库问答而言,知识幻觉(一本正经的胡说八道)是最头疼的事情。对于ChatGPT(gpt-3.5-turbo)而言,笔者实验过,当temperature<=0.1,可以有效避免"知识幻觉"(即Prompt中没有问题答案,ChatGPT就会回答"根据已知信息无法回答该问题"),不过这也可能让模型过分谨慎(即使有语义类似的答案,模型也倾向于那不是提问范围内的)。建议可以再尝试一些0.1~0.5之间的temperature,在最大化模型联想能力的同时,确保回复的可靠性

    • 但对于ChatGLM-6B(v1),该模型temperature取值范围在[0, 1],笔者尝试过0.1、0.01等数值,仍然会出现较为严重的模型幻觉。不知道ChatGLM2-6B效果会不会好些。不过6B的打不过号称175B的ChatGPT,也是正常

4.3 问题解决思路方面

这部分只是个人在问题解决过程中的一点想法,与大家分享。

  1. 明确多文档下的知识点问答 ,和单文档总结之间的区别

    • 该项目LangChain+LLM的思路,是针对多文档下的知识点问答的,目标是在海量语料中精准找到几小段参考文档然后总结出答案;
    • 而单文档总结,即类似ChatPDF的需求,和本项目目标不一致。ChatPDF的目标是尽可能全面地总结出整篇文档的信息。这个需求以目前ChatGPT可接受的16K上下文而言,不少文档可以直接丢进去问答,无需使用召回工具先做知识检索,不完全属于该项目解决的问题范围。
  2. 对于效果不佳的情况,首先应该从更高的角度思考,到底是当前技术不达标,还是使用场景不契合。用检索+问答这种范式解决其不擅长的问题、然后再此基础上搞一大批trick,是非常不可取的,应尽量避免

    • 举个例子,有人问这种问答模式能否解决Excel表格问答?这当然可以试试,结果对表格进行问答效果很差------这个结果很容易预期,毕竟CoT的思想就提到过,做推理、算数学题,这种需要一步步思考的任务在加以一步步引导的情况下,LLM才能得到不错的结果。那么,我们应该在检索+LLM这个项目上做优化以解决Excel表格的问答问题吗?非也。我们应该意识到Excel、或者其他文件中的表结构本身属性是表格,表格是结构化数据、文本是非结构化的,而检索系统(文本召回系统)本身就是为非结构化数据准备的。所以要解决excel文件问答的问题,或许可以把它归类到"NL2SQL"上,用代码/SQL的方式去解决。

    • 我做了简单的尝试,供大家参考,后台模型gpt-3.5-turbo-16K:

      来自你的消息:

      排名 球员 球队 得分 命中-出手 命中率 命中-三分 三分命中率 命中-罚球 罚球命中率 场次 上场时间
      1 乔尔-恩比德 76人 33.10 11.00-20.10 54.8% 1.00-3.00 33% 10.00-11.70 85.7% 66 34.60
      2 卢卡-东契奇 独行侠 32.40 10.90-22.00 49.6% 2.80-8.20 34.2% 7.80-10.50 74.2% 66 36.20
      3 达米安-利拉德 开拓者 32.20 9.60-20.70 46.3% 4.20-11.30 37.1% 8.80-9.60 91.4% 58 36.30
      4 谢伊-吉尔杰斯-亚历山大 雷霆 31.40 10.30-20.30 51% 0.90-2.50 34.5% 9.80-10.90 90.5% 68 35.50
      5 扬尼斯-阿德托昆博 雄鹿 31.10 11.20-20.30 55.3% 0.80-2.70 27.5% 7.90-12.20 64.5% 63 32.10
      6 杰森-塔特姆 凯尔特人 30.10 9.80-21.10 46.6% 3.20-9.30 35% 7.20-8.40 85.4% 74 36.90
      7 多诺万-米切尔 骑士 28.30 10.00-20.60 48.4% 3.60-9.30 38.6% 4.70-5.40 86.7% 68 35.80
      8 杰伦-布朗 凯尔特人 26.60 10.10-20.60 49.1% 2.40-7.30 33.5% 3.90-5.10 76.5% 67 35.90
      9 贾-莫兰特 灰熊 26.20 9.30-19.90 46.6% 1.50-4.90 30.7% 6.10-8.10 74.8% 61 31.90

      以上这些人谁的出战场次最多?所有人平均出战多少场次?直接给出答案


      来自 ChatGPT 的消息:

      以上这些人中,贾-莫兰特的出战场次最多,共计61场。

      所有人的平均出战场次为63.1场。


      来自你的消息:

      排名 球员 球队 得分 命中-出手 命中率 命中-三分 三分命中率 命中-罚球 罚球命中率 场次 上场时间
      1 乔尔-恩比德 76人 33.10 11.00-20.10 54.8% 1.00-3.00 33% 10.00-11.70 85.7% 66 34.60
      2 卢卡-东契奇 独行侠 32.40 10.90-22.00 49.6% 2.80-8.20 34.2% 7.80-10.50 74.2% 66 36.20
      3 达米安-利拉德 开拓者 32.20 9.60-20.70 46.3% 4.20-11.30 37.1% 8.80-9.60 91.4% 58 36.30
      4 谢伊-吉尔杰斯-亚历山大 雷霆 31.40 10.30-20.30 51% 0.90-2.50 34.5% 9.80-10.90 90.5% 68 35.50
      5 扬尼斯-阿德托昆博 雄鹿 31.10 11.20-20.30 55.3% 0.80-2.70 27.5% 7.90-12.20 64.5% 63 32.10
      6 杰森-塔特姆 凯尔特人 30.10 9.80-21.10 46.6% 3.20-9.30 35% 7.20-8.40 85.4% 74 36.90
      7 多诺万-米切尔 骑士 28.30 10.00-20.60 48.4% 3.60-9.30 38.6% 4.70-5.40 86.7% 68 35.80
      8 杰伦-布朗 凯尔特人 26.60 10.10-20.60 49.1% 2.40-7.30 33.5% 3.90-5.10 76.5% 67 35.90
      9 贾-莫兰特 灰熊 26.20 9.30-19.90 46.6% 1.50-4.90 30.7% 6.10-8.10 74.8% 61 31.90

      以上这些人谁的出战场次最多?所有人平均出战多少场次?使用pandas计算,给出可执行代码,无需将全部数据重新列举一遍


      来自 ChatGPT 的消息:

      您可以使用以下代码来计算出战场次最多和平均出战场次:

      python 复制代码
      import pandas as pd
      
      # 创建数据表
      data = {
          '排名': [1, 2, 3, 4, 5, 6, 7, 8, 9],
          '球员': ['乔尔-恩比德', '卢卡-东契奇', '达米安-利拉德', '谢伊-吉尔杰斯-亚历山大', '扬尼斯-阿德托昆博', '杰森-塔特姆', '多诺万-米切尔', '杰伦-布朗', '贾-莫兰特'],
          '球队': ['76人', '独行侠', '开拓者', '雷霆', '雄鹿', '凯尔特人', '骑士', '凯尔特人', '灰熊'],
          '得分': [33.10, 32.40, 32.20, 31.40, 31.10, 30.10, 28.30, 26.60, 26.20],
          '命中-出手': ['11.00-20.10', '10.90-22.00', '9.60-20.70', '10.30-20.30', '11.20-20.30', '9.80-21.10', '10.00-20.60', '10.10-20.60', '9.30-19.90'],
          '命中率': ['54.8%', '49.6%', '46.3%', '51%', '55.3%', '46.6%', '48.4%', '49.1%', '46.6%'],
          '命中-三分': ['1.00-3.00', '2.80-8.20', '4.20-11.30', '0.90-2.50', '0.80-2.70', '3.20-9.30', '3.60-9.30', '2.40-7.30', '1.50-4.90'],
          '三分命中率': ['33%', '34.2%', '37.1%', '34.5%', '27.5%', '35%', '38.6%', '33.5%', '30.7%'],
          '命中-罚球': ['10.00-11.70', '7.80-10.50', '8.80-9.60', '9.80-10.90', '7.90-12.20', '7.20-8.40', '4.70-5.40', '3.90-5.10', '6.10-8.10'],
          '罚球命中率': ['85.7%', '74.2%', '91.4%', '90.5%', '64.5%', '85.4%', '86.7%', '76.5%', '74.8%'],
          '场次': [66, 66, 58, 68, 63, 74, 68, 67, 61],
          '上场时间': [34.60, 36.20, 36.30, 35.50, 32.10, 36.90, 35.80, 35.90, 31.90]
      }
      
      df = pd.DataFrame(data)
      
      # 找出出战场次最多的球员
      max_appearances = df['场次'].max()
      player_with_max_appearances = df.loc[df['场次'] == max_appearances, '球员'].values[0]
      
      print("出战场次最多的球员是:", player_with_max_appearances)
      
      # 计算平均出战场次
      average_appearances = df['场次'].mean()
      
      print("所有球员的平均出战场次是:", average_appearances)

      输出结果为:

      出战场次最多的球员是: 杰森-塔特姆
      所有球员的平均出战场次是: 64.33333333333333
      

      ###来自你的消息:

      排名 球员 球队 得分 命中-出手 命中率 命中-三分 三分命中率 命中-罚球 罚球命中率 场次 上场时间
      1 乔尔-恩比德 76人 33.10 11.00-20.10 54.8% 1.00-3.00 33% 10.00-11.70 85.7% 66 34.60
      2 卢卡-东契奇 独行侠 32.40 10.90-22.00 49.6% 2.80-8.20 34.2% 7.80-10.50 74.2% 66 36.20
      3 达米安-利拉德 开拓者 32.20 9.60-20.70 46.3% 4.20-11.30 37.1% 8.80-9.60 91.4% 58 36.30
      4 谢伊-吉尔杰斯-亚历山大 雷霆 31.40 10.30-20.30 51% 0.90-2.50 34.5% 9.80-10.90 90.5% 68 35.50
      5 扬尼斯-阿德托昆博 雄鹿 31.10 11.20-20.30 55.3% 0.80-2.70 27.5% 7.90-12.20 64.5% 63 32.10
      6 杰森-塔特姆 凯尔特人 30.10 9.80-21.10 46.6% 3.20-9.30 35% 7.20-8.40 85.4% 74 36.90
      7 多诺万-米切尔 骑士 28.30 10.00-20.60 48.4% 3.60-9.30 38.6% 4.70-5.40 86.7% 68 35.80
      8 杰伦-布朗 凯尔特人 26.60 10.10-20.60 49.1% 2.40-7.30 33.5% 3.90-5.10 76.5% 67 35.90
      9 贾-莫兰特 灰熊 26.20 9.30-19.90 46.6% 1.50-4.90 30.7% 6.10-8.10 74.8% 61 31.90

      以上这些人谁的出战场次最多?所有人平均出战多少场次?使用sql计算,无需将全部数据重新列举一遍


      ###来自 ChatGPT 的消息:

      根据给出的数据,可以使用SQL查询来计算出战场次最多的球员和平均出战场次。

      出战场次最多的球员可以通过查询最大的场次数来得到:

      sql 复制代码
      SELECT 球员, MAX(场次) AS 最多场次
      FROM 表名;

      平均出战场次可以通过查询所有球员的场次数并计算平均值来得到:

      sql 复制代码
      SELECT AVG(场次) AS 平均场次
      FROM 表名;

      请将"表名"替换为实际的表名,然后执行上述SQL查询即可得到结果。

      总结一下:

      1. "以上这些人谁的出战场次最多?所有人平均出战多少场次?"这2个问题的答案分别是"杰森塔图姆"和65.67场
      2. 我分别让模型用3种方式输出结果:1-直接输出、2-给出pandas代码后输出、3-只给SQL代码。
      3. 三种方式里,方法1 直接错误,两个问题皆错;方法2 最后给出答案时,问题2"平均出战场次 "算错了。但它给出了正确的pandas代码,执行后两个问题都能得到正确结果;方法3我禁止其输出答案,只给出SQL代码,最后验证sql代码准确无误;
      4. 此外,为了减少大量数据反复传给LLMs增加其负担,实际上可以只给它带表头的2~3行数据,让其明白数据格式即可。

      所以由此可见,与其让它自己推理复杂计算,还不如让它给出代码,然后你自己去执行代码。当然,这里的问题确实是超级简单了,大家可以再试试上难度的统计问题

    • 此外,据了解,OpenAI官方推出的Code Interpreter就是专门干这个的(你可以把它当成个Data Analyzer,感兴趣的童鞋可以试试:OpenAI-Code Interpreter

5. 总结

本文从实战角度出发,对垂直领域知识库问答这个问题,在Bert时代和LLMs时代的解决方案均做了简述。此外,本文还对LLM时代的LangChain+LLM的方案在实战中出现的问题进行了探讨,最后,还简单尝试了一下ChatGPT在数据分析方面的潜力。

欢迎大家多多点赞关注,一起交流学习吧😊

PS,转载请注明出处哦!

相关推荐
LZXCyrus2 小时前
【杂记】vLLM如何指定GPU单卡/多卡离线推理
人工智能·经验分享·python·深度学习·语言模型·llm·vllm
狸克先生5 小时前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
机智的小神仙儿5 小时前
GPT-1.0、GPT-2.0、GPT-3.0参数对比
gpt·语言模型·gpt-3
新加坡内哥谈技术8 小时前
Mistral推出“Le Chat”,对标ChatGPT
人工智能·chatgpt
nbsaas-boot9 小时前
如何利用ChatGPT加速开发与学习:以BPMN编辑器为例
学习·chatgpt·编辑器
Guofu_Liao19 小时前
大语言模型---LoRA简介;LoRA的优势;LoRA训练步骤;总结
人工智能·语言模型·自然语言处理·矩阵·llama
ZHOU_WUYI1 天前
3.langchain中的prompt模板 (few shot examples in chat models)
人工智能·langchain·prompt
hunteritself1 天前
ChatGPT高级语音模式正在向Web网页端推出!
人工智能·gpt·chatgpt·openai·语音识别
sp_fyf_20241 天前
【大语言模型】ACL2024论文-19 SportsMetrics: 融合文本和数值数据以理解大型语言模型中的信息融合
人工智能·深度学习·神经网络·机器学习·语言模型·自然语言处理
AI_小站1 天前
RAG 示例:使用 langchain、Redis、llama.cpp 构建一个 kubernetes 知识库问答
人工智能·程序人生·langchain·kubernetes·llama·知识库·rag