亚马逊AWS官方博客

基于大语言模型知识问答应用落地实践 – 知识召回调优(上)

知识召回在基于大语言模型的知识问答的流程中是非常关键的步骤,它决定了大语言模型的输入,对后续回答的可靠性以及回复质量影响非常大。
目前社区中基于语义向量召回是提及比较多的方式,但在实际的生产实践中,倒排的召回方式也非常实用,它具备精确匹配、索引效率和可解释的优势。在目前的全文检索系统中,基于倒排的 BM25 相关性打分中依然是核心机制,当然随着近些年深度学习的发展,语义召回表现了强大的检索效果,这两路召回在不同场景下都具备自己独立的优势,在知识召回中的互补性很强。
本文分为上下两篇,会讨论知识召回的所有环节,涉及两种召回方式的多方面实践经验。
上篇主要关注了两种检索方式的宏观对比,以及倒排召回在 Amazon OpenSearch 上的一些实践经验。
下篇主要关注向量召回的最佳实践,包括对称召回和非对称召回的分析比较、知识增强、模型演进和选型、向量模型 Finetune 等等。

倒排召回 & 向量召回的优劣势对比

  • 倒排召回
    • 优势:发展成熟,易达到非常好的 baseline 性能
      • 检索速度更快
      • 可解释能力强
      • 精确匹配能力强
      • 支持自定义停用词表,同义词表
    • 劣势:没有语义信息,对”一词多义”现象解决的不好
      • 关键字不匹配,用户在搜索时并无法知道 Document 中准确的 terms,需要通过 term expansion(同义词),query 改写来解决
      • 语义偏移问题, 虽然关键词字面上匹配,但是命中顺序和用户输入不一样,语义上完全不相关
  • 向量召回
    • 优势:考虑语义相似性,更加智能
      • 语义相近即可召回,无需 term 命中
      • 无需考虑复杂的传统倒排的调优手段
      • 具备支持跨模态召回的潜力
    • 劣势:需要模型训练,对垂直领域落地支持有限
      • 预训练模型对于公开数据集的 benchmark 参考性不足。对垂直领域泛化性不足,可能出现以下情况:
        • 不理解专有词汇
        • 容易出现语义相似但主题不相似的情况
      • 对精准匹配支持不足,难以用专业词汇精准召回
      • 对”多词一义”情况的支持不如倒排召回中的同义词表简单直接
      • 可解释能力弱
      • 需要更多的计算资源

倒排召回

  • 基本原理
    倒排索引(Inverted index)作为一种广泛使用的索引方式,也常被称为反向索引、置入档案或反向档案,它是文档检索系统中最常用的数据结构。离线索引构建时,通过分词器对文档进行切词,得到一系列的关键词(Term)集合,然后以 Term 为 key 构建它与相关文档的映射关系。在线搜索流程中,首先通过切词器对用户输入进行切词,得到 Term 列表,然后根据如下 BM25 打分公式进行打分排序返回给客户。

    上述公式中:

    其中 k1 与 b 是可以定制调节的参数,其中 k1 默认值 1.2,会影响词语在文档中出现的次数对于得分的重要性,比如希望搜索词在文档中出现越多则越接近我希望的内容,那么可以将这个参数调大一点。b 默认 0.75,它能控制文档长度对于分数的惩罚力度,如果不希望文档长度更大的相似度搜索更好,可以把 b 设置得更小,如果设置为 0,文档的长度将与分数无关。

  • BM25 打分 – 分析方法
    仅仅通过一个 BM25 分数以及对应的计算公式,去分析 BM25 分数的合理性十分困难。通过 Amazon OpenSearch Dashboard 我们可以可视化的去了解 BM25 分数的细节,对于我们分析排序错误和明确改进方向有很大帮助。
    在 Amazon OpenSearch Dashboard 的 dev tools 中,我们可以在查询 DSL 中设置 explain=true,参考下面这个 DSL。

    GET chatbot-index/_search?explain=true
    {
      "size": 10,
      "query": {
        "match": { "content" : "我看有的城市外观粉色的!我的为什么没有呢" }
      },
      "sort": [
          { "_score": { "order": "desc" } }
        ]
    }
    

    通过这个 DSL 执行查询分析,我们可以得到整个 BM25 打分的计算逻辑,每个关键词的得分具体是多少,对应的 idf 和 tf 值是多少,他们的计算公式是怎样的,甚至每个参数的具体含义等等信息。这对我们有的放矢去优化倒排效果十分重要。

    ...
    "_explanation": {
      "value": 26.044685,
      "description": "sum of:",
      "details": [
        {
          "value": 3.9140875,
          "description": "weight(content:没有 in 9235) [PerFieldSimilarity], result of:",
          "details": [
            {
              "value": 3.9140875,
              "description": "score(freq=1.0), computed as boost * idf * tf from:",
              "details": [
                {
                  "value": 2.2,
                  "description": "boost",
                  "details": []
                },
                {
                  "value": 2.9598494,
                  "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
                  "details": [
                    {
                      "value": 1306,
                      "description": "n, number of documents containing term",
                      "details": []
                    },
                    {
                      "value": 25208,
                      "description": "N, total number of documents with field",
                      "details": []
                    }
                  ]
                },
                {
                  "value": 0.6010883,
                  "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
                  "details": [
                    {
                      "value": 1,
                      "description": "freq, occurrences of term within document",
                      "details": []
                    },
                    {
                      "value": 1.2,
                      "description": "k1, term saturation parameter",
                      "details": []
                    },
                    {
                      "value": 0.75,
                      "description": "b, length normalization parameter",
                      "details": []
                    },
                    {
                      "value": 30,
                      "description": "dl, length of field",
                      "details": []
                    },
                    {
                      "value": 74.24738,
                      "description": "avgdl, average length of field",
                      "details": []
                    }
                  ]
                }
              ]
            }
          ]
        },
        {...},
        {...},
    
  • 常见问题 & 优化措施
    在垂直领域的 FAQ 知识语料的倒排检索实践中,我们通过上述的分析方法,发现了两类常见的得分偏差:

    • 私有领域文档的 IDF 失真问题
      由于 BM25 打分的 IDF 是基于 Index 内的所有的文档统计得到的,在一些领域内场景,所有的文档的总量并不大,有些专有词汇在全部文档中的出现频率和一些常见的停用词是差不多的,比如在游戏场景中”专属技能”与”怎么”这两个词的 IDF 是差不多的。后续在打分的时候,对于这两个词的 term weight 也不能反映真实的重要性,这就是所谓的术语 IDF 失真的问题。由于 Amazon OpenSearch 并没有直接的方法导入自定义的词频数据。面对这种问题,一般有两种简单有效解决方案:

      • 导入一些其他来源的文本语料
        这些数据仅仅是为了调整 IDF,使它更加接近真实分布。由于一般这种情况下数据总量不大,加入一些无关的数据并不太会改变集群的查询速度,但需要注意的是在搜索时需要通过字段标记过滤掉这些假数据。
      • 录入停用词,停用词不参与 BM25 的得分计算,具体操作可以参考如下步骤:
        • 上传停用词表到 S3,停用词表为每个词单独一行的纯文本文件
          为什么
          不能
          我
          没有
          是什么
          
        • 在 OpenSearch 创建 Package 并制定 S3 路径
        • 把 Package 关联到 OpenSearch Domain
        • 创建 Index 时构建包含同义词表的 analyzer,同时在 text 字段上指定对应的 analyzer,具体参考下面的示例代码
          PUT chatbot-index
          {
              "settings" : {
                  "index":{
                      "number_of_shards" : 1,
                      "number_of_replicas" : 0,
                      "knn": "true",
                      "knn.algo_param.ef_search": 32,
                      "analysis": {
                        "analyzer": {
                          "customize_analyzer": {
                            "type": "custom",
                            "tokenizer": "ik_smart",
                            "filter": ["synonym_filter", "stop_filter"]
                          }
                        },
                        "filter": {
                          "synonym_filter": {
                            "type": "synonym",
                            "synonyms_path": "analyzers/F65056645",
                            "updateable": true
                          },
                          "stop_filter": {
                            "type": "stop",
                            "stopwords_path": "analyzers/F237830428",
                            "updateable": true
                          }
                        }
                      }
                  }
              },
              "mappings": {
                  "properties": {
                      ...
                      "doc": {
                          "type": "text",
                          "analyzer": "ik_smart",
                          "search_analyzer": "customize_analyzer"
                      },
                      "content": {
                          "type": "text",
                          "analyzer": "ik_smart",
                          "search_analyzer": "customize_analyzer"
                      }
              ...
          
    • 无关键词匹配
      这种是倒排召回缺乏语义信息的典型现象,现实中多词一意是非常普遍的。比如在医疗健康场景,一个用户用”症状”来查询与 COVID-19 相关的文档,但一些十分相关的文档中出现的是“病症”、“临床表现”这样的词。除了向量检索能改善这个问题以外,在非常垂直的领域,比如说某个游戏场景中,经常会存在一些专有领域的“黑话”,向量模型的效果可能不见的好,这种情况下可以采用同义词表去枚举这些同义词。
      举下面这个例子说明,这里的盾和护盾是场景内的同义词,在不配置同义词表的情况下,用户输入中的关键词是难以匹配的。

      User Query:  “秘密武器为什么不能开盾”Doc: “问题: 秘密武器在危险地带提示不能开启护盾,是正常的吗?

      回答: 您好,秘密武器的护盾功能与游戏本身限制相同。游戏内如危险地带等本身无法开启护盾的地点,秘密武器的护盾也无法开启。是游戏的正常设定。”

      构建同义词表时,每行为多个同义词(≥2)用逗号分隔,示例如下:

      盾, 护盾
      强化, 强化部件
      服, 服务器
      ...
      

      具体配置步骤与配置停用词表类似,这里不做重复介绍。

总结

本文介绍了基于倒排的召回策略的基本原理,并对这种方式下的一些分析优化手段做了介绍。总的来说,这种方式比较简单易用,不要求技术人员有算法知识背景。这种召回策略在很多场景下非常有效,特别是一些对领域专词非常敏感的场景。但同时它的缺点在于,与搜索引擎不同,在对话机器人的交互形态下,用户的输入更多表现为完整的句子而不是搜索关键词,这时候倒排召回中的 BM25 打分逻辑对语义信息的捕捉能力弱的问题比较明显,所以一般我们建议把它作为向量召回的一种必要补充。
另外在不少学术论文中,经常把倒排召回归属于稀疏向量召回(Sparse Vector Retrieval),而把语义向量召回称为稠密向量召回(Dense Vector Retrieval)。虽然在倒排检索中,用户的输入本质上也是一个非常稀疏的向量, 但是从检索逻辑和引擎实现方式上看,两者是有本质区别的。倒排检索不涉及模型推理,其查询速度上是要远超过向量检索的,这也是倒排召回一个优势。
下篇会关注向量召回,同时也会讨论两者结合的一些实践。具体内容请关注下篇。

本系列博客包括:

  1. <基于大语言模型知识问答应用落地实践 – 知识库构建(上)>
  2. <基于大语言模型知识问答应用落地实践 – 知识库构建(下)>
  3. <基于大语言模型知识问答应用落地实践 – 知识召回调优(上)>(本篇)
  4. <基于大语言模型知识问答应用落地实践 – 知识召回调优(下)>

另外,本文提到的代码细节可以参考配套资料:

  1. 代码库 aws-samples/private-llm-qa-bot
  2. Workshop <基于Amazon Open Search + 大语言模型的智能问答系统> (中英文版本)

本篇作者

李元博

AWS Analytic 与 AI/ML 方面的解决方案架构师,专注于 AI/ML 场景的落地的端到端架构设计和业务优化,同时负责数据分析方面的 AWS Clean Rooms 产品服务。在互联网行业工作多年,对用户画像,精细化运营,推荐系统,大数据处理方面有丰富的实战经验。

谢川

亚马逊云科技项目经理,负责云服务在中国的部署以及客户体验改进。曾在通信,电商,互联网等行业有多年的产品设计和开发经验,并且拥有多个 AI 应用方面的发明专利。

何孝霆

亚马逊云科技创新解决方案架构师,负责基于 AWS 的云计算方案的架构设计,在应用开发,人工智能,Serverless 方向有丰富的实践经验。