• LLM应用实战:当KBQA集成LLM(二)


    1. 背景

    又两周过去了,本qiang~依然奋斗在上周提到的项目KBQA集成LLM,感兴趣的可通过传送门查阅先前的文章《LLM应用实战:当KBQA集成LLM》。

    本次又有什么更新呢?主要是针对上次提到的缺点进行优化改进。主要包含如下方面:

    1. 数据落库

    上次文章提到,KBQA服务会将图谱的概念、属性、实体、属性值全部加载到内存,所有的查询均在内存中进行,随之而来的问题就是如果图谱的体量很大呢,那内存不爆了么…

    2. 支持基于属性值查实体

    上篇文章不支持属性值查找实体,比如”最会照顾宝宝的是什么龙”,”什么龙是大龙和大龙生活,小龙和小龙生活”。本次已经此问题优化。

    此篇文章是对这两周工作的一个整体总结,其中包含部分工程层面的优化。

    2. 整体框架

    整体框架和上篇大致相同,不同之处在于:

    1. 对齐模块:先前是基于SIM筛选候选实体,本次基于ES进行候选实体召回

    2. 解析模块:先前是基于hugegraph和内存中的实体信息进行解析,本次优化为基于hugegraphelasticsearch

    3. 核心功能

    3.1 数据库选型

    由于需要支撑语义相似度检索,因此数据库选型为Milvus与Elasticsearch

    二者之间的比对如下:

    Milvus

    Elastic

    扩展性层面

    存储和计算分离

    查询和插入分类

    组件级别支持

    服务器层面支持

    多副本

    动态分段 vs 静态分片

    动态分段

    静态分片

    云原生

    十亿级规模向量支持

    功能性层面

    权限控制

    磁盘索引支撑

    混合搜索

    分区/命名空间/逻辑组

    索引类型

    11个(FLAT, IVF_FLAT, HNSW)等

    1个(HNSW)

    多内存索引支持

    专门构建层面

    为向量而设计

    可调一致性

    流批向量数据支持

    二进制向量支持

    多语言SDK

    python, java, go, c++, node.js, ruby

    python, java, go, c++, node.js, ruby, Rust, C#, PHP, Perl

    数据库回滚

    但由于Milvus针对国产化环境如华为Atlas适配不佳,而Es支持国产化环境,因此考虑到环境通用性,选择Es,且其文本搜索能力较强。

    3.2 表结构设计

    由于知识图谱的概念、属性一般量级较少,而实体数随着原始数据的丰富程度客场可短。因此将实体及其属性值在Es中进行存储。

    针对KBQA集成LLM的场景,有两块内容会涉及语义搜索召回。

    1. 对齐prompt中的候选实体

    2. 解析模块中存在需要基于属性值查询实体的情况。

    3. 涉及到数值类型的查询,如大于xx,最大,最小之类。

    综合考虑,将Es的index结构设计如下:

    属性

    含义

    类型

    备注

    name

    实体名

    keyword

    concepts

    所属概念

    keyword

    一个实体可能存在多个概念

    property

    属性

    keyword

    属性名称

    value

    属性值

    text

    ik分词器进行分词

    numbers

    数值属性值

    double_range

    会存在一个区间范围

    embeddings

    向量

    elastiknn_dense_float_vector

    1. 非数值属性对应value的向量

    2. 使用elastiknn插件

    3.3 安装部署

    项目使用的Es版本是8.12.2,原因是elastiknn插件和Ik插件针对该版本均支持,且8.12.2版本是当前阶段的次新版本。

    3.3.1 基于docker的ES部署

    # 拉取镜像(最好先设置国内镜像加入)

    docker pull elasticsearch:8.12.2

    # es容器启动,存在SSL鉴权

    docker run -d --name es01 --net host  -p 9200:9200 -it -e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" elasticsearch:8.13.2

    # 容器中拉取需要鉴权的信息到本地

    docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .

    chmode 777 http_ca.crt

    # 密码第一次启动的日志中有,需要保存下来

    export ELASTIC_PASSWORD=xxxxxx

    # 验证es是否启动成功

    curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD https://localhost:9200

    3.3.2 elastiknn插件集成

    elastiknn插件是为了优化ES自身的向量检索性能,安装此插件后,ES的向量检索性能会提升数倍,如果再增加SSD固态硬盘,性能会进一步提升数倍

    #下载插件包

    wget https://github.com/alexklibisz/elastiknn/releases/download/8.12.2.1/elastiknn-8.12.2.1.zip

    # 导入容器中指定目录

    docker cp  elastiknn-8.12.2.1.zip es01:/usr/share/elasticsearch/

    # 进入容器,默认目录即为/usr/share/elasticsearch/

    docker exec -it es01 bash

    # 安装插件

    elasticsearch-plugin install file:elastiknn-8.12.2.1.zip

    # 退出,重启容器

    docker restart es01

    # 验证

    # 创建mapping

    curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD -XPOST https://localhost:9200/test/_mapping -H 'Content-Type:application/json' -d '

    {

             "properties": {

                      "embeddings": {

                              "type": "elastiknn_dense_float_vector",

                              "elastiknn": {

                                       "model": "lsh",

                                       "similarity": "cosine",

                                       "dims": 768,

                                       "L": 99,

                                       "k": 3

                              }

                      }

             }

    }'

    # 验证mapping是否生效

    curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD -XGET https://localhost:9200/test/_mapping?pretty

    采坑总结:

    1. elastiknn插件导入始终无法安装,且报错。

    解决:

    (1) 一定要注意,安装es插件需要指定路径,且增加”file:” 的前缀,不加此前缀,那就等着报错吧

    (2) 拷贝到容器内部,一定要注意,不要将elastiknn-8.12.2.1.zip拷贝至/usr/share/elasticsearch/plugins目录,否则安装也报错。

    3.3.3 ik分词器插件集成

    #下载插件包

    wget https://github.com/infinilabs/analysis-ik/releases/download/v8.12.2/elasticsearch-analysis-ik-8.12.2.zip

    # 导入容器中指定目录

    docker cp elasticsearch-analysis-ik-8.12.2.zip es01:/usr/share/elasticsearch/

    # 进入容器,默认目录即为/usr/share/elasticsearch/

    docker exec -it es01 bash

    # 安装插件

    elasticsearch-plugin install file:elasticsearch-analysis-ik-8.12.2.zip

    # 退出,重启容器

    docker restart es01

    # 验证是否生效

    curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD -XPOST https://localhost:9200/_analyze?pretty -H 'Content-Type:application/json' -d '{"text":"三角龙或者霸王龙","analyzer": "ik_smart"}'

    # 返回结果中不包含”或者”,因为”或者”在默认的停用词表中。

    采坑总结:

    1. ik分词器插件导入始终无法安装,且报错。

    解决:一定要注意,安装es插件需要指定路径,且增加”file:” 的前缀,不加此前缀,那就等着报错吧

    2. ik分词器添加自定义专有名词以及停用词不生效(浪费了1天的时间来排查)

    解决:

    (1) 一定要注意,8.12.2版本的ik分词器如果想要配置自定义专有名词或停用词,配置的完整目录是/usr/share/elasticsearch/config/analysis-ik,而不是/usr/share/elasticsearch/plugins/analysis-ik,这点需要注意下。

    在config/analysis-ik中配置IKAnalyzer.cfg.xml,修改内容如下:

             IK Analyzer 扩展配置

            

             extra_main.dic

              

             extra_stopword.dic

            

            

            

            

    (2) 一定要注意,extra_main.dicextra_stopword.dic的编码格式是UTF-8,如果编码格式不对的话,分词也不生效。

    4. Es操作相关源码

    4.1 es_client连接

    1. self.es_client = Elasticsearch(config['url'],
    2. basic_auth=(config['user'], config['password']),
    3. ca_certs=config['crt_path'],
    4. http_compress=True,
    5. request_timeout=int(config['request_timeout']) if 'request_timeout' in config else 60,
    6. max_retries=int(config['max_retries']) if 'max_retries' in config else 5,
    7. retry_on_timeout=True)

    4.2 构建表结构

    1. def index(self, kg_id, force=False):
    2. """
    3. 构建表
    4. """
    5. if force:
    6. try:
    7. self.es_client.indices.delete(index=kg_id, ignore_unavailable=True)
    8. except EngineError as e:
    9. logger.exception(f"code:{ES_DELETE_INDEX_ERROR}, message:{str(e)}")
    10. raise e
    11. if not self.es_client.indices.exists(index=kg_id):
    12. body = {
    13. 'settings': {'index': {'number_of_shards': 2}},
    14. 'mappings': {
    15. 'dynamic': False,
    16. 'properties': {
    17. 'name': {'type': 'keyword'},
    18. 'concepts': {'type': 'keyword'},
    19. 'property': {'type': 'keyword'},
    20. 'value': {'type': 'text', 'analyzer': 'ik_max_word', 'search_analyzer': 'ik_smart'},
    21. 'numbers': {'type': 'double_range'},
    22. 'embeddings': {'type': 'elastiknn_dense_float_vector', 'elastiknn': {'dims': 768, 'model': 'lsh', 'similarity': 'cosine', 'L': 99, 'k': 3}}
    23. }
    24. }
    25. }
    26. try:
    27. self.es_client.indices.create(index=kg_id, body=body)
    28. except EngineError as e:
    29. logger.exception(f"code:1008, message:{str(e)}")
    30. raise e
    31. try:
    32. self.es_client.indices.refresh(index=kg_id, ignore_unavailable=True)
    33. except EngineError as e:
    34. logger.exception(f"code:1008, message:{str(e)}")
    35. raise e

    说明:

    1. value字段需要经过IK分词,分词方式ik_max_word,查询方式是ik_smart

    2. embeddings的类型为elastiknn_dense_float_vector,其中向量维度为768,相似度计算使用cosine

    4.3 候选实体查询

    1. def get_candidate_entities(self, kg_id, query, limit=15):
    2. """
    3. 基于查询串查找候选实体名称
    4. """
    5. body = {
    6. '_source': {'excludes': ["embeddings"]},
    7. 'query': {
    8. 'function_score': {
    9. 'query': {
    10. 'bool': {
    11. 'must': [
    12. {'match': {'value': query}},
    13. {'bool': {
    14. 'filter': {
    15. 'bool': {
    16. 'should': [
    17. {'term': {"property": "名称"}},
    18. {'term': {"property": "别名"}},
    19. ]
    20. }
    21. }
    22. }}
    23. ]
    24. }
    25. },
    26. 'functions': [
    27. {
    28. 'elastiknn_nearest_neighbors': {
    29. 'field': 'embeddings',
    30. 'vec': self.get_callback_ans({'query': [query]})['result'][0]['embeddings'],
    31. 'model': 'lsh',
    32. 'similarity': 'cosine',
    33. 'candidates': 100
    34. }
    35. }
    36. ]
    37. }
    38. },
    39. 'size': limit
    40. }
    41. return self.es_client.search(index=kg_id, body=body)['hits']['hits']

    说明:

    1. '_source': {'excludes': ["embeddings"]}表示输出结果中过滤embeddings字段

    2. 查询以function_score方式,其中的query表示别名或名称与问题的匹配程度,functions表示打分方式,目前的打分是基于向量相似度进行打分,其中, self.get_callback_ans表示语义相似度模型将文本转换为向量。注意:最终的得分由两部分组成,一部分是文本匹配,一部分是语义相似度匹配,不过可以增加参数boost_mode进行设置。

    4.4 基于属性及属性值进行查询

    1. def search_by_property_value(self, kg_id, property, value, limit=100):
    2. body = {
    3. '_source': {'excludes': ["embeddings"]},
    4. 'query': {
    5. 'function_score': {
    6. 'query': {
    7. 'bool': {
    8. 'must': [
    9. {'match': {'value': value}},
    10. {'term': {"property": property}}
    11. ]
    12. }
    13. },
    14. 'functions': [
    15. {
    16. 'elastiknn_nearest_neighbors': {
    17. 'field': 'embeddings',
    18. 'vec': self.get_callback_ans({'query': [value]})['result'][0]['embeddings'],
    19. 'model': 'lsh',
    20. 'similarity': 'cosine',
    21. 'candidates': 100
    22. }
    23. }
    24. ],
    25. 'boost_mode': 'replace'
    26. }
    27. },
    28. 'size': limit
    29. }
    30. try:
    31. return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    32. except EngineError as e:
    33. logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
    34. raise e

    4.5 数值属性范围查询

    主要解决的场景有:体重大于9吨的恐龙有哪些?身长小于10米的角龙类有哪些?

    其中,如果提供了实体名称,则查询范围是基于这些实体进行查询比较。

    1. def search_by_number_property(self, kg_id, property, operate, entities, limit=100):
    2. musts = [{'term': {'property': property}}, {'range': {'numbers': operate}}]
    3. if entities:
    4. musts.append({'terms': {'name': entities}})
    5. body = {
    6. '_source': {'excludes': ['embeddings']},
    7. 'query': {
    8. 'bool': {
    9. 'must': musts
    10. }
    11. },
    12. 'size': limit
    13. }
    14. try:
    15. return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    16. except EngineError as e:
    17. logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
    18. raise e

    4.6 数值属性最大最小查询

    实现最大最小的逻辑,采用了sort机制,按照numbers进行排序,最大则顺排,最小则倒排。

    1. def search_by_number_property_maxmin(self, kg_id, property, entities, sort_flag):
    2. musts = [{'term': {'property': property}}]
    3. if entities:
    4. musts.append({'terms': {'name': entities}})
    5. body = {
    6. '_source': {'excludes': ["embeddings"]},
    7. 'query': {
    8. 'bool': {
    9. 'must': musts
    10. }
    11. },
    12. 'sort': {'numbers': sort_flag},
    13. 'size': 1
    14. }
    15. try:
    16. return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    17. except EngineError as e:
    18. logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
    19. raise e

    5. 效果

    上一版未解决的问题,在本版本优化的结果,主要是基于属性值查找实体。

    1. 问:头像鸭头的龙有哪些?

    答:头像鸭头的有慈母龙、原角龙、鹦鹉嘴龙、姜氏巴克龙、奇异辽宁龙、多背棘沱江龙、陆家屯鹦鹉嘴龙、盖斯顿龙、小盾龙、肿头龙、弯龙

    2. 问:老师说的有一个特别会照顾宝宝的恐龙是什么龙?

    答:慈母龙会照顾宝宝。

    3. 问:有哪些恐龙会游泳啊?

    答:滑齿龙、慢龙和色雷斯龙是会游泳的恐龙。

    4. 问:科学家在意大利阿尔卑斯山脉Preone山谷的乌迪内附近发现了一个会飞的史前动物化石,它是谁的化石?

    答:科学家在意大利阿尔卑斯山脉Preone山谷的乌迪内附近发现的会飞的史前动物化石是沛温翼龙的化石。

    6. 总结

    一句话足矣~

    本文主要是针对KBQA方案基于LLM实现存在的问题进行优化,主要涉及到图谱存储至Es,且支持Es的向量检索,还有解决了一部分基于属性值倒查实体的场景,且效果相对提升。

    其次,提供了部分Es的操作源码,以飧读者。

    附件:

    1. es vs milvus: Milvus vs Elastic | Zilliz

    2. docker安装es:Install Elasticsearch with Docker | Elasticsearch Guide [8.12] | Elastic

    3. elastiknn性能分析

    ES 8.x 向量检索性能测试 & 把向量检索性能提升100倍!_elastiknn-CSDN博客

    4. es的function_score: Function score query | Elasticsearch Guide [8.12] | Elastic

  • 相关阅读:
    配置WampServer在局域网内通过内网IP访问(解决403 Forbidden问题)
    Taurus.MVC 微服务框架 入门开发教程:项目部署:7、微服务节点的监控与告警。
    Linux中如何禁止普通用户使用su命令
    GIT 基础命令使用
    【计算机网络系列】物理层
    【毕业设计】前后端分离——解决cookies跨域
    Node.js之ES6模块化及Promise对象
    【多线程】常见的锁策略
    java之List集合入门必备篇
    深度学习中对抗生成网络GAN背后的数学原理
  • 原文地址:https://blog.csdn.net/MENGERN/article/details/138199323