原文:
zh.annas-archive.org/md5/21ba3c60d34dd9d22a6cd85e93119fca译者:飞龙
前言
聊天机器人正在日常生活中变得越来越普遍,目前 Alexa 已经进入 16%的家庭,并且有超过 10 万个 Facebook 聊天机器人。聊天机器人提供了一种更自然的人与人交互技术,即通过对话。
在本书中,我们将学习如何使用亚马逊 Alexa 和 Lex 构建自己的语音和文本聊天机器人。这些平台使我们能够使用非常强大的技术来理解用户所说的话。我们还了解创建聊天机器人的设计过程以及我们如何为用户提供出色的体验。
本书面向对象
本书适用于任何想要能够构建 Alexa 技能或 Lex 聊天机器人的人。无论您是想为个人项目构建它们,还是作为您工作的一部分,本书都将为您提供所有需要的工具。您将能够将一个想法转化为构建对话流程图,用用户故事进行测试,然后构建您的 Alexa 技能或 Lex 聊天机器人。
本书涵盖内容
第一章,理解聊天机器人,首先解释了构建对话界面的相关概念。我们将学习如何从一个示例用户对话开始,构建流程图来可视化用户与聊天机器人的交互路径。本章接着将讨论聊天机器人的类型,并介绍亚马逊 Alexa 的语音技能和亚马逊 Lex 的基于文本的聊天机器人。
第二章,AWS 和 Amazon CLI 入门,教我们关于 AWS Lambda 的知识,以及这些无服务器函数如何在浏览器中构建和测试。在构建我们的第一个 Lambda 之后,我们讨论了三种不同的构建和部署方法,比较了每种方法的优点和局限性。为了创建最强大的开发环境,我们使用aws-cli构建一个脚本,允许我们从本地开发环境部署 Lambda。
第三章,创建您的第一个 Alexa 技能,介绍了 Alexa 技能套件,并指导我们构建第一个 Alexa 技能。我们学习如何构建 Lambda 来处理用户的请求并返回我们想要发送给用户的响应。为了创建一个更真实的情况,我们创建了一个基于一系列问题的技能,为用户推荐一辆车。我们使用第一章,理解聊天机器人中讨论的流程设计过程,在创建意图之前绘制出用户与我们的技能的交互图。我们使用的 Lambda 随着槽位提取和包含存储在 S3 中的数据的加入而变得更加复杂。
第四章,将您的 Alexa 技能连接到外部 API,通过访问外部 API 将我们的 Alexa 技能提升到新的功能水平。API 访问可以为您的聊天机器人提供大量的功能,但需要正确执行。我们将了解两种处理错误和构建天气技能的最佳方法。
第五章,构建您的第一个 Amazon Lex 聊天机器人,将焦点转向 Amazon Lex 聊天机器人。概念和组件与我们用来构建我们的 Alexa 技能的类似,因此在我们构建第一个 Lex 聊天机器人之前,我们只需要快速复习一下。虽然 Lex 和 Alexa 很相似,但我们很快就能看到在处理意图方面存在一些关键差异。为了创建一个更真实的项目,我们构建了一个 FAQ 聊天机器人。这个 Lex 聊天机器人利用意图处理,根据触发的意图触发三个 Lambda 中的一个。这些 Lambda 从 S3 获取响应,并使用我们将会构建的LexResponses类进行回复。
第六章,将 Lex 机器人连接到 DynamoDB,介绍了 DynamoDB 数据库以及我们如何使用它们来存储有关用户交互的信息。我们使用它构建了一个购物聊天机器人,该机器人可以存储用户的购物车,甚至允许他们保存购物车以供以后使用。这个聊天机器人的流程复杂性更接近于您从真实项目中期望的,这在代码量上得到了体现。
第七章,将您的聊天机器人发布到 Facebook、Slack、Twilio 和 HTTP,教我们如何发布我们的聊天机器人并将它们集成到平台中,包括 Facebook 和 Slack。我们使用 Amazon Lex 内置的集成工具使这个过程尽可能简单。接下来,我们使用 API Gateway 和 Lambda 构建一个 API 端点,以便我们可以为其他服务开发集成。我们使用这个 API 来创建我们自己的前端界面,我们可以将其集成到其他网站中。
第八章,提升您的机器人用户体验,讨论了几种让用户体验更加愉悦的方法。这包括在 Lex 对话中创建和发送卡片,以及在 Alexa 技能中使用搜索查询槽类型。卡片为用户提供了一种更直观的交互方式,而搜索查询槽允许用户搜索比自定义或内置槽类型更广泛的价值范围。
第九章,回顾与持续发展,为我们提供了关于我们可以继续发展我们的聊天机器人技能的方向的一些指导。对于那些更喜欢 Alexa 的人和那些想要追求更多 Lex 技能的人,以及一套可以提升你在两个聊天机器人平台上的能力的一组技能。在此之后,我们讨论了聊天机器人的未来,它们将走向何方,以及它们真正融入我们日常生活之前需要发生什么。
为了充分利用这本书
这里有一份你应该拥有的清单,以便充分利用这本书。你可以不使用第二或第三项内容完成这本书,但缺少它们会使过程更难:
-
至少了解 AWS 支持的一种高级编程语言的知识,但最好是 JavaScript
-
使用 Linux 或 macOS 的命令行工具的基本经验
-
对 AWS 服务的初步了解有帮助,但不是必需的
下载示例代码文件
你可以从www.packt.com的账户下载这本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
在www.packt.com上登录或注册。
-
选择支持选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入书名,并遵循屏幕上的说明。
下载文件后,请确保您使用最新版本的以下软件解压缩或提取文件夹:
-
Windows 上的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 上的 7-Zip/PeaZip
书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Chatbot-Development-with-Alexa-Skills-and-Amazon-Lex。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包,可在**github.com/PacktPublishing/**找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781788993487_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”
代码块设置如下:
html, body, #map {
height: 100%;
margin: 0;
padding: 0
}
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出都应如下所示:
$ mkdir css
$ cd css
粗体: 表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会像这样显示。以下是一个例子:“从管理面板中选择系统信息。”
警告或重要注意事项会像这样显示。
小贴士和技巧会像这样显示。
联系我们
我们欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com将邮件发送给我们。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这个错误。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果你在互联网上以任何形式遇到我们作品的非法副本,如果你能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
一旦你阅读并使用了这本书,为何不在你购买它的网站上留下评论呢?潜在的读者可以看到并使用你的客观意见来做出购买决定,Packt 出版社可以了解你对我们的产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
关于 Packt 的更多信息,请访问packt.com。
第一章:理解聊天机器人
要使用 Alexa 或 Lex 创建成功的聊天机器人,你首先需要了解构成聊天机器人的组件。然后可以使用这些部分来创建对话图和流程图,帮助可视化用户在对话中的路径。拥有用户对话的这种地图可以使构建聊天机器人变得更容易、更快。
在本章的结尾,我们还将介绍 Alexa 和 Lex,并探讨它们的相似之处和不同之处。我们还将快速查看它们的一些用例。
本章将解释以下主题:
-
介绍聊天机器人
-
设计对话流程图
-
最佳实践
-
亚马逊 Alexa 和亚马逊 Lex
什么是聊天机器人?
聊天机器人是一种以更人性化的方式与用户互动的新方法,通过对话。这与现有方法大相径庭,现有方法提供的交互或个性化非常有限。
聊天机器人可以是基于语音或文本的交互,这使得它们可以被集成到现有的网站和应用程序中,或者用于电话和虚拟助手。
最近,随着亚马逊 Echo 和 Google Home 等产品的推出,以及大量 Facebook Messenger 聊天机器人的出现,它们受到了广泛关注。这些技术进步使得你可以在不查看屏幕的情况下检查天气或订购披萨,或者在没有等待与呼叫中心交谈的情况下获取个性化信息。
什么是聊天机器人?
聊天机器人在与用户互动的方式上非常不同,因此其工作原理也非常不同。聊天机器人主要有三个组成部分:意图、插槽和utterances。
意图
意图是聊天机器人的最重要部分。它们是聊天机器人可以处理的任务或对话。它们被称为意图,因为它们是用户打算做的事情。
意图可以从非常简单到极其复杂。一个基本的意图可能只是SayHello,它只是对用户说“Hi”。一个更复杂的意图可能是预订假期、选择并购买一双鞋或者订购披萨。它们可以被设计得像你的想象力一样复杂。
当用户说出其中一个示例 utterances时,它们就会被启动或触发。示例 utterances 是一系列用户可能在尝试启动意图时可能说出的单词或短语。每个意图可以有大量的示例 utterances。在SayHello示例中,它们可能是“Hello chatbot”、“Hey there chatbot”或者仅仅是“Hi”。
插槽
为了使聊天机器人真正有用,它必须能够收集有关用户请求的详细信息。如果你想要订购披萨,聊天机器人需要知道你想要什么配料,你想要什么风味的底料,以及你希望它送到哪里。这些信息被收集并存储在插槽中。
槽位被设计成只接受某些类型的信息。如果你试图了解他们是否想要大、中或小披萨,如果他们可以输入任何随机信息,那么这将不会很有用。定义可以存储在某个槽位中的信息被称为创建槽类型。
要利用在槽位中收集到的信息,它们可以在聊天机器人逻辑的下一阶段被访问。这可能只是简单地表示“您已订购一个大 夏威夷披萨”,其中大小和配料正是用户之前订购的。
话语
话语是一个已经被说出的词或短语。这对于聊天机器人来说至关重要,因为这是用户与聊天机器人交互的方式。
这些话语可以触发用户试图访问的意图,它们也可以用来获取填充槽位所需的精确信息。
设计对话流程
现在我们已经了解了构成聊天机器人的组件,我们可以开始设计我们希望聊天机器人处理的对话。现在设计对话使得可视化聊天机器人的工作方式变得容易得多,从而使得构建更加容易和快速。以这种方式设计对话使得它们易于理解,对于不能编写代码的人来说,这是一个创建聊天机器人的伟大工具。
这种设计方法适用于语音或文本聊天机器人;只需想象文本框为气泡即可。
从完美的对话图开始
每件事都需要有一个起点,所以最好是完美的。这个阶段的目标是有一个基本的对话图,我们稍后会将其扩展为详细的流程图。
要做到这一点,你需要考虑与用户进行完美对话的内容。首先写下用户会说什么,以及机器人将如何回应。以下是一个订购披萨的例子:
订单披萨对话
这可以通过许多方式完成:使用流程图软件、使用两部手机或两个消息账户,或者简单地用笔和纸。目标是理解聊天机器人将如何与用户互动以及用户可能说些什么。
对话流程图
现在我们已经有一个基本的对话图,我们需要将其转换为流程图。流程图在几个关键方面与对话图不同:
-
流程图的每一部分都有自己的符号,这使得理解每个阶段的状况变得容易。
-
流程图不仅仅包含对话。它还描述了幕后发生的逻辑、信息和流程。
-
流程图不是线性的。这意味着它们可以描述许多对话,其中用户说不同的话。
为了正确描述我们的聊天机器人,我们需要为对话的每个部分都有一个符号。首先,我们将使用六个,但稍后可以添加更多符号:
流程图符号
为了创建我们的流程图,我们将使用流程图软件。我们想要使用流程图软件而不是普通文档甚至手工制作的原因有几个:
-
它们很容易编辑。在我们通过这本书的工作过程中,我们将改变对话流程的阶段和话语及回复的文本。每次更改都要重新绘制图表将会非常耗时。
-
这是制作流程图最简单的方法。符号会自动对齐,并且易于编辑和修改。在 Word 中制作流程图将会花费更多的时间。
在本书的所有示例中,我们将使用www.draw.io,但如果你有更喜欢的其他流程图软件,那也可以。我们使用 draw.io,因为它免费、在线且易于使用。
创建对话流程图
现在我们已经了解了对话流程图的组成部分,让我们来创建一个吧。我们将使用之前用过的相同的比萨订购对话。
从对话的起始点开始。为用户的第一次话语创建一个符号。这条来自用户的第一条信息非常重要,因为它将触发一个意图:
话语触发意图
现在已经触发了OrderPizza意图,我们的聊天机器人可以开始询问用户他们想要订购的比萨。我们将首先询问他们想要什么配料,他们回复“夏威夷”:
开始意图
之后,我们想要记住他们选择了夏威夷作为配料,因此我们需要将这个信息存储为一个槽位。我们将信息存储在槽位名称下,在这种情况下,它将是topping = Hawaiian。除了存储槽位,我们还需要继续对话,询问他们想要多大份的比萨:
存储槽位值
在收到用户的回复后,我们将大小存储在槽位中,并继续到下一个阶段。我们重复询问、回答、槽位的过程,以确定用户想要的比萨大小。
现在我们已经拥有了所有需要的信息,我们需要告诉比萨店有人订购了一份中份夏威夷比萨。为此,我们将使用动作符号,并确保包括所需的槽位。当我们将槽位信息包含在任何内容中时,通常将其写成括在花括号中的槽位名称。
除了告诉比萨店订单信息,我们还需要让用户知道他们的订单已经下单,并告诉他们何时取货。同样,我们使用括在花括号中的槽位名称来定制包含槽位信息的消息:
完整的比萨订购流程图
用户故事
用户故事是聊天机器人设计和测试中的关键工具。它们是关于虚构用户的故事,包括他们的需求以及他们如何与你的机器人互动。当我们创建用户故事时,它需要尽可能接近真实用户。它们应该基于真实用户或可能使用你的聊天机器人的用户类型。如果你有希望将聊天机器人针对的目标客户,你可以创建数据驱动的用户故事。
要创建用户故事,首先描述用户以及他们为什么与你的机器人交谈。以下是一些披萨订购机器人的示例:
-
克里斯,一位 23 岁的实习生。他希望在手机上订购披萨,以便在下班回家的路上取。
-
克莱尔,一位 35 岁的银行经理。在看电视的同时使用 Alexa 订购披萨。
用户描述不需要非常长或复杂,但它们必须代表机器人将遇到的用户类型。
对于每个用户,请通过流程图模拟机器人与该用户对话。这样做的目的是在我们开始构建机器人之前测试你的流程图。如果你发现某个部分的对话不顺畅,现在修改它将节省你以后的时间。
对于像这样的简单例子,所有对话之间可能没有太大区别,但随着我们创建更复杂的流程图,用户故事将变得更加重要。
最佳实践
任何人都可以制作聊天机器人。经过一点练习,你可以在几小时内构建一个简单的机器人。构建这种机器人的问题是,随着它们的范围和复杂性的增长,它们很容易变得难以管理。简单的更改可能导致数小时甚至数天的错误修复,这可能会破坏你最终让聊天机器人工作时的喜悦。
为了避免与一个无序且复杂的聊天机器人一起工作的恐怖经历,有一些最佳实践。遵循这些实践将减少你以后的头痛,并允许你快速轻松地添加新功能。
处理错误
在用户与聊天机器人的整个对话过程中,有很多可能出现错误的地方。错误可能发生在语句没有被理解、API 返回错误或开发者的代码中存在错误时。每个错误都需要被捕捉并妥善处理。我们将在第四章“将你的 Alexa 技能连接到外部 API”中介绍如何使用try/catch和to()方法来捕捉这些错误。
错过的语句
最常见的错误可能是当语句没有被理解或不是聊天机器人所期望的。这可能是因为用户输入错误,拼写错误,或者只是输入了你没有考虑到的响应。Alexa 和 Lex 都使用自然语言理解(NLU)来尝试减少拼写错误和不同响应的错误,但它们并不完美。
由于不理解用户的表述是一个如此常见的错误,Lex 和 Alexa 也都有系统来处理它们。这包括当聊天机器人不理解用户刚刚说了什么时可以发送给用户的失败短语。确保这一点设置正确,并且你要求用户再次尝试或选择不同的选项:
失败的表述
Alexa 和 Lex 还有一个功能,可以存储所有它无法理解表述的时间。使用这个列表,你可以添加更多样本表述来帮助聊天机器人理解更多。定期这样做可以极大地提高用户满意度,同时也有助于你了解用户如何与你的机器人互动。
外部 API
每次你处理代码之外的事情时,都存在出错的风险。这可能是一个第三方 API、你自己的 API,或者仅仅是向数据库的查询。你应该始终编写这些请求,以便如果请求返回错误,你能完全处理它。这意味着记录错误是什么以及它发生在哪里,并确保在发生错误时聊天机器人仍然可以工作。
确保在发生错误时聊天机器人仍然可以工作是非常重要的,因为没有人愿意和一个在对话中途停止说话的聊天机器人交谈。为了确保这种情况不会发生,你有三个选择:为每个外部调用创建错误消息,让所有错误流到一个非常低级的错误处理器,该处理器发送一个通用的“我们遇到了错误”消息,或者两者的组合。想法是使用自定义消息来处理可能发生的每个错误,但随着你的聊天机器人变得更大、更复杂,这可能会变得非常耗时。
处理错误的有效方法之一是创建一个低级错误处理器,除非提供了特定的错误消息,否则传递一个通用的错误消息。这让你在需要时能够确切地让用户知道出了什么问题,但同时也节省了你创建大量类似错误消息的时间:
try {
let result = AccessPeopleAPI();
if (result === null || typeof result !== 'number'){
throw 'I've failed to get the number of people';
}
return 'We have ' + result + ' people in the building';
} catch (error) {
console.log(error || 'The system has had an error');
return error || "Unfortunately I've had an error";
}
代码中的错误
没有开发者愿意承认他们的代码中存在错误,但如果你创建的不仅仅是简单的聊天机器人,那么很可能会有。处理这个问题有不同的方法,从为每个函数编写测试,到彻底的端到端测试,到使用try/catch将一切包裹起来。这本书将让你决定如何处理这些错误,但期望代码无错误是一个非常危险的道路。
无论你想要如何阻止错误进入你的代码,你都需要在遇到它们时处理它们。这就是为什么拥有一个低级错误处理器也可能很有用。你可以用它来捕获代码中发生的错误,就像你处理外部 API 的错误一样。
语调
聊天机器人最棒的地方之一是它们具有对话性和更接近人类的感受。正因为如此,你需要给你的机器人赋予一个个性,并且需要根据聊天机器人的目的和与之互动的用户来调整这个个性。
拥有一个使用俚语的银行聊天机器人可能会让用户对聊天机器人的信任度降低,而拥有一个使用大量非常正式或过时语言的服装销售聊天机器人可能同样令人反感。
尽量设计聊天机器人使用的语言与您的品牌形象保持一致。如果您没有品牌形象,可以通过采访您的员工和客户来构建一个。利用这些采访创建一个与客户紧密相关的人物(类似于用户故事)。
确定合适的用例
聊天机器人很棒!能够为用户提供一种全新的交互方式是一种非常美妙的感觉,以至于你想要为每一件事都制作一个聊天机器人。不幸的是,聊天机器人并不适合所有情况,在实施之前需要仔细考虑一些事情。你需要考虑用户是否愿意与聊天机器人讨论某些事情,以及聊天机器人将如何进行回应。
考虑机器人将如何进行交流对于基于语音的聊天机器人尤为重要,因为聊天机器人所说的每一句话都将通过扬声器发送给周围的人听到。这对访问您的银行信息、阅读您的电子邮件或处理任何其他个人信息的人工智能聊天机器人来说可能会很糟糕。在设计您的 Alexa 对话时,问问自己你是否希望 Alexa 告诉你所有的朋友和同事你的医生预约结果,或者朗读你伴侣关于他们当晚计划的电子邮件。
设计用于交付方式的信息
由于信息交付方式与现有方式(电子邮件、网站和印刷媒体)非常不同,你还需要考虑用户会有什么感受。例如,当创建一个报纸聊天机器人时,让 Alexa 花 15 分钟读完整份报纸或 Lex 发送一大块文本可能并不太友好。相反,你可以将信息分解成更小的部分,或者提供信息的简要概述。
在提供优质信息和说得太多的聊天机器人之间可能有一条很细的界限。确保信息量是以适合最终交付方式的方式设计的。
亚马逊 Alexa 和 Lex
Alexa 和 Lex 是亚马逊开发的一对工具,旨在改变用户与技术互动的方式。它们是平台,允许开发者创建极其强大的对话界面,而无需深入研究深度学习、自然语言处理或语音识别。
它们是 Amazon Web Services (AWS) 组的一部分,因此与其它服务配合得非常好,使开发过程更加顺畅和一致。
Alexa 和 Lex 之间的主要区别在于,Alexa 平台允许开发者为 Alexa 兼容设备创建技能,而 Lex 允许开发者创建通用的文本或语音聊天机器人。
Amazon Alexa
Amazon Alexa 是一种基于语音的聊天机器人,它是亚马逊 Echo 系列产品的智能大脑。用户可以通过向他们的 Alexa 账户添加 技能 来定制他们的 Echo 体验,就像在智能手机上添加应用程序一样。这些技能可以从 Alexa 技能商店下载,有数千种可供选择。
与应用程序类似,每个这些技能都设计用来执行单一任务,无论是引导你烹饪食谱,指导你完成早晨的锻炼,还是仅仅讲笑话。
Alexa 于 2014 年 11 月发布,并越来越受欢迎。到 2017 年底,亚马逊已经售出了数千万台与 Alexa 连接的设备。这使得到 2018 年 2 月,Alexa 设备在虚拟助手市场中占据了 55% 的份额。
Amazon Lex
Amazon Lex 是一种聊天机器人服务,允许开发者创建基于文本或语音的聊天机器人,利用亚马逊开发的深度学习、自然语言理解和语音识别的惊人力量。Lex 与 Alexa 的不同之处在于它可以集成到不同的设备和服务中。
Lex 最常被用作基于文本的聊天机器人。用户与基于文本的聊天互动的方式有很多种,Lex 可以与其中很多种集成。开发者可以通过 Lex 平台内置的集成创建 Facebook Messenger 机器人、Slack 机器人、Kik 机器人和 Twilio 短信机器人。
Lex 还可以通过 AWS-SDK 触发,这意味着它可以放在端点后面。这意味着开发者可以设置一个系统,他们向 API 发送消息,然后从 Lex 获取响应。这使您可以从几乎任何系统中发送消息到 Lex。这可以用来在网站上创建聊天窗口,在几乎任何消息服务上创建聊天机器人,或者将其与任何可以连接到互联网的系统集成。
使用 Amazon Transcribe 进行语音识别,您可以创建一个与 Alexa 非常相似的系统。这已经在呼叫中心中被非常有效地使用,允许客户与虚拟服务代表交谈,而不是仅仅等待有人类服务代表可用。这意味着许多呼叫者可以在不与人类交谈的情况下获得他们所需的信息。这意味着如果机器人可以解决你的问题,可以减少获得答案的时间,同时减少通过呼叫中心的人数,减少通话等待时间。
摘要
在本章中,我们学习了聊天机器人的组成部分——意图、槽位和话语,以及它们各自扮演的角色。
接下来,我们学习了如何设计对话流程,从理想的对话开始,将其转换为对话流程图。使用流程图软件,我们创建了对话流程图,以帮助可视化我们的聊天机器人如何与用户互动。
我们讨论了创建聊天机器人的最佳实践,从处理错误到设计对话以在聊天机器人上良好工作,从语气到好的聊天机器人用例。
本章的最后部分介绍了 Amazon Alexa 和 Amazon Lex。我们了解了这两种类型聊天机器人的相似之处和不同之处,以及它们的一些背景信息。
问题
-
聊天机器人的三个主要组成部分是什么?
-
列出两个 Alexa 和 Amazon Lex 共有的特点。
-
列出 Alexa 和 Amazon Lex 之间的两个不同点。
-
设计对话流程时,你应该从哪里开始?
-
“语气”是什么意思?
-
聊天机器人中可能发生的三种主要错误类型是什么?
第二章:AWS 和 Amazon CLI 入门
亚马逊网络服务(AWS)是亚马逊为开发者提供的所有工具和服务的集合。提供了大量的服务,从服务器托管到机器学习,从游戏流媒体到数字营销。每个服务都设计得非常好,能够完成一项任务,但最大的好处是每个服务之间协作得非常好。
在本章中,我们将创建一个 AWS 账户并探索 AWS 控制台。一旦我们设置了账户,我们将了解 Lambda 函数,创建我们自己的一个。这将从一个非常简单的 Lambda 开始,但随着我们继续阅读本书的其余部分,我们将增加其功能。
本章的下一段将讨论我们可以编辑 Lambdas 的不同方法以及每种方法的优缺点。
最后的部分将介绍如何使用 AWS CLI、构建脚本和 Git 创建一个出色的本地开发环境。到本章结束时,我们将拥有一个本地环境,我们可以轻松地部署我们的 Lambda,而无需进入 AWS,并且可以将所有工作备份到远程 Git 仓库。
本章将涵盖以下内容:
-
创建和配置 AWS 账户
-
在 AWS 控制台中创建 Lambda
-
编辑 Lambdas 的三种方法
-
使用 AWS CLI、构建脚本和 Git 创建一个出色的本地开发环境
技术要求
在本章中,我们将创建几个 Lambda 以及创建一个构建脚本。
所有代码都可以在 bit.ly/chatbot-ch2 找到。
创建账户
要访问所有这些服务,您需要创建一个免费的 AWS 开发者账户。访问 aws.amazon.com 并点击创建免费账户。要创建账户,您需要遵循注册流程。这个过程非常彻底,需要您输入付款详情并接收自动电话呼叫。这个过程是为了验证您是一个真正的用户。
一旦您创建了 AWS 账户,您就可以通过 Amazon 控制台(console.aws.amazon.com)访问所有服务。控制台页面上有大量有用的信息。构建解决方案和学习构建是关于如何使用一些服务的教程和信息。
设置您的区域
对于这本书,您需要将您的区域设置为弗吉尼亚州北部或爱尔兰。Lex 目前(2018 年 4 月)在这两个区域可用。
AWS 有一个概念,即区域,这些区域是全球各地亚马逊的云服务中心的位置。对于大多数应用程序,每个区域都与所有其他区域分开。将服务部署到它们将被使用的位置附近是最佳实践。如果您的客户位于美国西海岸,那么选择北加州或俄勒冈州将是最佳选择,而选择爱尔兰则不是很好的选择。每次他们使用您的产品时,他们的数据都必须绕地球半圈再回来。
对于区域,还有一个考虑因素,那就是并非每个区域都是平等的。一些区域有更大的工作容量,而一些区域甚至没有所有服务。
在 AWS 中导航
在 AWS 中导航已被设计得尽可能简单。在每一页的顶部都有一个横幅,其中包含控制台主页的链接、包含所有可用服务的下拉菜单、账户和位置设置以及支持菜单:
AWS 菜单和服务下拉菜单
在 AWS 期间,您将大量使用主页链接和服务下拉菜单这两个选项。当您正在编辑 Lambda 并需要检查 DynamoDB 中的表名,或者您正在为 EC2 创建 API 网关时,您将频繁地在服务之间切换。
您还可以使用图钉图标将您最喜欢的服务固定到您的横幅上。这使得在您最常用的服务之间切换变得更加快捷。
创建 Lambda
AWS Lambda 函数非常出色!它们是托管在 AWS 上的函数,可以通过许多不同的方式触发。Lambda 函数是无服务器的,这意味着您不需要运行服务器就可以使用它们。这使得设置和使用变得更加快速和简单。
AWS Lambda 最好的部分之一是您只为 Lambda 函数运行的时间付费。有什么东西每小时只运行一次,只持续两秒钟?您每天只需支付 48 秒!与全天候运行的 AWS EC2 服务器或您自己的私有服务器相比,这简直是疯狂。
今天,我们将创建一个 Lambda 函数,并查看三种最佳的工作代码方式。
一旦您设置了 AWS 账户,就有几种方法可以创建一个新的 Lambda 函数。我们将从使用 AWS 控制台开始。
AWS 控制台
在 AWS 控制台中,您可以在服务 | 计算 | Lambda 中找到 AWS Lambda,这将带您进入 Lambda 控制台:
AWS 计算服务
如果这是您第一次使用 Lambda,您将看到这个界面。点击创建函数按钮开始设置您的第一个函数。
您将进入设置页面,在那里您可以配置函数的一些方面(名称、运行时、角色)。您可以从蓝图或无服务器应用程序仓库创建 Lambda,但在这个例子中,我们将从零开始选择作者。
设置 Lambda
输入您函数的名称(这必须对您的用户或子账户是唯一的),选择您的运行时(我们将使用 Node.js 8.10),并选择“从模板创建新角色”。给这个新角色一个相关的名称,例如lambdaBasic或NoPolicyRol,并留空策略模板。当我们创建更复杂的 Lambda 时,我们必须创建具有策略和权限的角色:
带有新角色的新 Lambda
编写您的 Lambda 函数代码
一旦您创建了 Lambda,您将被发送到 Lambda 管理控制台中的函数编辑器。这个页面上有很多事情,但我们专注于标题为“函数代码”的部分。
当您首次创建 Lambda 时,它已经包含了一个非常基本的函数。这很好,因为它为您构建函数提供了一个起点。由于我们使用 Node.js 8.10 作为我们的运行时,将有一个名为event的单个参数,然后我们将返回我们的答案。
作为基本示例,我们将创建一个 Lambda,它接受您的姓名和年龄,并告诉您您的最大心率是多少。这可以比我们即将要做的方式更高效,但这更多的是作为一种在 Lambda 中演示一些技术的方法。
首先,我们将使用console.log输出事件并提取name和age。我将使用 ES6 解构,但您也可以使用常规变量声明来完成此操作:
exports.handler = async (event) => {
console.log(event);
let { name, age } = event;
// same as => let name = event.name; let age = event.age
return 'Hello from Lambda!'
};
现在我们已经从事件中获得了name和age,我们可以将它们传递到一个将它们转换为字符串的函数中:
const createString = (name, age) => {
return `Hi ${name}, you are ${age} years old.`;
};
如果您之前没有见过这种字符串,它们被称为模板字符串,它们比之前的字符串连接更整洁。反引号开始和结束字符串,您可以使用${data}插入数据。
现在,我们可以将'Hello from Lambda!'更改为createString(name, age),我们的函数将返回我们的新字符串:
exports.handler = async (event) => {
console.log(event);
let { name, age } = event;
// same as => let name = event.name; let age = event.age
return createString(name, age);
};
确保通过点击 Lambda 工具栏右上角的醒目橙色“保存”按钮来保存您的更改:
Lambda 工具栏
要测试这一点,我们可以在 Lambda 工具栏中点击“测试”。
当我们点击“测试”时,会弹出一个“配置测试事件”窗口。我们可以使用它来决定在事件有效负载中发送什么。给您的测试起个名字,然后我们可以配置我们的数据。对于这个 Lambda,这非常简单,只是一个具有"name"和"age"键的对象。这是我的:
{
"name": "Sam",
"age": "24"
}
您可以将您的值设置为任何您想要的,然后点击配置屏幕底部的“保存”。现在,测试按钮左侧的下拉菜单已更改为您的新的测试名称。要运行测试,只需点击测试按钮:
Lambda 结果
如果你的响应仍然是 'Hello from Lambda!',那么请确保你已经保存了你的函数并再次运行测试。正如你所见,我得到了“Hi Sam, you are 24 years old.”的响应,这正是我们预期的。此外,我们还得到了一个 RequestID 和函数日志。记得我们之前在代码中添加了那个console.log(event)吗?你现在可以看到,对象{ name: 'Sam', age: '24' }已经被记录下来。如果你想查看更多的日志或之前 Lambda 调用的日志,它们都存储在 CloudWatch 中。要访问 CloudWatch,你可以在服务中搜索它,或者通过在 Lambda 控制台顶部选择“监控”然后点击“在 CloudWatch 中查看日志”来访问。
在“监控”中也有一些有趣的图表,可以告诉你你的函数工作得有多好:
在 CloudWatch 中查看日志
Lambda 函数可以像我们这样做,创建在一个文件中,但它们也可以与多个文件一起工作。当你的 Lambda 执行非常复杂的任务时,你可以将每个部分拆分到自己的文件中,以改善组织和可读性。
我们将创建一个名为hr.js的新文件,并在其中创建并导出另一个函数。这个函数将根据你的年龄计算你的最大心率。通过在文件夹菜单中右键单击并选择“新建文件”来创建新文件,并将其命名为hr.js。打开该文件,我们将创建一个calculateHR函数:
module.exports = {
calculateHR: (age) => {
return 220 - age;
}
}
现在,回到我们的index.js文件,我们需要导入我们的hr.js文件并调用calculateHR函数:
const HR = require('./hr');
exports.handler = async (event) => {
console.log(event);
let { name, age } = event;
return createString(name, age);
};
const createString = (name, age) => {
return `Hi ${name}, you are ${age} years old and have a maximum heart rate of ${HR.calculateHR(age)}.`;
};
当我们再次运行最后的测试时,我们得到了一个新的响应:"Hi Sam, you are 24 years old and have a maximum heart rate of 196."。这本来可以做得更加有效,但这样做更多的是为了向你展示一些你可以在 Lambda 函数中编写代码的方式。
触发 Lambda
在你的第一个 Lambda 中,我们测试它的方式是通过触发一个测试。为了使 Lambda 更有用,我们需要能够从不同的地方触发它。
在 Lambda 控制台顶部附近,有一个“设计器”部分。这个部分允许你更改 Lambda 与其他服务交互的方式,因此也影响用户。在部分的左侧是一个“添加触发器”菜单,其中包含一系列选项。每个选项都是一个可以设置以触发函数的系统服务。这些并不是触发 Lambda 的所有方式,我们将在未来使用其他方法。
对我们来说,最重要的是 API 网关和 Alexa 技能套件,但其他触发器对其他项目也非常有用。API 网关是将 Lambda 暴露给外部世界的方式。你创建一个 API 端点,任何人都可以访问该端点,数据将由你的 Lambda 处理。我们将在第七章发布您的聊天机器人到 Facebook、Slack、Twilio 和 HTTP 中创建一个 API。Alexa 技能套件是用于构建 Alexa 技能的服务,这些技能也可以触发 Lambda,我们将在下一章中这样做。
与 Lambda 一起工作的方法
Lambda 的一大优点是你可以选择如何编写和编辑它们。主要有三种方法可以实现:
-
Lambda 控制台
-
Cloud9
-
在你的本地机器上
我将涵盖所有三种方法,并讨论每种方法的优缺点。
方法 1 – Lambda 控制台
这就是我们创建的第一个 Lambda 函数的方式。在 Lambda 控制台中,我们有一个基本的编辑器。它基于 Cloud9 IDE,非常适合简单的 Lambda 函数。
优点:
-
它是一个好的编辑器
-
你可以通过你的 AWS 控制台从任何电脑访问它
缺点:
-
它似乎不是很稳定。有时它不允许你保存,所以你必须将所有的工作复制到本地文件,重新加载页面,然后将工作复制回来。我希望这个问题很快就能得到解决!
-
它没有命令行界面。这意味着你不能仅使用这种方法安装
npm软件包。 -
你需要互联网访问才能在 Lambda 上工作。
方法 2 – Cloud9 编辑器
亚马逊最近收购了 Cloud9,一个在线开发平台。它运行了一个与 AWS 平台集成的非常基础的 Ubuntu 版本。
在 AWS 控制台中搜索 Cloud9,进入页面,然后选择创建环境。从这里你可以给你的环境命名,然后进入下一步。
这里你可以选择你想在这个环境中运行什么。很棒的是,t2.micro 是免费层可用的,所以如果你在免费层,你可以使用这种方法而不必支付任何费用。我从未需要比 t2.micro 更强大的东西。
完成设置过程,你将进入你新的 Cloud9 环境!
这很酷,因为你可以从你的 Cloud9 环境内部访问所有的 Lambda 函数。点击 AWS 资源,在远程函数下,你会找到所有的函数。点击你想要编辑的 Lambda 函数,然后点击上面的下载图标将其导入到你的环境中:
访问远程 Lambda
一旦完成,它就会像你在本地工作一样。
完成后,只需从本地列表中选择你一直在工作的函数,然后点击上传按钮。几秒钟内,所有更改都会生效。
优点:
-
再次强调,这一切都是远程的,所以您不需要担心忘记提交工作或保存到 U 盘,如果您在多台机器上工作。
-
访问您的函数并重新上传它们非常简单。这是这种方法中最好的部分。
-
您现在拥有一个集成的终端,允许您安装
npm软件包并使用终端完成所有其他您想做的事情。
缺点:
-
它仍然存在与 Lambda 控制台编辑器相同的不稳定问题。我多次尝试保存函数但未能成功,不得不复制到本地,刷新,然后重新复制到 Cloud 9。这会很快变得非常烦人。
-
您需要互联网连接来处理您的 Lambda。
方法 3 – 本地编辑
我将稍微改变一下做法。我会列出基本使用的优缺点,然后向您展示如何使其变得更好。
优点:
-
本地编辑是大多数开发者将采用的工作方式。我们可以使用我们喜欢的 IDE、扩展和配色方案。
-
它很稳定(只要您的电脑是稳定的)。
-
您可以在没有互联网连接的情况下处理您的 Lambda。
缺点:
-
没有花哨的按钮来获取和上传您的工作到 AWS。
-
您的工作是本地的,因此拥有多个用户或同时在多个设备上工作会更复杂。
为了使这种方法成为完美的系统,我们将利用 Amazon CLI 和 Git。设置我们所需的一切大约需要 15 分钟!
创建最佳本地开发环境
正如我们已经看到的,本地编写 Lambda 有一些非常出色的方面,这就是为什么我们将在这本书中一直使用它。我们将选择一个 IDE,安装 NodeJS 和 NPM,然后在为 Lambda 设置文件夹结构之前。最后,我们将使用 AWS CLI 和 Git 创建一些工具,以消除本地工作的正常缺点。
选择 IDE
您使用哪个 IDE 取决于个人喜好;市面上有一些非常出色的 IDE,包括 Atom、Komodo 和 Brackets。如果您已经有了个人偏好的 IDE,那么您可以使用它,但所有示例都将使用 Visual Studio Code (VS Code)。
VS Code 是由微软开发的开源 IDE,适用于 macOS、Linux 和 Windows。它内置了对 JavaScript、Node 和 TypeScript 的支持,并且您可以从扩展库中安装扩展。这些扩展是使用 VS Code 的最大优势之一,因为它们允许您对您的体验进行大量自定义。从彩色缩进来代码检查,从更好的图标到自动格式化器。它们从“有点有趣”到“使您的生活变得容易得多”不等。
除了扩展之外,VS Code 还具有更多出色的功能,如集成终端、Git 集成和内置调试器。如果您之前没有尝试过,我建议您尝试一周,看看它与您当前选择的 IDE 相比如何。
要安装 VS Code,只需访问code.visualstudio.com并下载适合你操作系统的版本。
安装 Node 和 NPM
Node 是允许我们在服务器上运行 JavaScript 代码的运行时。在过去的几年中,它获得了巨大的青睐,几乎在技术领域的每个行业中都在运行应用程序。它也是可以在 Lambda 函数中选择的一种运行时。
除了 Node 之外,我们还获得了Node 包管理器(npm),这是世界上最大的开源库生态系统。这对我们来说是个好消息,我们将在本书中用到其中的一些包。
要安装 Node 和npm,你可以从nodejs.org下载安装包,或者通过包管理器进行安装。确保你安装至少版本 8.11.1,因为我们将在我们的工作中使用async/await,这至少需要版本 8。一旦安装了所有东西,你可以通过输入node -v来测试它是否正常工作;你应该得到类似v8.11.1的结果。你还可以通过输入npm -v来测试npm。
文件夹结构
为了正确组织所有的 Lambdas,将它们都存储在单个文件夹中是个好主意。这将允许一个脚本创建和更新所有的 Lambdas。在这个主文件夹内,拥有包含 Lambdas 组的子文件夹绝对是个好主意。你可以非常快速地构建大量的 Lambdas。
设置 AWS CLI
为了将我们的工作直接上传到 AWS,我们可以使用 AWS CLI。这允许我们从命令行管理我们的 AWS 服务并创建脚本来自动化常见任务。对我们来说,最重要的 CLI 命令是那些允许我们创建和更新 Lambdas 的命令。通过自动脚本,我们现在能够快速轻松地创建和部署 Lambdas,解决了本地编辑的第一个限制。
要使用 AWS CLI,我们首先需要设置它。你可以在终端中输入npm install -g aws-cli来安装它。
现在我们需要为我们的 CLI 设置一个用户。登录到你的 AWS 控制台并导航到或搜索IAM。点击添加用户,这样我们就可以为 CLI 设置一个用户。你需要为用户命名,所以选择像cli-user这样的名字,这样它就很容易被识别。选择程序访问,这将允许我们远程代表用户操作,然后点击下一步:权限。
在权限屏幕上,选择直接附加现有策略并选择 AdministratorAccess。这将允许你通过 CLI 做任何你想做的事情。如果你想的话,可以为此用户设置更严格的政策,或者如果你正在将账户访问权限授予另一个人。
在你最终看到访问密钥之前,还有一个屏幕。复制你的访问密钥并打开一个终端。运行命令aws configure,它将要求你提供四件事:
AWS Access Key ID [None]: "Your Access Key ID
"AWS Secret Access Key [None]: "Your Secret Access Key"
Default region name [eu-west-1]:
Default output format [json]:
前两个可以在用户创建的最后页面找到。第三个必须是之前选择的区域(eu-west-1 或 us-east-1),最后一个可以保留为默认值。
使用 AWS CLI 创建 Lambda
现在我们已经设置了 CLI,我们可以使用它来使我们的生活更加轻松。要创建一个新的函数,你需要有一个包含一个 index.js 文件并包含基本 Lambda 代码的文件夹。在终端中进入该文件夹,现在你可以运行这些命令:
zip ./index.zip *
aws lambda create-function \
--function-name your-function-name \
--runtime nodejs8.10 \
--role your-lambda-role \
--handler index.handler \
--zip-file fileb://index.zip
将 your-lambda-role 替换为你之前创建的角色 ARN。你可以通过回到 AWS 中的 IAM 服务并选择 Roles,然后点击你的 Lambda 角色来找到它:
查找您的角色 ARN
当你运行这个脚本时,它将返回一个包含有关新创建的 Lambda 的信息的 JSON 块。
如果你编辑你的 index.js 代码并想要更新 Lambda,那么你需要运行三个命令:
rm index.zip
zip ./index.zip *
aws lambda update-function-code \
--function-name your-function-name \
--zip-file fileb://index.zip
使用这些脚本,你现在可以在本地编写代码并将其部署到 AWS。这很好,但它可以改进,这正是我们接下来要做的。
AWS CLI 构建脚本
这些 CLI 命令很好,但每次你想上传新的 Lambda 版本时都要全部输入一遍,这会变得很烦人。我们将使用一个构建脚本来自动化这些命令并添加一些额外功能。
这个脚本将是一个 bash 脚本,所以如果你正在运行 macOS 或 Linux,那么它将原生工作。如果你在 Windows 上,那么你需要在你的机器上安装一个 bash 终端。
为了使这个脚本正常工作,你需要有一个如以下截图所示的文件夹结构。每个 Lambda 都有一个包含相关文件的文件夹:
文件夹结构
我们将创建一个脚本,它不仅运行基本的 AWS CLI 命令,还进行额外的检查,运行 npm install,并输出有关进度的详细信息。这个脚本将通过运行 ./build lambda-folder 来执行。
在你的 lambdas 文件夹中创建一个名为 build.sh 的新文件。或者,你可以从 bit.ly/chatbot-ch2 下载此文件,并按照说明查看它是如何工作的。
首先,我们将检查命令中是否恰好传递了一个参数。"$#" 表示参数的数量,-ne 1 表示不等于 1:
if [ "$#" -ne 1 ]; then
echo "Usage : ./build.sh lambdaName";
exit 1;
fi
接下来,我们需要进入所选 Lambda 的文件夹并检查该文件夹是否存在:
lambda=${1%/}; // # Removes trailing slashes
echo "Deploying $lambda";
cd $lambda;
if [ $? -eq 0 ]; then
echo "...."
else
echo "Couldn't cd to directory $lambda. You may have mis-spelled the lambda/directory name";
exit 1
fi
我们不希望在确保安装了所有依赖项之前上传 Lambda,所以我们确保运行 npm install 并检查它是否成功:
echo "npm installing...";
npm install
if [$? -eq 0 ]; then
echo "done";
else
echo "npm install failed";
exit 1;
fi
设置步骤的最后一步是检查 aws-cli 是否已安装:
echo "Checking that aws-cli is installed"
which aws
if [ $? -eq 0 ]; then
echo "aws-cli is installed, continuing..."
else
echo "You need aws-cli to deploy this lambda. Google 'aws-cli install'"
exit 1
fi
一切设置妥当后,我们可以创建新的 ZIP 文件。在创建新的.zip文件之前,我们将删除旧的文件。这次创建新文件比以前稍微复杂一些。我们排除了.git、.sh和.zip文件,以及排除test文件夹和node_modules/aws-sdk文件。我们可以排除aws-sdk,因为它已经安装在了所有 Lambda 函数上,而且我们不希望上传 Git 文件、bash 脚本或其他.zip文件:
echo "removing old zip"
rm archive.zip;
echo "creating a new zip file"
zip archive.zip * -r -x .git/\* \*.sh tests/\* node_modules/aws-sdk/\* \*.zip
现在,我们剩下的唯一任务是将其上传到 AWS。我们希望尽可能简化这个过程,所以我们将尝试创建一个新的函数。如果创建过程中出现错误,我们将尝试更新该函数。这可以作为一个get操作然后是create或update操作,但create操作失败实际上比get操作更快:
echo "Uploading $lambda to $region";
aws lambda create-function --function-name $lambda --runtime nodejs8.10 --role arn:aws:iam::095363550084:role/service-role/Dynamo --handler index.handler --zip-file fileb://index.zip --publish
if [ $? -eq 0 ]; then
echo "!! Create successful !!"
exit 1;
fi
aws lambda update-function-code --function-name $lambda --zip-file fileb://archive.zip --publish
if [ $? -eq 0 ]; then
echo "!! Update successful !!"
else
echo "Upload failed"
echo "If the error was a 400, check that there are no slashes in your lambda name"
echo "Lambda name = $lambda"
exit 1;
fi
要使脚本可执行,我们需要更改文件的权限。为此,我们运行chmod +x ./build.sh。
现在,你只需要导航到 Lambda 函数所在的主文件夹,并运行./build.sh example-lambda。如果你有嵌套在组中的 Lambdas 文件夹,那么进入组文件夹并运行../build.sh lambda-in-group。
如果你想,你可以将构建脚本移动到你的主目录中,使其执行命令为~/build.sh lambda-function,这仅在你在单独的文件夹或高度嵌套的文件夹中有 Lambdas 时才有用。
这个脚本可以被修改和扩展,以包括特定区域的上传、批量上传多个 Lambda 函数、Git 集成以及更多功能。
Git
阅读这篇文档的很多人可能已经使用 Git 了。这有原因——它使生活变得更简单。为所有 Lambda 函数创建一个 Git 仓库是与开发团队或个人在多台机器上协作的绝佳方式。
在你的系统上安装 Git 的方式取决于你的操作系统。Linux 用户可以通过命令行安装 Git,macOS 用户可以使用 Homebrew 或下载安装,Windows 用户必须通过下载安装 Git。有关如何为你的系统安装的详细信息,可在git-scm.com上找到。
一旦安装了 Git,在终端导航到你的 Lambda 文件夹。当你在该文件夹内时,运行git init以创建一个空仓库。当你用 VS Code 打开 Lambda 文件夹时,现在你会在 Git 符号上看到一个悬停的数字。这意味着自你上次 Git 提交以来,你已经编辑了这么多文件。
将更改提交到 Git 就像是对文件夹中所有工作的快照并保存它。这很有用,因为它允许你看到你的工作是如何随时间变化的。要提交你的工作(进行快照),你有两种选择。
你可以使用 VS Code 的 Git 集成来创建提交。点击带有数字悬停的 Git 符号。当你点击它时,它会打开更改菜单,显示自你上次提交以来你更改的所有文件,或者如果你这是第一次提交,显示所有文件。要提交更改的工作,请在顶部的消息框中输入一条消息,然后点击上面的勾号:
使用 VS Code 集成进行 Git 提交
如果你想要使用命令行,你需要输入git add *将所有更改的文件添加到你即将进行的提交中。然后输入git commit -m "我的第一次 git 提交!"。引号之间的文本是你的提交信息。
在这两种情况下,你的提交信息应该描述你在这次提交中做出的更改。你的第一次提交可能将是“创建我的第一个函数”。
Git 的另一个巨大优势是你可以轻松创建远程 Git 仓库。这些数据中心将存储你的 Git 提交,这样你就可以从世界任何地方访问它们。主要两个是 GitHub 和 Bitbucket,但还有很多。它们都有免费版本,但 GitHub 只有公共仓库是免费的,所以任何人都可以看到你的工作。
一旦你注册了账户并创建了一个仓库,你将获得一个用于它的 URL。在你的终端中,导航到你的文件夹并运行git add remote origin <your url>。这意味着你可以从你的本地机器发送工作到你的在线仓库。只需输入git push origin master将你的最新提交发送到你的在线仓库。获取它们同样简单;只需输入git pull origin master,你的本地代码将更新以添加你在仓库中做出的任何更改。
这对于团队来说非常棒,因为它允许你们每个人在自己的机器上工作,但又能获取到彼此的更改。
本地开发设置
总结你的新本地开发设置,你拥有以下内容:
-
强大的 IDE - VS Code
-
由 Amazon CLI 驱动的构建脚本用于创建和更新函数
-
使用 Git 远程存储你的工作并允许更轻松的团队合作
摘要
在本章中,我们学习了亚马逊网络服务并创建了一个账户,这使我们能够访问所有这些服务。
我们使用 Lambda 控制台创建了我们的第一个 Lambda 函数,并将其升级为使用多个函数、模板字符串和从其他文件中引入代码。
接下来,我们讨论了创建 Lambda 的三个主要方法,即 Lambda 控制台、Cloud9 和本地开发。我们还探讨了每种方法的优缺点。
最后,我们使用了 AWS-CLI 和 Git 来使我们的本地开发设置更加强大。我们使用的构建脚本允许我们创建和更新 Lambda 函数,而无需进入 AWS。
在下一章中,我们将学习如何使用 Alexa 技能套件构建我们的第一个 Alexa 技能。
问题
-
列出创建和编辑 Lambda 函数的三个主要方法。
-
AWS 代表什么?
-
基本本地开发设置的两个主要局限性是什么?
-
我们使用哪些工具来改进我们的本地开发设置?
第三章:创建您的第一个 Alexa 技能
本章将向您介绍构建 Alexa 技能所需的过程,我们将一起创建我们的第一个 Alexa 技能。我们将学习如何构建和测试我们的技能,以确保一切正常工作。
然后,我们将创建第二个 Alexa 技能,它将与用户进行更真实的对话。这个技能将通过一系列问题收集一组信息,我们将使用这些信息来决定哪辆车最适合用户。这还将涵盖从远程存储访问数据。
本章最后我们将介绍部署您的技能,让您能够发布您的技能供全世界使用。
本章将涵盖以下内容:
-
创建我们的第一个 Alexa 技能
-
在 Lambda 中使用 Alexa SDK 处理来自 Alexa 的请求
-
测试您的 Lambda
-
创建一个使用存储在 S3 上的数据的更复杂的 Alexa 技能
-
部署您的 Alexa 技能
技术要求
在本章中,我们将为我们的技能创建一个 Lambda 函数,并使用我们在上一章中创建的本地开发设置来创建和部署它。
本章中使用的所有代码都可在bit.ly/chatbot-ch3找到。
Alexa 技能套件
要创建我们的第一个 Alexa 技能,我们将使用 Alexa 技能套件。搜索 Alexa 技能套件或访问www.developer.amazon.com/alexa-skills-kit,你应该会看到一个带有“创建技能”或“开始技能”按钮的屏幕:
创建您的第一个技能
首先,给您的技能起一个名字。这应该是描述技能做什么的东西。为此,我们可以称之为Hi。点击下一步,您将能够选择技能的模型。我们想选择自定义,这样我们就可以创建我们想要的技能:
创建一个自定义技能
点击创建技能,一旦设置完成,您将进入 Alexa 技能构建页面。要开始,我们需要点击左侧菜单中的“触发名称”。这就是我们设置启动技能的命令的地方。我将为这个第一个技能使用sams demo bot。当你创建更大的技能时,花些时间思考你将使用什么作为你的触发短语,并大声练习说它是个好主意:
技能触发
现在我们可以开始我们的技能了,我们需要创建一个意图,以便我们的技能做些事情。点击左侧菜单中意图旁边的“添加”按钮来创建一个新的意图。在这里,您可以选择创建自定义意图或使用亚马逊库中的现有意图。亚马逊的大多数意图都与页面导航或音乐控制有关,所以我们选择自定义意图。
给你的意图起一个名字,描述它将要做什么。在我们的例子中,它是说Hello,所以这就是它的名字。点击创建自定义意图以开始编辑意图。
现在我们已经进入了Hello意图的意图窗口,我们需要添加一些语句。正如我们在第一章,“理解聊天机器人”中讨论的那样,这些是用户可能会说的以触发此意图的短语。对于这个意图,这些语句可能是hi、hello或hey:
Hello 语句
我们已经完成了我们的第一个 Alexa 意图,所以我们需要保存并构建这个模型。在意图窗口的顶部是保存模型按钮和构建模型按钮,所以保存它然后构建它。构建模型有时需要一段时间,所以只需等待它完成。
创建 Lambda 来处理请求
要处理我们新的 Alexa 技能中的意图,我们需要创建一个 Lambda 函数。这将包含我们理解意图并向用户发送回复所需的所有逻辑。
要创建一个 Lambda,我们可以使用第二章,“AWS 和 Amazon CLI 入门”中描述的任何方法,但我们将使用我们的本地开发设置。导航到你的基本 Lambda 文件夹,创建一个名为hello-alexa-skill的新文件夹。在那个文件夹内,我们需要创建一个新的index.js文件并打开它以创建我们的函数。
首先,我们需要require在alexa-sdk中,这使得创建 Alexa 的逻辑变得容易很多:
const Alexa = require('alexa-sdk');
因为我们需要它,我们还需要确保我们已经安装了它。在命令行界面中,导航到你的hello-alexa-skill文件夹,并运行npm init命令。这个过程会创建一个包信息,并允许你在文件夹中安装其他包。你可以边设置边设置值,或者通过按Enter使用默认值。一旦完成设置,你将有一个名为package.json的文件,其中包含此文件夹的配置。
要安装一个新的包并将其添加到我们的package.json文件中,我们可以运行npm install --save package-name命令。我们想安装ask-sdk,所以我们需要运行npm install --save ask-sdk。当这个命令运行时,你会看到一个新文件夹被创建,名为node_modules,其中包含所有已安装的 npm 包中的代码。
创建处理程序
当用户的意图被我们的某个语句触发时,我们需要在代码内部处理它。为此,我们创建一个对象,其中包含针对我们每个意图的方法。目前,我们只有一个hello意图,所以我们只需要创建一个处理程序:
const helloHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
handlerInput.requestEnvelope.request.intent.name === 'hello';
},
handle(handlerInput) {
const speechText = `Hello from Sam's new intent!`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
};
这个hello处理程序有两个部分:canHandle和handle。canHandle函数决定此处理程序是否可以处理此请求,如果可以则返回 true,如果不能则返回 false。这是通过请求类型和意图名称来计算的。如果两者匹配,则这是正确的处理程序。handle告诉 Alexa 如何响应。对于这个意图,我们只想让 Alexa 说出*来自 Sam 的新意图的问候!*然后获取用户的下一条消息。
现在我们需要将我们的helloHandler添加到我们的技能中。
我们可以通过将多个处理程序作为多个参数传递给.addRequestHandlers方法来添加多个处理程序:
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
helloHandler)
.lambda();
构建 和 配置 Lambda
现在函数已完成,我们可以使用我们在第二章,AWS 和 Amazon CLI 入门中制作的构建脚本。运行./build.sh hello-alexa-skill命令来创建我们的 Lambda 并将其部署到 AWS。
当构建脚本完成后,导航到 AWS 中的 Lambda 控制台,你现在应该能看到你新创建的函数。点击这个新的hello-alexa-skill Lambda 以打开编辑器。
要使此 Lambda 能够通过 Alexa 技能触发,我们需要将 Alexa 技能套件添加为触发器。这通过在设计师中的“添加触发器”下点击 Alexa 技能套件来完成,创建的 Alexa 技能套件触发器将出现在主设计师截图:
添加 Alexa 技能套件触发器
这也打开了 Alexa 技能套件配置部分。在这里,我们需要提供我们技能的 Alexa 应用 ID。要找到这个,请打开 Alexa 技能套件控制台,转到端点,并选择 Lambda。这将打开一些额外的详细信息和选项。我们的Skill ID是第一条信息,可以复制到剪贴板并插入到 Lambda 配置中:
技能端点配置
在退出 Lambda 编辑器之前,我们应该在编辑器屏幕的右上角找到 ARN。复制它,因为我们将在配置技能的最后一步需要它。
完成技能配置
现在我们已经配置了 Lambda 并且 ARN 已经复制到剪贴板,我们可以回到我们的技能控制台。在端点下,我们可以在默认区域旁边的文本框中插入我们的 Lambda ARN。这确保了技能正在触发正确的 Lambda。您也可以为不同的区域创建不同的 Lambda,这样您可以为不同的人群提供特定的响应。
点击保存端点以保存您的技能,您已经完成了您的第一个 Alexa 技能。现在到了有趣的部分:尝试您的技能!
测试您的技能
现在我们已经构建和部署了新的 Alexa 技能,我们需要测试它看看是否工作。在页面顶部,有四个标签:构建、测试、发布和衡量。我们已经完成了构建,所以我们可以点击测试。点击页面顶部的切换按钮以启用此技能的测试:
测试屏幕
要与你的新技能互动,你可以键入你的消息,或者点击并按住麦克风按钮,像与 Alexa 交谈一样与你的电脑交谈。要通过与它交谈来测试你的技能,你需要在笔记本电脑或 PC 上有一个麦克风,并且已经允许网页访问该麦克风。一旦你按下Enter或松开麦克风按钮,你将看到 Alexa 正在加载,然后她会根据你的意图回复并添加消息到聊天窗口。除了 Alexa 的回复外,你还可以在屏幕的技能 I/O 部分获得信息。如果意图被成功触发,你将得到发送到你的 Lambda 的完整 JSON 输入以及它给出的响应:
工作中的 hello 测试
这是你与你的机器人聊天时应得到的内容。确保你正在说正确的表述。
故障排除你的技能
在最初制作技能或 Lambda 时出现一些错误是非常正常的。关键是学习如何查找错误并修复它们。
在这本书的附录中有一个有用的指南,用于在 Lambdas 中查找错误。遵循那些流程,你应该很快就能让你的技能工作。
创建一个更有用的技能
当你说hi时,技能会回复hello,看到它工作是非常好的,但它并不很有用。接下来我们要制作的下一个技能将会更有用。
我们将创建一个建议购买车型并能够提供所建议车型详细信息的技能。
我们将使用的数据将包含三种车型尺寸、两个价格区间,以及小型车(车门数量)和大车型(手动或自动传动)的额外类别。
对话流程图
为了确保我们制作一个有效的聊天机器人,我们需要创建我们的对话流程图。这从我们的完美对话开始:
车辆对话
用户选择了一款大型车,因此,我们不得不询问他们想要的价格区间以及他们想要的传动类型。这种逻辑将在对话流程图中变得明显。我们可以为选择中型或小型车的用户创建类似的对话,其中所有对话都会略有不同。当根据用户之前所说的问题不同时,你可能会得到数百种不同的对话。这就是对话流程图真正变得有用之处。
在这个对话流程图中,我们有一个非常重要的逻辑组件。它检查用户是否选择小型、中型或大型车,并根据这一点引导对话。这意味着我们现在可以在一个图中展示许多不同的对话选项:
车辆流程图
在流程的末尾,我们还有一个查找功能,以找到适合用户的理想车型。这是本章后面将详细讨论的全新内容。
创建 Alexa 技能
我们以之前相同的方式开始创建这个技能。进入你的 Alexa 技能套件开发者控制台,选择创建技能。选择一个合适的名称,例如carHelper,并选择自定义技能。
现在我们再次进入技能控制台,我们需要从顶部开始设置唤醒词。输入Car Helper或类似的好记且容易说的名称。
创建意图
现在我们可以继续到技能的主要部分——添加意图。为此,我们添加一个新的意图,我们可以将其命名为whichCar,因为我们试图帮助用户选择哪辆车。
这个意图首先需要的是表述。添加用户可能会说的短语,如果他们想知道要买什么车:
意图表述
内部槽位
这是我们需要开始使技能比上次更高级的地方。我们需要存储有关尺寸、成本、车门以及用户是否想要自动或手动变速器的信息。要添加一个新的槽位,滚动到意图槽位并点击创建新槽位。在这里,您可以命名您的槽位,然后通过按Enter键或点击+图标将其添加到意图中。为尺寸、成本、车门和变速器都这样做:
意图槽位
在我们能够将这些信息存储在这些槽位之前,我们需要设置它们的槽位类型。门的数量很简单,因为它只是一个数字,所以可以选择 AMAZON.NUMBER 作为其槽位类型。对于其他三个槽位,情况要复杂一些。
我们需要为这三个槽位创建自定义槽位类型。要创建一个新的槽位类型,点击槽位类型旁边的+号,这将带您进入添加槽位类型屏幕。输入您新槽位类型的名称,然后点击创建自定义槽位类型。我们将从一个名为carSize的槽位类型开始。
现在您已经进入了槽位类型编辑屏幕,您将在左侧菜单中看到您的新槽位类型。我们需要添加用户可以选择的三个值:large、medium和small。这样就可以正常工作,但如果用户说的是big而不是large怎么办?我们可以通过同义词捕获这些表述。我们可以输入尽可能多的新值,如果用户说了这些值,它们将被注册为主要值:
车辆尺寸槽位类型
这个过程需要重复进行,以创建一个具有luxury和value值的carCost槽位类型,以及一个具有automatic和manual值的carGear槽位类型。你还应该为这些值中的每一个添加同义词,以提高你机器人的灵活性。
现在我们已经创建了三个新的槽位类型,我们可以将它们添加到我们的槽位中。您现在应该在槽位类型下拉菜单中找到您的新槽位类型。确保每个槽位都有正确的槽位类型,我们几乎完成了技能编辑器。
我们知道用户总是会要求选择 size 和 cost,因此我们可以将这些槽位设置为必需。点击意图下的槽位名称将带您进入槽位配置屏幕,在那里我们有槽位类型、槽位填充和槽位确认部分。
在槽位填充部分,有一个切换按钮可以更改槽位为必需。当我们点击这个切换按钮时,它会为我们打开更多设置,以便我们进行配置。第一个是 Alexa 语音提示,我们可以输入一个提示,让用户正确填写槽位:
必需的槽位
我们还可以输入用户可能会回复的语句。第一个可以是大小,因此我们需要输入用大括号括起来的槽位名称。除了简单地说出“大”,用户还可能说“我想买一辆大车”或“我在找一辆中档车”。为了处理这些,我们输入这些语句,但将大和中改为 {size}:
槽位语句
对于 cost,使用类似“我想买一辆 {cost} 的车”的语句进行相同的过程。如果您想的话,可以添加一些其他语句。
对于齿轮或车门,我们不需要这样做,因为它们不是每次对话都必须的,但我们将能够从我们的 Lambda 中请求它们。
一旦您创建了三个自定义槽位并将槽位类型添加到所有槽位中,您应该会有看起来像这样的意图槽位:
槽位类型完成
查找技能 ID
最后一件要做的事情是找到并复制技能 ID,以便我们可以在 Lambda 中使用它。在左侧菜单中选择“端点”,然后选择 AWS Lambda ARN 作为服务端点方法。这将暴露我们需要复制的技能 ID。
创建 Lambda
现在我们已经完成了控制台设置,我们可以构建一个 Lambda,它将处理技能背后的逻辑。
首先,在您的 lambdas 文件夹中创建一个新的文件夹,取一个合适的名字,例如 carHelper。在里面,我们需要创建一个 index.js 文件并运行 npm init。我们再次使用 alexa-sdk,因此需要运行 npm install --save alexa-sdk。
设置就绪后,我们可以开始编写 Lambda。我们可以从一个与我们在第一个函数中创建的 Lambda 非常相似的 Lambda 开始:
const Alexa = require('alexa-sdk');
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers()
.lambda();
我们将要创建的第一个处理程序是用来处理启动请求的。这是当用户说类似“Alexa 启动车助手”的话时;我们的技能被启动,但没有触发任何意图。我们需要通过告诉他们如何触发我们的意图来帮助他们触发我们的意图之一。然后我们可以将其添加为 .addRequestHandlers() 中的第一个处理程序:
const LaunchRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
},
handle(handlerInput) {
const speechText = `Hi there, I'm Car Helper. You can ask me to suggest a car for you.`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.getResponse();
}
};
处理 whichCar 意图
我们可以开始处理我们的whichCar意图。我们首先创建WhichCarHandler并将其添加到addRequestHandlers()中的列表:
const WhichCarHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
handlerInput.requestEnvelope.request.intent.name === 'whichCar';
},
async handle(handlerInput) {}
}
在这个handler函数内部,我们首先需要做的是从事件中获取槽。我们可以使用es6解构来简化我们的代码:
const slots = handlerInput.requestEnvelope.request.intent.slots;
const {size, cost, gears, doors} = slots;
现在我们有权访问所有四个槽变量。尽管我们创建了槽类型,但我们需要检查我们是否有有效的值。我们将从大小和成本开始,因为我们知道我们总是需要这些槽的值:
if (!size.value || !(size.value === 'large' || size.value === 'medium' || size.value === 'small')) {
const slotToElicit = 'size';
const speechOutput = 'What size car do you want? Please say either small, medium or large.';
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
if (!cost.value || !(cost.value === 'luxury' || cost.value === 'value')){
console.log('incorrect cost')
const slotToElicit = 'cost';
const speechOutput = 'Are you looking for a luxury or value car?';
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
这两段代码检查槽是否存在,然后检查它们是否等于预期的响应之一。如果槽未填写或与预期值不匹配,我们使用.addElicitSlotDirective让 Alexa 再次请求该槽。
如果请求已经超过这两个块,我们知道我们有一个有效的大小和成本。在我们的流程图中,这就是我们有一个逻辑步骤来决定将他们引导到哪个路径的地方,所以这就是我们现在需要实现的内容。
如果用户选择了一辆大车,我们需要看看他们是否已经选择了档位。如果没有,我们会询问他们是否想要自动或手动变速箱。对于小车和车门数量,我们执行同样的过程:
if (size.value === 'large' && ( !gears.value || !(gears.value === 'automatic' || gears.value === 'manual') )){
// missing or incorrect gears
const slotToElicit = 'gears';
const speechOutput = 'Do you want an automatic or a manual transmission?';
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
if (size.value === 'small' && ( !doors.value || !(doors.value == 3 || doors.value == 5) )){
// missing or incorrect doors
const slotToElicit = 'doors';
const speechOutput = 'Do you want 3 or 5 doors?';
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
如果请求已经超过这个点,有三种可能性:
-
他们选择了一辆小车,并选择了车门数量
-
他们选择了一辆中型车,因此不需要选择车门或档位
-
他们选择了一辆大车,并选择了他们的档位
下一步是根据用户选择找到最好的汽车。为了选择最好的汽车,我们需要有一个排序的汽车选择。我们可以在处理程序外部创建一个对象来存储我们需要的排序汽车的数据:
const cars = [
{name: 'fiat500', size:'small', cost: 'luxury', doors: 3, gears: 'manual'},
{name: 'fordFiesta', size:'small', cost: 'luxury', doors: 5, gears: 'manual'},
{name: 'hyundaiI10', size:'small', cost: 'value', doors: 3, gears: 'manual'},
{name: 'peugeot208', size:'small', cost: 'value', doors: 5, gears: 'manual'},
{name: 'vauxhallAstra', size:'medium', cost: 'value', doors: 5, gears: 'manual'},
{name: 'vwGolf', size:'medium', cost: 'luxury', doors: 5, gears: 'manual'},
{name: 'scodaOctaviaAuto', size:'large', cost: 'value', doors: 5, gears: 'automatic'},
{name: 'fordCmax', size:'large', cost: 'value', doors: 5, gears: 'manual'},
{name: 'mercedesEClass', size:'large', cost: 'luxury', doors: 5, gears: 'automatic'},
{name: 'vauxhallInsignia', size:'large', cost: 'luxury', doors: 5, gears: 'manual'}
];
在这个对象包含我们想要的全部汽车选项的情况下,我们需要找到最适合用户的汽车。为此,我们可以使用Array.filter()函数。这个函数会遍历数组中的每个项目并对其应用一个函数。如果函数返回 true,则该项目保留在数组中,否则,它将被移除:
// find the ideal car
let chosenCar = cars.filter(car => {
return (car.size === size.value && car.cost === cost.value &&
(gears.value ? car.gears === gears.value : true) &&
(doors.value ? car.doors == doors.value: true));
});
为了找到最适合用户的汽车,这个过滤函数会检查car.size和car.cost是否等于用户选择的内容,然后使用三元表达式来检查档位和车门。如果用户选择了档位类型或车门数量,它会检查汽车信息是否与用户的选择匹配,否则返回true。
当我们运行这个函数时,我们会得到匹配用户选择的汽车。如果用户选择了一辆small、luxury的3门车,那么chosenCar将等于[{name: 'fiat500', size:'small', cost: 'luxury', doors: 3, gears: 'manual'}]。
在我们获取所选汽车更多详细信息之前,我们需要检查我们的函数是否选择了一辆汽车。这可以通过检查我们的新chosenCar数组长度为1来完成。如果不为1,则表示出现了某种错误,我们需要让用户知道。在过滤方法之后添加此代码:
if (chosenCar.length !== 1) {
const speechOutput = `Unfortunately I couldn't find the best car for you. You can say "suggest a car" if you want to try again.`;
return handlerInput.responseBuilder
.speak(speechOutput)
.getResponse();
}
亚马逊 S3
现在我们已经选择了车辆,我们可以从 S3 存储桶中获取更多关于该车辆的信息。S3 存储桶允许我们在任何我们想要的地方存储对象并访问它们。
要创建一个 S3 存储桶,在 AWS 控制台中搜索 S3。在 S3 页面,点击创建存储桶按钮开始创建过程。为你的存储桶选择一个名称,注意该名称必须在 S3 的所有存储桶中是唯一的。在存储桶名称的开头或结尾添加你的名字或别名可以帮助使你的存储桶独一无二。在这个例子中,我们不需要在存储桶上设置任何其他属性或权限,所以我们可以直接点击下一步,直到到达最后。
在创建好新的存储桶后,我们可以开始创建将要上传到其中的数据。上传数据到 S3 存储桶非常简单;点击你想要上传的存储桶,然后点击上传按钮。然后你可以拖放文件或点击添加文件以更传统的方式上传文件。对于这个项目,我们不需要为这些文件设置任何权限或属性。
我们将要上传的所有数据都可在bit.ly/chatbot-ch3的car-data文件夹中找到。我们将查看一个示例文件,以了解我们将要访问哪些数据:
{
"make": "Vauxhall",
"model": "Astra",
"rrp": 16200,
"fuelEcon": "44-79 mpg",
"dimensions": "4.258 m L x 1.799 m W x 2.006 m H",
"NCAPSafetyRating": "5 star",
"cargo": 370
}
使用这些信息,我们可以为用户提供一个关于我们的聊天机器人为他们推荐的汽车的简要总结。这可以进一步扩展,但向用户提供过多的数据可能会使交互变得过于复杂。
访问我们的 S3 数据
现在我们已经将所有数据存储在我们的 S3 存储桶中,并且根据用户的选择选择了一辆车,我们需要获取相关数据。为此,我们可以使用aws-sdk与我们的 Lambda 交互来访问S3。
在我们的 Lambda 顶部,我们需要引入AWS以便我们可以使用S3方法。将这两行代码添加到 Lambda 的顶部:
const AWS = require('aws-sdk');
var s3 = new AWS.S3();
现在我们已经可以访问 AWS 上的 S3 方法,我们可以获取我们选择的车辆的 JSON 数据。在whichCar处理器的末尾,添加以下代码:
var params = {
Bucket: YOUR BUCKET NAME,
Key: `${chosenCar[0].name}.json`
};
return new Promise((resolve, reject) => {
s3.getObject(params, function(err, data) {
if (err) { // an error occurred
console.log(err, err.stack);
reject(handleS3Error(handlerInput));
} else { // successful response
console.log(data);
resolve(handleS3Data(handlerInput, data));
}
});
})
这段代码片段的第一部分是选择我们要访问的数据的位置和内容。确保你输入你的存储桶名称。密钥是通过使用模板字符串生成的,这样我们就能获取到与用户选择的车辆相关的文件。
然后,我们返回一个包含s3.getObject()方法的 promise,传递我们的params和一个callback函数。.getObject()方法的回调传递err和data参数。如果有错误,我们将reject一个名为handleS3Error的函数。如果成功,我们将resolve名为handleS3Data的函数。我们稍后会创建这些函数。
添加 S3 权限
由于我们现在正在从 S3 访问数据,我们还需要更新执行角色以包括 S3 只读权限。在你的 AWS 控制台中,导航到 IAM,这是你控制你的用户、角色和策略的地方。
在左侧菜单中选择“角色”,你应该会看到一个角色列表。如果你是第一次使用 AWS,你将只有一个角色:LambdaBasic。当你选择它时,你会进入一个摘要页面,其中有一个“附加策略”按钮。我们需要附加 S3 权限,以便我们可以点击该按钮。
这将打开一个列表,显示您账户上可用的所有策略。亚马逊为几乎所有场景都创建了数百个默认策略。我们将搜索 S3。我们应该至少得到四个选项,包括 Redshift、FullAccess、ReadOnly 和 QuickSight。由于我们只从 S3 获取数据,我们可以勾选 AmazonS3ReadOnlyAccess 复选框,然后点击右下角的“附加策略”按钮:
添加 Amazon S3 权限
处理我们的数据
完成对 S3 的请求后,我们收到了数据或错误。无论哪种情况,我们都需要处理它并向用户发送响应。我们将创建两个新函数来处理数据或错误:
const handleS3Error = handlerInput => {
const speechOutput = `I've had a problem finding the perfect car for you.`
return handlerInput.responseBuilder
.speak(speechOutput)
.getResponse();
};
function handleS3Data(data){
let body = JSON.parse(data.Body);
console.log('body= ', body);
let { make, model, rrp, fuelEcon, dimensions, NCAPSafetyRating, cargo} = body;
let speech = `I think that a ${make} ${model} would be a good car for you.
They're available from ${rrp} pounds, get ${fuelEcon} and have a ${cargo} litre boot.`;
return handlerInput.responseBuilder
.speak(speechOutput)
.getResponse();
}
错误函数会告诉用户我们找不到最适合他们的车,而数据函数会使用数据创建一个简短的车辆描述。我们需要解析数据的主体,因为数据以缓冲区形式下传。我们需要将缓冲区转换为我们可以使用的格式。
测试我们的 Lambda
使用最后一个技能,Lambda 简单到我们甚至可以不对其进行测试。这个 Lambda 更复杂,有多个可能出错的地方,所以我们将正确地对其进行测试。
在 Lambda 控制台中,找到你的函数并打开它。一旦进入,点击测试旁边的下拉菜单,选择配置测试事件。确保选择了“创建新测试事件”选项,我们可以使用 Alexa Intent - GetNewFact 模板。
大多数模板可以保持默认设置,但我们需要更改槽位和 intentName(第 20 和 21 行)以及应用程序 ID(第 10 和 35 行)。首先,将 intentName 更改为等于我们创建的意图(whichCar)。接下来,我们可以添加我们可用的槽位。目前,我们可以将它们都设置为 null,因为它们在尚未填充时就是这样:
"slots": {
"size": null,
"cost": null,
"gears": null,
"doors": null
},
"name": "whichCar"
使用你在 Alexa 技能控制台端点部分获得的 ARN,更改第 10 和 40 行的 applicationId 值。
将此意图命名为 whichCarEmpty 并点击创建。
在我们运行此测试之前,我们可以考虑我们期望发生什么。因为没有槽位被填充,我们预计它将在 size 检查处失败,因此我们将得到一个询问我们想要什么尺寸的车的响应。在运行测试之前考虑你期望发生的事情总是好的。这有助于你构建代码理解,如果你没有得到那个响应,它会在你的脑海中拉响一个红灯。
现在我们可以点击测试,我们应该得到执行结果:成功,并带有输出语音"你想要什么尺寸的汽车?请说出小型、中型或大型"的响应。
这正是我们预期的,所以太好了!如果你没有收到这个响应,请查看错误消息,并使用它来找出可能出了什么问题。附录中有一个有用的部分,可以用来调试常见的 Lambda 错误。
在这个测试工作之后,我们可以创建另一个包含一些填充槽位的测试。点击测试下拉菜单,再次选择配置测试事件。确保选择创建新测试事件,但这次选择 whichCarEmpty 作为模板。这意味着我们知道应用程序 ID 是正确的,我们唯一需要更改的是槽位。将槽位更改为以下代码:
"slots": {
"size": { "value": "large"},
"cost": { "value": "luxury"},
"gears": { "value": "automatic"},
"doors": { "value": null}
},
将此测试保存为 whichCarLargeLuxuryAuto。当你运行这个测试时,你应该得到以下成功的响应:
“我认为奔驰对你来说是一辆好车。它们的价格从 35,150 英镑起,油耗为 32-66 mpg,后备箱容量为 425 升。”
你可以为每种可能的结果组合创建测试,但由于我们知道我们的 Lambda 正在响应并访问 S3,我们知道所有代码都在正常工作。
完成 Alexa 技能套件配置
为了完成我们技能的配置,我们需要获取我们的 Lambda 的 ARN。从 Lambda 页面的顶部或从你的构建脚本的结果中复制它,然后转到 Alexa 技能套件控制台。将其粘贴到默认区域并保存端点。在我们开始测试我们的技能之前,我们只需要做这些。
测试
现在我们可以尝试我们的新技能。在这里,你可以看到我与我的汽车助手机器人之间的对话:
测试汽车助手技能
这个技能并不完美——它不会对你说出的每一个话语都做出响应,而且这个技能还能做更多的事情。好事是,你现在知道你需要的一切来修复所有这些问题。
启动你的技能
要将你的技能发布到 Alexa 技能商店,我们需要切换到下一个标签页。这是你将设置将在 Alexa 技能商店中显示的信息的地方。你需要给你的技能起一个独特的名字,简短和长描述,以及示例话语。然后你可以上传一个图标,并选择你的技能的类别和关键词。类别和关键词应该仔细考虑,因为这可能是用户找到你的技能的方式。
本页的最后一部分是隐私政策和使用条款的 URL。如果你想在技能商店中拥有一个技能,你需要这些。外面有很多例子,对于不存储或甚至不要求用户提供信息的技能来说,它们不应该很复杂。任何使用并存储用户信息的应用程序都需要一个更详细的隐私政策,并且可能值得联系律师:
启动设置
下一页会问你关于你的技能的许多隐私和合规性问题。诚实地回答这些问题,然后在部署之前向将测试你的技能的人提供一些信息。
接下来,我们必须选择我们技能的可用性。我们可以使用它来仅允许某些组织访问该技能。如果你为一家公司创建了一个专门的技能并且不希望其他人使用它,这可能会很有用。你还可以选择技能可用的国家。你可以将其限制在一两个国家,或者让每个人都可以使用。
最后一页是一个审阅页面,它会告诉你你的提交是否缺少任何内容。当你修复了一切之后,你可以点击提交以供审查。在技能处于测试状态时,你将无法编辑技能的配置。你仍然可以编辑你的 Lambda,但这样做可能会导致你的技能被拒绝。
一旦经过测试并获得批准,你将拥有一个实时的 Alexa 技能!
摘要
本章向我们展示了如何做很多事情。我们首先使用 Alexa Skills Kit 创建了我们第一个 Alexa 技能。这包括了解和创建意图、槽位和语音。配置完成后,我们使用 Alexa-SDK 创建了一个 Lambda 来处理请求。这个 Lambda 是我们定义将发送给用户的响应的地方。最后,我们使用内置的测试工具构建和测试了我们的新 Alexa 技能。
在创建了一个基本的第一个技能之后,我们开始创建一个更有用的第二个技能。我们使用了一个自定义槽位类型并将其应用于意图中的槽位。然后我们使用亚马逊的 S3 服务来存储我们需要的在 AWS SDK 中使用之前的数据,以便轻松获取数据并在我们的 Lambda 中使用它。
使用本章学到的技能,你可以为 Alexa 构建一大系列强大的技能。
在下一章中,我们将学习如何访问 API,这将使我们能够创建更强大的技能。
问题
-
我们在 Lambda 中用于处理 Alexa 请求的工具是什么?
-
我们需要做哪三件事才能将 Lambda 连接到 Alexa 技能?
-
我们从我们的 S3 存储桶获取信息的方法是什么?
-
我们需要对 S3 的响应体进行什么操作,以及为什么?
-
我们如何创建一个 Lambda 测试?
进一步阅读
如果你想尝试不同的响应类型,请查阅 Alexa SDK 响应构建器文档:ask-sdk-for-nodejs.readthedocs.io/en/latest/Building-Response.html。
我们只使用了 S3 来获取我们手动存储的数据;还有其他方法可以提供更多 S3 功能:docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html。
第四章:将您的 Alexa 技能连接到外部 API
在本章中,我们将扩展我们在上一章中学到的基本知识,以改进 Alexa 的功能性和用户体验。我们将通过学习使用外部 API 与其他人提供的服务进行交互来增加功能。然后,我们将通过让我们的聊天机器人记住现有对话以及使用语音合成标记语言(SSML)来控制 Alexa 与用户交谈的方式来增加用户体验。
为了让我们能够学习这一点,我们将为 Alexa 构建一个天气技能。您将能够要求全球 200,000 个城市当前或五天的天气预报。
本章将涵盖以下主题:
-
访问和交互外部 API
-
使用会话属性存储会话内存
-
使用 SSML 控制 Alexa 与用户交谈的方式
技术要求
在本章中,我们将为我们的技能创建一个 Lambda 函数,并使用我们在第二章中讨论的本地开发设置来部署它,即AWS 和 Amazon CLI 入门。
我们将使用Open Weather Map API根据用户请求获取天气数据。我们将通过创建账户和获取 API 密钥的过程。
我们将使用 Postman 来测试我们将要向 Open Weather Map API 发出的请求。这是一个跨平台的应用程序,可以在getpostman.com上安装。
本章所需的所有代码都可以在bit.ly/chatbot-ch4找到。
外部 API
应用程序编程接口(API)是一个您可以发送请求的接口,它将给您一个响应。这些用于让其他人控制您的软件的部分,无论是从 API 数据库获取信息、更改用户的设置,还是让 API 发送文本。
它们是开发者的非常强大的工具,为您提供比您自己收集或构建更多的数据和服务的访问权限。
外部 API 不必由他人构建。如果您有一个希望从聊天机器人访问的系统,您可以添加 API 访问,或者可能已经为它构建了一个 API。使用 API 来分离代码或公司的部分可以是一个允许并改进模块化的好方法。
Open Weather Map API
Open Weather Map API 是一个非常强大的 API,让您能够获取全球 200,000 个城市的当前天气以及天气预报。最好的部分是,有一个免费层,允许您每分钟进行 60 次关于当前天气和五天预报的请求。这使得我们能够开发一个使用真实世界数据而不需要订阅月费费用的 Alexa 技能。
要访问此 API,我们需要创建一个账户以获取 API 密钥。访问OpenWeatherMap.org,然后在页面右上角点击“注册”。输入您的详细信息,阅读条款和条件,然后注册。接下来,您将被提示说明您使用 API 的原因。没有“Alexa”选项,因此您可以选择移动应用开发,因为这最接近我们的实际使用。
现在您已登录,您可以访问您的 API 密钥。在您向 API 发出的任何请求中都会使用此密钥,以便它可以检查您是否有权发出请求。导航到 API 密钥,找到您账户的默认密钥。我们将在此项目中使用该密钥,以确保您可以再次找到它:
OpenWeatherMap API 密钥
使用我们的 API 密钥,我们现在可以查看我们可以发出的请求。在 API 页面上,有一个不同 API 的列表,但我们有权访问的是当前天气数据和 5 天/3 小时预报。在每个部分下方都有一个按钮可以进入 API 文档,我们将查看当前天气数据的 API 文档。
在 Current weather data API 上有三种方式请求数据:调用一个地点的当前天气数据,调用多个城市的当前天气数据,以及批量下载。我们每次只会获取一个地点的数据。
在“调用一个地点的当前天气数据”部分中,也有几种不同的方式来选择区域。您可以提供城市名称、城市 ID、地理坐标或 ZIP 代码。用户将告诉我们一个城市名称,因此使用这些数据最有意义。
有两种方式可以通过城市名称获取当前天气数据:
https://api.openweathermap.org/data/2.5/weather?q={city name}
https://api.openweathermap.org/data/2.5/weather?q={city name},{country code}
每当我们调用这些端点中的任何一个时,我们都会以预定义的格式获得响应。了解数据将如何返回,这样我们就可以在我们的技能内部正确处理它。网页上的 API 响应部分提供了响应示例,以及一个功能列表,每个功能都有简短描述。这是一个请求可能返回的响应示例:
{
"city": {
"id":1851632,
"name":"Shuzenji",
"coord": { "lon":138.933334, "lat":34.966671 },
"country": "JP",
"cod":"200",
"message":0.0045,
"cnt":38,
"list":[{
"dt":1406106000,
"main":{
"temp":298.77,
"temp_min":298.77,
"temp_max":298.774,
"pressure":1005.93,
"sea_level":1018.18,
"grnd_level":1005.93,
"humidity":87,
"temp_kf":0.26},
"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],
"clouds":{"all":88},
"wind":{"speed":5.71,"deg":229.501},
"sys":{"pod":"d"},
"dt_txt":"2014-07-23 09:00:00"
}]
}
}
创建我们的天气技能
创建天气技能将遵循我们之前创建的技能相同的步骤。这是一个在创建任何新的 Alexa 技能时都值得遵循的绝佳流程。为了回顾这个过程,它如下所示:
-
从完美的对话中创建对话流程
-
在 Alexa Skills Kit 上创建技能,包括所有意图、槽位和表述
-
创建 Lambda 来处理请求
-
测试技能
-
提升技能
对话流程设计
用户将与该技能进行的对话大多数都很简单。用户真正可以询问的只有两件事:位置和预报数据。以下是一个完美的对话示例:
天气对话
这个对话的有趣之处在于两个问题都很相似。{位置} {日期}的天气怎么样?
这意味着我们可以用单个意图来处理它们。这个意图需要检查他们是否提供了位置和日期,然后使用这两个东西来调用 API。这个意图的流程图将如下所示:
天气流程图
与我们之前工作过的流程相比,这个流程的不同之处在于用户可以在一次对话中多次通过一个意图,通常带有不同的槽位值。我们可以为“当前天气”、“天气变化日期”和“天气变化位置”构建单独的意图,但它们都会做类似的事情。
在 Alexa Skills Kit 上创建技能
我们需要开始使用 Alexa Skills Kit 开发者控制台。点击创建技能按钮,给你的技能命名,并选择自定义作为技能类型。
每次我们创建一个新技能时,我们首先添加一个调用短语。
在创建技能时立即执行意味着你不会忘记稍后填充它。你可以在发布技能之前随时更改短语。
接下来,我们需要创建我们的getWeather意图。添加一个名为getWeather的新自定义意图,然后我们可以开始填充意图。
用户将使用许多不同的语句来触发这个意图。我们还将学习如何从用户语句中填充槽位。首先,将我们的两个槽位添加到意图中,“位置”和“日期”。位置槽位的类型可以是 AMAZON.US_CITY,数据可以是 AMAZON.DATE。如果你想在你所在地区获得更好的城市识别,可以选择GB_CITY、AT_CITY、DE_CITY或EUROPE_CITY:
意图槽位
创建好槽位后,我们可以创建我们的语句。这些语句将不同于我们的常规语句,因为我们需要在同一时间填充槽位。这可以通过一个如“伦敦的天气怎么样”这样的语句来演示。我们试图填充的槽位是一个值为“伦敦”的“位置”。为了捕获这个槽位,我们可以使用花括号方法,其中意图变为“伦敦的天气怎么样{位置}”。这意味着在花括号“{位置}”处输入的任何值都将被捕获并存储在位置槽中。
这也可以用于其他类似的语句。“明天怎么样”变为“关于{日期}”,而一个“纽约明天的天气怎么样”的语句变为“{日期}在{位置}的天气怎么样”。从初始语句中捕获槽位的功能非常强大,因为它意味着我们不必询问用户每个槽位的值。提出一系列这样的问题会导致非常不自然的对话。以下是一些示例样本语句:
获取天气语句
在完成意图槽位和话语之后,我们可以在创建处理请求的 Lambda 之前,从端点部分获取技能 ID。
构建 Lambda 处理请求
要创建我们的 Lambda 函数,我们可以在Lambdas文件夹内创建一个新的文件夹,并将其命名为weatherGods。在这个文件夹内,我们可以创建一个index.js文件,在其中我们将创建我们的处理程序。首先,从本章代码库中的boilerplate Lambda文件夹中复制文本。我们还需要运行npm init,以便我们稍后可以安装npm包。
在开始编写主要代码之前,我们需要修改我们的LaunchRequestHandler。这可以通过更改speechText变量来完成。对于这个技能,我们可以输入一个响应消息“你可以向天气之神询问你所在城市的天气或天气预报”。这会提示用户说出一个将触发getWeather意图的短语。
现在我们可以开始编写逻辑,以获取用户想要的天气信息。我们需要创建另一个处理程序来处理getWeather请求:
const GetWeatherHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
handlerInput.requestEnvelope.request.intent.name === 'getWeather';
},
handle(handlerInput) {}
}
在我们能够获取天气之前,我们需要检查我们是否有位置和日期的值。如果我们没有这两个值中的任何一个,我们需要获取它们:
const { slots } = this.event.request.intent;
let { location, date } = slots;
location = location.value || null;
date = date.value || null;
if (!location) {
let slotToElicit = 'location';
let speechOutput = 'Where do you desire to know the weather';
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
if (!date){
date = Date.now()
}
你可能会注意到,缺失的位置和日期被处理得不同。如果位置缺失,我们会要求用户提供位置。如果我们缺少日期,我们将日期设置为Date.now()。这是一个设计选择,因为它感觉更自然地说“洛杉矶的天气怎么样?”而不是“洛杉矶现在的天气怎么样?”。正是这样的小细节使得与优秀的聊天机器人交谈变得如此愉快。
我们知道我们有一个位置和一个日期,因此可以继续编写其余的逻辑。有了位置和日期,我们可以向 Open Weather Maps API 发起请求。
发起 API 请求
发起 API 请求包括在 URL 上使用GET、PUT、POST或DELETE方法,并附带一些可选数据。一个设计良好的 API 将设计成在 URL 中包含大部分关于请求的信息。这意味着我们将根据用户的选择更改 URL。
对于 Open Weather Maps API,我们需要发送请求的 URL 结构如下:
不幸的是,API 需要我们定义一个国家代码。在这个例子中,我们应该使用US,因为我们选择了 US_CITY 作为我们的槽类型。如果你选择了不同的槽类型,请确保输入你国家的ISO 3166代码。
要向这些 URL 发出请求,我们需要使用一个请求库。Node 内置了一个HTTP标准库可以用来发出请求,但还有一些其他库可以使我们的生活更加简单。我们将使用的一个库叫做axios。使用axios而不是标准HTTP库有两个主要原因:
-
它更加用户友好
-
它是基于承诺的,因此你可以控制数据流
要使用axios发出请求,我们首先需要安装它并在代码中引入。导航到你的weatherGods Lambda 文件夹,运行npm install --save axios并在index.js文件的顶部添加const axios = require('axios');。
现在发出请求可以简单到只需在任何想要发出请求的地方添加这一行代码:
axios.get(*URL*)
对于我们的请求,我们还需要传递我们的 API 密钥。对于 Open Weather Maps API,我们需要在 URL 的末尾添加查询字符串appid=${process.env.API_KEY}。
我们将 API 密钥存储在环境变量中,这样它就不会被提交到源代码控制(GIT),否则其他人可以访问它。它们可以在你的 Lambda 控制台中访问和更改。要存储环境变量,请在 Lambda 控制台中向下滚动到环境变量并输入你想要存储的键和值:
环境变量
当我们发出请求时,我们无法访问结果。从承诺中获取结果有几种不同的方法,但我们将使用async和await来使我们的代码尽可能干净和易于阅读。为了使async和await工作,我们需要稍微修改我们的处理函数。在我们声明输入值的地方,我们需要声明这个函数是一个async函数。我们还需要检查我们的 Lambda 是否运行在支持async函数的 node 8.10 上。如果你使用的是我们在第二章中创建的构建脚本,使用 AWS 和 Amazon CLI 入门,那么我们所有的函数都是自动使用 node 8.10 设置的,但你总是可以通过查看 Lambda 控制台上的运行时来检查。我们通过在方法名前添加async来使我们的处理方法异步:
async handle(handlerInput) {}
要使用async和await从承诺中获取结果,我们需要在承诺前加上await。这意味着代码的其余部分将不会开始运行,直到承诺返回:
let result = await promise();
现在我们已经对axios和async/await进行了快速介绍,我们可以开始编写我们将要发出的请求。因为我们有针对当前天气和天气预报的不同 URL,我们需要检查所选日期是否是当前日期,或者他们是否在寻找预报。
比较日期是一个令人惊讶的复杂任务,因此我们将使用一个 npm 包来使它更容易。这个包叫做 moment,它是一个专为与日期一起使用而制作的包。使用 npm install --save moment 在我们的 Lambda 中安装它,然后通过在 index.js 文件的顶部添加 const moment = require('moment'); moment().format(); 来将其引入 Lambda。
在 handler 中,我们可以添加以下检查:
let isToday = moment(date).isSame(Date.now(), 'day');
if (isToday) {
// lookup todays weather
} else {
// lookup forecast
}
接下来,我们需要添加我们将要向 openWeatherMaps 发出的请求。我们从 axios 收到的响应包含了请求的所有信息。因为我们只关心返回的数据,所以我们可以解构响应并重命名数据。解构允许我们从对象中选择一个键并将其命名为其他名称:
let { key: newKeyName } = { key: 'this is some data' };
我们可以使用这种解构来将当前天气数据和预报数据重命名为不同的名称,以避免未来的混淆:
if (isToday) {
let { data: weatherResponse } = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${location},us&&appid=${process.env.API_KEY}');
} else {
let { data: forecastResponse } = await axios.get(`https://api.openweathermap.org/data/2.5/forecast?q=${location},us&&appid=${process.env.API_KEY}`);
}
对于这些请求的响应,我们需要提取我们想要发送给用户的信息。为此,我们需要知道我们将要接收的数据和我们想要的数据。
检查你将收到的确切数据的一个很好的方法是向 API 发送测试请求。制作 API 请求的一个很好的工具是 Postman,因为它允许你发送 GET、PUT、POST 和 DELETE 请求并查看结果。为了测试我们的 API 请求,我们可以打开 Postman 并将 https://api.openweathermap.org/data/2.5/weather?q={$location},us,&APPID=${API_KEY} 放入请求栏。在发送请求之前,只需将 ${location} 改为测试城市,将 ${API_KEY} 改为我们生成的 Open Weather Map 网站上的 API 密钥。它看起来可能像这样:https://api.openweathermap.org/data/2.5/weather?q=manchester,us,&APPID=12345678。
从这个请求中,我们将得到一个类似以下的结果:
{
"coord": {
"lon": -71.45,
"lat": 43
},
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10n"
},
{
"id": 701,
"main": "Mist",
"description": "mist",
"icon": "50n"
}
],
"base": "stations",
"main": {
"temp": 283.98,
"pressure": 1016,
"humidity": 93,
"temp_min": 282.15,
"temp_max": 285.15
},
"visibility": 16093,
"wind": {
"speed": 1.21,
"deg": 197
},
"clouds": {
"all": 90
},
"dt": 1526794800,
"sys": {
"type": 1,
"id": 1944,
"message": 0.0032,
"country": "US",
"sunrise": 1526807853,
"sunset": 1526861265
},
"id": 5089178,
"name": "Manchester",
"cod": 200
}
从这些数据中,我们可能想要告诉用户的信息将来自天气和主要部分,其余的数据对我们来说不太相关。为了删除这些信息,我们可以再次使用解构:
let { weather, main: { temp, humidity } } = weatherResponse;
我们需要对预报请求做同样的事情。数据是不同的,因此我们需要进行一些额外的处理来提取我们想要的数据:
let { list } = forecastResponse;
let usefulForecast = list.map(weatherPeriod => {
let { dt_txt, weather, main: { temp, humidity } } = weatherPeriod;
return { dt_txt, weather, temp, humidity }
});
现在,我们已经有了未来五天每三小时的预报数据。这些数据量太大,以至于很难告诉用户,即使他们只询问一天的数据。为了减少数据量,我们可以将预报减少到 9:00 和 18:00 各一个。我们可以使用 usefulForecast 数组上的过滤器,使得 dt_txt 必须以 09:00:00 或 18:00:00 结尾:
let reducedForecast = usefulForecast.filter(weatherPeriod => {
let time = weatherPeriod.dt_txt.slice(-8);
return time === '09:00:00' || time === '18:00:00';
});
现在,我们可以得到用户请求的那天的两个预报。我们可以再次使用 moment 来比较结果和用户选择的日期:
let dayForecast = reducedForecast.filter(forecast => {
return moment(date).isSame(forecast.dt_txt, 'day');
});
现在我们应该有一个包含两个预报的数组,这些预报包含了用户询问的那天的 9:00 和 18:00 的天气、温度和湿度。
使用当前天气和预报的数据,我们可以开始创建用户响应。我们将从一个当前天气请求开始。我们可以使用模板字符串来简化格式化。你可以根据需要修改措辞或结构,只要使用正确的变量即可:
let speechText = `The weather in ${location} has ${weatherString} with a temperature of ${formattedTemp} and a humidity of ${humidity} percent`;
你可能已经注意到我们使用了两个尚未定义的变量。让我们来看看。
weatherString 需要从当前正在发生的天气类型数组中构建。为了处理这些,我们可以创建一个新的函数,该函数接受 weather 数组并返回一个更易于人类/Amazon Alexa 读取的字符串。这个函数应该放在 handlers 对象之外作为一个新的函数声明:
const formatWeatherString = weather => {
if (weather.length === 1) return weather[0].description
return weather.slice( 0, -1 ).map( item => item.description ).join(', ') + ' and ' + weather.slice(-1)[0].description;
}
如果只有一种天气类型,此函数返回描述。当有多个天气类型时,在类型之间插入逗号,除了最后一个,它使用 and 来添加。这将创建如 破碎的云,小雨和雾 这样的字符串。
接下来,我们需要将温度转换为大多数人都能理解的尺度。我们给出的温度是开尔文,所以我们需要将其转换为摄氏度或华氏度。我已经提供了这两个函数,但我们只需要在 Lambda 中使用一个:
const tempC = temp => Math.floor(temp - 273.15) + ' degrees Celsius ';
const tempF = temp => Math.floor(9/5 *(temp - 273) + 32) + ' Fahrenheit';
在我们的 getWeather 处理程序内部,我们现在可以向 isToday 块中添加对这些函数的调用。你可以取消注释你不想使用的温度函数:
let weatherString = formatWeatherString(weather);
let formattedTemp = tempC(temp);
// let formattedTemp = tempF(temp);
现在我们已经拥有了创建将传递给用户的 speechText 变量的所有必要信息,我们需要为预报数据遵循类似的步骤集。我们可以从一个我们想要构建的短语开始,这个短语比第一个更长更复杂:
let speechText = ` The weather in ${location} ${date} will have ${weatherString[0]} with a temperature of ${formattedTemp[0]} and a humidity of ${humidity[0]} percent, whilst in the afternoon it will have ${weatherString[1]} with a temperature of ${formattedTemp[1]} and a humidity of ${humidity[1]} percent`
为了填充这些变量,我们需要在 dayForecast 数组的两个元素上使用 formatWeatherString() 和 tempC() 函数。如果你想使用华氏度,可以将 tempC() 替换为 tempF():
let weatherString = dayForecast.map(forecast => formatWeatherString(forecast.weather));
let formattedTemp = dayForecast.map(forecast => tempC(forecast.temp));
let humidity = dayForecast.map(forecast => forecast.humidity);
这将把早晨的预报放入数组的第一个索引中,正如我们在 speechText 字符串中所要求的。
现在我们已经有了当前天气和预报的字符串响应,我们需要告诉用户:
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
保存此函数后,我们就准备好部署这个 Lambda。使用我们的构建脚本,这通过进入主 Lambda 文件夹并运行 ./build.sh weatherGods 来完成。
最终设置和测试
在创建并上传 Lambda 之后,我们可以完成设置的最后一步,然后测试我们的技能。在技能开始工作之前,我们需要做两件事:
-
将 Alexa 技能套件添加为 Lambda 的触发器
-
将 Lambda ARN 添加到技能端点
我们之前已经做过两次,所以这只是一个简要的指南。打开 Lambda 控制台并导航到 weatherGods Lambda。在设计部分,添加 Alexa 技能套件作为触发器,然后添加技能 ID 到配置窗口中,并保存 Lambda。复制 Lambda 的 ARN 并导航到 Alexa 技能套件开发者控制台,在那里我们可以进入 WeatherGods 技能并将 Lambda ARN 添加到技能端点。
现在技能的设置已经完成,我们可以开始测试它。在 Alexa 技能套件控制台中,确保你处于 WeatherGods 技能,并且技能构建清单上的所有项目都已完整。如果你有任何缺失,请返回并完成该部分:
技能构建清单
现在,我们可以进入测试选项卡并尝试这个技能。我们可以启动这个技能,然后请求预报,我们应该被告知给定城市的预报:
城市天气预报
这是一个尝试不同方式询问相同内容并扩展意图表述的好地方。
提升用户体验
虽然我们这个技能的第一个版本运行良好,但在几个关键部分可以进行改进。
-
处理错误
-
会话内存
-
SSML
处理我们的 API 调用错误
当我们最初设置这个函数时,我们没有为我们的 API 调用包含任何错误处理。有可能 API 或我们的调用发生了一些导致它失败的事情。这可能是大量的事情,例如断开的互联网连接、不正确的请求、未知的位置、过期的 API 密钥或 API 崩溃。
为了处理这个问题,我们需要修改我们的技能向 Open Weather Maps API 发送请求的方式。使用纯 async 和 await 的一种限制是我们无法判断请求是否成功或失败。有两种处理方法:
-
你可以使用
try…catch块来捕获发生的任何错误。我们这样做的方式是将isToday块内的所有内容包裹在一个try块中,然后有一个catch告诉用户我们无法处理这个请求。 -
你可以将请求传递给一个返回
[error, result]数组的函数。如果没有发生错误,那么它将是null,因此我们可以根据这个事实进行逻辑处理。
这两种方法都适用,但它们最好在不同的情境中使用。
第一个 try…catch 方法用于捕获代码中的错误。我们可以通过将大部分逻辑包裹在一个单独的 try…catch 中来利用这一点:
try {
if (isToday) {
...
} else {
...
}
} catch (err) {
console.log('err', err);
return handlerInput.responseBuilder
.speak(`My powers are weak and I couldn't get the weather right now.`)
.getResponse();
}
保持错误信息轻松愉快通常是个好主意,因为用户不太可能感到烦恼。
第二种方法通常用于当你想要捕获特定承诺错误时。我们需要创建一个新的函数,该函数接受一个承诺并返回错误和结果状态。这个函数通常被称为 to:
const to = promise => promise.then(res => [null, res]).catch(err => [err, null]);
如果这个函数得到一个解决的承诺,它将错误作为null返回并返回结果。但如果发生错误,它将返回一个错误和一个null结果。由于一个称为错误优先编程的标准设计,错误总是位于第一个位置。
这种方法非常适合在非常具体的位置捕获错误,无论是要不同地处理它还是仅仅在那个点记录更多信息。我们可以在当前天气请求上使用这个方法,给出一个稍微不同的响应:
let [error, response] = await to(axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${location},us&appid=${process.env.API_KEY}`));
if (error) {
console.log('error getting weather', error.response);
let errorSpeech = `We couldn't get the weather for ${location} but you can try again later`;
return handlerInput.responseBuilder
.speak(errorSpeech)
.getResponse();
}
let { data: weatherResponse } = response;
我们可以用来处理错误的最后一个工具是为整个 Alexa 技能提供一个错误处理器。我们可以创建另一个处理器,当我们的代码中发生未捕获的错误时会被调用。这可能是我们返回了不正确的响应,有一个未定义的变量,或者是一个未捕获的承诺拒绝。
因为我们希望每次发生错误时都调用这个函数,所以我们的canHandle函数总是返回 true。然后处理器会接收到handlerInput,还会接收到一个error变量。我们可以console.log出错误的响应,然后向用户发送错误消息:
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
console.log(`Error handled: ${error.message}`);
return handlerInput.responseBuilder
.speak(`Sorry, I can't understand the command. Please say again.`)
.getResponse();
},
};
要将此处理器应用于我们的技能,我们可以在Alexa.SkillBuilders中的.addRequestHandlers之后添加.addErrorHandlers(ErrorHandler)。
有了这些措施,如果我们的代码或向 Open Weather Map API 发出请求时出现错误,我们的技能将工作得更好。你应该始终在 API 调用周围有一些错误处理过程,因为你永远不知道它们何时可能会出错。
会话内存
目前有一件事不起作用,那就是询问后续问题。从最初的完美对话中,我们必须跟随问题,例如*明天怎么样**?和在迈阿密怎么样?*这些问题使用关于先前请求的知识来填充日期或位置。拥有能够在交互之间记住某些信息的技能意味着它可以以更人性化的方式进行交互。我们做出的交互中很少有完全不依赖于先前信息的。
为了在交互之间保持信息,我们有会话属性的概念。这些是附加到会话上的键值对,而不仅仅是单个交互。一旦 Alexa 认为她已经完成了任务,她就会关闭会话。在 Alexa 中,设置和检索会话属性也非常简单。获取会话属性就像调用以下代码一样简单:
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
这意味着我们可以访问我们之前存储在会话属性中的值。要存储值在会话属性中,我们可以将一个对象传递给.setSessionAttributes:
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
我们需要做的最后一件事是告诉 Alexa 会话还没有结束。我们通过在响应构建器中的.getResponse()之前添加.withShouldEndSession(false)来实现这一点,当我们想要保持会话属性时。
如果用户在设定的时间内没有做出回应,会话仍然会被关闭:
return handlerInput.responseBuilder
.speak(speechText)
.withShouldEndSession(false)
.getResponse();
我们可以使用这个强大的工具来存储成功请求的日期和位置,然后使用它们来填充用户未填充的位置或日期槽位。
我们需要做的第一件事是从存储中获取会话属性。然后我们可以使用这些值来填充日期和位置变量。如果我们从槽位中没有获取到值,我们尝试会话属性;否则,我们将它们设置为null。然后我们将本地的sessionAttributes变量设置为等于我们的日期和位置。这意味着来自槽位的新值会覆盖现有的会话属性值:
let sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
location = location.value || sessionAttributes.location || null;
date = date.value || sessionAttributes.date || null;
sessionAttributes = { location, date };
我们已经更改了本地会话属性,但还没有在会话中设置它们。我们留到即将响应用户之前再进行设置。我们选择不立即保存,因为如果用户提供了无效的槽位,它就会被存储。如果我们就在发送消息之前存储它,那么我们就知道 API 调用已经成功:
let speechText = `The weather in ${location} has ${weatherString} with a temperature of ${formattedTemp} and a humidity of ${humidity} percent`;
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
return handlerInput.responseBuilder
同样,我们需要在返回预报消息之前添加handlerInput.attributesManager.setSessionAttributes(sessionAttributes);。
这个例子很好地使用了会话属性,但它可以用于更多的事情。它可以用来存储特定意图的信息、之前的对话主题或关于用户的信息。
有一个需要注意的事项是,会话属性只存在于与用户的对话会话期间。如果你想从一个会话保持属性到另一个会话,你可以使用持久属性,但这需要配置你的技能与持久化适配器。更多详细信息可以在本章末尾找到。
SSML
当你向用户发送响应时,你可能不希望 Alexa 以她通常的方式说出它。Alexa 已经很智能了,可以处理标点符号,在句末提高音调并在句点后暂停,但如果你想要更大的控制权呢?
SSML 是语音合成的标准标记语言,Alexa 支持 SSML 的一个子集,允许使用 13 个不同的标签。这些标签允许你指定文本的朗读方式。这意味着你可以在你的语音中添加<break time="2s">来添加两秒的停顿,使用<emphasis level="moderate">要强调的文本</emphasis>来强调语音的某个部分,或者使用<prosody rate="slow" pitch="-2st">来改变语音的音调和速度,</prosody>。
有很多方法可以改变 Alexa 说话的方式,所有这些都可以在 Alexa SSML 参考页面上找到(developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html#emphasis)。
我们对用户说的语音已经被 Alexa 对标点符号和问题的处理处理得很好。这意味着我们现有的消息中,我们用 SSML 改进的并不多。为了给我们一些总是需要额外语音控制的东西,我们将添加一个新的意图——tellAJoke。如果你曾经听到有人把一个笑话讲砸了,那么你就知道笑话需要适当的语调、速度和时机。
我们需要在 Alexa 技能套件控制台中添加tellAJoke意图,然后添加一些语音,但这次我们不需要任何槽位。
保存并构建模型后,我们可以回到我们的代码来处理这个新的意图:
添加讲笑话的意图
这个意图的处理程序非常简单。它需要做的只是从笑话数组中随机获取一个笑话并告诉用户。我们使用Math.floor(Math.random() * 3);来获取一个小于 3 的随机整数。如果你想添加更多的笑话,只需将3改为你拥有的笑话数量:
const JokeHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
handlerInput.requestEnvelope.request.intent.name === 'tellAJoke';
},
async handle(handlerInput) {
let random = Math.floor(Math.random() * 3);
let joke = jokes[random];
return handlerInput.responseBuilder
.speak(joke)
.getResponse();
}
};
更有趣的部分是创造笑话。我们需要先创建一个名为jokes的变量,它是一个数组。在这个数组中,我们可以放入一些与天气相关的笑话。我已经添加了前三个,但请随意添加你自己的(并移除我那些不太有趣的笑话):
let jokes = [
`Where do snowmen keep their money? In a snow bank.`,
`As we waited for a bus in the frosty weather, the woman next to me mentioned that she makes a lot of mistakes when texting in the cold. I nodded knowingly. It’s the early signs of typothermia.`,
`Don’t knock the weather. If it didn’t change once in a while, nine tenths of the people couldn’t start a conversation.`
];
如果我们现在发布这个技能,那些笑话会比预期得更糟糕。我们首先想要修复的是时间。在笑点前添加断句标签可以使笑话变得更好:
let jokes = [
`Where do snowmen keep their money? <break time="2s" /> In a snow bank.`,
`As we waited for a bus in the frosty weather, the woman next to me mentioned that she makes a lot of mistakes when texting in the cold. I nodded knowingly. <break time="1s" /> It’s the early signs of typothermia.`,
`Don’t knock the weather. <break time="1s" /> If it didn’t change once in a while, nine tenths of the people couldn’t start a conversation.`
];
精确的时间可能并不完美,但它们已经比之前好得多。讲好笑话的另一个关键是你在某些词上所加的强调。在 Alexa 中为语音部分添加强调是通过将这些词包裹在emphasis标签中实现的:
`This sentence uses both <emphasis level="strong">increased</emphasis> and <emphasis level="reduced">decreased</emphasis> emphasis`;
在我们的笑话中添加emphasis标签,我们得到这个:
let jokes = [
`Where do snowmen keep their money? <break time="2s" /> In a <emphasis> snow bank </emphasis>`,
`As we waited for a bus in the frosty weather, the woman next to me mentioned that she makes a lot of mistakes when texting in the cold. I nodded knowingly. <break time="1s" /> It’s the early signs of <emphasis> typothermia </emphasis>`,
`Don’t knock the weather. <break time="1s" /> If it didn’t change once in a while, nine tenths of the people <emphasis> couldn’t start a conversation</emphasis>`
];
当使用emphasis标签但没有提供级别时,默认使用中等级别。
有很多其他的 SSML 标签可以用来改变 Alexa 说响应的方式,它们可以在 Alexa SSML 页面找到(developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)。
测试
现在我们已经将这些更改添加到我们的 Lambda 中,我们可以构建并测试它。
测试 API 错误发生时会发生什么相当困难,但我们可以测试会话属性和 SSML。
会话属性可以通过询问后续问题来测试,我们期望从上一个问题中存储了一些数据。我们可以询问一个地点的预报,然后询问一个新的地点。日期应该已经保存在会话属性中,所以我们应该得到新地点的预报而不是当前天气。然后我们可以询问今天的天气,新的地点应该已经被保存,所以我们不应该被提示输入地点:
会话属性测试
我们也可以通过请求一个笑话来测试 SSML。你收到的笑话应该包含我们添加的停顿,可能还有一些强调。当你自己测试时,你将能够清楚地听到这些:
笑话
摘要
在本章中,我们介绍了如何使用外部 API 来增加聊天机器人可用的信息,从而让你能够创建更强大的技能。
然后,我们探讨了如何让用户体验更加愉快。我们采取了以下三种方式:
-
我们使用错误处理来减少用户请求不工作时产生的挫败感。
-
我们使用会话内存来记住关于对话的细节,这样我们就可以稍后使用它们。这阻止了我们每次用户没有提供所有信息时重复和提示用户。
-
我们使用 SSML 来修改 Alexa 说出我们响应的方式,使句子听起来更人性化。我们还使用 SSML 使笑话更有趣,但它可以用来强调要点或改变说话的语气。
问题
-
什么是 API?
-
Axios 与标准 HTTP 请求库有何不同?
-
处理
async和await错误的两种常见方法是什么? -
我们如何在会话属性中存储 颜色?
-
可以在会话属性中存储哪些类型的数据?
-
为什么你会使用 SSML?
进一步阅读
如果你想了解 持久属性,你可以在 ASK SDK 文档中阅读它们(ask-sdk-for-nodejs.readthedocs.io/en/latest/Managing-Attributes.html)。
要查看 Alexa 支持的完整 SSML 标签列表,请访问 Alexa SSML 参考页面(developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)。
如果你想了解不同的 SSML 标签如何改变文本的朗读方式,请查看 Google SSML 参考页面(developers.google.com/actions/reference/ssml)。它包含许多 SSML 的工作示例,但你不能编辑它们。
第五章:构建您的第一个 Amazon Lex 聊天机器人
前两章仅关注 Amazon Alexa 和构建 Alexa 技能。接下来的三章将教你如何使用 Amazon Lex 构建聊天机器人。在本章中,我们将学习如何构建和测试一个 Lex 聊天机器人,然后我们将通过集成 S3 来提高难度。
Amazon Lex 与 Amazon Alexa 非常相似,但主要区别在于 Lex 被设计成主要用于文本交互。这意味着你可以使用 Lex 来为 Facebook messenger 机器人提供动力,为 Slack 添加功能,甚至向用户发送短信。这并不会阻止你使用 Lex 进行语音交互,并且它可以用来在 Amazon Alexa 生态系统之外构建基于语音的聊天机器人。
本章将涵盖以下主题:
-
使用槽和内置响应创建 Lex 聊天机器人
-
使用 Lambda 实现 FAQ 聊天机器人
-
从 S3 存储中检索答案
技术要求
在本章中,我们将为我们的技能创建一个 Lambda 函数,并使用我们在第二章,AWS 和 Amazon CLI 入门中创建的本地开发设置来创建和部署它。
本章所需的所有代码和数据都可以在bit.ly/chatbot-ch5找到。
创建一个 Amazon Lex 聊天机器人
创建一个 Lex 聊天机器人与创建一个 Alexa 技能的过程非常相似。我们需要创建带有表述的意图,我们可以在这些意图上设置带有槽类型的槽,然后我们可以构建一个对用户的响应。尽管 Lex 和 Alexa 非常相似,但它们之间有一些关键的区别,我们将在本章中探讨。
设置聊天机器人
要开始创建我们的第一个 Lex 聊天机器人,我们需要打开 AWS 控制台并搜索“Lex”。一旦进入 Lex 页面,点击“开始”以进入机器人设置页面。您将看到使用三个样本机器人之一或创建自定义机器人的选项。我们将创建一个自定义机器人,因此请选择该选项。其他三个选项是样本机器人。这些机器人被构建来展示您可以使用 Lex 聊天机器人实现的应用程序:
机器人创建选项
在选择了自定义机器人后,我们可以为我们的机器人命名并设置一些其他设置。所有这些设置都可以稍后编辑,因此我们可以从一些默认设置开始。
为您的聊天机器人选择一个声音。如果您想为基于语音的聊天机器人设置 Lex 机器人,这将使用该声音。由于我们将仅使用 Lex 进行基于文本的交互,我们可以选择“无”。我们将构建一个基于文本的应用程序,但您仍然应该选择一个声音,以便您可以进行语音测试。
最后两部分是设置超时;我们可以使用默认的 5 分钟,并选择“否”回答 COPPA 问题。如果你想创建一个与儿童交谈的聊天机器人,勾选“是”将阻止 Lex 存储任何对话,以符合儿童在线隐私保护法。现在我们已经完成了设置,我们可以点击“创建”。这将带我们到 Lex 仪表板:
Lex 仪表板
创建 Lex 聊天机器人的组件和过程与创建 Alexa 技能的过程非常相似。它们都有意图、话语、槽位和槽位类型,其中大多数的创建几乎与在 Alexa 中的创建相同。
创建意图
我们首先想做的事情是创建意图。与 Alexa 不同,我们有创建意图、导入意图或搜索现有意图的选项。由于这是我们 Lex 中的第一个意图,我们需要创建意图。我们将被提示为新意图输入一个名称;我们应该将我们的第一个意图命名为sayHello:
意图屏幕
首先,我们需要添加话语,以便用户可以触发意图。我们可以添加hi、hey和hello这些话语。这些话语不区分大小写,添加逗号和句号等标点符号是不必要的,尽管可以接受撇号。
Lex 与 Alexa 之间最大的不同之一是我们可以在不需要 Lambda 的情况下发送响应。滚动到页面底部,你会看到满足和响应部分。满足部分让你决定是否将此意图发送到 Lambda。目前,我们将保持将此选项设置为返回参数给客户端。
在响应部分,我们可以告诉 Lex 向用户发送什么信息。点击“添加消息”按钮,将出现一个消息块。在文本区域,我们可以输入我们想要发送给用户的响应。添加一个响应短语,例如Hi there,然后按Enter。与话语不同,响应是区分大小写的,可以包含你想要的任何标点符号:
满足和响应
我们实际上可以添加多个响应消息,Lex 会随机选择其中之一。这使得与聊天机器人的多次互动感觉更自然,更不机械。
完成话语和响应后,我们可以通过意图底部的“保存意图”按钮保存意图。一旦保存,我们就可以构建我们的聊天机器人。
构建聊天机器人需要将你的意图中的所有话语添加到语言模型中。点击屏幕右上角的“构建”按钮,等待系统将聊天机器人组合起来。
测试你的聊天机器人
当 Lex 完成构建后,您将收到通知,屏幕右侧将打开一个新的测试机器人部分。这是一个基本的文本聊天界面,您可以在这里尝试您的机器人。尝试在聊天中输入“嗨”,您应该得到“嗨,”或“嘿:”的响应。
初始测试
如果没有,请检查您是否已添加了话语和响应,并重新构建聊天机器人。
您也可以通过与其对话来测试您的机器人。点击麦克风符号,说出“你好”,然后再次点击麦克风。您应该能看到您说的话,并获得语音响应以及文本响应。如果您收到关于未选择声音的错误,请转到设置 | 通用并更改输出声音。
发布您的机器人
使用一个工作聊天机器人,我们可以发布机器人。点击发布按钮会弹出一个窗口,我们可以选择要发布的别名。这在您想测试机器人新版本是否完全功能而不替换现有实时版本时很有用。您可以创建一个开发或测试别名,而不会覆盖现有的生产机器人:
发布您的机器人
一旦发布完成,您就可以从其他服务访问这个新的别名。
使用槽位
正如我们与 Alexa 所看到的那样,有一个固定的响应是可以的,但使用槽位来自定义交互会更好。进入 sayHello 意图界面,向下滚动到槽位。这与 Alexa 中的槽位配置相同。
给您想要获取的槽位起一个名字;在这种情况下,我们可以要求他们提供他们的名字,所以我们称这个槽位为usersName。我们必须选择一个槽位类型,我们可以选择 GB_FIRST_NAME 或 US_FIRST_NAME。最后一件我们需要做的事情是配置提示。输入一个将引导他们输入名字的问题,例如“你叫什么名字?”。要添加这个槽位,我们需要点击行末的蓝色加号按钮。
当我们看到创建的新行时,我们可以检查是否勾选了“必需”复选框,这样 Lex 就知道要询问用户这个槽位:
槽位创建
现在我们有了槽位,我们需要在我们的响应中使用答案。要将槽位添加到响应中,我们可以将槽位名称用大括号括起来。这意味着我们的响应变为“嗨,{usersName}”。
我们现在可以再次保存这个意图并重新构建聊天机器人。现在我们得到一个稍微长一点的对话:
测试带名字
创建一个 FAQ 聊天机器人
现在我们已经学会了如何制作 Lex 聊天机器人,我们可以开始构建一些更可能在现实世界中看到的机器人。FAQ 聊天机器人越来越受欢迎;它们相对简单易创建,是向网站或 Facebook 群组介绍聊天机器人的好方法。
要开始创建 FAQ 聊天机器人,我们需要找到一个 FAQ 页面作为基础。现在大多数公司网站都有 FAQ 页面,所以你可以找到一个你感兴趣公司的 FAQ 页面,或者跟随我在 CircleLoop(circleloop.com)上的操作。这个网站被选中是因为我在那里工作,并且它将问题分为三组。如果你只是练习,你可以使用任何网站,但如果你想发布你的聊天机器人,请请求公司的许可。你永远不知道,他们最终可能会为此支付你费用!
CircleLoop 也很好,因为它总共有 24 个问题,这是一个很好的数量——太多的话会花费很长时间,Lex 可能会混淆类似的问题。
设置 Lex
正如我们在本章的前半部分所做的那样,我们需要创建一个新的 Lex 聊天机器人。在 Lex 控制台页面上,将有一个所有 Lex 聊天机器人的列表,在其上方将有一个创建按钮。
按照之前的过程,选择自定义机器人,给你的聊天机器人命名,选择声音,选择五分钟的超时时间,并对于 COPPA 问题选择否。如果你正在制作一个为 13 岁以下儿童设计的聊天机器人,你应该研究 COPPA 并根据你的答案进行修改。
收集数据
本节的所有数据文件都可在data文件夹中的bit.ly/chatbot-ch5找到,但如果你使用的是自己的公司,你必须按照你公司的常见问题解答(FAQ)流程进行。
在我们开始创建意图之前,我们需要获取我们将要使用的数据。前往你选择的常见问题解答(FAQ)页面,并打开一个名为faq-setup.json的新文件。
此文件将包含一组意图和答案,格式如下:
{
"intentName1": "This is the answer to question 1",
"intentName2": "You do this by selecting 'A' and then pressing 'START'"
...
}
意图名称应该是描述问题所询问内容的唯一字符串。例如,如果你问“公司在哪里?”你可能将意图命名为companyLocation。
遍历网站中“设置与使用 CircleLoop”部分的全部问题。为“用户与数字”和“其他问题”部分使用新文件重复此过程。你应该最终得到包含网站上所有答案的三个 JSON 文件。以下是faq-setup.json文件中的一个部分:
{
"howItWorks": "CircleLoop is a cloud-based business phone system, which allows ... settings.",
"technicalKnowledge": "No. We’ve made it really easy with our simple apps. As long as ... and you’re ready to go.",
...
"sevenDayTrial": "Full user privileges, including the ability to add users ... during your trial period."
}
我们现在将这三个文件上传到 S3 存储桶,以便我们的 Lambda 函数可以访问它们。在你的 AWS 控制台中,导航到 S3 并点击创建存储桶。给你的存储桶起一个独特的名字并继续配置。对于这个项目,我们不需要为此存储桶添加任何额外的权限:
上传文件
现在我们已经创建了存储桶,我们可以上传我们的 FAQ 文件。点击进入你刚创建的存储桶,然后点击上传按钮。同样,我们不需要从默认设置更改任何权限。
创建意图
一旦我们创建并上传了 JSON 文件,我们需要创建意图以匹配它们。检查您的 JSON 文件,并为每一行创建一个新的意图。意图名称必须与 JSON 文件对象中的键完全相同。然后,您可以使用 FAQ 页面的问题作为该意图的第一个表述:
意图
到此过程结束时,你应该有与你的 JSON 文件中的行数一样多的意图。然后你应该为每个意图添加更多的表述。这些新的表述应该是相同问题的其他表述方式。扩展表述列表增加了用户获得正确答案的机会。
创建 Lambda 处理器
现在我们有了意图——捕捉用户的表述——我们需要创建发送给用户的响应。因为我们有三个文件,所以我们可以创建三个 Lambda。
每个 Lambda 将处理一个部分的问题。
在您的主 Lambda 文件夹中创建三个文件夹,分别命名为 CL-setup、CL-users 和 CL-other。在每个文件夹中创建一个 index.js 文件。打开 CL-setup 中的 index.js 文件,我们可以开始编写处理器,从一个空的处理器开始:
exports.handler = async event => {
};
首先,我们需要找出哪个意图触发了 Lambda。Lex 收到的数据结构与 Alexa 收到的数据结构略有不同:
let intentName = event.currentIntent.name;
现在我们有了意图名称,我们需要向 S3 发送请求以获取包含答案的文件。正如我们在第三章,创建您的第一个 Alexa 技能中所做的那样,我们首先需要在 AWS 中要求并创建一个新的 S3 实例。在文件顶部,在 exports.handler 之前添加此代码:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
要向 S3 发送请求,我们需要传递一些查询参数。这是一个包含我们的对象所在的 Bucket 和我们想要的对象的 Key 的对象。
由于我们使用的是 Node 8.10 和 async 函数,我们需要返回一个承诺值。这意味着我们需要创建一个新的 Promise 并然后解析和拒绝我们的结果。回到我们的处理器函数中,我们可以添加此代码。与第三章,创建您的第一个 Alexa 技能不同,我们可以为这个 Lambda 将 Key 设置为一个固定的值 faq-setup.json,因为这个 Lambda 只会被 设置和使用 CircleLoop 部分的问题调用:
var params = {
Bucket: 'cl-faq',
Key: `faq-setup.json`
};
return new Promise((resolve, reject) => {
// do something
resolve(success);
reject(failure);
})
我们可以将我们的 s3.getObject() 代码放在这个 Promise 中,以便在 handleS3Data() 被解析时执行,在 handleS3Error() 被拒绝时执行:
return new Promise((resolve, reject) => {
s3.getObject(params, function(err, data) {
if (err) { // an error occurred
reject(handleS3Error(err));
} else { // successful response
console.log(data);
resolve(handleS3Data(data, intentName));
}
});
})
我们现在需要创建两个用于 S3 响应的处理程序。这些函数可以在处理器之后创建:
const handleS3Error = err => {
}
const handleS3Data = (data, intentName) => {
}
我们将首先创建数据处理器。在这里,我们首先需要解析数据的主体。这是因为它以 buffer 的形式下来,在我们能够处理它之前需要将其转换为 JSON:
let body = JSON.parse(data.Body);
在 JSON 格式的数据中,我们现在可以检查intentName是否是对象中的一个键。如果不是,我们需要返回handleS3Error函数来发送错误消息给用户:
if (!body[intentName]){
return handleS3Error(`Intent name ${intentName} was not present in faq-setup.json`);
}
在handleS3Error中,我们可以console.log错误并创建一个错误响应字符串。这应该告诉用户发生了错误,并要求他们尝试再问一个问题:
console.log('error of: ', err);
let errResponse = `Unfortunately I don't know how to answer that. Is there anything else I can help you with?`;
创建响应
在 Lex 中创建响应的方式与在 Alexa 中创建的方式非常不同。在 Lex 中,需要遵循一个对象结构:
sessionAttributes: {},
dialogAction: {
type: '',
fulfillmentState: '',
slots: {},
slotToElicit: '',
message: { contentType: 'PlainText', content: ''};
}
因为这是一段我们可能会多次使用的代码,我们可以为每种类型创建函数。这里是一个用于完成对话流程最后阶段的函数。这个函数可以被添加到index.js文件的底部:
const lexClose = ({ message, sessionAttributes = {}, fulfillmentState = "Fulfilled"}) => {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState,
message: { contentType: 'PlainText', content: message }
}
}
}
这个函数使用默认值sessionAttributes和fulfillmentState,因为我们大多数情况下不会设置它们,但如果我们想设置的话,这也是好的。
使用这个新函数,我们现在可以在我们的处理函数中创建响应。在我们的handleS3Data函数内部,我们可以返回这个lexClose函数,将文件中的答案作为消息:
return lexClose({ message: body[intentName] });
我们还需要在文件底部创建一个lexElicitIntent函数,以便当我们告诉用户再问一个问题的时候使用。这告诉 Lex 它应该期待一个意图表述作为其下一条消息:
const lexElicitIntent = ({ message, sessionAttributes = {} } ) => {
return {
sessionAttributes,
dialogAction: {
type: 'ElicitIntent',
message: { contentType: 'PlainText', content: message }
},
};
}
然后,这个lexElicitIntent可以在handleS3Error函数的末尾返回,告诉用户再问一个问题:
return lexElicitIntent({ message: errResponse });
这个文件可以被复制到其他两个文件夹中。我们只需要更改 params 对象中的key和错误控制台日志中的文本以及响应。做出这些更改后,我们可以使用我们的构建脚本来部署我们的三个 Lambdas。
部署了所有三个 Lambdas 之后,我们需要确保它们的角色包括访问 S3 存储桶的权限。在每一个 Lambdas 中,向下滚动到角色部分,我们应该能看到 lambdaBasic 的角色。我们应该在第三章,创建您的第一个 Alexa 技能中更新了这一点,但我们应该再次检查。导航到 IAM 服务并确保 lambdaBasic 有 S3 读取权限。如果没有,那么将 AmazonS3ReadOnlyAcess 附加到这个角色上。
Lambda 满足
我们可以使用 Lambdas 来创建我们意图的响应。这比仅仅有一个文本响应给我们更多的控制。Lex 的伟大之处在于每个意图都可以有自己的 Lambda 处理程序,或者多个意图可以共享一个 Lambda。
部署了三个 Lambdas 之后,我们可以使用它们来满足意图。我们将把所有关于设置的意图都共享给CL-setup Lambda,所有关于用户和数字的意图都共享给CL-users Lambda,所有其他问题都共享给CL-other Lambda。
打开你的 Lambda 控制台并进入你的 FAQ 聊天机器人。打开一个意图并滚动到满足部分。
有两种选择:
-
AWS Lambda 函数
-
返回参数给客户端
由于我们已经创建了 Lambdas,我们可以选择 AWS Lambda 函数,这为我们打开了更多的菜单项供我们选择。主要的一个是 Lambda 下拉菜单,我们可以从中选择哪个 Lambda 会在意图满足时被触发:
Intent fulfillment options
选择 Lambda 之后,我们需要保存意图并继续下一个意图。这需要在聊天机器人中的每个意图上完成,确保将正确的意图发送到正确的 Lambda。
Building and testing
当所有意图都指向一个 fulfillment Lambda 时,我们可以构建我们的聊天机器人,然后测试它。点击屏幕右上角的构建按钮,等待构建过程停止。这可能需要几分钟,当构建过程结束时,测试部分将打开。
要测试我们的聊天机器人,输入一个问题,你应该会收到正确的答案:
FAQ tests
如果你没有得到正确的答案,或者完全没有得到错误,那么有几个地方需要检查:
-
查看 Lambda 日志并检查是否调用了正确的 Lambda。你也应该能看到一个包含错误信息的日志,这可以帮助你定位错误。
-
检查 Lambda 是否有权限访问 S3 存储桶。
-
请参考本书末尾的 Lambda 调试指南。
Lex responses
我们刚刚看到了 Lex 可以返回的两种不同类型的响应。目前 Lex 可以处理五种不同类型的响应:
-
elicitSlot -
`elicitIntent` -
confirmIntent -
close -
delegate
这些都可以在All-Lex-Responses.js文件中的bit.ly/chatbot-ch5找到。然后你可以将它们复制到你的未来项目中。
elicitSlot
当你对 slot 值进行检查并发现其中一个值不正确时,elicitSlot响应类型非常有用。然后你可以要求用户重新输入该 slot 的值,并确保 Lex 将其存储在正确的 slot 中。
要调用elicitSlot,你需要传递一个消息、slots(一个包含所有 slots 和当前值的对象)、slotToElicit值和intentName:
const lexElicitSlot = ({ sessionAttributes = {}, message, intentName, slotToElicit, slots }) => {
return {
sessionAttributes,
dialogAction: {
type: 'ElicitSlot',
intentName,
slots,
slotToElicit,
message: { contentType: 'PlainText', content: message }
},
};
}
如果我们在 Lex 中重新构建了汽车助手机器人,当验证 slot 值时,我们会使用lexElicitSlot函数。如果有一个 slot 值不正确,我们会像这样调用这个函数:
return lexElicitSlot({
intentName: 'whichCar',
slotToElicit: 'size',
slots: {
size: null,
cost: 'value',
doors: 5,
gears: null
}
})
elicitIntent
我们已经看到了这个 Lex 响应,它接受消息和会话属性。这通常用于继续对话或以新的意图重新开始:
const lexElicitIntent = ({ message, sessionAttributes = {} } ) => {
return {
sessionAttributes,
dialogAction: {
type: 'ElicitIntent',
message: { contentType: 'PlainText', content: message }
},
};
}
confirmIntent
当你想询问用户是否想要做某事时,会使用confirmIntent响应。这可以在 FAQ 机器人的末尾使用,例如询问Would you like to sign up?,这将是一个confirmIntent响应,用于signUp意图。你需要传递message、intentName和该意图的slots。任何你不想预先填充的 slots 应该有一个值为null:
const lexConfirmIntent = ({ sessionAttributes = {}, intentName, slots, message }) => {
return {
sessionAttributes,
dialogAction: {
type: 'ConfirmIntent',
intentName,
slots,
message: { contentType: 'PlainText', content: message }
},
};
}
close
这是 Lex 最简单且最常用的响应。您需要传递的唯一东西是 message:
const lexClose = ({ sessionAttributes = {}, fulfillmentState = 'Fulfilled', message }) => {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState,
message: { contentType: 'PlainText', content: message }
},
};
}
委派
delegate 响应是您希望 Lex 决定向用户发送什么内容的地方。这通常用于您已验证输入并且希望 Lex 请求下一个槽位或进入满足阶段。它只需要一个包含当前意图所有槽位的 slots 对象:
const lexDelegate = ({ sessionAttributes = {}, slots }) => {
return {
sessionAttributes,
dialogAction: { type: 'Delegate', slots, }
};
}
摘要
本章是 Amazon Lex 的介绍。你已经了解到 Lex 和 Alexa 在形式和功能上非常相似,但在构建方式和工作方式上存在一些差异。
我们现在可以创建一个具有意图、槽位和硬编码响应的 Lex 聊天机器人。然后我们可以通过创建 Lambda 来处理意图满足来增加其功能。Lex 相比于 Alexa 的一个优点是我们可以使用多个 Lambda 来处理不同的意图。为了帮助我们更容易地响应 Lex,我们创建了一个将值映射到正确响应格式的 Lex 类。
我们使用这些技能构建了一个从 S3 获取数据并用于生成响应的常见问题解答聊天机器人。
在下一章中,我们将利用本章所学,通过向聊天机器人添加数据库来构建在它之上。我们将使用 DynamoDB 存储有关聊天信息,使我们能够进行更真实的聊天机器人对话。
问题
-
你能否在不使用 Lambda 的情况下创建一个 Lex 聊天机器人?
-
你如何在 Lex 的响应中包含一个槽位?
-
Lex 与 Alexa 在使用 Lambda 方面有何不同?
-
Lex 处理多少种响应类型?
-
你能全部列举出来吗?
-
获取 S3 数据的函数名称是什么?
1323

被折叠的 条评论
为什么被折叠?



