多轮对话设计器
概述
多轮对话设计器 (Conversation Designer)是以自然语言为输入,定义聊天机器人逻辑思维的工具。它可以很方便的通过Chatopera支持的脚本语法描述复杂的对话逻辑,并且通过函数的形式集成企业的其它服务。企业的业务人员可以很容易的学习脚本语法,制作满足企业业务需求的聊天机器人。多轮对话设计器是设计满足业务需求的对话机器人的PC端应用程序,现已支持Windows和Mac OSX平台。多轮对话能力是聊天机器人模仿人的对话能里的一大挑战,在复杂的上下文和需要很多背景知识的前提下,现有的人工智能技术是无能为力的,在Chatopera,我们相信在企业服务中,当话术或流程固定的情况下,依赖Chatopera的产品可以输出用对话完成任务的服务,比如用对话完成点餐、报销、请假。这些对话可以在企业的聊天工具中,也可以通过智能音箱的等其他客户端。
安装
仅支持Mac OSX和Windows操作系统。下载地址
下载地址:https://www.chatopera.com/product/conversation-designer
- MacOS:dmg为文件后缀的安装文件,双击打开,根据安装向导安装。
- Windows:exe为文件后缀可执行文件,双击打开,根据安装向导安装。
启动应用
- 安装完打开应用程序,如下图:

可能遇到的问题
1. Macos上首次启动警告MacOS权限问题

在应用中心,找到“多轮对话设计器”:
应用中心

右键打开

快速开始
下载安装包后,双击打开,进入安装向导。安装完打开应用图标后进入主面板,如下图:
应用主面板

导入示例程序
下载示例程序包文件:小叮当-1.0.0-conversations.c66https://github.com/chatopera/conversation-sampleapp
示例程序下载地址

导入对话框

多轮对话列表
![]() |
|
对话 | 功能 |
chatopera | 关于Chatopera的公司信息 |
profile | 机器人的画像 |
weather | 提供天气查询功能的对话 |
测试示例程序
可以针对上面的三个多轮对话,进行一番聊天测试,下面测试主要以“问天气”为例子。选择 “weather” 对话的编辑按钮,进入weather对话的编辑窗口,包括:
概念 | 描述 |
脚本 | 按照多轮对话语法规则来描述机器人对话逻辑 |
函数 | 执行JavaScript代码的环境,声明的接口可以直接从脚本中调用 |
日志 | 函数中debug方法的输出 |
逻辑 | 聊天机器人的思维逻辑导图,保存脚本后自动生成 |
对话 | 实时测试聊天机器人的窗口 |
多轮对话编辑窗口

测试对话

多轮对话

增加新的对话
测试新对话我:今天北京适合游玩么?
机器人:风清气爽,当然可以啊~
在脚本区域可以自行设计脚本,非常简单,例如增加:
+ 今天 (*) 适合游玩么?
- 风清气爽,当然可以啊~
点击界面上方“保存”按钮,会使得刚才定义的对话生效,并且可以在聊天区域直接测试。
保存新版本
刚才点击“保存”按钮时,同时将weather的对话生成一个快照,我们随时可以回退到某个快照。查看快照

发布新版本

各个版本之间比较
对于聊天机器人的发布的各个版本差异,可以使用版本比较工具,比较具体的差异,包括:脚本比较和函数比较。 点击上图1.0.1对应的“对比差异”,可以“对比变化”界面,其中分为三个部分,- “左上”为被对比的参考版本;本例子为:1.0.0
- “右上”为要对比的目标版本,本例子为:1.0.1
- “下方”为两版本数据的差异,具体:
* 绿色:新增的内容;
* 红色:删除的内容;
* 灰色:没有变化的内容;
刚进入“对比变化”界面,默认对比的是:函数,本例子请在选择“版本号”的右边切换“脚本”为“对话”,如图:
版本之间比较差异

导出特定版本
多轮对话设计器属于设计阶段,在机器人满足需求后,可以导出为对话应用.c66文件,方便分发和部署。在生产环境,导入到智能问答引擎中,作为多轮对话应用的运行时。在“版本管理”界面,选择刚才发布的“1.0.1”的“导出”,可以选择一个路径存储该导出的文件,如图:
导出文件

脚本语法
术语
在正式介绍脚本语法前,我们先来认识下面的术语:概念 | 描述 |
对话 | 满足设定需求的多轮对话 |
输入 | 用户向聊天机器人发送的消息的文字形式 |
触发器 | 匹配用户输入文字的字符串,可以声明槽位,当用户的输入发生时,会按照算法顺序匹配触发器 |
回复 | 机器人回复用户输入的文字 |
多轮对话 | 根据上一次回复的状态,声明下轮对话的优先匹配规则 |
函数 | 可以从脚本中接受输入,并通过JavaScript执行任务返回结果的代码 |

触发器
触发器是对话的基础,当用户向聊天机器人发送一条消息时,机器人引擎会从所有定义的触发器中找到匹配的一个。在机器人引擎中,触发器用半角字符加号( + )表示。机器人的回答用半角字符减号( - )表示。例如,我们可以这样定义一个对话:
+ 晚饭吃什么
- 北京烤鸭
注意:这里( + )和( - )和文字之间需要隔一个空格。
槽位
为了让触发器能适应复杂的需求,机器人引擎使用槽位规则,槽位既能让规则具有更好的匹配能力,也能让回复和函数中使用不同槽位的值。注意:下面的某些槽位左右带有空格,这些空格是必须的。
通用槽位
解释示例:通用槽位匹配:客服你好
匹配:你好
匹配:你好吗
通用槽位会匹配零到无穷个字符、单词。此处的输入也会被系统捕获或者存储。
+ (*) 你好 (*)
- 欢迎光临
确定长度槽位
解释示例:确定长度槽位匹配:早安北京
不匹配:早安乌鲁木齐
如果你知道你想要的字符长度,可以试试确定长度槽位。此处的槽位可以被系统捕获,而且可以在回答中使用
语法为:*n, 其中n代表长度。
+ 早安 *2
- 早安
可控长度槽位
解释示例:可控长度槽位匹配:早安
匹配:早安北京
匹配:早安哈尔滨
匹配:早安乌鲁木齐
不匹配:早安君士坦丁堡
如果只想匹配一些字符,可控长度的槽位是个不错的选择。语法为:*~n, n代表你想匹配的最大长度
+ 早安 *~4
- 早安
区间槽位
解释示例:区间槽位匹配:早安北京
匹配:早安乌鲁木齐
不匹配:早安
如果想匹配一个确定的区间,比如2到4个字符之间,区间槽位绝对可以满足需要。语法为:*(最短-最长),此槽位可以被系统捕获和用在回复中。
+ 早安 *(2-4)
- 早安
必选项
解释示例:必选项匹配:早安北京
不匹配:早安西安
不匹配:早安
必选项用在你有一系列可选项,但是必须有一个被匹配。输入中的可选项会被系统捕获和用在回复中
+ 早安(北京|上海|天津)
- 早安
可选项
解释示例:可选项匹配:早安北京
匹配:早安美丽的北京
不匹配:早安热闹的北京
可选项用来确定一些额外的内容
+ 早安 [美丽的] 北京
- 早安
回复
在触发器中,我们已经学到了怎么添加一个回答。事实上你可以添加任意数量的回答。这里还有一些高级功能可以帮助你完成更多的任务。简单形式
+ 在吗- 你好,在的
如果添加了多个回答,系统会从中随机挑选一个作为回复, 然后丢掉这个回答。
+ 在吗
- 亲,在的
- 亲,有什么需要帮助
- 你好,请问遇到什么问题了吗?
所谓丢掉这个答案,是指机器人针对同一个用户,在半个小时内再次匹配上该触发器时,选择回复时,不考虑使用过的回复。 在一个触发器中声明多个回复后,保存,逻辑中将出现分支。
机器人对话逻辑

+ 在吗
- {keep} 亲,在的
- 亲,有什么需要帮助
- 你好,请问遇到什么问题了吗?
也可以在触发器前添加{keep},就不用在每个回答中都添加了
+ {keep} 在吗
- 亲,在的
- 亲,有什么需要帮助
- 你好,请问遇到什么问题了吗?
如果回答很长,可以通过“^”分割以方便可读性。可以通过“\n”实现换行
+ 在吗
- 你好,这里是客服中心,\n
^ 请问遇到什么问题了吗?
它等价于
+ 在吗
- 你好,这里是客服中心,请问遇到什么问题了吗?
槽位取值
解释示例:槽位取值匹配:小明比小红高
回答:你确定小明比小红高吗?
有些时候,在回答中需要使用输入中的槽位值,这时可以使用达到目的。
+ 我是 *~3
- 你好,
如果用户输入,“我是张三”,那么系统将回复“你好,张三”,当有多个槽时,可以使用多个。
+ *2 比 *2 高
- 你确定比高吗?
在对话中,我们有时候会需要以前的槽位值,看一下下面这个例子:
+ 我叫 *~3
- 你好,
+ 你猜我叫什么?
% 你好,
- 你刚说了,你叫
代表了以前的槽位。其中N代表在在对话中之前的问答,M代表捕获的位移。
槽位取值
另外,(+, %, -) 前的空格不是必须的,在多轮对话中,空格可以增强脚本的可读性,但是系统是忽略的。
重定向 {@__reply__}
有些时候,在问答对中重用一些回复能使编写脚本效率更高,这时可以定义一个问答对,并在脚本其它位置引用它。+ 在吗
- {@__greeting__} 请问有什么能帮助您?
+ __greeting__
- 亲,在的。
- 你好,客服小美为您服务
- 亲亲,稍等,客服马上就到
引用的方式就是 “{@触发器}”,触发器中的下划线不是必须的,但是它能增强脚本的可读性。
多轮对话
在实际应用中,和机器人聊天时,很可能要通过多轮对话完成一个任务。我们用(%)来定位之前回复,声明新的触发器,(%)后的内容是和某个回复内容一样的字符串。+ *
- 您身高多少
+ *(3-5)
% 您身高多少
- 我的身高也是
让我们一起看看这个例子:
- 当用户输入任何文字,我们用通用槽位触发回答,然后系统回复“您身高多少”。
- 当用户继续输入时,系统会先从历史中查看之前的回复中是否有对应的上下文,在这里指的是“% 您身高多少”
- 最后,如果用户输入3到5个字符,系统匹配触发器“+ *(3-5)”, 并且回复“我也是”。“”代表的就是用户输入的内容。
函数
函数是一个强大而有趣的设计。在回复中,可以使用函数来获取整条消息对象,用户对象或者其它资源,比如数据库。把槽位值当做变量传给函数,例如下面这个例子:+ 我的用户名是 *(2-10)
- ^getUserAccount()
所以,调用函数的方式就是使用“^”。在函数的编辑窗口中,可以这样定义:
exports.getUserAccount(account, cb) {
cb(null, "对不起,系统没有找到" + account);
}
函数的声明中,参数列表首先是槽位的值,可以传多个,然后最后一个参数始终是回调函数(cb),cb的参数列表为(error, text)。text作为文本添加到回复中。
复合函数
在回复中,可以添加任意多的函数,比如+ ...
- 联合 ^callFunction1() 和 ^callFunction2()
嵌套函数
在函数的回调函数中,函数名会被解析成对应的函数,所以放心的在回复中添加任意合法的函数,比如在脚本中这样写:+ ...
- ^nestedAFunction()
然后,在函数中,定义如下:
exports.nestedAFunction = function(cb) {
cb(null, "张三 ^nestedBFunction()");
}
exports.nestedBFunction = function(cb) {
cb(null, "和李四");
}
总结
以上是多轮对话设计器 v1.x 版本中支持的脚本语法,这些语法能够保证业务人员实现满足需求的聊天机器人,除了函数部分的有一点门槛外,其它内容是非常容易掌握的。对于函数,只需要一点JavaScript基础知识,就可以掌握。示例应用
本节介绍使用多轮对话设计器实现一个“聊天机器人”的具体过程。我们以实现天气问答机器人为例,我们选择这个场景并不是因为它简单,而是因为它容易理解,使用多轮对话设计器可以实现更复杂,更有价值的应用。我们先一睹为快,这个机器人是什么样子的。视频:天气查询机器人Demo
是不是很实用?如果你掌握了多轮对话设计器,就可以实现聊天机器人。
需求分析
首先,我们需要梳理一下需求:1) 我想知道任意城市的天气信息,比如“今天上海天气怎么样”;
2) 我还比较关心空气,我可以通过“今天上海空气怎么样”获得空气质量信息;
3) 我想知道今天适不适合户外运动,就问“今天上海适合运动么”;
4) 如果我问了一个城市的天气状况,我还想继续询问这个城市更多信息,这样我不用每次都告诉机器人城市名称。
当然,我的每个意图都有多种表述方式,机器人能支持一些变化的问法。如果我的问题不够严谨,机器人还应该提醒我合理的表达。
调研提供天气信息查询的API
现在很多服务以API的形式提供,从搜索引擎中查找“天气查询服务 API”,我们就能得到一些供应商,经过一些比较,我选择了和风天气,它数据丰富,免费额度大方。和风天气

AI音箱
Chatopera与杭州任你说科技达成战略合作伙伴关系,所以,我们的对话系统产品与任你说音箱可以直接集成。任你说官网

第一条规则
第一次打开多轮对话设计器后,我们看到如下的面板,我们称之为主面板。和风天气

创建聊天机器人

聊天机器人

- 管理:管理聊天机器人的多轮对话。
- 版本管理:管理不同版本的机器人,导出机器人和在不同版本之间进行比对。
- 环境变量:机器人函数中依赖的全局变量,这些变量在“设计对话”的阶段和在IT人员“部署到生产环境”下的值是不同的,比如一些接口服务的认证键值对。
- 发布:发布当前机器人为最新版本。
- 删除:将机器人删除。
创建对话

+ 今天 (*) 天气 [怎么样]
- {keep} 天气挺好的
点击“保存”,这时右侧的“逻辑区域”有了变化,出现了一个线条,在线条左右两端分别是问题和答案。在“对话区域”,我们输入“今天北京天气怎么样”,点击发送,这时机器人回复了。
对话编辑窗口

添加函数
在多轮对话设计器中,怎么请求和风天气的数据呢?使用函数。函数是多轮对话支持的使用JavaScript实现的程序。我们在“对话编辑窗口”点击函数,粘贴如下代码:
var WForewast = function (apiKey) {
if (!apiKey) throw new Error('Invalid token, get it from http://www.heweather.com/my/service');
this.key = apiKey;
}
WForewast.prototype.getWeatherByCity = function (city) {
return new Promise((resolve, reject)=>{
let url = "https://free-api.heweather.com/v5/weather?city=" + encodeURIComponent(city) + "&key=" + this.key
http
.get(url)
.then((res)=>{
resolve(res.data.HeWeather5[0].suggestion);
})
.catch(function (err) {
if (err) return reject(err);
});
})
}
const wf = new WForewast('182f1b6826d94c6285a489d2414f3ad0');
exports.getWeatherByCity = function(city, cb){
debug("getWeatherByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["comf"]["txt"]
})
}, (err)=>{
debug("error:%j", err)
cb(null, {
text: `很抱歉,没有获得${city}的天气信息。`
})
})
}
exports.getAirByCity = function(city, cb){
debug("getAirByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["air"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的空气信息。`
})
})
}
exports.getSportByCity = function(city, cb){
debug("getSportByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["sport"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的信息。`
})
})
}
exports.getDresscodeByCity = function(city, cb){
debug("getDresscodeByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["drsg"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的信息。`
})
})
}
在函数中,我们实现了根据城市请求天气、空气质量、着装建议和运动建议的接口,分别是getWeatherByCity,getAirByCity,getDresscodeByCity和getSportByCity。
细心的读者会发现,在函数中,多轮对话设计器直接支持了http,debug作为工具类,发起网络请求和输出日志信息。这两个接口极大的扩展了函数的能力,我们也会在函数中详细描述它们的使用。
然后,回到“脚本区域”,修改一下规则,更新如下:
+ 今天 (*) 天气 [怎么样]
- {keep} ^getWeatherByCity()
在回复中,我们调用了getWeatherByCity,并且传入了城市名称。 接着,在“对话区域”,输入“今天北京天气怎么样”,回复与上次不一样了。
测试对话

使用环境变量
在上面的函数中,我们有一个敏感的信息:和风天气的API密钥。在实际应用中,我们希望设计阶段和部署阶段,它的值是不同的。这时,就需要使用环境变量,环境变量正是为解决这个问题而设计的。回到主面板,在“小叮当”操作中,点击环境变量,创建如下键值对:
设置环境变量

"HEWEATHER_URL": "https://free-api.heweather.com/v5",
"HEWEATHER_KEY": "182f1b6826d94c6285a489d2414f3ad0"
保存后,回到天气对话脚本的“对话编辑窗口”,在函数中,使用下面的脚本:
var WForewast = function (apiKey) {
if (!apiKey) throw new Error('Invalid token, get it from http://www.heweather.com/my/service');
this.key = apiKey;
}
WForewast.prototype.getWeatherByCity = function (city) {
return new Promise((resolve, reject)=>{
let url = config["HEWEATHER_URL"] + "/weather?city=" + encodeURIComponent(city) + "&key=" + this.key
http
.get(url)
.then((res)=>{
resolve(res.data.HeWeather5[0].suggestion);
})
.catch(function (err) {
if (err) return reject(err);
});
})
}
const wf = new WForewast(config["HEWEATHER_KEY"]);
exports.getWeatherByCity = function(city, cb){
debug("getWeatherByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["comf"]["txt"]
})
}, (err)=>{
debug("error:%j", err)
cb(null, {
text: `很抱歉,没有获得${city}的天气信息。`
})
})
}
exports.getAirByCity = function(city, cb){
debug("getAirByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["air"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的空气信息。`
})
})
}
exports.getSportByCity = function(city, cb){
debug("getSportByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["sport"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的信息。`
})
})
}
exports.getDresscodeByCity = function(city, cb){
debug("getDresscodeByCity: %s", city);
wf.getWeatherByCity(city)
.then((suggestions)=>{
cb(null, {
text: suggestions["drsg"]["txt"]
})
}, (err)=>{
cb(null, {
text: `很抱歉,没有获得${city}的信息。`
})
})
}
这次,代码内容和前一版本相比,使用了config对象,config是一个包含环境变量的JSON数据。所以,我们更加利于将来部署对话应用了。
支持更多对话
回想我们需要的几种天气信息,我们根据需求变更脚本,一个满足需求的脚本呈现如下:// 技能介绍
+ 你知道哪些天气信息
- 我知道今天的空气,着装建议和适不适合运动
// 天气
+ 今天 (*) 天气 [怎么样]
- {keep} ^getWeatherByCity()
+ [今天] (天气|气候) [怎么样]
- {@__wf_guide_}
+ (*) 今天天气 [怎么样]
- {keep} ^getWeatherByCity()
+ (*) 空气 (*)
% ^getWeatherByCity()
- {keep} ^getAirByCity()
+ __wf_guide_
- {keep} 添加城市名哦,比如“今天北京天气怎么样”或者“北京天气怎么样”
- 我需要知道城市名称,比如“今天北京天气怎么样”或者“北京天气怎么样”
- 要告诉我城市名,比如“今天北京天气怎么样”或者“北京天气怎么样”
// 空气
+ [今天] 空气 [怎么样]
- {@__wf_guide_air}
+ (*) 今天空气 [怎么样]
- {keep} ^getAirByCity()
+ 今天 (*) 空气 [怎么样]
- {keep} ^getAirByCity()
+ __wf_guide_air
- {keep} 添加城市名哦,比如“今天北京空气怎么样”或者“北京空气怎么样”
- 我需要知道城市名称,比如“今天北京空气怎么样”或者“北京空气怎么样”
- 要告诉我城市名,比如“今天北京空气怎么样”或者“北京空气怎么样”
// 运动
+ [今天] 适(合|宜)运动(么|吗)
- {@__wf_guide_sport}
+ (*) 今天适(合|宜)运动(么|吗)
- {keep} ^getSportByCity()
+ 今天 (*) 适(合|宜)运动(么|吗)
- {keep} ^getSportByCity()
+ __wf_guide_sport
- {keep} 添加城市名哦,比如“今天北京适合运动么”或者“北京今天适合运动么”
- 我需要知道城市名称,比如“今天北京适合运动么”或者“北京今天适合运动么”
- 要告诉我城市名,比如“今天北京适合运动么”或者“北京今天适合运动么”
// 衣着
+ [今天] 适(合|宜)穿什么
- {@__wf_guide_dresscode}
+ (*) 今天适(合|宜)穿什么
- {keep} ^getDresscodeByCity()
+ [今天] (*) 适(合|宜)穿什么
- {keep} ^getDresscodeByCity()
+ __wf_guide_dresscode
- {keep} 添加城市名哦,比如“今天北京适合穿什么”或者“北京今天适合穿什么”
- 我需要知道城市名称,比如“今天北京适合穿什么”或者“北京今天适合穿什么”
- 要告诉我城市名,比如“今天北京适合穿什么”或者“北京今天适合穿什么”
这也就是我们在天气查询机器人Demo中看到的机器人的脚本,在设计过程中,我们通过对话区域来测试机器人的回复是否符合预期,我们通过逻辑窗口来查看当前机器人的思维逻辑导图,当前机器人对话的状态会被高量,被命中的规则呈现为路径。
机器人思维逻辑

快照管理

发布机器人
现在,有了可以工作的脚本,我们想发布一个版本,这时回到主面板,点击“发布”,填入如下信息,点击“确认”。发布机器人导出机器人

- 对比差异:在多个版本中比较差异,包括脚本和函数。
- 导出:将机器人导出为对话应用文件。
- 覆盖:使用这个版本覆盖当前机器人,包括脚本和函数等。