导航菜单
首页 » 问答 » 正文

问答系统实践(三)微信聊天机器人构建:从产品和工程的角度聊小天2.0

简介

小天机器人是我在研二的时候就很想做的一款“产品”,之所以在产品二字上加上双引号,是因为这不是一个真正意义上的产品。做小天聊天机器人的初衷是在读研的时候研究方向是 ,那个时候(现在也是)觉得拥有一个属于自己亲手打造的是一件非常酷的事情~ 因此就有了专栏里的《问答系统实践(一):中文检索式问答机器人初探》和《问答系统实践(二)构建聊天机器人小天1.0》这2篇文章。这也是小天机器人第一次从我的脑海里走向文章,然后再从文章走向控制台,最后从控制台走向微信公众号。

产品-小天的定位

目前,问答系统的功能分类主要有:FAQ检索式问答(FAQ),闲聊问答(Chat-QA),任务型问答(Task-QA),知识图谱问答(KG-QA),阅读理解问答(MRC-QA)等。从宏观角度来看,即使问答系统具备多种功能的能力,并且每个功能彼此差异都较大;但是一个共同目标就是更好的为提问者找到正确的答案或者执行精准的指令。在生活中,FAQ检索式问答是当中较为简单也是被频繁使用的一个功能;闲聊问答经常会被当做兜底话术使得机器人显得并没有那么枯燥;任务型问答具有强烈的任务导向性,比如订机票,订会议室等;而知识图谱问答具体也要根据开发域和垂直域进行区分,开放域的KBQA是一个升级版的闲聊QA,如果说闲聊QA让人觉得机器人是“有趣儿”的,那么开放域的KBQA则会让人觉得机器人是“博学有智慧”的,当然,在我的观点里,开发域KG-QA < 搜索引擎,而 语音+ 开发域KG-QA > 搜索引擎;垂直域的KBQA更加强调知识的精准性,是一种更加灵活的 FAQ,对于偏向事实性质的问答有着较大的优势;对于阅读理解问答,我觉得它的潜力应该是自动根据模板问题或者自己编辑的问题从一系列的文档中抽出答案,然后经过人工整理合成问答对从而作为FAQ知识库的补充。

那么,作为一个生活在个人微信公众号的聊天助手,他的需求是什么呢?在做小天2.0之前,我曾经很认真的思考过这个问题。我只能说难,真的很难。首先聊天助手的产生是为了帮助我们方便解决一些问题。然而微信公众号的运营方式和运营内容决定着聊天助手发挥的作用大小。公众号是流量的入口,类比于淘宝京东携程等一些线上平台的商家店铺,聊天助手可以为涌入的流量解决吃喝玩乐、衣食住行等方面的问题,这些问题大部分是偏内容侧的,对微信公众号也同样适用。抛开线上电商相关属性的微信公众号不谈,我在思考,作为一个输出内容为文章而不是商品的微信公众号,里面内置的助手的作用是什么呢?最常见的应该就是类似一个搜索引擎去做检索。好家伙,对于我这种低产的作者,岂不是连个屁都崩不响。那完了,芭比Q了,老板经常会说,不行,你得做出来,要先落地。那我就当自己的老板,我要让我的小天2.0落地。

产品-小天的功能导向

目前小天聊天助手内置闲聊功能,faq问答功能,任务型对话功能。其中闲聊功能与faq问答功能的底层均是检索式问答框架,不同点在于语料不同。

闲聊部分

首先看小天的闲聊部分吧。对于一些日常的口水式闲聊基本上能够满足,不会显得那么生硬。

FAQ问答部分

首先我规划的垂直域在金融,医疗,法律,电信政策等几个方面。最终我还是选择了法律领域的语料库,最大的原因是因为法律相比于其他几个领域更加实用,而且也更贴近生活。有时候接地气的才会有更多的人用,换句话说,受益面广的功能往往更有市场。农村包围城市,伟大领袖早已实践过了,这是一个慢慢收窄的过程,但是前提是你得让大家接受你的内容设定。在我的设定中,聊天助手是用来“发泄”以及承接情绪的,而法律咨询或多或少会为你的情绪找一个出口,倘若以后大家在调戏小天助手的同时能够想到小天助手还有法律案例咨询的这一块小功能,那我觉得我的设定还算不赖。(下面的例子,我真的不是在,不是在,不是在散播焦虑啊~~~)

任务型对话部分

目前小天内部预置了【查天气】、【识别文本中的地名】、【解析身份证号码】、【解析电话号码归属地】、【靓仔指南】等任务,选择这五个任务也是优先考虑他的实用性,这些功能足以让小天助手变的独特,他不仅是个聊天机器人,他更是一个助手,能够快速帮助你节省上浏览器去百度或者的时间,同时,他,嗯~,可能一定程度上会带你成为一个合格的靓仔!哈哈哈

【查天气】

【识别文本中的地名】

后面的【解析身份证号码】、【解析电话号码归属地】就不示范了,可以输入识别身份证,识别电话号码等意图(不局限于此哦)进行触发,结果显示如下:

【靓仔指南】

靓仔指南汇聚了早期野生人类在恋爱过程中摔过的一次次跤,碰过的一次次壁,它宛如《葵花宝典》,稍有不慎,可能会”引火自焚”;它又如那一杯劲酒儿,柔烈并具。靓仔指南虽好,可不要贪杯哦~

这些任务之间是可以互相穿插的,比如【识别文本中的地名】中突然来了一个查天气任务,任务间的转换涉及到了对话状态的转换,这方面一直不好做,但是还是强行给上车了。

其实也很想把知识图谱问答加入到小天中去,但是对于一个双核4g的云主机,我真的担心会崩掉。暂时搁置一下,等到明年换个好点的配置。恰好也可以好好思考知识图谱问答给小天带来的闪光点,因为我从生活和工作中也渐渐地领悟了奥卡姆剃刀原则,如无必要,勿增实体。

工程-小天的架构

完成一个聊天机器人需要前端,后端,算法三者的协同配合。我觉得微信的聊天页面是一个很好的前端展示,也省的我再去操心这一方面的事情。app端采用微信提供的一些接口进行集中处理,主要做接收消息(请求)+发送消息(请求)两大功能块,具体可参考 微信公众平台开发文档接入指南 ,处理请求的方式参考微信 官方Demo,这里我也提供小天的核心后端代码方便大家快速搭建自己的微信机器人。

架构-APP客户端

核心功能块一:.py

涉及到了被关注后的event事件以及文本处理

# -*- coding:utf-8 -*-
import xml.etree.ElementTree as ET
def parse_xml(web_data):
    if len(web_data) == 0:
        return None
    xmlData = ET.fromstring(web_data)
    msg_type = xmlData.find('MsgType').text
    if msg_type == 'text':
        return TextMsg(xmlData)
    elif msg_type == 'image':
        return ImageMsg(xmlData)
    elif msg_type == "event":
        return EventMsg(xmlData)
class Msg(object):
    def __init__(self, xmlData):
        self.ToUserName = xmlData.find('ToUserName').text
        self.FromUserName = xmlData.find('FromUserName').text
        self.CreateTime = xmlData.find('CreateTime').text
        self.MsgType = xmlData.find('MsgType').text
        self.MsgId = xmlData.find('MsgId').text
        self.Content = xmlData.find('Content').text
class Event(object):
    def __init__(self, xmlData):
        self.ToUserName = xmlData.find('ToUserName').text
        self.FromUserName = xmlData.find('FromUserName').text
        self.MsgType = xmlData.find('MsgType').text
        self.Event = xmlData.find('Event').text
class TextMsg(Msg):
    def __init__(self, xmlData):
        Msg.__init__(self, xmlData)
        self.Content = xmlData.find('Content').text.encode("utf-8")
class ImageMsg(Msg):
    def __init__(self, xmlData):
        Msg.__init__(self, xmlData)
        self.PicUrl = xmlData.find('PicUrl').text
        self.MediaId = xmlData.find('MediaId').text
class EventMsg(Event):
    def __init__(self, xmlData):
        Event.__init__(self, xmlData)
        self.Event = xmlData.find('Event').text  # 取Event这个参数里的内容

核心功能块二:reply.py

# -*- coding:utf-8 -*-
# -*- coding: utf-8 -*-
# filename: reply.py
import time
class Msg(object):
    def __init__(self):
        pass
    def send(self):
        return "success"
class TextMsg(Msg):
    def __init__(self, toUserName, fromUserName, content):
        self.__dict = dict()
        self.__dict['ToUserName'] = toUserName
        self.__dict['FromUserName'] = fromUserName
        self.__dict['CreateTime'] = int(time.time())
        self.__dict['Content'] = content
    def send(self):
        XmlForm = """
        
        {ToUserName}]]>
        {FromUserName}]]>
        {CreateTime}
        
        {Content}]]>
        
        """
        return XmlForm.format(**self.__dict)
class ImageMsg(Msg):
    def __init__(self, toUserName, fromUserName, mediaId):
        self.__dict = dict()
        self.__dict['ToUserName'] = toUserName
        self.__dict['FromUserName'] = fromUserName
        self.__dict['CreateTime'] = int(time.time())
        self.__dict['MediaId'] = mediaId
    def send(self):
        XmlForm = """
        
        {ToUserName}]]>
        {FromUserName}]]>
        {CreateTime}
        
        
        {MediaId}]]>
        
        
        """
        return XmlForm.format(**self.__dict)

核心功能块三:.py

这里的 = () 每个人都不同,主要功能就是嵌入算法服务在其中,提供一个的函数接口

def get_result(self, question):
     # 解析主流程
     return result             

.py

# -*- coding:utf-8 -*-
import web
import hashlib
from weixin import receive, reply
from service_helper import Server
server_app = Server()
class Handle(object):
    def __init__(self):
        # 初始化query
        #  self.query = Query()
        pass
    def GET(self):
        try:
            data = web.input()
            if len(data) == 0:
                return "hello, this is handle view"
            signature = data.signature
            timestamp = data.timestamp
            nonce = data.nonce
            echostr = data.echostr
            # 和公众平台官网-->基本配置中信息填写相同
            token = "chatbot"
            list = [token, timestamp, nonce]
            list.sort()
            sha1 = hashlib.sha1()
            for item in list:
                sha1.update(item.encode('utf-8'))
            # map(sha1.update, list)
            hashcode = sha1.hexdigest()
            print("handle/GET func: hashcode, signature: ", hashcode, signature)
            if hashcode == signature:
                return echostr
            else:
                return "I don't Know"
        except Exception as err:
            print('ERROR: ' + str(err))
            return err
    def POST(self):
        try:
            webData = web.data()
            # 后台打印日志
            print('Handle Post webdata is ', webData)
            recMsg = receive.parse_xml(webData)
            if isinstance(recMsg, receive.Event):
                toUser = recMsg.FromUserName
                fromUser = recMsg.ToUserName
                if recMsg.MsgType == 'event':
                    event = recMsg.Event
                    if event == 'subscribe':  # 判断如果是关注则进行回复
                        content = "嗨~ 我是小天,感觉你今天有点奇怪,怪让人心动的 \n" \
                                  "\n" \
                                  "我可能不太会聊天,但是我可能会撩到你,看招! \n" \
                                  "\n" \
                                  "红糖王糖白砂糖,爱雷爱到饮森堂,咪超中意你咯~ \n" \
                                  "\n" \
                                  "PS:聊天厌倦了,输入【任务查看】即可查看小天更多的技能~"
                        replyMsg = reply.TextMsg(toUser, fromUser, content)
                        return replyMsg.send()
            elif isinstance(recMsg, receive.Msg):
                toUser = recMsg.FromUserName
                fromUser = recMsg.ToUserName
                if recMsg.MsgType == 'text':
                    # result = "彩虹屁屁"
                    question = recMsg.Content
                    result = server_app.get_result(question)
                    replyMsg = reply.TextMsg(toUser, fromUser, result)
                    return replyMsg.send()
                if recMsg.MsgType == 'image':
                    mediaId = recMsg.MsgId
                    replyMsg = reply.ImageMsg(toUser, fromUser, mediaId)
                    return replyMsg.send()
                else:
                    return reply.Msg().send()
            else:
                print('暂且不处理')
            return reply.Msg().send()
        except Exception as err:
            print('ERROR: ' + str(err))
            return err
urls = (
    '/chatbot', 'Handle'
)
if __name__ == '__main__':
    qa_web = web.application(urls, globals())
    qa_web.run()

架构-算法服务端

算法服务就是一个基本的NLU + DM + Route 的过程,用微信客户端的一个弊端就是状态不是很好传,因此我将有关单元放在了算法route类中的全局变量,实现起来会有些愚蠢,但是,凑合着还能用吧。

文本匹配部分基本的算法模型就是 -bert做匹配,可以参考我之前写的这篇文章《我叫大王来巡山:训练预测不一致也能有奇效?看-bert的cos相似度推理》那里有一些实验推理。

意图识别部分目前就是用基本的规则解析类去hit关键词,上模型感觉没啥必要,反而会增加延时。

模型部署的话就是比较常见的做法 部署 ,这样也方便后续项目的迁移等。

总结

写完这篇文章心情还是很愉快的,这是TODO List 中的一项。2021年完成了TODO LIST中一些比较贴近实际的计划,也让自己执行力的变得越来越好。最近几个月,压力剧增,迎来了很多挑战,感觉不是一般的累。但是周末的话还是坚持劳逸结合,让自己有更多实践自己想法的时间,事实证明,运动还是有用的,可以让你的精神更集中!

马上要过年了,祝大家新年哈皮~

胖友,请不要忘了一键三连点赞哦!

评论(0)

二维码