'AI实战:搭建客服机器人'
‘AI实战:搭建客服机器人’
本文简要介绍了但前客服机器人的发展情况,并实际开发了一个Demo作为演示。
客服机器人
随着科技发展,廉价劳动力正在不断被淘汰。客服机器人就是这样一个例子,具体参见巨头、创业者都在搞的人工智能客服,真能取代人力? 苹果的Siri,微软的小冰都极具战略意义。而作为一家电商,阿里巴巴也开发了自己的客服机器人:阿里小蜜,并在购物、招聘等场合投入使用。 腾讯有QQ小冰,并且正在开发自己的客服机器人平台 而市场上,已经有智齿科技、网易七鱼等解决方案 只是价格不菲。 由此可见,客服机器人也因为人工智能的大热而进入产业化。而本文就试图实现一个类似的客服机器人。
问题分析
制作一个客服机器人,可以说这是一个非常笼统的需求。如果沿着这个思路去做,我们会发现,已经有不少可以直接集成的平台。比如网易七鱼,如下图: 通过注册帐号,集成SDK,在后台添加知识库,我只用了20分钟就搭建好了一个客服系统。但这样也有很多问题,比如很多细节被隐藏、数据需要走别人的服务器以及高昂的价格等,所以这不是我们需要的解决方案。 将问题分解下,可以用下图表示 用户在前台(网页或手机)提问,后台进行处理,处理的核心无非是回答接收到的问题,所以,客服机器人的核心是回答问题,于是提炼出了第一个关键词:问答系统,其核心在于同一个问题有很多不同的提问方式,我们的问答系统需要正确地理解这个问题;在正确的理解问题之后,我们面临第二个问题:如何回答,程序是不能凭空产生回答的,他需要有一个知识库,就像大脑需要经常读书、储备知识一样。于是,提炼出了第二个关键词:知识库。 通过以上梳理,我们明确了问题的核心,接下来进行解决!
问答系统选型
我们需要一个问答系统,通过百度、Google进行查找,我们可以找到很多解决方案,最后我决定选用ChatterBot,主要有以下原因:
- 文档齐全,https://chatterbot.readthedocs.io
- 功能对口,ChatterBot is a machine learning, conversational dialog engine for creating chat bots
- 性能优异,后文会涉及
其实,选型是最困难的一步。需要不断的试错、尝试、否定、证明等,就像爱迪生在发明灯泡前尝试了几千次一样。 关于ChatterBot的使用不做赘述,因为文档十分齐全。以下是一个简单例子,结合注释十分容易理解:
## !/usr/bin/python3.5
## 导入库
from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
my_bot = ChatBot("Training demo")
my_bot.set_trainer(ListTrainer)
## 训练集
my_bot.train([
"嗳,渡边君,真喜欢我?",
"那还用说?",
"那么,可依得我两件事?",
"三件也依得",
])
while True:
print(my_bot.get_response(input(">")))
运行结果
$ python3.5 chatrobot.py
>真的喜欢?
那还用说?
>可依我两件事
三件也依得
可见,ChatterBot对于问题的分析还是很有一定鲁棒性的。
制作知识库
具体的制作方法由上一节选用的问答系统决定,如ChatterBot可以通过脚本注入sqlite数据库(关闭只读模式)作为知识库,所以我可以写一个脚本来把知识库注入sqlite数据库,这里我选用的是腾讯招聘的FAQ页面: 注入代码如下:
## !/usr/bin/python3.5
from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
my_bot = ChatBot("腾讯招聘FAQ")
my_bot.set_trainer(ListTrainer)
my_bot.train([
"提前批针对哪些岗位的招聘?",
"提前批面向2018年应届毕业生,开放软件开发、技术运营、安全技术、软件测试、技术研究等技术类岗位,以及游戏策划、游戏运营岗位。"
])
my_bot.train([
"什么时候面试?以什么样的形式面试?",
"简历通过筛选就会被发起面试,将会在7月27日-9月8日安排电话面试或视频面试,部分同学会被邀请至北上广深的办公大厦进行现场面试,我们将报销来回路费和住宿费用,请同学们保持手机畅通,并留意短信和邮件通知。考虑到筛选简历的时间,建议投递简历的时间为7月27日-8月25日。"
])
my_bot.train([
"意向事业群的选择有什么用处?",
"提前批中,你所选择的意向事业群会优先看到你的简历。而在正式校招中,意向事业群只作为参考,并不会完全按照意愿分配。如果你有强烈想加入某一个事业群的意愿,提前批是唯一可以主动选择事业群的机会。如果你的简历没有通过意向事业群的筛选,一样有机会被其他事业群发起面试。建议在面试时询问面试官所在的事业群,以免发生不必要的误会。"
])
my_bot.train([
"如果提前批失败了会影响之后的正式校招么?",
"如果提前批面试失败,或是未被发起面试,都会自动转为参加正式校招的对应岗位,也可以修改应聘岗位。正式校招不受影响。 需要说明的是,提前批未通过的面试记录将会留在系统中,供后续面试官参考。"
])
my_bot.train([
"其他岗位什么时候开始招聘?",
"其他岗位将在8月初全面开放投递,涵盖产品、市场、设计、职能等众多岗位,大家可以持续关注“腾讯招聘”微信公众号,将在第一时间推送。也可关注校招官网join.qq.com的更新。"
])
运行之后我们就可以看到目录下生成了一个db.sqlite3
文件,这就是我们的知识库了!接下来,我们写一个脚本测试一下,注意打开只读模式,否则ChatterBot会不断学习,修改知识库,而这个过程实际不应该让用户来参与,所以应该关闭!测试脚本如下:
## !/usr/bin/python3.5
from chatterbot import ChatBot
from chatterbot.trainers import ListTrainer
## 建议打开只读,防止修改数据库
my_bot = ChatBot("FAQ Demo", read_only=True)
## test
while True:
print(my_bot.get_response(input(">")))
运行结果:
$python3.5 faq-terminal.py
>面试形式
简历通过筛选就会被发起面试,将会在7月27日-9月8日安排电话面试或视频面试,部分同学会被邀请至北上广深的办公大厦进行现场面试,我们将报销来回路费和住宿费用,请同学们保持手机畅通,并留意短信和邮件通知。考虑到筛选简历的时间,建议投递简历的时间为7月27日-8月25日。
>选择事业群的用处
提前批中,你所选择的意向事业群会优先看到你的简历。而在正式校招中,意向事业群只作为参考,并不会完全按照意愿分配。如果你有强烈想加入某一个事业群的意愿,提前批是唯一可以主动选择事业群的机会。如果你的简历没有通过意向事业群的筛选,一样有机会被其他事业群发起面试。建议在面试时询问面试官所在的事业群,以免发生不必要的误会。
>
可以看到,虽然问题稍有变化,但是还是得到了正确答案。
搭建前端后台
通过前面的努力,我们成功解决了核心问题,接下来就好办了!用户反馈的渠道一般是手机或者网页,所以我们要为之前的成果搭建一个前端,也就是完成下图的左半边,这里只是用于演示,所以我们直接用Socket传输数据。客户端代码:
public class MainActivity extends AppCompatActivity {
private Button mSendMessage;
private EditText mQuestion;
private TextView mAnswer;
private EditText mIP;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSendMessage = (Button) findViewById(R.id.send_message);
mQuestion = (EditText) findViewById(R.id.question);
mAnswer = (TextView) findViewById(R.id.answer);
mIP = (EditText) findViewById(R.id.ip_info);
mSendMessage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAnswer.setText("查询中.....");
new Thread(){
@Override
public void run() {
super.run();
try {
Socket socket = new Socket(mIP.getText().toString(), 8085);
OutputStream os = socket.getOutputStream();
os.write(mQuestion.getText().toString().getBytes());
os.flush();
socket.shutdownOutput();
// Java代码真是罗嗦
InputStream is = socket.getInputStream();
InputStreamReader reader = new InputStreamReader(is);
BufferedReader bufReader = new BufferedReader(reader);
String s = null;
final StringBuffer sb = new StringBuffer();
while((s = bufReader.readLine()) != null){
sb.append(s);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mAnswer.setText(sb.toString());
}
});
//注:实际开发中需要放到finally中
bufReader.close();
reader.close();
is.close();
os.close();
socket.close();
} catch (UnknownHostException e) {
Log.e("未知主机", mIP.getText().toString());
e.printStackTrace();
} catch (IOException e) {
Log.e("IO错误", "-------------");
e.printStackTrace();
}
}
}.start();
}
});
}
}
服务器端我们需要通过Java来调用Python脚本,将接收到的前台消息传入问答系统处理,然后获取系统输出反馈到前端。这一部分的本质就是Socket通信,比较简单,不做赘述,后台代码如下:
public class ServerThread extends Thread{
private Socket socket;
//在构造中得到要单独会话的socket
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
super.run();
InputStreamReader reader = null;
BufferedReader bufReader = null;
OutputStream os = null;
try {
reader = new InputStreamReader(socket.getInputStream());
bufReader = new BufferedReader(reader);
String s = null;
StringBuffer sb = new StringBuffer();
while((s = bufReader.readLine()) != null){
sb.append(s);
}
Date date = new Date();
DateFormat format = new SimpleDateFormat("HH:mm:ss");
String time = format.format(date);
System.out.println("\033[32m"+ time +"--> \033[0m" + sb.toString());
//关闭输入流
socket.shutdownInput();
// 开始调用python脚本
String[] command = {"python3.5", "faq-server.py", sb.toString()};
Process process = Runtime.getRuntime().exec(command);
// 实际开发时要设定阻塞时间限制
process.waitFor();
BufferedReader screenOutput = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
StringBuilder builder = new StringBuilder();
while ((line = screenOutput.readLine()) != null) {
builder.append(line);
System.out.println(builder.toString());
}
String response = "Answer:" + builder.toString();
//返回给客户端数据
os = socket.getOutputStream();
os.write(response.getBytes());
os.flush();
socket.shutdownOutput();
} catch (Exception e) {
e.printStackTrace();
} finally{
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bufReader != null){
try {
bufReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class MultiThreadServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8085);
while(true){
//accept方法会阻塞,直到有客户端与之建立连接
Socket socket = serverSocket.accept();
ServerThread serverThread = new ServerThread(socket);
serverThread.start();
}
} catch (IOException e) {
e.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
}
}
后台开始监听:
$java MultiThreadServer
最终效果:
小插曲
本来实现前后端通信是最容易的部分,但是一开始把电脑和手机都连在公司WiFi的时候,竟然出现了找不到路由的奇怪错误,弄得我一直以为是自己代码出了问题,花了几个钟头!!最后才知道是公司WiFi的安全措施,使得无法通过代码在公司网络上随便传包(还有这种操作)!!后来连在自己租的腾讯云服务器又发现连接超时。最后改成电脑开WiFi,手机连入电脑WiFi果然好了。自己还是too young,sometimes naive啊!