CorePlus:Microsoft Bot Framework v4 模板





5.00/5 (13投票s)
一个 MBFv4 模板(Node.js 和 TypeScript),可让您快速设置一个事务型、问答型和会话型人工智能聊天机器人
数百万年来,人类与动物无异
然后,一些事情发生了,释放了我们想象的力量
我们学会了说话
史蒂芬·霍金
目录
引言
自从在高中演示会上看到 Eliza 的实际应用后,我一直对聊天机器人充满好奇。一本电脑杂志曾刊登了一个 Basic 语言版本,我们的一位电脑老师在我们当时拥有的一台家用电脑上(不记得是 Atari 还是 MSX)输入了源代码。因此,几年后,当微软在 2016 年开始谈论他们新的 Bot Framework 时,我被它所开启的激动人心的可能性所吸引。然后,一个工作机会来了。
在与 Microsoft Bot Framework v3 合作的一个开创性项目之后,我意识到需要几乎从头开始重新学习该平台。微软正在发布一个新版本,其中包含许多突破性的更改。实际上,这是一个完全不同的框架,它使所有 v3 项目都过时了。BFv4 是该框架的完全重写,具有新的概念、术语、文档、架构等。引用 微软 的话:
Bot Framework SDK V4 是非常成功的 V3 SDK 的演进。V4 是一个主要版本发布,其中包括破坏性更改,这些更改阻止了 V3 机器人在新版 V4 SDK 上运行。
微软已经开发了许多 示例 来帮助您开始使用 Bot Builder SDK v4,以及一套由脚手架工具 Yeoman 提供支持的 模板。
本文介绍了 **CorePlus**,这是一个我创建的 Microsoft Bot Framework v4 模板,它基于 generator-botbuilder Yeoman 生成器支持的 **Core Bot 模板**(Node.js)的早期版本。这是一个扩展和高级版本,旨在作为快速启动,用于设置 **事务型**、**问答型** 和 **会话型** 聊天机器人,所有功能集于一身,并使用核心 AI 能力。该模板提出了一种修改后的项目结构和架构,并为出现的技术和设计挑战提供了解决方案。
背景与要求
虽然建议具备 Microsoft Bot Framework 的一些基本知识:Node.js SDK、LUIS、QnA Maker、Bot Framework Emulator 等,但这不是必需的。代码有完整的注释,文章提供了大量外部链接,包括示例、文档和其他文章,可以帮助您扩展对微软框架以及聊天机器人设计和开发的视野和知识。Visual Studio Code 被推荐为首选代码编辑器。不过,您也可以使用您喜欢的其他编辑器,例如 WebStorm。
**CorePlus Bot** 模板提供两种语言版本
- Node.js
- TypeScript
CorePlus Bot 模板功能
**CorePlus** 支持事务型、问答型和会话型聊天机器人,三者合一。它能够处理从最简单到高级功能的常见场景。以下是您在下载并安装代码后即可获得的内置功能。
事务型聊天机器人
微软的 **Core Bot** 模板,是 core-bot 示例(以前名为 basic-bot)的模板版本,展示了如何使用多步骤 瀑布对话 来收集和验证用户信息。**CorePlus Bot** 保留了原始的 Greeting
对话代码和逻辑,进行了一些小的重构和修改,例如使用本地化器和输入指示器。
虽然这是一个非常基本的示例,但它为事务型任务导向的聊天机器人奠定了基础,能够理解请求并执行有限数量的操作。基于 **CorePlus** 的聊天机器人可以响应用户查询,一次完成一项任务,符合业务需求,例如:“**显示我的余额**”、“**帮我找航班**”或“**我想要一个披萨**”。
问答型聊天机器人
基于 **CorePlus** 的聊天机器人还可以作为问答对话系统,为用户问题提供信息性答案。借助 Microsoft 的 QnA Maker,您可以训练您的模型并通过对话界面公开现有的 FAQ(常见问题解答)。QnA Maker 将对问题进行分类,并返回与用户查询最匹配的问题答案。
在回答用户问题后,建议请求对答案进行评估(**有帮助**、**无帮助**)。此反馈允许我们在 **Application Insights** 中记录有关交互的遥测数据(提出的问题、给出的答案、评估……)。通过这种方式,我们可以从客户那里学习,了解他们正在问什么,他们正在收到什么答案以及这些答案是如何被评估的,以便我们可以提高我们机器人的性能。
会话界面的美妙之处在于,您的用户会准确地告诉您他们想要什么,以及他们对您的机器人的看法(Dashbot — 获取机器人指标?)。
因此,实施您自己的分析并尝试从失败中提取价值。当然,**Application Insights** 已经为您记录了基本数据,但它无法在没有特定编码的情况下记录自定义数据,例如用户反馈的**有帮助**或**无帮助**。不过,将自定义数据记录到 **Application Insights** 的源代码超出了模板的范围。
另一方面,当用户将答案评估为“**无帮助**”或根本没有找到答案时,我们可以实施一些响应选项
- 回复帮助文本,可能包含有效问题的示例
- 在另一个知识库或外部问答服务中寻找答案
- 转接给人工客服
- 建议一个链接,用于向社区提问
- 建议一个电子邮件地址,用于联系真人
- 要求用户用其他词语重新表达问题
- 建议用户向聊天机器人请求帮助
您应该在您的上下文中考虑哪种解决方案最符合您的业务需求。该模板实现了最后两个,因为在它的上下文中,它们是更简单的解决方案。
会话型聊天机器人
除了完成任务和回答经过训练领域中的常见问题外,基于 **CorePlus** 的聊天机器人还能够就随机话题与客户聊天。也就是说,它能回答非正式问题,也称为闲聊或小聊。
微软的 人格聊天项目 “为机器人开发者提供了一种立即为其对话代理添加个性和避免失败响应的方法”。人格聊天数据集 可与 QnA Maker 配合使用。这是 **CorePlus** 采用的方法。该模板附带三个文件:
- 一个 KB 文件(`CoreplusKB.tsv`),用于保存常见问题解答中的问答对。
- 一个个性聊天文件(`CoreplusPC.tsv`),用于自定义闲聊语句。
- 来自 微软人格聊天项目 的数据集(`qna_chitchat_friendly.tsv`)。您可以根据您的业务需求选择任何一个,或者不选择任何一个。
使用 QnA Maker 搭配闲聊数据集是快速原型开发或基本机器人的绝佳解决方案。如果您需要以自定义方式处理特定场景,您还需要使用 LUIS。例如,开箱即用的闲聊数据集考虑了**笑话**交互。如果用户输入“讲个笑话”,如果您使用默认的闲聊响应,则总是会返回相同的笑话。然而,使用 LUIS 处理此场景将为您提供**笑话**意图。这样,您将能够创建一个对话,用随机笑话进行响应,就像 **CorePlus** 所做的那样。
**CorePlus** 使用 LUIS 处理四种闲聊意图的特殊情况:
ChitchatCancel
:用户希望取消正在进行的事务性对话ChitchatHelp
:用户寻求帮助ChitchatJoke
:用户请求讲个笑话ChitchatProfanity
:LUIS 在用户话语中检测到不当词语
在模板的上下文中,我将与事务性机器人功能或问答问题无关的用户问题都视为闲聊问题。这种关注点分离允许考虑特殊情况并返回更详细的响应。所有其他问题都使用 QnA Maker 服务处理。其中一些闲聊意图也可以理解为中断。
国际化和多语言对话
**CorePlus Bot** 模板支持国际化和多语言对话。对于本地化任务,在 MBFv4 中,我们需要依赖第三方提供商。我选择了 i18n,但代码已准备好使用任何其他解决方案。让我们看看最重要的元素。
首先,我们需要**本地化文件**。也就是说,JSON 文件包含键值对,将消息 ID 映射到本地化文本 `string`。本地化文件看起来像:
en-US.json
{
"welcome": {
"tittle": "👋 Hello, welcome to Bot Framework!",
},
"readyPrompt": "What can I do for you?"
}
一个有用的特性是能够使用对象表示法,这样我们也可以使用像`'readyPrompt'`或`'welcome.tittle'`这样的键。在 *index.(js|ts)* 文件中,我们应该导入和配置本地化包
Node.js
const localizer = require('i18n');
...
localizer.configure({
defaultLocale: 'en-US',
directory: path.join(__dirname, '/locales'),
objectNotation: true
});
TypeScript
import * as localizer from './dialogs/shared/localizer';
// Configuration is the same as in Node.js
});
**CorePlus** 还通过模仿我们 MBFv3 中常用的旧版 session.localizer.gettext(),提供了一个简化和统一的翻译函数。
Node.js
localizer.gettext = function(locale, key, args) {
return this.__({ phrase: key, locale: locale }, args);
};
对于 **TypeScript** 版本,我们需要在 `dialogs/shared/localizer` 内部进行模块增强,因为 TypeScript 不允许我们设置 `localizer.gettext = ...`。由于 TypeScript 类型检查的限制,我们还必须声明 gettext()
函数的几个重载。
TypeScript
import * as i18n from 'i18n'
declare module 'i18n' {
function gettext(locale: string | undefined, key: string, ...replace: string[]): string;
function gettext(locale: string | undefined, key: string, replacements: Replacements): string;
function gettextarray(locale: string | undefined, key: string): string[];
function getobject(locale: string | undefined, key: string): {};
}
(i18n.gettext as any) = function(locale: string | undefined, key: string, ...replace: string[]): string {
return i18n.__({ phrase: key, locale: locale }, ...replace);
};
(i18n.gettext as any) = function(locale: string | undefined, key: string, replacements: i18n.Replacements): string {
return i18n.__({ phrase: key, locale: locale }, replacements);
};
(i18n.gettextarray as any) = function(locale: string | undefined, key: string): string | string[] {
return i18n.__({ phrase: key, locale: locale });
};
(i18n.getobject as any) = function(locale: string | undefined, key: string): any {
return i18n.__({ phrase: key, locale: locale });
};
如果您想使用其他本地化包,请实现您自己的 `localizer.gettext()` 版本。配置包后,您可以在整个代码中使用它。例如:
WelcomeDialog/index.js
const localizer = require('i18n');
...
// Restart command should be localized.
const restartCommand = localizer.gettext(locale, 'restartCommand');
WelcomeDialog/index.ts
import * as localizer from '../shared/localizer';
...
// Restart command should be localized.
const restartCommand: string = localizer.gettext(locale, 'restartCommand');
多语言对话至少可以通过两种方式实现:使用自动翻译或使用按语言划分的认知服务实例。我采用了后一种解决方案。以下代码片段位于 *index.(js|ts)* 文件中,为每个区域设置添加了 **LUIS** 和 **QnAMaker** 识别器:
Node.js
const luisRecognizers = {};
const qnaRecognizers = {};
const availableLocales = localizer.getLocales();
// Add LUIS and QnAMaker recognizers for each locale
availableLocales.forEach((locale) => {
// Add LUIS recognizers
let luisConfig = appsettings[LUIS_CONFIGURATION + locale];
if (!luisConfig || !luisConfig.appId) {
throw new Error(`Missing LUIS configuration for locale "${ locale }" in appsettings.json file.\n`);
}
luisRecognizers[locale] = new LuisRecognizer({
applicationId: luisConfig.appId,
endpointKey: luisConfig.subscriptionKey,
endpoint: luisConfig.endpoint
}, undefined, true);
// Add QnAMaker recognizers
let qnaConfig = appsettings[QNA_CONFIGURATION + locale];
if (!qnaConfig || !qnaConfig.kbId) {
throw new Error(`Missing QnA Maker configuration for locale "${ locale }" in appsettings.json file.\n`);
}
qnaRecognizers[locale] = new QnAMaker({
knowledgeBaseId: qnaConfig.kbId,
endpointKey: qnaConfig.endpointKey,
host: qnaConfig.hostname
}, QNA_MAKER_OPTIONS);
});
TypeScript
const luisRecognizers: LuisRecognizerDictionary = {};
const qnaRecognizers: QnAMakerDictionary = {};
const availableLocales: string[] = localizer.getLocales();
// Add LUIS and QnAMaker recognizers for each locale
availableLocales.forEach((locale) => {
// Add LUIS recognizers
const luisConfig = appsettings[LUIS_CONFIGURATION + locale];
if (!luisConfig || !luisConfig.appId) {
throw new Error(`Missing LUIS configuration for locale "${ locale }" in appsettings.json file.\n`);
}
luisRecognizers[locale] = new LuisRecognizer({
applicationId: luisConfig.appId,
endpointKey: luisConfig.subscriptionKey,
endpoint: luisConfig.endpoint
}, undefined, true);
// Add QnAMaker recognizers
const qnaConfig = appsettings[QNA_CONFIGURATION + locale];
if (!qnaConfig || !qnaConfig.kbId) {
throw new Error(`Missing QnA Maker configuration for locale "${ locale }" in appsettings.json file.\n`);
}
qnaRecognizers[locale] = new QnAMaker({
knowledgeBaseId: qnaConfig.kbId,
endpointKey: qnaConfig.endpointKey,
host: qnaConfig.hostname
}, QNA_MAKER_OPTIONS);
});
正如您从代码中推断的,我们将在 *appsettings.json* 文件中为每个区域设置进行配置。因此,它将如下所示:
{
"LUIS-en-US": {
...
},
"QNA-en-US": {
...
},
"LUIS-es-ES": {
...
},
"QNA-es-ES": {
...
},
...
}
更改机器人的语言
如果您需要开发多语言聊天机器人,您应该实现您自己的用户体验解决方案来收集所需的语言。获取语言后,您所要做的就是使用本地化文件中的匹配值更新 `UserData.locale`。此值用于检索本地化文本,以及查询 LUIS 和 QnAMaker 服务。每个用户都有自己关联的区域设置值,该值存储在用户状态中。
const userData = await this.userDataAccessor.get(step.context);
userData.locale = '<the new value>';
一个可能的解决方案是使用 `ChangeLanguage` 意图训练 LUIS,并将其绑定到一个 `ChangeLanguage` 对话框,该对话框向用户显示一个菜单。这是西班牙聊天机器人 Goio 采用的方法,它允许更改语言。
正在进行的对话的取消和确认
在与机器人聊天时,在正在进行的事务性操作中,用户可能希望取消对话或决定重新开始。
这就是为什么一个有用的建议是:
设计您的机器人时要考虑到用户可能随时尝试改变对话的走向。
原始的微软模板已经实现了解决此问题的方案。**CorePlus** 保留了该方案,并引入了一个确认提示来与用户进行再次确认,而微软的 Core Bot 一旦识别出 **Cancel** 意图,就会立即取消正在进行的对话,无需确认。
帮助处理
原始的 Core Bot 模板已经处理了 **Help** 意图。**CorePlus** 对此基本解决方案进行了重构和扩展,显示了三个元素:
- 用户可以输入的实际语句列表
- 菜单介绍
- 链接到聊天机器人功能的菜单
这里列出的实际语句和菜单选项旨在展示聊天机器人模板所具备的全部功能。在实际的生产机器人中,此列表可能不会很短,因此您应该设计一个最能支持用户体验的解决方案。
ChitchatHelp
意图被作为中断处理,以便用户可以随时请求帮助。
笑话处理
用户经常要求机器人讲笑话。根据聊天机器人分析公司 Dashbot.io 的联合创始人兼首席执行官 Arte Merritt 的说法:
我们平台上的 Facebook 机器人中,约有 12% 的用户要求机器人讲个笑话或说些有趣的话。
**CorePlus** 开箱即用地支持此场景,具有:
- 一个经过一组笑话话语训练的 LUIS 模型,包括 Arte Merritt 在其文章中列出的那些。
- 一个绑定到
ChitchatJoke
意图的专用对话,能够返回不同的随机笑话。
笑话定义在本地化文件中。您可以根据需要定义任意数量的笑话。多部分笑话可以用“`&&`”分隔。
en-US.json
{
...
"jokes": {
"0": "My boss told me to have a good day so I went home 😂",
"1": "What do you call a guy with a rubber toe? && Roberto 🤣",
...
"5": "I ordered a chicken and an egg from Amazon... && I'll let you know 🤣",
...
}
...
}
上一个讲过的笑话在下一次迭代中永远不会重复。这是对话代码,位于 *dialogs/chitchat* 文件夹的 *index.(js|ts)* 文件中。
Node.js
async jokeDialog(dc) {
const userData = await this.userDataAccessor.get(dc.context);
// Retrieve the jokes list.
const jokes = localizer.gettext(userData.locale, 'jokes');
// Randomly select a joke from the list. Do not repeat the last one.
let jokeNumber;
do {
jokeNumber = Utils.getRandomInt(0, Object.keys(jokes).length).toString();
} while (jokeNumber === userData.jokeNumber);
// Save the last joke number.
userData.jokeNumber = jokeNumber;
await this.userDataAccessor.set(dc.context, userData);
const parts = jokes[jokeNumber].split('&&');
// Send every joke part in a different bubble, leaving a short space time between them.
for (let i = 0; i < parts.length; i++) {
await Utils.sendTyping(dc.context);
await dc.context.sendActivity(parts[i]);
}
return true;
}
TypeScript
async jokeDialog(dc: DialogContext): Promise<boolean> {
const userData: UserData = await this.userDataAccessor.get(dc.context, UserData.defaultEmpty);
const locale: string = userData.locale || localizer.getLocale();
// Retrieve the jokes list.
const jokes: StringDictionary = localizer.getobject(locale, 'jokes');
// Randomly select a joke from the list. Do not repeat the last one.
let jokeNumber: string;
do {
jokeNumber = Utils.getRandomInt(0, Object.keys(jokes).length).toString();
} while (jokeNumber === userData.jokeNumber);
// Save the last joke number.
userData.jokeNumber = jokeNumber;
await this.userDataAccessor.set(dc.context, userData);
const parts: string[] = jokes[jokeNumber].split('&&');
// Send every joke part in a different bubble, leaving a short space time between them.
for (let i = 0; i < parts.length; i++) {
await Utils.sendTyping(dc.context);
await dc.context.sendActivity(parts[i]);
}
return true;
}
ChitchatJoke
意图被作为中断处理,因此用户可以随时请求讲个笑话。
在为聊天机器人设计笑话时要谨慎,因为有一些关键点需要注意:
幽默是主观的,容易出错。一个人的笑点可能是另一个人的尴尬(……)。幽默的复杂性足以促使许多公司开发出平淡无奇、毫无个性的聊天机器人。但创建一个没有幽默感的聊天机器人将违背大多数聊天机器人的根本目的:创建一种人们愿意进行的、看似人性的对话。因此,诀窍在于找到平衡,创造一种能够建立联系和娱乐用户的对话,而不是冒犯或疏远。(聊天机器人应该有多有趣?)
不雅语言处理
众所周知,当人们与聊天机器人交谈时,由于他们知道自己是在与电脑交谈,所以不雅语言的出现频率更高,包括性暗示词语和脏话。**CorePlus** 内置了不雅语言识别和处理功能。
如前所述,**CorePlus** 使用 LUIS 和 `ChitchatProfanity` 意图来过滤不需要的内容。一旦识别出此类内容,聊天机器人会做出相应响应,建议用户寻求帮助。这是代码,位于 *dialogs/chitchat* 文件夹的 *index.(js|ts)* 文件中:
Node.js
async profanityDialog(dc) {
const userData = await this.userDataAccessor.get(dc.context);
const msg = localizer.gettext(userData.locale, 'profanity');
await dc.context.sendActivity(msg);
return true;
}
TypeScript
async profanityDialog(dc: DialogContext): Promise<boolean> {
const userData: UserData = await this.userDataAccessor.get(dc.context, UserData.defaultEmpty);
const locale: string = userData.locale || localizer.getLocale();
const msg: string = localizer.gettext(locale, 'profanity');
await dc.context.sendActivity(msg);
return true;
}
ChitchatProfanity
意图被作为中断处理,以便它可以在任何时候被识别。
LUIS 模型训练了基本短语,例如:“你太糟糕了”、“下地狱吧”和“你很笨”,以及其他更具冒犯性和粗鲁的短语。它还训练了 BanBuilder 项目收集的脏话,谷歌禁止的脏话和顶级脏话列表,以及 LUIS 交互界面建议的**相关值**。
您可以根据您想要视为不雅语言的用例来完善或修改训练。处理不当内容和不良文本的另一种解决方案是使用专业服务,例如 内容审核器。
支持“首次用户交互”最佳实践
入门交互 对聊天机器人的成功至关重要,因此精心设计对于用户体验至关重要。
用户与机器人之间的首次交互对用户体验至关重要。在设计机器人时,请记住,第一个消息不仅仅是打个招呼。当您构建应用程序时,您设计第一个屏幕以提供重要的导航线索。用户应该直观地了解菜单在哪里以及如何工作,哪里可以获取帮助,隐私政策是什么等等。当您设计机器人时,用户与机器人的首次交互应该提供相同类型的信息。(设计机器人的首次用户交互)
**CorePlus** 带有占位符,强制执行一些 UX 最佳实践:
如“**帮助处理**”中所述,*菜单选项列表旨在展示聊天机器人模板所具备的全部功能。在实际的生产机器人中,此列表可能不会很短,因此您应该设计一个最能支持用户体验的解决方案*。一个广为接受的经验法则是只显示3-5个关键功能。
**CorePlus** 采用一致的方式显示主菜单。主菜单在三个地方或时刻显示:
- 欢迎对话结束时
- 用户取消正在进行的事务对话后
- 在帮助对话结束时,当根对话(`MainDialog`)处于活动状态时。
“*机器人菜单操作/命令应始终可调用,无论对话状态或机器人所处的对话如何*”。在正在进行的事务对话中,如果用户请求帮助,我们应该提供一些指导。然而,如果我们此时显示主菜单,我们将通过提供未作为中断处理的并行任务来分散用户的注意力。这就是为什么主菜单仅从根对话显示的原因。
这是从任何对话显示主菜单的代码
Node.js
const { Utils } = require('../shared/utils');
...
await Utils.showMainMenu(context, locale);
TypeScript
import { Utils } from '../shared/utils';
...
await Utils.showMainMenu(context, locale);
主菜单代码位于 *dialogs/shared* 文件夹的 *utils.(js|ts)* 文件中,如下所示:
Node.js
static async showMainMenu(context, locale) {
const hints = localizer.gettext(locale, 'hints');
const buttons = [];
Object.values(hints).forEach(value => {
buttons.push(value);
});
await context.sendActivity(this.getHeroCard(buttons));
}
TypeScript
static async showMainMenu(context: TurnContext, locale: string | undefined): Promise<void> {
const hints: StringDictionary = localizer.getobject(locale, 'hints');
const buttons: string[] = [];
Object.values(hints).forEach(value => {
buttons.push(value);
});
await context.sendActivity(this.getHeroCard(buttons, ''));
}
showMainMenu()
函数从本地化文件中加载一个 hints
对象,用其值填充一个数组,并将其传递给一个也在同一 *utils.js* 文件中的函数 getHeroCard()
,该函数返回一个包含选项的 HeroCard
(菜单)。
是/否问题的“是/否”同义词
Bot Framework v4 SDK 附带内置的专用对话类,用于管理对话。其中一个“*提示用户通过‘是’或‘否’响应来确认某事*”的类是ConfirmPrompt 类。作为一个专用组件,该类在基本层面表现良好。不过,在撰写本文时,我发现有几个任务很难或无法完成:
- 带自定义文本的按钮
- 识别大量语句作为“**是**”或“**否**”的同义词
我想出了一个解决方案,使用一个类似但更通用的对话类:ChoicePrompt。顺便说一句,这两个类都接受一个 Choice 列表,并且它们的使用应该可以互换。
这个想法是实现一对函数,它们返回一个 Choice 对象:一个包含“**是**”数据,另一个……嗯,您可能猜到了,包含“**否**”数据。例如,“**是**”版本如下,位于 *dialogs/shared* 文件夹的 *utils.(js|ts)* 文件中。
Node.js
static getChoiceYes(locale, titleKey, moreSynonymsKey) {
const title = localizer.gettext(locale, titleKey);
let yesSynonyms = localizer.gettext(locale, 'synonyms.yes');
if (moreSynonymsKey) {
const moreSynonyms = localizer.gettext(locale, moreSynonymsKey);
yesSynonyms = yesSynonyms.concat(moreSynonyms);
}
return {
value: 'yes',
action: {
type: 'imBack',
title: title,
value: title
},
synonyms: yesSynonyms
};
}
TypeScript
static getChoiceYes(locale: string | undefined, titleKey: string, moreSynonymsKey?: string): Choice {
const title: string = localizer.gettext(locale, titleKey);
let yesSynonyms: string[] = localizer.gettextarray(locale, 'synonyms.yes');
if (moreSynonymsKey) {
const moreSynonyms: string[] = localizer.gettextarray(locale, moreSynonymsKey);
yesSynonyms = yesSynonyms.concat(moreSynonyms);
}
return {
value: 'yes',
action: {
type: 'imBack',
title: title,
value: title
},
synonyms: yesSynonyms
};
}
**是/否** `同义词`在本地 JSON 文件中定义为 `string` 数组。提示对话框将识别其中任何一个,以及按钮标题和细微变化。
en-US.json
{
...
"synonyms": {
"yes": ["yes", "y", "yeah", "yay", "👍", "awesome", "great",
"cool", "sounds good", "works for me", "bingo", "go ahead",
"yup", "yes to that", "you're right", "that was right",
"that was correct", "that's accurate", "accurate", "ok",
"yep", "that's right", "that's true", "correct",
"that's right", "that's true", "sure", "good", "confirm", "thumbs up"],
"no": ["no", "n", "nope", "👎", "ko", "uh-uh", "nix", "nixie",
"nixy", "nixey", "nay", "nah", "no way",
"negative", "out of the question", "for foul nor fair",
"not", "thumbs down", "pigs might fly", "fat chance",
"catch me", "go fish", "certainly not",
"by no means", "of course not", "hardly"],
"cancel": ["cancel", "abort"]
},
...
}
以下是如何使用 `Choice` 函数构建自定义确认对话框:
Node.js
const { Utils } = require('../shared/utils');
...
async promptFeedbackStep(step) {
const userData = await this.userDataAccessor.get(step.context);
const locale = userData.locale;
const prompt = localizer.gettext(locale, 'qna.requestFeedback');
const choiceYes = Utils.getChoiceYes(locale, 'qna.helpful');
const choiceNo = Utils.getChoiceNo(locale, 'qna.notHepful');
return await step.prompt(ASK_FEEDBACK_PROMPT, prompt, [choiceYes, choiceNo]);
}
TypeScript
import { Utils } from '../shared/utils';
...
async promptFeedbackStep(step: WaterfallStepContext): Promise<DialogTurnResult> {
const userData: UserData = await this.userDataAccessor.get(step.context, UserData.defaultEmpty);
const locale: string = userData.locale || localizer.getLocale();
const prompt: string = localizer.gettext(locale, 'qna.requestFeedback');
const choiceYes: Choice = Utils.getChoiceYes(locale, 'qna.helpful');
const choiceNo: Choice = Utils.getChoiceNo(locale, 'qna.notHepful');
return await step.prompt(ASK_FEEDBACK_PROMPT, prompt, [choiceYes, choiceNo]);
}
为了用户体验和可用性,您的机器人能够理解比按钮上显示的更多的话语非常重要。请注意,用户使用机器人不像使用应用程序——他们想聊天,而不是点击按钮。通常,建议对聊天机器人采用混合方法,即同时利用自然语言处理 (NLP) 和按钮。
输入指示器
对于聊天机器人用户体验而言,输入指示器的重要性等同于网页和移动应用中的繁忙或加载指示器。它告诉用户屏幕另一端正在发生一些事情。机器人已经听到了用户的请求,现在正在“思考”并准备预期的回复。
设计您的机器人,使其能够立即确认用户输入,即使在机器人可能需要一些时间来编译其响应的情况下。(...)通过立即确认用户的输入,您可以消除任何关于机器人状态的潜在混淆。如果您的响应需要很长时间才能编译,请考虑发送一个“正在输入”消息,以表明您的机器人正在工作。(设计机器人导航 - “神秘机器人”)
输入指示器也可以用于增加额外消息的等待时间,并在长消息被分割成短消息时给用户更多时间阅读。
在撰写本文时,Bot Framework v4 的 Node.js 版本缺乏内置功能,无法根据开发人员的意愿随时随地发送输入指示器事件。不过,它可以通过几行代码实现。**CorePlus** 开箱即用地提供了此功能。您可以在 *dialogs/shared/utils.(js|ts)* 文件中找到其实现,该实现是不言自明的。
Node.js
static async sendTyping(context) {
await context.sendActivities([
{ type: 'typing' },
{ type: 'delay', value: this.getRandomInt(1000, 2200) }
]);
}
TypeScript
static async sendTyping(context: TurnContext): Promise<void> {
await context.sendActivities([
{ type: 'typing' },
{ type: 'delay', value: this.getRandomInt(1000, 2200) }
]);
}
以下是发送“正在输入”指示器消息的示例:
Node.js
// Import Utils for sendTyping() function
const { Utils } = require('../shared/utils');
...
async promptForNameStep(step) {
await Utils.sendTyping(step.context);
// Do more things
}
TypeScript
// Import Utils for sendTyping() function
import { Utils } from '../shared/utils';
...
async promptForNameStep(step: WaterfallStepContext): Promise<DialogTurnResult> {
await Utils.sendTyping(step.context);
// Do more things
}
当前的输入指示器动画是静态的,总是相同的。也许在不久的将来我们会看到更动态的实现,因为微软已经申请了一项专利,其中描述了此类改进。
本文公开了一种技术,可改善用户在使用打字动画方面的体验。在一种实现中,近端客户端应用程序接收到用户在远端客户端应用程序中打字的指示。近端客户端应用程序响应性地选择一个代表打字模式的动画。在某些实现中,选择可能是随机的(或伪随机的),或者选择可能与特定的打字模式相对应。然后,近端客户端操纵其用户界面中的省略号以产生所选动画。
重启命令
有时,用户可能会觉得被困在对话中,并希望从头开始。因此,聊天机器人应该提供一种解决这种情况的方法。一种常见的解决方案是实现一个 **重启** 命令(“`restart`”、“`start over`”、“`reset`”等),它告诉聊天机器人重新发送欢迎消息并重复“首次用户交互”,其中可能还包括清除一些收集到的用户数据。**重启**命令的可用性应在欢迎对话和帮助对话中提前告知,以便用户了解此“超能力”。我们之前已经看过这两种情况的示例。
**CorePlus** 通过处理一个重启关键字提供内置逻辑,该关键字可以根据您的业务需求随意修改。更灵活的方法是使用 LUIS 和意图识别,并使用许多不同的话语。然而,意图识别总是具有可变的置信度,而单个关键字的置信度为 1。如果用户输入 **restart**,我们可以假定他或她想重新开始对话。
**重启**命令在根对话(`MainDialog`)中处理,位于 *dialogs/main/index.(js|ts)* 文件中。请注意,该命令应进行本地化。
...
// Handle commands
if (utterance === localizer.gettext(locale, 'restartCommand').toLowerCase()) {
let userData = new UserData();
// Save locale and any other data you need to persist between resets
userData.locale = locale;
await this.userDataAccessor.set(dc.context, userData);
await dc.cancelAllDialogs();
turnResult = await dc.beginDialog(WelcomeDialog.name);
}
...
Web 聊天和可最小化 Web 聊天组件
本模板基于 MBFv4 Web Chat 示例,提供了一个开箱即用的自定义版本,展示了如何将 Web Chat 控件添加到您的网站。该示例位于 *public* 文件夹的 *webchat.html* 文件中。您需要将 **YOUR_DIRECT_LINE_TOKEN** 替换为您的机器人密钥。HTML 文件可以直接用您喜欢的互联网浏览器打开,或者在您的机器人本地运行在您的计算机上后,从 *https://:3978/public/webchat.html* 加载。
另一方面,该模板还提供了一个可最小化版本的 Web Chat 组件,位于 *webchat* 文件夹的 *minwebchat.html* 文件中。
此示例借鉴了 customization-minimizable-web-chat 官方示例的一些想法。所有逻辑都打包在一个 HTML 文件中,因此无需使用 React。在那里,您将找到两个 iframe:一个指向您的机器人 URL,远程托管在 Azure 上(*https://**your-bot-handle**.azurewebsites.net/public/webchat.html*)。另一个指向本地运行的实例(*https://:3978/public/webchat.html*)。在这两种情况下,都将加载上一个 Web Chat 文件。这两个 iframe 旨在相互替代,因此您可以决定使用和测试哪一个。
请注意,对于本地版本,有一种巧妙的方法来加载 iframe 内容以覆盖浏览器缓存的版本。这会强制浏览器在每次加载网页时重新获取 iframe 内容。
<iframe id='botiframe' src='' style='width: 100%; height: 100%;'></iframe>
<script>
const iframe = document.getElementById('botiframe');
iframe.src = 'https://:3978/public/webchat.html?r=' + new Date().getTime();
</script>
CorePlus Bot 模板概念验证
想看用此模板创建的功能性概念验证吗?观看 **HAL Fintech Chatbot** 的视频,这是一个个人财务管理助手,可以跟踪收入和支出。它还可以从以下概念的先前交易中检索信息:
- 账户余额
- 收入和支出数据
- 最大收入和支出
- 随时进行预算评估
您可以使用查询过滤器,例如:
- 日期(当前日期或特定日期,或日期范围)
- 结合来源、概念、金额、地点、项目和/或类别
HAL Fintech 聊天机器人还可以回答常见的金融问题,并进行闲聊。所有交互均通过自由格式文本对话和自然语言处理进行。
结论
基于微软官方 Bot Framework v4 模板 Core Bot (Node.js) 的基础,我创建了一个高级版本(Node.js 和 TypeScript),旨在作为快速启动,用于设置事务型、问答型和会话型聊天机器人,所有功能集于一身,并支持设计最佳实践。本文介绍了该模板,描述了其最重要的功能,并讨论了它们存在的原因以及如何利用其优势。我希望它能有所帮助并实现其目标。
**CorePlus Bot** 模板是学习微软在 Github 上分散的样本和模板集,以及我自己的机器人编写经验和发现的结果。实际上,该模板是一个更复杂聊天机器人的摘要版本,该聊天机器人是我设计和开发的。以下是微软的 Github 链接:
聊天机器人应该如何设计?需要考虑多少事情?最佳实践是什么?作为一项新兴技术,聊天机器人和对话界面的广泛商业应用仍处于起步阶段,尽管它们已经存在多年。该领域的从业者,如产品经理、项目经理、用户体验设计师、开发人员、文案、计算机科学和人工智能研究人员,仍在学习和建立应指导这一激动人心领域的设计和开发的规则,同时分享他们的发现。以下是一些文章精选,可能会帮助您思考和理解社区的集体知识。
- 聊天机器人应该做什么不应该做什么——这些是最好和最差的聊天机器人实践
- 每个品牌都应该遵循的 AI 和聊天机器人道德准则
- 聊天机器人的用户体验
- 为什么聊天机器人会失败
- 聊天机器人曾是下一个大事件:发生了什么?
该模板已准备好使用,尽管它是一个未完成的工作。底层框架的开发正在进行中,**CorePlus** 也应如此。它应随着 MBFv4 的变化和改进而不断适应。它也可以并且应该通过社区的贡献来改进。您的意见、建议和贡献都非常受欢迎。那么,我们接下来该怎么做?*我们所需要做的就是确保我们**继续对话**。*
历史
- 2019年5月20日:版本1.0 - 初稿提交。
- 2019年11月27日:版本2.0 - 添加TypeScript版本代码。
- 2019年12月10日:版本2.1 - 添加视频部分(PoC)。
- 2020年3月1日:版本2.2 - 删除“.bot文件弃用”部分。代码已更新。