大病无情人有情,此次发生的严重疫情,给大家的生活和工作带来严重影响。有人说,疫情是面镜子,照出大家的社会责任心,当我们宅在家抗击疫情的时候,有无数专家医护人员勇敢逆行,去到抗疫一线。照出企业社会责任心。腾讯设立 15 亿抗疫基金,阿里设立价值10亿元人民币的医疗物资供给专项基金。字节跳动医务救助基金总额增至3.91亿元,张一鸣个人捐款1亿元。疫情面前就能看出这些企业的使命感。
那么作为个人也想能为疫情做点什么,下面我将用自己所学,为大家展示如何搭建一个疫情服务机器人。
系统最终效果
最终实现的结果如下:
系统架构
系统架构如图,用户通过手机App、PC页面、公众号、微信小程序等渠道接入应用,应用将问答请求转发至TBP-NLU模块,TBP 解析用户意图并将具体槽位值传递给后端,后端可以做具体复杂的业务逻辑处理,并将结果返回。
系统处理时序图
系统处理时序图如图
系统实现
关键词
在系统实现开始前先介绍几个相关的专业术语。
- Bot,机器人,可以理解为能够处理特定场景对话的一个语言系统。
- NLU,(Natural-language understanding), 自然语言理解,就是研究如何让计算机理解我们人类的语言,再根据我们输入的语义进行对应的流程处理。
- 意图,打算达到某种目的,即我们输入一句话背后的真实意愿。举个例子,“新冠疫情最新确诊人数多少?”这句话我们可以理解为他的意图就是“查疫情”。
- 槽位,就是一句话中的变量,需要单独做流程处理的。举个例子,“我想问 深圳 最新的疫情情况”,这里的 深圳 我们可以设置为一个变量 cityName,根据不同的查询地址,检索最新的疫情情况。
- 词典,在配置意图的槽位时,需要为各个槽位设置对应的词典。也就是上面变量的取值,这里的词典可以添加 北京、上海、深圳 等,那么计算机的表示就是“cityName = shenzhen”。这里可以配置自己的用户词典,也可以引用内置的词典。
申请使用
访问 腾讯云官网:【产品】>【人工智能】>【AI 平台服务】>【腾讯智能对话平台】,或直接访问 腾讯智能对话平台 页面,单击【立即申请】https://cloud.tencent.com/product/tbp,登录腾讯云账号并填写申请表。将会在 15 个工作日内完成审批,并通过短信和站内信通知你。
创建和配置 Bot
初次使用 TBP 首先需要新建一个 Bot,并对其进行配置。Bot 的配置主要包括意图和词典的配置,分别对应【意图管理】和【词典管理】模块。这里已经配置好了一个对话机器人。在 Bot 页我们可以设置兜底话术,是否开启闲聊,猜你想问等模块。开启闲聊后,无需配置语义模型,机器人也可对用户进行日常聊天。
这里展示的是开启闲聊和不开启闲聊的对比,为了更好的用户体验,还是建议默认开启闲聊功能。
配置意图
申请成功后,进入 Bot 配置页面,需要新建和配置意图,意图的配置包括用户说法、槽位、服务实现以及机器人自动回复。在配置意图的槽位时需要对词典进行相关配置,包括新建自定义词典或引用内置词典,并在意图配置完成后为所需的自定义词典添加词条。这里先配置一个用户“查疫情”的意图。具体配置如下:
首先配置用户的问题问法和槽位,相应的槽位需要用“{}”标识
配置相应的服务实现,这里先选择直接返回 NLU 结果到客户端,下面会展示如何根据用户的问题处理我们的服务逻辑。
同时,意图串联能将当前意图与多个相关意图连接起来,实现复杂的多轮对话逻辑。比如,用户查询完疫情信息可能会串联到附近的医院查询。
机器人自动回复是指机器人结束该意图的解析后发送给用户的反馈语句。这里先简单展示国家卫健委的最新疫情数据。
“好的,下面是{guojia}最新疫情情况,{guojia}卫健委最新疫情通报:http://www.nhc.gov.cn/xcs/xxgzbd/gzbd_index.shtml”
词典管理
我们上面设置槽位后,可以配置具体槽位的属性值,这些都是在用户词典里配置。下面是配置了两个国家的标准词及同义词。
外部服务调用
在对接外部服务之前,我们先看下外部服务调用的接口 API 文档 https://cloud.tencent.com/document/product/1060/37447
接下来,用 start.pring.io 初始化我们的应用服务,
最终引入初始化的 pom 文件如下
代码语言:txt复制<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>chatbot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chatbot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里先定义接口请求和返回信息
代码语言:txt复制package com.example.chatbot.entity;
import lombok.Data;
import java.util.List;
/**
* @author gaiserchen
*/
@Data
public class TbpRequest {
public String RequestId;
public int AppId;
public String BotId;
public String BotName;
public String IntentName;
public List<SlotInfoList> SlotInfoList;
public String UserId;
public String SessionAttributes;
}
返回信息
代码语言:txt复制package com.example.chatbot.entity;
import lombok.Data;
/**
* @author gaiserchen
*/
@Data
public class TbpResponse {
public String RequestId;
public String SessionAttributes;
public String ResponseMessage;
public String WebHookStatus;
}
系统错误码
代码语言:txt复制package com.example.chatbot.constant;
/**
* @author gaiserchen
*/
public enum ErrorCode {
/**
* 系统错误码
*/
SUCCESS(0000, "返回成功"),
BOT_AUTH_ERROR(-1001, "BotId 不支持"),
INTENT_AUTH_ERROR(-1002, "IntentName 不支持"),
MISS_SLOT_ERROR(-1003, "槽位缺失"),
PARAM_ERROR(400, "其他错误");
private int code;
private String msg;
private ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public String getMsg() {
return this.msg;
}
public int getCode() {
return this.code;
}
@Override
public String toString() {
return "code is: " this.code "msg is:" this.msg;
}
}
编写接口的业务逻辑。这里为了演示只是简单判断用户输入的信息,并直接返回对应的卫健委网址。 当然,你可以实现很多复杂的业务服务。
代码语言:txt复制package com.example.chatbot.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.chatbot.constant.BotConstant;
import com.example.chatbot.constant.CountryConstant;
import com.example.chatbot.constant.ErrorCode;
import com.example.chatbot.entity.SlotInfoList;
import com.example.chatbot.entity.TbpRequest;
import com.example.chatbot.entity.TbpResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.log4j.Logger;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
/**
* @author gaiserchen
*/
@RestController
@Api(tags = {"查询疫情数据", "queryInfo"})
public class ChatbotController {
Logger logger = Logger.getLogger(ChatbotController.class);
/**
* 通过国家查询疫情数据
*
* @param request 国家信息
* @return 单条数据
*/
@PostMapping("queryInfo")
@ApiOperation(value = "queryInfo", tags = {"queryInfo"}, httpMethod = "POST")
public String queryInfo(@ApiParam("请求参数") @RequestBody JSONObject request) {
TbpRequest tbpRequest = new TbpRequest();
TbpResponse tbpResponse = new TbpResponse();
HashMap<String, String> responseMessage = new HashMap<>(2);
List<SlotInfoList> slotInfoList;
try {
tbpRequest = JSONObject.parseObject(request.toJSONString(), TbpRequest.class);
slotInfoList = tbpRequest.getSlotInfoList();
} catch (Exception e) {
logger.error("parse request json error");
tbpResponse.setRequestId(tbpRequest.getRequestId());
tbpResponse.setWebHookStatus(ErrorCode.PARAM_ERROR.toString());
return JSON.toJSONString(tbpResponse);
}
// 接口请求鉴权
if (tbpRequest.getBotId() == null || !tbpRequest.getBotId().equals(BotConstant.BOT_ID)) {
tbpResponse.setWebHookStatus(ErrorCode.BOT_AUTH_ERROR.toString());
tbpResponse.setRequestId(tbpRequest.getRequestId());
return JSON.toJSONString(tbpResponse);
}
// 判断槽位
if (tbpRequest.getSlotInfoList() == null || tbpRequest.getSlotInfoList().size() == 0) {
tbpResponse.setWebHookStatus(ErrorCode.MISS_SLOT_ERROR.toString());
tbpResponse.setRequestId(tbpRequest.getRequestId());
return JSON.toJSONString(tbpResponse);
}
// 判断用户意图,询问疫情国家
if (slotInfoList.get(0).getSlotName().equals(CountryConstant.COUNTRY)) {
responseMessage.put("ContentType", "PlainText");
tbpResponse.setRequestId(tbpRequest.getRequestId());
switch (slotInfoList.get(0).getSlotValue()) {
case CountryConstant.CHINA:
responseMessage.put("Content",
"你好,下面是中国最新疫情情况,国家卫健委最新疫情通报:http://www.nhc.gov.cn/xcs/xxgzbd/gzbd_index.shtml");
tbpResponse.setResponseMessage(responseMessage);
break;
case CountryConstant.AMERICA:
responseMessage.put("Content",
"hello,this is the america cdc website: https://www.cdc.gov/coronavirus/2019-ncov/index.html");
tbpResponse.setResponseMessage(responseMessage);
break;
default:
break;
}
} else {
tbpResponse.setWebHookStatus(ErrorCode.MISS_SLOT_ERROR.toString());
}
return JSON.toJSONString(tbpResponse, SerializerFeature.DisableCircularReferenceDetect);
}
}
最终工程目录结构如下:
运行测试
首先测试我们自己的接口服务,用官方的测试用例通过 postman 调用接口测试。
这里还有个问题,上面可以看到,因为官方的接口字段是非驼峰式的,用 lomock 的插件工具自动生成 setter 和 getter 方法会序列化出来两份数据,一份是小写驼峰式的。这里建议官方接口字段可以改为驼峰式命名规范。
接下来将我们的服务部署到云服务器,并提供接口给 chatbot。测试我们的 Bot,在主界面的任意位置点击“测试”开始测试我们的 Bot。测试结果如下:
同时,查看系统日志如下:
代码语言:javascript复制tail -100f logs/chatbot.log
可以看到,Bot 能正确识别用户意图,并且正确与后端进行交互。
发布上线
测试无问题后可以发布上线,上线管理中可以直接点击上线。上线完成后我们应用对应的渠道也是非常方便,这里有几种发布模板:
- API接入,可以通过API接口将Bot灵活对接到任意系统中,参考文档中的指引,帮你快速接入。
- 微信公众号,Bot接入公众号后,将接管公众号的消息接收和回复,同一个Bot可接入多个公众号。
- 微信小程序,提供微信小程序插件,让你的小程序更容易的对接腾讯智能对话平台的对话能力。
- web接入,使用官方提供的Web页作为与客户的沟通界面,兼容PC与手机,接入简单,样式可调。
- 腾讯小微,Bot接入到腾讯小微后,将作为小微的第三方Skill,在小微上向用户提供对话能力。
这里仅展示如何接入微信公众号和小程序。其他几种应用接入方式官方参考文档也是很丰富。
公众号接入
微信公众号的接入方式比较简单,直接扫描微信公众号二维码,授权接管微信公众号的对话即可。接入效果在文章开篇展示。
小程序接入
小程序的接入需要引用插件,具体可以查看接入文档。
https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx65f00fe52f504192&token=&lang=zh_CN
代码语言:txt复制//app.json
{
...
"plugins": {
...
"QCloudTBP": {
"version": "1.0.4",
"provider": "wx65f00fe52f504192"
}
}
}
//index.js
var plugin = requirePlugin("QCloudTBP")
plugin.SetQCloudSecret(config.secretid, config.secretkey) //设置腾讯云账号信息,重要!!
这里我们需要新建一个页面,并在 app.json 中注册
在 chatbot.js 中编写我们的请求处理函数
代码语言:txt复制sendClick: function(e) {
var that = this
var plugin = requirePlugin("QCloudTBP")
plugin.SetQCloudSecret(config.secretid, config.secretkey) //设置腾讯云账号信息,重要!!
plugin.PostText({
Version: '2019-06-27',
BotId: 'xxx',
BotEnv: 'release',
InputText: e.detail.value,
TerminalId: '300180199810091921',
success: function(resData) {
console.log('robot调用成功:', resData)
var jsonStr = resData.Response.ResponseText;
console.log('response is',jsonStr)
msgList.push({
speaker: 'server',
contentType: 'text',
content: jsonStr
}),
that.setData({
msgList,
inputVal
})
},
fail: function(err) {
console.error('robot调用失败', err)
}
})
这里里在处理的时候需要特别注意,因为插件请求的返回是异步调用的,所以有可能在机器人回复时,数据还未取到。所以需要设置调用的数据监听。
最终实现效果见文章开篇。
系统监控及运营数据分析
腾讯云提供完整的机器人问答监控及返回日志,同时还支持问题流水导出,可以很好的分析及完善我们的用户意图。