基于聊天记录的问答——问答篇

发布于:2025-07-08 ⋅ 阅读:(19) ⋅ 点赞:(0)


前言

前一篇文章 基于聊天记录的问答——数据分块篇 把整个项目的基本情况介绍过了,这篇我们主要聚焦于问答部分,没阅读过前文的同学建议先看一下,观感会好一些。

前面有提到,问答部分包含了三块:

  • 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基本一致,这里就不展开叙述了

总结

这么多组合拳下来,要回答一个用户的问题还是蛮耗时的,差不多要一分钟以上,好在,我们这个业务场景,用户可以接受回答慢,但无法接受回答不准。

如果各位同学有更好的想法,欢迎大家在评论区留言讨论


网站公告

今日签到

点亮在社区的每一天
去签到