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,转载请注明出处哦!

相关推荐
掘我的金22 分钟前
20_LangChain多数据源生成
langchain
掘我的金26 分钟前
19_LangChain结合SQL实现数据分析问答
langchain
王国强20091 小时前
LangChain 设计原理分析¹⁴ | 模拟实现一个精简版 LangChain
langchain
王国强20093 小时前
LangChain 设计原理分析¹³ | LangChain Serve 快速部署
langchain
nenchoumi31194 小时前
Tello无人机与LLM模型控制 ROS
人工智能·语言模型·机器人·无人机
什么都想学的阿超6 小时前
【大语言模型 00】导读
人工智能·语言模型·自然语言处理
前端双越老师9 小时前
【干货】使用 langChian.js 实现掘金“智能总结” 考虑大文档和 token 限制
人工智能·langchain·node.js
Dajiaonew10 小时前
Spring AI RAG 检索增强 应用
java·人工智能·spring·ai·langchain
z千鑫10 小时前
【OpenAI】 GPT-4o-realtime-preview 多模态、实时交互模型介绍+API的使用教程!
人工智能·gpt·语言模型·aigc
小白杨树树1 天前
【LLM】文献阅读-ISOLATE GPT:基于大语言模型的执行隔离架构
人工智能·gpt·语言模型