文章目录
前言
前一篇文章 基于聊天记录的问答——数据分块篇 把整个项目的基本情况介绍过了,这篇我们主要聚焦于问答部分,没阅读过前文的同学建议先看一下,观感会好一些。
前面有提到,问答部分包含了三块:
- Text2SQL
- GraphRAG
- NaiveRAG
实际上,工程上的实现只有两块:
- GraphRAG
- Text2SQL
因为NaiveRAG被包含在GraphRAG中去做了。
接下来,我们就围绕这两块展开叙述
首先,回顾一下数据样例
记住这个数据样例,便于后续理解
分析
首先,为什么需要GraphRAG、Text2SQL这两块结合一起来做呢?
毋庸置疑,肯定是他们有各自擅长的地方,这就要结合我们的业务场景来分析了。
GraphRAG
GraphRAG 擅于解决全局性的问题,例如:
- 张三是个怎样的人
- 有哪些人聊到了旅游的话题
但是GraphRAG 无法解决精确的查询,例如:
- 张三和李四在2025年7月7日聊了什么
Text2SQL
GraphRAG无法解决的精确查询类的问答,用Text2SQL就很好解决了。
从数据样例中可以发现,每一个最终的根节点,都是一个Table,以上数据样例只展示了微信中的好友消息,实际上其它APP的根节点也是这样的格式,都是数据表,而我们最终的业务,也不止是聊天记录的问答。
所以,Text2SQL在这里,就能够很好地解决除聊天记录以外的数据的问答了
甚至如果不需要关注回答的全局性,Text2SQL可以把活全干了。
GraphRAG如何做
整个项目基于LightRAG重写,抽取实体、关系。格式如下
{{
"entities": [
{{
"entity_name": "实体名称",
"entity_type": "实体类型",
"entity_description": "结合上下文的客观描述内容"
}},
...
],
"relationships": [
{{
"source_entity": "源实体名称",
"target_entity": "目标实体名称",
"relationship_description": "结合上下文整合关联的总结性描述",
"relationship_strength": "关系紧密程度,数字类型,0-10",
"relationship_keywords": ["关键词1", "关键词2", "关键词3"]
}},
...
]
}}
整个结构、流程与LIghtRAG相差不大,我们主要针对我们的业务做了适配。
对这块感兴趣的同学,建议直接去LightRAG找个demo数据debug一下。
个人认为,这里面最值得分享的是数据处理上的技巧,如下:
首先,我们发现,数据拼接格式无非以下几种选择:
发送者 + “: ” + 消息内容
发送者ID + “: ” + 消息内容
发送者(发送者ID) + “: ” + 消息内容
这里面无论哪一种,实际上都差不多
如果这样拼接,会面临以下几个问题:
发送者昵称多种多样,十分奇怪,还可能有各种特殊字符,如 “明天的太阳”, “⑩🌂少” 之类的奇怪昵称,又或者是看起来像是一句完整的话的昵称,这就容易导致:
- LLM抽取效果不好:这种类型的昵称 “明天的太阳” 很容易被抽取出多个实体,实际上他们就是一个人物实体。
- LLM变成了复读机:包含各种重复字符的昵称,如“⑩🌂少”,很容易导致LLM不断重复输出
发送者ID太长,非常占用Token (很多模型都是一个数字一个token),导致速度变慢,且容易复读(多处出现同一串相同的数字或字符容易导致LLM重复输出)
为了解决这几个问题,我们可以将发送者与接收者进行昵称映射,如下:
{
"明天的太阳": "张三",
"⑩🌂少": "李四"
}
拼接的时候,用张三、李四代替当前发送者,最后再将LLM的输出替换回来就好了。
我们实验发现,用这种办法能够非常有效地降低LLM重复输出的概率(1000块内容,从原有的几十处复读,降为只有三处复读)。
以上,是私聊的处理方式,群聊也有相应的解决办法:
{
"明天的太阳": "用户00",
"⑩🌂少": "用户01",
"葬爱家族你张哥": "用户02"
...
}
毫不夸张地说,这对于后续的摘要等其它处理有非常大的帮助。并且,这样做之后,还顺便解决了敏感数据的问题,让调用外部LLM也成为了可能。
实体关系抽取完成后,还有非常关键的一步:合并实体、关系
个人觉得,这是GraphRAG能够关注全局性的至关重要的一点,正是这一步,将散落在知识库中各处的实体进行合并,生成了一个全局性的描述,才让GraphRAG回答问题时,能够有全局性。
并且,这里面你甚至可以加入时间的概念,根据抽取的时间先后顺序去合并实体,生成最终的实体描述。
Text2SQL如何做
不了解Text2SQL的同学可以看下我之前写的文章:Text2SQL之不装了,我也是RAG
Text2SQL中比较重要的一环是如何选择到正确的表。
很显然,我们这里的业务场景,也会面临这个问题。但是我们这里几乎不涉及复杂查询。
通过数据样例可以发现,我们这里的数据结构大致如下:
|-王五微信
|- 账号1
|- 好友消息
|- 张三
|- 聊天记录表
|- 李四
|- 聊天记录表
|- 群聊消息
|- 群聊1
|- 聊天记录表
|- 群聊2
|- 聊天记录表
|- 王五QQ
|- 账号1
|- 好友消息
|- 张三
|- 聊天记录表
|- 李四
|- 聊天记录表
|- 群聊消息
|- 群聊1
|- 聊天记录表
|- 群聊2
|- 聊天记录表
首先,我们将具体的表和路径进行映射:
{
"王五微信-账号1-好友消息-张三": "table name"
"王五微信-账号1-好友消息-李四": "table name"
}
然后将路径进行向量化,和用户Query进行匹配,再让LLM进行挑选(可以理解为用LLM进行重排),挑选出最有可能用到的表,最后再根据表信息生成查询,根据查询结果生成回答(这里会根据多个可能的表生成回答,最后再汇总答案,生成最终答案)
其中,这里也有一些技巧去提速,比如,在LLM挑选表时,不要直接输出表名,而是输出表的序号,再去拿表。
这种技巧可以用在多个地方,其核心就是想办法让LLM输出变短,输出越短,耗时越少。
最终结构
为了保证效果,里面还加入了Text2CQL(Cypher Query Language)
原理和Text2SQL基本一致,这里就不展开叙述了
总结
这么多组合拳下来,要回答一个用户的问题还是蛮耗时的,差不多要一分钟以上,好在,我们这个业务场景,用户可以接受回答慢,但无法接受回答不准。
如果各位同学有更好的想法,欢迎大家在评论区留言讨论