NLP(21)--任务型对话机器人

前言

仅记录学习过程,有问题欢迎讨论

问答系统

  • 闲聊、任务型(帮我设闹钟)、回答型(Q&A)

任务型对话机器人:(帮我定火车票/多轮次)

  • 领域识别(分类、匹配)+意图识别(分类、匹配)+槽填充(序列标注)
  • 对话状态跟踪(DST–Dialogue state):
    对于用户目标的达成状态跟踪
  • 对话策略(DPO-- Dialogue policy):
    反问获取信息,向用户确认,回答等
    • 基于模版:根据模版进行槽位填充(死板。但是快,可以拓展)
    • 基于神经网络:将语义槽信息和对话策略做成1-hot向量融入模型
  • 按照策略进行回复

常见的评估方式:

  • 模型层面:意图识别率,槽位填充率
  • 应用层面:节点到达率,任务达成率,场景覆盖度
  • 功能层面:新场景迁移效率,跨场景交互能力,人工复杂度

问答型:

1、基于Faq库 --文本匹配问题相似度

优势:

  • 实施简单:构建FAQ问答库相对容易,只需收集常见问题及对应答案即可,不需要复杂的知识建模。
  • 维护成本较低:对于新增或更新的问题,直接在库中增删改即可,操作简便。
  • 适用快速响应:对于明确、结构化的问题,能够迅速提供准确答案。

劣势:

  • 泛化能力弱:对于变化形式多样的提问方式可能无法有效应对,需要为相似问题创建多个条目。
  • 理解局限性:缺乏对问题深层次理解和语境识别的能力,难以处理需要推理或理解复杂关系的问题。
  • 扩展性差:随着问题数量增加,维护成本和难度会逐渐上升,且难以自然地扩展到新领域。

适用场景:

  • 客户服务领域,如产品使用帮助、常见问题解答等,问题类型固定且范围明确。
    加粗样式简单信息查询,如营业时间、地址查询等。
2、基于知识图谱 – 文本转sql 去数据库查询数据

优势:

  • 强大的语义理解:通过知识图谱,机器人能更好地理解问题的语义和上下文,支持更自然语言的交互。
  • 推理能力强:可以进行一定程度的逻辑推理和关系链分析,回答需要综合多源信息的问题。
  • 高度可扩展:知识图谱的结构化特性便于添加新知识,支持跨领域的知识整合和应用。
  • 个性化服务:基于用户历史和上下文,提供更加个性化的信息和服务。

劣势:

  • 构建复杂:构建知识图谱需要大量时间和资源,包括数据采集、清洗、实体识别、关系建模等。
    加粗样式维护成本高:知识图谱的持续更新和优化是一大挑战,需要不断监控和调整以保证数据的准确性和时效性。
    加粗样式技术门槛:相比FAQ系统,实现基于知识图谱的问答需要更高级的技术支持,如自然语言处理、图数据库等。

适用场景:

  • 高级客服场景,处理复杂、需要推理的问题,如金融咨询、医疗诊断辅助。
  • 智能助手,如个人助理、教育辅导等,需要根据用户需求灵活提供信息和服务。
    大规模信息整合查询,如企业内部知识管理、百科全书类应用。
3、基于文档(逐渐被LLM打击)

要求文档中有问题和答案
训练的输入为 : 问题++文章
输出为 :文章中答案的位置【start,end】(两次分类)

优势:

  • 信息丰富性:可以直接利用现有文档资源,包括但不限于手册、报告、网页等,不需要额外构建专门的知识库或图谱,信息来源广泛。
  • 动态更新:随文档内容的更新而自动获得最新信息,适合需要实时性信息的场景。
  • 灵活性:对于长尾或非常规问题,如果文档中包含相关信息,机器人仍有可能给出答案,适应性强。

劣势:

  • 准确性问题:由于直接从原始文档中抽取答案,没有经过专门的知识结构化处理,可能抓取到的信息不够精确或者上下文不完全匹配。
  • 响应速度:相对于预编译好的FAQ或知识图谱,基于文档检索可能需要更多时间来查找和分析文档,影响用户体验。
  • 理解深度有限:对于需要深入理解或逻辑推理的问题,单纯依赖文档检索可能难以提供满意答案,缺乏高级语义理解能力。

适用场景:

  • 企业内部知识管理:员工可以通过机器人查询公司政策、操作指南、技术文档等,尤其是在文档体系庞大、更新频繁的环境中。
  • 法律咨询、科研文献查询:用户需要查询具体的法律条款、学术论文内容时,机器人可以辅助检索相关文档段落。
  • 新闻媒体、出版业:用于快速检索和引用过往新闻报道、文章内容,提升内容创作效率。

闲聊型:

  • faq库
  • 生成式:seq2seq模型做文本生成
    检索+生成
  • LLm全黑盒处理

代码

提供一个任务型机器人demo
关键是思路,流程逻辑

"""
实现一个简单的订票小助手

"""
import json
import re

import pandas


class memory:
    def __init__(self):
        # 本轮对话命中的节点
        self.hit_node_id = {}
        # 当前对话可用节点
        self.available_node = {}
        # 节点路径
        self.node_path = ""
        # 模版路径
        self.template_path = ""
        # 还没填的槽位
        self.miss_slot = {}
        # 已经填的槽位
        self.fill_slot = {}
        # 对话策略
        self.policy = ""
        # 客户的提问
        self.query = ""
        # 每轮的回答
        self.answer = ""

    def __str__(self):
        # 打印所有字段信息
        return f"hit_node_id: {self.hit_node_id}, available_node: {self.available_node},\n " \
               f"node_path: {self.node_path}, template_path: {self.template_path}, \n" \
               f"miss_slot: {self.miss_slot}, fill_slot: {self.fill_slot},\n" \
               f" policy: {self.policy}, query: {self.query}, answer: {self.answer}\n"


class ticketAssistant:
    def __init__(self, memory):
        # 加载模版和节点数据
        self.memory = memory
        # 保存所有的node info
        self.node_list = {}  # node_id : node_info
        # 保存所有的slot info
        self.slot_list = {}  # slot : [query,value]
        self.load_data()

    def load_data(self):
        # 加载节点数据
        self.load_node_data()
        # 加载模版数据
        self.load_template_data()
        print(self.node_list)
        print(self.slot_list)

    def load_node_data(self):
        # 加载json数据
        with open(self.memory.node_path, "r", encoding="utf-8") as f:
            for node in json.load(f):
                self.node_list[node["id"]] = node
                # 如果包含node1
                if "node1" in node["id"]:
                    # 初始化默认在第一个节点
                    self.memory.available_node = [node["id"]]

        return

    def load_template_data(self):
        # 加载模版
        self.template = pandas.read_excel(self.memory.template_path)
        for index, row in self.template.iterrows():
            self.slot_list[row["slot"]] = [row["query"], row["values"]]
        return

    def run(self, query):
        self.memory.query = query

        # 意图识别 + 槽位填充
        self.nlu()
        # 对话状态
        self.dst()
        # 对话策略
        self.dpo()
        # 生成对话
        self.nlp()
        return self.memory

    def nlu(self):
        # 意图识别
        self.intention_recognition()
        # 槽位填充
        self.fit_slot()

    # 检查当前状态
    def dst(self):
        # 检查当前槽位状态是否还有没填充的
        # 获取命中节点
        hit_node_id = self.memory.hit_node_id
        # 获取槽位list
        slot_list = self.node_list[hit_node_id].get("slot", [])
        for slot in slot_list:
            if slot not in self.memory.fill_slot:
                self.memory.miss_slot = slot
                return
        # 槽位填充完毕
        self.memory.miss_slot = None
        return

    def dpo(self):
        # 根据槽位状态 选择对话策略
        if self.memory.miss_slot:
            self.memory.policy = "slot_filling"
            # 留在当前节点
            self.memory.available_node = [self.memory.hit_node_id]
        else:
            self.memory.policy = "dialogue_continue"
            # 去下个节点
            # 如果存在 childnode
            if self.node_list[self.memory.hit_node_id].get("childnode"):
                self.memory.available_node = self.node_list[self.memory.hit_node_id]["childnode"]
            else:
                # 没有childnode 对话结束
                self.memory.available_node = []
        return

    # 按照对话策略 生成返回用户的answer
    def nlp(self):
        # 槽位填充 询问获取具体槽位信息
        if self.memory.policy == "slot_filling":
            # 获取槽位信息
            slot = self.memory.miss_slot
            # 获取槽位query
            query, _ = self.slot_list[slot]
            # 生成回答
            self.memory.answer = query
        elif self.memory.policy == "dialogue_continue":
            # 获取命中节点的response
            hit_node_id = self.memory.hit_node_id
            response = self.node_list[hit_node_id]["response"]

            # 替换槽位
            slot_list = self.node_list[hit_node_id].get("slot", [])
            for slot in slot_list:
                response = response.replace(slot, self.memory.fill_slot[slot])
            self.memory.answer = response
            # 如果当前节点为回滚修改节点 需要跳转到对应节点
            if hit_node_id == "ticket-node5":
                # 对于命中节点的childnode,遍历每个slot,看改正的信息位于哪个slot
                # query_slot = self.memory.query.sub(0, self.memory.query.index("改正为"))
                # 截取 xx改正为xxx的后半部分

                query = self.memory.query
                query_slot_value = query[query.index("改为"):]
                for node in self.memory.available_node:
                    node_info = self.node_list[node]
                    slot_list = node_info.get("slot", [])
                    # 这里可以用文本匹配
                    for slot in slot_list:
                        slot = slot.replace("#", "")
                        # 回答如果命中了slot
                        if slot in self.memory.query:
                            # 修改槽位
                            self.memory.hit_node_id = node
                            self.memory.fill_slot[slot] = query_slot_value
                            self.memory.answer = node_info["response"]
                            self.memory.available_node = node_info["childnode"]
                            # 需要手动跳转到nlp
                            self.nlp()
                            return

        return

    def intention_recognition(self):
        hit_node_id = None
        hit_score = 0
        for node_id in self.memory.available_node:
            # 获取 node信息
            node_info = self.node_list[node_id]
            # 获取每个节点的intent 做匹配 返回最相似的node
            intent = node_info["intent"]
            # 计算相似度
            cal_score = self.cal_similarity(intent, self.memory.query)
            # 更新命中节点 更新命中分数
            if cal_score >= hit_score:
                hit_node_id = node_id
                hit_score = cal_score
        self.memory.hit_node_id = hit_node_id
        return

    # 使用jaccard相似度计算
    def cal_similarity(self, str1_list, str2):
        score = []
        # 可能有多个 intent
        for str1 in str1_list:
            score.append(len(set(str1) & set(str2)) / len(set(str1) | set(str2)))
        return max(score)

    # 槽位填充 根据用户的输入信息 填入槽位
    def fit_slot(self):
        # 获取命中节点
        hit_node_id = self.memory.hit_node_id
        # 获取槽位list
        slot_list = self.node_list[hit_node_id].get("slot", [])
        for slot in slot_list:
            # 不能同时命中出发和目的地
            if "出发城市" in self.memory.answer and slot == "#目的地#":
                continue
            if "目的城市" in self.memory.answer and slot == "#出发地#":
                continue
            # 对于每个槽,看用户输入信息是否包含该槽位信息
            # 获取槽的values信息
            _, values = self.slot_list[slot]
            # 搜索问题中是否含有想要的slot信息
            search_result = re.search(values, self.memory.query)
            # 如果搜到了 就保存 后面替换
            if search_result:
                self.memory.fill_slot[slot] = search_result.group()
            # else:
            # 如果没有搜到 记录下没搜到的slot
            # self.memory.miss_slot.append(slot)

        return


if __name__ == '__main__':
    memory = memory()
    memory.node_path = r"scenario/scenario-ticket-order.json"
    memory.template_path = r"scenario/slot_fitting_templet.xlsx"

    assistant = ticketAssistant(memory)
    while True:
        query = input()
        memory = assistant.run(query)
        print(memory)
        print(memory.answer)
        if "出票成功" in memory.answer:
            break

scenario-ticket-order.json

[
{
"id":"ticket-node1",
"intent":["我想要订车票"],
"slot":["#车票类型#", "#出发地#", "#目的地#" , "#日期#"],
"action":[""],
"response":"请您确认信息:订购时间为:#日期# ,从 #出发地# 到 #目的地# 的#车票类型#",
"childnode":["ticket-node2", "ticket-node3", "ticket-node4"]
},
{
"id":"ticket-node2",
"intent":["我要帮别人购买车票","我要帮其他人购买车票"],
"slot":["#名字#", "#身份证#"],
"response":"请您确认信息:乘坐人为#名字#,身份证为#身份证#",
"childnode":["ticket-node3","ticket-node4"]
},

{
"id":"ticket-node3",
"intent":["确认","正确"],
"response":"好的,正在帮你出票,出票成功!!"
},
{
"id":"ticket-node4",
"intent":["有信息错误","有地方不对","错误","不正确"],
"childnode":["ticket-node5"],
"response":"请问什么信息不对呢?请你重新输入该信息,格式如:xx改为xxx"
},
{
"id":"ticket-node5",
"intent":["xx改正为xx"],
"childnode":["ticket-node1","ticket-node2"],
"response":"好的,正在帮您更正中==="
}
]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值