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

1. 背景

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

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

  1. 数据落库

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

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

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

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

2. 整体框架

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

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

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

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:" 的前缀,不加此前缀,那就等着报错吧

  1. 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,修改内容如下:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">extra_main.dic</entry>
     <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">extra_stopword.dic</entry>
    <!--用户可以在这里配置远程扩展字典 -->
    <!-- <entry key="remote_ext_dict">words_location</entry> -->
    <!--用户可以在这里配置远程扩展停止词字典-->
    <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

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

4. Es操作相关源码

4.1 es_client连接

复制代码
self.es_client = Elasticsearch(config['url'], 
                               basic_auth=(config['user'], config['password']), 
                               ca_certs=config['crt_path'],
                               http_compress=True,
                               request_timeout=int(config['request_timeout']) if 'request_timeout' in config else 60,
                               max_retries=int(config['max_retries']) if 'max_retries' in config else 5,
                               retry_on_timeout=True)

4.2 构建表结构

复制代码
def index(self, kg_id, force=False):
    """
    构建表
    """
    if force:
        try:
            self.es_client.indices.delete(index=kg_id, ignore_unavailable=True)
        except EngineError as e:
            logger.exception(f"code:{ES_DELETE_INDEX_ERROR}, message:{str(e)}")
            raise e

    if not self.es_client.indices.exists(index=kg_id):
        body = {
            'settings': {'index': {'number_of_shards': 2}},
            'mappings': {
                'dynamic': False,
                'properties': {
                    'name': {'type': 'keyword'},
                    'concepts': {'type': 'keyword'},
                    'property': {'type': 'keyword'},
                    'value': {'type': 'text', 'analyzer': 'ik_max_word', 'search_analyzer': 'ik_smart'},
                    'numbers': {'type': 'double_range'},
                    'embeddings': {'type': 'elastiknn_dense_float_vector', 'elastiknn': {'dims': 768, 'model': 'lsh', 'similarity': 'cosine', 'L': 99, 'k': 3}}
                }
            }
        }
        try:
            self.es_client.indices.create(index=kg_id, body=body)
        except EngineError as e:
            logger.exception(f"code:1008, message:{str(e)}")
            raise e
    try:   
        self.es_client.indices.refresh(index=kg_id, ignore_unavailable=True)
    except EngineError as e:
        logger.exception(f"code:1008, message:{str(e)}")
        raise e

说明:

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

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

4.3 候选实体查询

复制代码
def get_candidate_entities(self, kg_id, query, limit=15):
    """
    基于查询串查找候选实体名称
    """
    body = {
        '_source': {'excludes': ["embeddings"]},
        'query': {
            'function_score': {
                'query': {
                    'bool': {
                        'must': [
                            {'match': {'value': query}},
                            {'bool': {
                                'filter': {
                                    'bool': {
                                        'should': [
                                            {'term': {"property": "名称"}},
                                            {'term': {"property": "别名"}},
                                        ]
                                    }
                                }
                            }}
                        ]
                    }
                },
                'functions': [
                    {
                       'elastiknn_nearest_neighbors': {
                           'field': 'embeddings',
                           'vec': self.get_callback_ans({'query': [query]})['result'][0]['embeddings'],
                           'model': 'lsh',
                           'similarity': 'cosine',
                           'candidates': 100
                       } 
                    }
                ]
            }
        },
        'size': limit
    }
    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 基于属性及属性值进行查询

复制代码
def search_by_property_value(self, kg_id, property, value, limit=100):
    body = {
        '_source': {'excludes': ["embeddings"]},
        'query': {
            'function_score': {
                'query': {
                    'bool': {
                        'must': [
                            {'match': {'value': value}},
                            {'term': {"property": property}}
                        ]
                    }
                },
                'functions': [
                    {
                       'elastiknn_nearest_neighbors': {
                           'field': 'embeddings',
                           'vec': self.get_callback_ans({'query': [value]})['result'][0]['embeddings'],
                           'model': 'lsh',
                           'similarity': 'cosine',
                           'candidates': 100
                       } 
                    }
                ],
                'boost_mode': 'replace'
            }
        },
        'size': limit
    }
    try:
        return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    except EngineError as e:
        logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
        raise e

4.5 数值属性范围查询

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

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

复制代码
def search_by_number_property(self, kg_id, property, operate, entities, limit=100):
    musts = [{'term': {'property': property}}, {'range': {'numbers': operate}}]
    if entities:
        musts.append({'terms': {'name': entities}})

    body = {
        '_source': {'excludes': ['embeddings']},
        'query': {
            'bool': {
                'must': musts
            }
        },
        'size': limit
    }
    try:
        return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    except EngineError as e:
        logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
        raise e

4.6 数值属性最大最小查询

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

复制代码
def search_by_number_property_maxmin(self, kg_id, property, entities, sort_flag):
    musts = [{'term': {'property': property}}]
    if entities:
        musts.append({'terms': {'name': entities}})

    body = {
        '_source': {'excludes': ["embeddings"]},
        'query': {
            'bool': {
                'must': musts
            }
        },
        'sort': {'numbers': sort_flag},
        'size': 1
    }
    try:
        return self.es_client.search(index=kg_id, body=body)['hits']['hits']
    except EngineError as e:
        logger.exception(f"code:{ES_SEARCH_ERROR}, message:{str(e)}")
        raise e 

5. 效果

上一版未解决的问题,在本版本优化的结果。

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

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

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

答:慈母龙会照顾宝宝。

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

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

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

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

6. 总结

一句话足矣~

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

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

附件:

  1. es vs milvus: https://zilliz.com/comparison/milvus-vs-elastic

  2. docker安装es:https://www.elastic.co/guide/en/elasticsearch/reference/8.12/docker.html

  3. elastiknn性能分析:https://blog.csdn.net/star1210644725/article/details/134021552

  4. es的function_score: https://www.elastic.co/guide/en/elasticsearch/reference/8.12/query-dsl-function-score-query.html

相关推荐
viperrrrrrrrrr79 小时前
GPT系列模型-详解
人工智能·gpt·llm
美人鱼战士爱学习9 小时前
2025 AAAI HLMEA: Unsupervised Entity Alignment Based on Hybrid Language Models
chatgpt·知识图谱
大熊猫侯佩9 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(下)
llm·ai编程·mcp
大熊猫侯佩9 小时前
大内密探零零发之 iOS 密探神器 AI 大模型 MCP 服务开发记(上)
llm·ai编程·mcp
302AI10 小时前
体验升级而非颠覆,API成本直降75%:DeepSeek-V3.2-Exp评测
人工智能·llm·deepseek
聚客AI12 小时前
🥺单智能体总是翻车?可能是你缺了这份LangGraph多Agent架构指南
人工智能·llm·agent
爱可生开源社区12 小时前
2025 年 9 月《大模型 SQL 能力排行榜》发布,新增 Kimi K2 最新版测评!
sql·llm
大模型教程12 小时前
半小时部署企业智能问答系统!MaxKB让知识管理效率翻倍
程序员·llm·agent
AI大模型12 小时前
告别数据隐私焦虑!我用FastGPT免费私有化部署了AI个人知识管理系统辅助写作
程序员·llm·agent
大模型教程13 小时前
基于Dify的RAG知识库搭建
程序员·llm·agent