AlexaSkill-聊天机器人开发实用指南-全-
AlexaSkill 聊天机器人开发实用指南(全)
原文:
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 数据的函数名称是什么?
第六章:将 Lex Bot 连接到 DynamoDB
在阅读上一章后,我们知道如何创建 Lex 聊天机器人。现在我们可以开始构建一个引导用户通过更复杂流程的聊天机器人。设计和构建更大的聊天机器人更接近你可能会做的事情,我们将探讨最佳的设计和设置方法。
我们将使用我们的聊天机器人从 S3 获取数据,以及从 DynamoDB 表中获取和写入数据。这使我们能够持久化关于用户选择和流程进度的信息。
本章将涵盖以下主题:
-
为更大、更复杂的聊天机器人创建流程图
-
创建一个 Lex 聊天机器人来涵盖所有意图和流程
-
从 S3 存储桶中检索数据并在其上执行逻辑
-
创建一个 Dynamo 表并使用它来存储和检索信息
技术要求
在本章中,我们将为我们的技能创建一个 Lambda 函数,并使用我们在第二章 入门 AWS 和 Amazon CLI 中创建的本地开发设置来创建和部署它。
本章所需的全部代码和数据可以在 bit.ly/chatbot-ch6 找到。
设计流程
在上一章中我们构建的 FAQ 聊天机器人不需要设计任何流程,因为一切都是简单的问题和答案。这个聊天机器人将更加复杂,具有多个流程,其中一些将引导到其他意图和流程。
完美的对话
和往常一样,我们可以从完美的对话开始构建流程图。这次的不同之处在于我们将有几个不同的对话。我们将有一些从询问库存到进行购买,其他一些在结账前停止,还有一些人甚至不会将任何东西添加到他们的购物车中。这些都是我们需要设计和构建的流程。
从头到尾进行对话是一个好主意。这里有一个这样的对话示例:

完整流程对话
我们还可以创建其他涉及流程一部分的对话。用户可以在他们的购物车中添加一些商品然后保存以备后用,而另一些用户在结账前会想要询问他们的购物车中有何物品。你可能已经看到,这些对话中的一些会有重叠。随着我们继续到流程图,这一点将变得更加明显。
流程图
由于这是一个大而复杂的对话,我们将把流程分成几个部分。这将使创建和可视化更容易。
完整对话的第一部分以及库存检查对话可以用来创建一个 productFind 流程图:

productFind 流程图
正如你所见,这里有一些逻辑与我们在第三章中使用的逻辑相似,在第三章中,我们讨论了如何创建你的第一个 Alexa 技能,即创建你的第一个 Alexa 技能。在这个流程结束时,我们知道用户在询问什么产品。
你可能在这个流程中注意到了一个新的符号。这个符号与intentTrigger类似,但这是用于启动另一个流程的。将整个流程分解成可以相互调用的较小块是保持图表组织最佳的方式:

启动另一个流程
现在我们知道用户在询问什么产品,我们可以创建一个检查库存并询问他们是否想将其添加到购物车的流程。这从请求 S3 开始,如果有库存并且他们想要,我们就将其添加到他们的 Dynamo 购物车中:

库存和购物车
这次对话的最后阶段是检查。这是获取一些关于用户的信息,以便我们可以下订单。这通常包括接受信用卡支付,但我们将不会在这个聊天机器人中这样做:

检查
我们将对话分解成多个较小的流程图的原因是,每个流程只做一件事情。这意味着我们可以将不同的流程连接起来。如果我们有一个知道项目 ID 的用户呢?我们可以让他们跳过productFind流程,直接从lookupAndCart流程开始。
当我们考虑其他一些对话时,我们最终会在主流程图中得到一个流程的网状结构:

主流程图
这个主流程图显示了每个子流程如何连接在一起,以映射任何对话。这个对话网使得聊天机器人能够以一种比老式聊天机器人更人性化的方式处理用户,老式聊天机器人只有一个用户必须遵循的路径。
构建聊天机器人
在所有子流程图和主流程图的基础上,我们可以开始构建聊天机器人。拥有这些子流程的另一个好处是,它们与意图非常相似。
在我们开始创建意图之前,我们需要设置我们的 Lex 机器人。在 Lex 控制台中,点击创建,然后按照第五章中描述的步骤创建自定义机器人,即构建你的第一个 Amazon Lex 聊天机器人。
产品查找
我们将从最常见的对话开始——查找产品。首先,我们将创建一个新的意图,称为productFind。
这个意图将处理想要将产品添加到购物车或只是检查库存水平的用户,因此我们需要提供表示这种意图的话语。我们还需要处理用户的话语,例如,“我想买一件新夹克”和“你们有库存的中号蓝色衬衫吗?”
为了捕获话语中的槽位值,我们可以在槽位名称周围使用花括号:

productFind 的表述
在创建了一些表述之后,我们需要创建槽位和槽位类型。对于productFind意图,我们需要相当多的槽位:type、size、color、length和itemNumber。前四个槽位是显而易见的,但itemNumber则不那么明显。
我们想包含一个项目编号槽位,这样如果客户想要购买他们已经拥有的产品,他们就不需要通过较长的问答式产品查找流程。这些小事情正是区分优秀机器人与卓越机器人的关键。
接下来,我们需要为每个槽位选择一个槽位类型。对于前四个槽位,我们需要创建自定义槽位:
- 类型:我们将销售三种类型的服装:衬衫、夹克和裤子(长裤)。
点击槽位类型旁边的+号并选择创建槽位类型。给你的槽位起一个像clothingType这样的名字,并选择限制到槽位值和同义词。在槽位类型命名时相对具体是一个好主意,因为你不能有两个同名槽位类型。
我们现在可以添加衬衫、夹克和裤子的值。然后我们需要添加用户可能会输入的同义词。例如,他们可能会用blouse、top或t-shirt代替shirt。对于trousers,他们可能会说a pair of trousers、pants或a pair of pants。将所有值的同义词都扩展到你想不出更多为止。
- 尺寸:尺寸的创建过程将与类型非常相似,具有大、中和小等值。如果这是一个真正的零售商,你会有更多的尺寸选项,并且可能基于物品类型提供尺寸选项。
确保为每个颜色值包含一些同义词。我们可以使用 AMAZON.Color 来表示颜色,但这将允许通过数百种颜色——为了使我们的工作更简单,我们将使用五种颜色。
创建一个包含黑色、白色、红色、粉色和蓝色等颜色的自定义槽位类型。你可以添加颜色的同义词,但更有可能他们只是说了一个我们不支持的色彩。
长度:长度的值有long、standard和short。确保添加任何你能想到的同义词,例如medium和normal作为标准的同义词。
- 项目编号:我们不需要为订单编号创建新的槽位类型,因为我们可以使用 AMAZON.NUMBER。如果我们想使用项目编号,例如SH429178,其中我们使用数字和字母,我们就必须使用自定义槽位类型:

完成的槽位
我们需要将所有槽位改为非必需。这是因为如果用户通过项目编号请求物品,我们不希望询问他们想要什么尺寸和颜色,因为物品已经有尺寸和颜色了。
创建 Lambda
处理此意图的 Lambda 需要做几件事情:
-
需要检查是否有项目编号或者所有正确的槽位都已填写。
-
然后它需要获取我们的 S3 库存数据并检查请求商品的库存水平。
-
如果有库存,它会询问用户是否想要将其添加到购物车。如果没有库存,它会告诉用户,并询问他们是否想要寻找另一个产品。
首先在Lambdas中创建一个名为productFind的新文件夹,并在其中创建一个index.js文件。index.js文件可以以我们的默认 Node 8.10 处理程序开始,并将事件传递给handleProductFind函数:
exports.handler = async (event) => {
return handleProductFind(event);
}
在handleProductFind函数内部,我们首先检查槽位值。首先检查的是itemNumber,因为如果这个槽位存在,我们就不需要检查其他任何槽位。之后,我们检查类型、尺寸和颜色,最后如果类型是trousers,再检查长度:
const handleProductFind = event => {
let { slots } = event.currentIntent;
let { itemNumber, type, size, colour, length } = slots;
if (itemNumber) return getItem(slots);
// No item number so using normal product find
if (!type) {
let message = 'Are you looking for a shirt, jacket or trousers?';
let intentName = 'productFind';
let slotToElicit = 'type';
return Lex.elicitSlot({ message, intentName, slotToElicit, slots })
}
...
}
我们可以复制用于类型检查的代码,并重复用于size、color和length槽位,只需为每个测试更改message和slotToElicit。对于length检查,还需要进一步修改,以便它也检查type是否为trousers:
if ( !length && type === 'trousers' ){ ... }
在最后的检查之后,我们可以调用一个函数来获取用户通过选择所选择的商品。我们需要传递槽位,以便我们可以根据用户的选择过滤商品:
return getItem(slots);
在我们的getItem()函数中,我们需要做三件事:获取数据、过滤出与用户答案匹配的商品,并创建响应。
为了从 S3 获取所有库存数据,我们将创建一个getStock()函数。这将与之前我们发出的 S3 请求相同。然后我们可以将其作为getItem()函数的第一部分调用:
const getStock = () => {
var params = {
Bucket: 'shopping-stock',
Key: `stock.json`
};
return new Promise((resolve, reject) => {
s3.getObject(params, function(err, data) {
if (err) { // an error occurred
reject(err)
} else { // successful response
resolve(JSON.parse(data.Body).stock)
}
});
})
}
我们还需要引入aws-sdk并创建一个s3实例。在你的文件夹中运行npm install --save aws-sdk。将此代码放在文件顶部:
const AWS = require('aws-sdk');
const s3 = new AWS.sS()'
现在我们有了数据,我们需要过滤出正确的商品。数组有一个非常有用的函数叫做.find。这个函数将遍历数组中的每个商品,并对该商品执行一些代码。这将会一直发生,直到一个商品返回true,此时函数返回满足条件的商品。如果没有商品满足条件,则返回undefined。
我们可以使用这个方法来获取用户想要的商品。我们希望如果所有槽位的值与商品上的值匹配,或者itemNumber匹配,则返回true。我们还需要确保如果类型是trousers,则长度也要匹配:
let matching = stock.find(item =>
itemNumber === item.itemNumber ||
type == item.type &&
size == item.size &&
colour == item.colour &&
(item.length == length || item.type != 'trousers'));
之后,我们预计将只有一个商品。如果没有,要么是我们创建函数的方式不正确,要么是数据有误。无论如何,我们需要告诉用户我们没有找到他们正在寻找的商品:
if (!matching) {
let message = `Unfortunately we couldn't find the item you were looking for`;
return Lex.Close({ message })
}
如果我们找到了一个商品,但没有库存,那么我们可以告诉用户,并询问他们是否想要寻找另一个产品。这意味着我们将使用confirmIntent Lex 响应。此响应需要一个intentName、一个message以及包含所有具有值或null的槽位的slots对象:
if (matching.stock < 1) {
let message = `Unfortunately we don't have anything matching your request in stock. Would you like to search again?`;
let intentName = 'productFind';
slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message })
}
如果我们找到了产品并且有库存,那么我们需要告诉用户我们有多少库存。我们必须处理的一个棘手问题是类型的复数形式。如果我们找到多个 shirt,它们被称为 shirts;当用户选择了 trousers 时,我们可能有一 pair of trousers 或多 pairs of trousers。为了避免使消息字符串变得过于复杂,我们可以创建一个函数,该函数接受类型和库存并返回正确的单位名称:
const units = (type, stock) => {
if (type === 'trousers') {
return `pair${stock !== 1 ? 's': ''} of trousers`
}
return `${type}${stock !== 1 ? 's': ''}`;
}
这意味着我们可以为用户创建一个更整洁的消息。我们将询问的消息将是他们是否想要将此项目添加到他们的购物篮中。我们可以使用另一个具有 intentName 为 addToBasket 且槽位设置为 matching.itemNumber 的 confirmIntent 响应:
let message = `There are ${matching.stock} ${matching.colour} ${units(matching.type, matching.stock)} in stock. Would you like to add one to your basket?`;
let intentName = 'addToBasket';
slots = { itemNumber: matching.itemNumber };
return Lex.confirmIntent({ intentName, slots, message });
在整个 Lambda 中,我们使用了大量的 Lex.something 响应。这些是 Lex 类上的方法。为了使这些方法工作,我们需要创建一个名为 Lex 的新类,它包含我们在上一章中讨论的所有 Lex 响应。
创建一个名为 LexResponses.js 的新文件,然后我们在其中创建我们的类:
module.exports = class Lex {
ElicitSlot({ sessionAttributes = {}, message, intentName, slotToElicit, slots }) { ... }
Close({ message, sessionAttributes = {}, fulfillmentState = "Fulfilled" }) { ... }
ElicitIntent({ message, sessionAttributes = {} }) { ... }
confirmIntent({ sessionAttributes = {}, intentName, slots, message }) { ... }
delegate({ sessionAttributes = {}, slots }) { ... }
}
我们从这些方法返回的对象可以在上一章的末尾找到,或者完整的 LexResponses.js 文件可以在 bit.ly/chatbot-ch6 找到。
然后,我们需要在这个文件中引入这个类并创建这个类的新实例。在 productFind/index.js 文件的顶部,添加这两行代码。第一行在第二行创建这个类的新实例之前,从我们的 LexResponses 文件中引入我们的 Lex 类:
const lex = require('./LexResponses);
const Lex = new lex();
在这个 Lambda 的早期,我们写道,如果没有库存,我们将使用 confirmIntent 询问用户是否想要使用 confirmIntent 查找另一个订单。这个 confirmIntent 响应将调用我们的同一个 Lambda,但调用格式会有所不同。我们需要寻找这些不同的请求并相应地处理它们。
如果 Lambda 被调用为 confirmIntent,则 event.currentIntent.confirmationStatus 将有一个值为 Confirmed 或 Denied。如果用户拒绝问题(说“不”),则我们可以给他们一个告别消息并关闭消息。如果他们确认,我们可以让流程继续到 handleProductFind() 函数。以下代码需要在 exports.handler 函数中 handleProductFind() 函数之前添加:
if (event.currentIntent && event.currentIntent.confirmationStatus) {
let confirmationStatus = event.currentIntent.confirmationStatus;
if (confirmationStatus == "Denied"){
console.log('got denied status');
let message = `Thank you for shopping with us today. Have a nice day`
return Lex.close({message})
}
if (confirmationStatus == 'Confirmed'){
console.log('got confirmed status');
}
}
创建数据
为这个 Lambda 创建数据并不困难,但需要生成大量的数据。需要为每种颜色、尺寸和物品类型的组合以及每条需要短、标准或长长度的裤子创建一个记录。每一行都需要在一个带有 stock 键的数组中。
您可以下载完成的文件数据在 bit.ly/chatbot-ch6。这个文件需要放入一个名为 shopping-stock 的新存储桶中,以便我们的 Lambda 可以访问它。正如前几章所述,我们不需要更改存储桶或文件的任何权限。
Lambda 测试
要测试这个 Lambda,我们可以创建一些测试。这些测试应该测试所有场景:
-
所有正常槽位都已填写
-
只有
itemNumber槽位已填写 -
缺少槽位值
-
Denied确认状态 -
Confirmed确认状态
我们需要使用四个测试来覆盖所有这些场景,因为我们可以在任何槽位填写场景中使用 Confirmed 进行测试。
在 Lex 控制台中,导航到 productFind Lambda,然后在页面顶部点击配置测试事件。我们可以测试的第一个测试事件是缺少槽位值。我们实际上可以不提供任何槽位值,并期望 Lambda 会要求我们选择衬衫、夹克或裤子。这是第一个测试的输入。为此测试命名并点击保存。当你点击测试时,你应该得到我们期望的成功响应格式:
{
"currentIntent": {
"slots": {
"type": null,
"size": null,
"colour": null,
"length": null,
"itemNumber": null
}
}
}
接下来,我们可以在一个测试中测试 Confirmed 确认状态以及所有槽位都已填写。点击下拉菜单并选择再次配置测试事件。这个测试对象现在在 currentIntent 对象上也有 confirmationStatus:
{
"currentIntent": {
"slots": {
"type": "shirt",
"size": "medium",
"colour": "blue",
"length": null,
"itemNumber": null
},
"confirmationStatus": "Confirmed"
}
}
可以创建类似的测试来测试 Denied 请求和 itemNumber 请求。测试的确切代码可以在 productFind 代码文件夹中的 tests 文件中找到。
完成意图
现在我们有一个 Lambda 来实现这个意图,我们需要回到 Lex 并确保我们的意图正在触发这个 Lambda。正如 第五章,构建您的第一个 Amazon Lex 聊天机器人,滚动到意图的满足部分并选择 Lambda 满足。从下拉菜单中,我们可以选择我们新的 productFind Lambda。
保存意图,我们就可以继续下一步了。
添加到购物车
这个意图是一个简单的意图。如果用户说 Yes 添加项目到购物车,那么它就会将项目添加到 Dynamo 中的购物车,并询问他们是否想要结账或添加另一个项目。如果用户说 No 添加项目到购物车,那么它会询问用户是否想要寻找另一个产品。
在 Lex 中,我们需要创建一个名为 addToCart 的新意图,它有一个名为 itemNumber 的单个槽位。这个 itemNumber 槽位可以被设置为具有 AMAZON.NUMBER 槽位类型,因为我们已经使用简单的数字作为我们的项目编号。
正如我们在上一个 Lambda 中所做的那样,我们需要将此槽设置为 非必需的。如果我们要求槽,而用户在没有槽的情况下启动了意图,他们将被要求输入商品编号。大多数人不会通过商品编号来识别商品,因此他们不知道该输入什么。如果他们输入任何无效的内容,Lex 将会重新提示他们输入商品编号,直到他们猜对或者失败三次。我们希望能够检查是否存在商品编号,并在没有商品编号的情况下将他们发送到 productFind。
创建 Lambda
要启动此 Lambda,请在 Lambda 目录中创建一个名为 addToCart 的文件夹,并在其中创建一个 index.js 文件。在您的文件夹中,我们需要运行 npm install --save aws-sdk 以确保我们有权访问 AWS。我们像往常一样,从默认的 node 8.10 函数开始,在这个函数的开始,我们需要做两件事:检查是否存在 Denied 确认状态,并调用 handleAddToCart 函数。
如果确认状态是 denied,我们可以使用 Lex.confirmIntent 询问用户是否想要寻找另一个产品。我们已经编程了 productFind Lambda 来处理 confirmIntent 触发,所以它应该已经正常工作:
exports.handler = async (event) => {
if (event.currentIntent && event.currentIntent.confirmationStatus === "Denied"){
let message = `Would you like to find another product?`;
let intentName = 'productFind';
let slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message })
}
return handleAddToCart(event);
}
由于我们正在使用与 productFind 中相同的 Lex.confirmIntent 函数,我们需要将 LexResponses.js 文件复制到这个文件夹中,并将此代码添加到文件的顶部:
const lex = require('./LexResponses');
const Lex = new lex();
确认状态处理完毕后,我们可以专注于将商品添加到购物车中。我们需要创建 handleAddToCart 函数;这个函数需要做的第一件事是检查我们是否有 itemNumber。这个检查将与我们的 productFind Lambda 开始时的检查非常相似,只是缺少 itemNumber 将在 productFind 上触发 confirmIntent:
const handleAddToCart = async event => {
let { slots } = event.currentIntent;
let { itemNumber } = slots;
if (!itemNumber) {
let message = `You need to select a product before adding it to a cart. Would you like to find another product?`;
let intentName = 'productFind';
slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message })
}
}
如果有商品编号,那么我们需要将商品添加到用户的购物车中。接下来,我们将创建一个新的类,称为 DB,这样我们就可以发出请求,但现在我们可以假设这些方法存在。
要将商品添加到用户的购物车中,我们需要检查用户是否已经有了购物车。如果没有,请求将出错,我们需要为他们创建一个新的购物车。我们正在使用与我们在 第四章 中讨论的相同的 to 错误捕获方法,即 将您的 Alexa 技能连接到外部 API,用于错误处理。
shopping-cart 表将包含四个键:
-
ID 是会话 ID 的字符串
-
商品列表是一个
itemNumbers的列表 -
name是您可以给您的购物车起的名字以保存它 -
TTL 是数据的 生存时间
TTL 用于在设定的时间自动删除记录。这有助于保持数据库的清洁,并且当您必须处理数据保护时非常有用。
我们可以尝试使用DB.get从数据库中获取一个记录,我们将在本节稍后创建它。如果它返回一个值,我们可以将其用作现有购物车。如果没有cartUser,我们将创建一个默认购物车。为了确保名称是唯一的,我们将使用 UUID(通用唯一标识符),将name设置为uuidv4():
let [err, cartUser] = await to(DB.get('ID', event.userId, 'shopping-cart'));
if (!cartUser) {
cartUser = { ID: event.userId, Items: [], name: uuidv4(), TTL: 0 }
}
为了让这个功能正常工作,我们需要在 Lambda 文件夹中运行npm install --save uuid。然后我们需要在index.js文件的顶部包含这一行:
const uuidv4 = require('uuid/v4');
现在我们为新的和现有的购物车都有一个cartUser的值。为了更新这个购物车行,我们可以使用spread操作符。这个操作符接受一个对象或数组,并将值扩展到新的对象或数组中。任何在扩展之后的值可以覆盖spread中的值:
let updatedCart = { ...cartUser, Items: [...cartUser.Items, itemNumber], TTL: Date.now() + 7 * 24 * 60 * 60 * 1000 };
这行代码将之前的购物车,添加一个新的项目编号到Items列表,并将生存时间更改为从现在开始的7天。
在更新了购物车之后,我们需要将其写入表中。如果写入表时发生错误,我们需要使用Lex.close告诉用户:
let [writeErr, res] = await to(DB.write(event.userId, updatedCart, 'shopping-cart'));
if (writeErr) {
let message = `Unfortunately we've had an error on our system and we can't add this to your cart.`
return Lex.close({ message });
}
如果将项目添加到购物车成功,我们可以询问用户他们是否想要添加另一个产品、结账或保存购物车。与询问他们是否想要寻找另一个产品或添加此项目到购物车不同,这不是一个是/否问题。他们应该回答“我想结账”、“我想保存我的购物车”或“我想添加另一个项目”,我们将这些设置为checkout、saveCart和productFind意图的示例话语。
因为我们试图找出用户想要使用哪个意图,我们可以向 Lex 发送一个elicitIntent响应:
let message = `Would you like to checkout, add another item to your cart or save your cart for later?`;
return Lex.elicitIntent({ message });
DynamoDB
如我们之前所述,我们将使用 DynamoDB 来存储购物车信息。我们将有两个 Dynamo 表,一个用于当前购物车,另一个用于已下订单。为了创建这些表,我们需要进入 AWS 控制台并导航到DynamoDB服务:

DynamoDB 控制台页面
点击创建表以开始创建一个新表。我们需要为我们的表命名并选择一个主键。我们将第一个表命名为shopping-cart并将主键设置为ID。主键是我们将能够用来查找记录的值,并且将ID用作主键的名称是最佳实践:

表创建
当我们点击创建按钮时,我们将被带到主 DynamoDB 控制台页面。这个页面上有大量信息,但我们只需要看到我们的shopping-cart表在表列表中。当你刚刚创建表时,它旁边可能有一个加载指示器,因为创建过程正在完成。
在创建了表之后,我们需要编写一些代码,使我们能够与之交互。因为我们将在多个意图和 Lambda 中使用 Dynamo,所以创建可重用代码是一个好习惯。为此,我们将创建一个 DB 类,它提供从数据库获取、写入、更新和删除记录的方法。在我们的 addToCart 文件夹中创建一个名为 DB.js 的新文件,然后我们将在其中创建一个新的类:
module.exports = class DB {};
为了让我们能够访问 Dynamo 表,AWS 提供给我们一个 DynamoDB 文档客户端。要创建 documentClient,我们需要传递一个包含区域的配置对象。这段代码可以放在我们的 DB 文件顶部。
确保将您的区域更改为您的表所在的位置。这应该是 eu-west-1 或 us-east-1。如果您不确定,请访问您的 AWS 控制台并检查您的位置设置。爱尔兰是 eu-west-1,美国东部(弗吉尼亚北部)是 us-east-1:
const AWS = require('aws-sdk');
let documentClient = new AWS.DynamoDB.DocumentClient({
'region': 'eu-west-1'
});
现在我们已经创建了 documentClient 变量,我们可以回到我们的类中创建我们的方法。我们将要创建的第一个方法是 write。要将数据写入表,我们需要三样东西:行 ID、我们想要写入的数据和表名。
为了提高这个类的可用性,我们将返回一个 Promise。在这个 Promise 中,我们首先需要检查 ID、data 和 table。如果它们中的任何一个缺失,或者如果 ID 或 table 不是字符串,我们需要抛出一个错误:
write(ID, data, table) {
return new Promise((resolve, reject) => {
if (!ID) throw 'An ID is needed';
if (typeof ID !== 'string') throw `the id must be a string and not ${ID}`
if (!data) throw "data is needed";
if (!table) throw 'table name is needed';
if (typeof table !== 'string') throw `the table must be a string and not ${table}`;
})
}
如果 ID、data 和 table 都正确,我们就可以将数据写入表。要将数据写入表,我们需要将请求传递到特定的格式。Item 需要包含所有数据,并添加一个具有我们传递的行 ID 值的 ID 字段:
let params = {
TableName: table,
Item: { ...data, ID: ID }
};
然后,这个 params 对象可以被传递到 documentClient.put() 方法中,该方法还接受一个 callback 函数。我们在解决数据或拒绝错误之前,在控制台中输出错误或响应中的数据:
documentClient.put(params, function(err, result) {
if (err) {
console.log("Err in writeForCall writing messages to dynamo:", err);
console.log(params);
return reject(err);
}
console.log('wrote data to table ', table)
return resolve({ ...result.Attributes, ...params.Item });
});
在创建这个类的同时,我们将创建 get、update 和 delete 方法。
get 与 write 非常相似,只需要 key、value 和 table。我们不是传递我们想要写入的项目,而是传递我们想要匹配的键。这个 key-value 对需要在 params 中设置:
get(key, value, table) {
if (!table) throw 'table needed';
if (typeof key !== 'string') throw `key was not string and was ${JSON.stringify(key)} on table ${table}`;
if (typeof value !== 'string') throw `value was not string and was ${JSON.stringify(value)} on table ${table}`;
return new Promise((resolve, reject) => {
let params = {
TableName: table,
Key: { [key]: value }
};
documentClient.get(params, function(err, data) {
if (err) {
console.log(`There was an error fetching the data for ${key} ${value} on table ${table}`, err);
return reject(err);
}
//TODO check only one Item.
return resolve(data.Item);
});
});
}
get 在根据主索引获取项目时按预期工作,但如果我们想根据第二个值获取项目怎么办?我们不能使用 documentClient.get(),因此我们需要创建一个新的函数 getDifferent。这个函数使用 documentClient.query() 而不是 documentClient.get():
getDifferent(key, value, table) {
if (!table) throw 'table needed';
if (typeof key !== 'string') throw `key was not string and was ${JSON.stringify(key)} on table ${table}`;
if (typeof value !== 'string') throw `value was not string and was ${JSON.stringify(value)} on table ${table}`;
if (!table) 'table needs to be users, sessions, or routes.'
return new Promise((resolve, reject) => {
var params = {
TableName : table,
IndexName : `${key}-index`,
KeyConditionExpression : `${key} = :value`,
ExpressionAttributeValues : {
':value' : value
}
};
documentClient.query(params, function(err, data) {
if (err) {
console.error("Unable to read item. Error JSON:", JSON.stringify(err));
reject(err);
} else {
console.log("GetItem succeeded:", JSON.stringify(data.Items));
resolve(data.Items);
}
});
})
}
delete 几乎与 get 相同,主要区别在于我们调用 documentClient.delete:
delete(ID, table) {
if (!table) throw 'table needed';
if (typeof ID !== 'string') throw `ID was not string and was ${JSON.stringify(ID)} on table ${table}`;
console.log("dynamo deleting record ID", ID, 'from table ', table);
let params = {
TableName: table,
Key: { 'ID': ID }
};
return new Promise((resolve, reject) => {
documentClient.delete(params, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
我们需要创建的最后一个方法是 update。这是一个非常简单的函数,因为它只是使用其他方法来完成大部分工作。它获取 ID 的数据,然后使用新的键和值添加或更改后写回:
async update(ID, table, key, value) {
let data = await this.get(ID, table);
return this.write(ID, { ...data, [key]: value }, table);
}
现在我们已经完成了 DB 类,我们需要将其导入到我们的 Lambda 中并创建一个新的实例。在 addToCart 文件夹中的 index.js 文件顶部,我们可以添加这些行:
const db = require('./DB');
const DB = new db();
我们的 Lambda 需要处理 Dynamo 以及 S3;我们需要给这些 Lambda 授予它们执行这些操作所需的权限。
打开 AWS 控制台并导航到 AIM。在左侧菜单中的角色下,找到我们在 第二章 中创建的角色,AWS 和 Amazon CLI 入门。我们将向此角色添加 Dynamo 权限。
添加 Dynamo 权限
现在我们 Lambda 正在与 Dynamo 和 S3 一起工作,我们需要更新我们用于构建 Lambda 的角色权限。导航到 IAM 服务并选择我们在 第二章 中创建的 lambdaBasic 角色,AWS 和 Amazon CLI 入门。点击附加策略并搜索 Dynamo。
我们需要添加 AmazonDynamoDBFullAccess。这会给 Lambda 授予读取和写入 Dynamo 的权限:

添加 DynamoDB 策略
测试
在我们运行任何测试之前,我们需要构建和部署我们的 Lambda。使用我们在 第二章 中创建的构建脚本,AWS 和 Amazon CLI 入门,我们可以运行 ./build.sh addToCart 来构建和部署我们的新 Lambda。
完成这些后,我们可以导航到我们的 Lambda 控制台并选择我们的新 addToCart Lambda。在测试旁边,我们可以点击配置测试事件。
对于这个 Lambda,我们需要测试一些事情:
-
一个
Denied确认状态 -
一个
Confirmed确认状态 -
一个没有
itemNumber的触发器 -
向新购物车添加一个项目
-
向现有购物车添加一个项目
一个 Denied 触发器是最容易测试的。你应该会得到一个询问“你想要找到另一个产品吗?”的响应:
{ "currentIntent": { "confirmationStatus": "Denied" } }
我们可以在一个测试中测试一个 Confirmed 确认状态和一个没有 itemNumber 的触发器。我们应该能够到达 handleAddToCart 函数,然后得到一个响应告诉我们需要选择一个产品并询问我们是否想要找到一个:
{
"currentIntent": {
"confirmationStatus": "Confirmed",
"slots": {
"itemNumber": null
}
}
}
添加项目到新购物车和现有购物车的测试将是相同的;你只需要运行测试两次。第一次,不会有任何现有订单。第二次,会有。每次你想测试一个新的购物车时,你都需要更改 ID 值:
{
"currentIntent": {
"slots": {
"itemNumber": 1034
}
},
"userId": "123-sdf-654-hjk2"
}
两次,你都应该得到一个询问“你想要结账还是添加另一个项目到你的篮子里?”的响应。
我们最后需要做的是将这个经过测试的 Lambda 添加为意图的处理程序。导航到 Lex 聊天机器人和 addToCart 意图。在页面的履行部分,我们可以将履行方式更改为 Lambda 履行,并选择我们新的 addToCart Lambda 来履行它。
结账
当用户想要结账时,我们将进行简化的结账流程。我们只需询问他们的邮寄地址,并告诉他们我们将送货时收取款项。幕后,我们将把他们的购物车移动到一个新的 shopping-orders 表中。
首先,我们需要在 Lex 中创建一个新的意图,命名为 checkout。我们可以添加诸如 我想结账、我可以结账吗 以及仅仅是 结账 这样的语句。你可以添加更多你期望用户可能回复的语句,例如 您想要结账还是添加另一个商品到您的购物篮中?。
我们可以使用事件中的 userId 访问他们的购物车,所以我们需要的其他信息只是他们的 deliveryAddress – 因此我们需要将其添加为一个槽位。此槽位的槽位类型可以设置为 AMAZON.PostalAddress,我们可以添加提示 您希望将产品送到哪个地址?。我们可以设置此槽位为必填,这意味着每次此意图被触发时,我们应已经拥有送货地址:

结账槽位
创建 Lambda
设置好意图后,我们可以创建 Lambda。创建一个名为 checkout 的新文件夹,并添加一个 index.js 文件。在你的文件夹中,我们需要运行 npm install --save aws-sdk。我们将从 node 8.10 处理器开始,使用 confirmationStatus 检查拒绝状态。我们可能希望在将来使用 confirmIntent 触发此意图:
exports.handler = async (event) => {
if (event.currentIntent && event.currentIntent.confirmationStatus === "Denied") {
let message = `Would you like to find another product?`;
let intentName = 'productFind';
slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message })
}
return handleCheckout(event);
}
我们已经再次使用了 Lex,因此我们需要将我们的 LexResponse.js 文件复制到这个文件夹中,并在文件顶部使用我们的设置代码:
const lex = require('./LexResponses');
const Lex = new lex();
现在,我们可以进入 Lambda 的主体部分,使用我们的 handleCheckout 函数。我们首先将检查 deliveryAddress 槽位中是否有值。如果没有,我们将使用 elicitSlot 请求它:
const handleCheckout = async event => {
let { slots } = event.currentIntent;
let { deliveryAddress } = slots;
if (!deliveryAddress) {
let message = `What address would you like this order delivered to?`;
let intentName = 'checkout';
slots = { deliveryAddress: null };
let slotToElicit = 'deliveryAddress';
return Lex.elicitSlot({message, intentName, slots, slotToElicit});
}
}
一旦我们知道我们有送货地址,我们可以将他们的购物车转换成订单。为此,我们将获取他们的购物车,然后将其放入一个带有他们的送货地址和订单日期的 shopping-orders 表中。
在我们可以创建代码之前,我们需要在 DynamoDB 中设置一个新的表。导航到 AWS 中的 Dynamo 控制台,点击创建表。将我们的新表命名为 shopping-orders 并将其主键设置为 ID。
在我们的 Lambda 代码中,我们现在可以创建放置订单的逻辑。首先我们需要获取购物车,然后删除该购物车。如果发生错误,我们需要告诉用户,并询问我们是否可以提供其他帮助:
let [cartErr, cart] = await to(DB.get("ID", event.userId, 'shopping-cart'));
if (!cart){
console.log('no cart');
let message = `We couldn't find your cart. Is there anything else I can help you with`;
return Lex.elicitIntent({ message });
}
如果我们成功获取了购物车,我们可以删除购物车,创建一个新的 orders 对象,并将其写入我们的 shopping-orders 表。我们删除购物车是因为我们预计不会下订单并且仍然保留购物车中的所有商品:
let order = { Items: cart.Items, address: deliveryAddress, date: Date.now() };
let ID = uuidv4();
我们再次使用 uuidv4() 生成一个随机 ID。这意味着我们还需要运行 npm install --save uuid 并在文件顶部包含 const uuidv4 = require('uuid/v4');。
我们将使用try/catch来处理这个问题,因为它允许我们在单个处理程序中同时执行这两个请求并处理任何错误。如果抛出错误,那么我们的代码可能有问题,我们不希望用户再次经历这个过程。因此,我们将告诉他们出现了错误,他们的订单无法下单:
try {
await to(DB.write(ID, order, 'shopping-orders'));
await to(DB.delete(event.userId, 'shopping-cart'));
} catch (err) {
console.log('error deleting the cart or writing the order', cartErr)
let message = `I'm sorry, there was a system error so your order hasn't been placed.`;
return Lex.close({ message });
}
如果没有错误,我们可以告诉用户他们的订单已成功下单:
let message = `Thank you. Your order has been placed and will be delivered in 3-5 working days`;
return Lex.close({ message });
在完成所有意图路径后,我们可以使用我们的构建脚本来构建和部署我们的 Lambda,以便在测试之前。导航到您的Lambdas文件夹,并运行以下代码:
./build.sh checkout
测试
使用这个 Lambda,有几个场景需要测试:
-
确认意图
-
拒绝意图
-
没有送货地址
-
用户没有购物车可以结账
-
用户有购物车
一个Denied意图应该询问我们是否想要找到一个新产品:
{ "currentIntent": { "confirmationStatus": "Denied" } }
Confirmed意图和没有送货地址可以一起测试。我们预计状态对流程没有影响,并且响应会要求我们提供送货地址:
{
"currentIntent": {
"confirmationStatus": "Confirmed",
"slots": { "deliveryAddress": null}
}
}
要测试没有购物车的用户,我们可以调用 Lambda,并使用一个永远不会下订单的userId。我们可以选择一个普通单词,因为这个单词永远不会在 Lex 中用作userID:
{
"currentIntent": {
"confirmationStatus": "None",
"slots": { "deliveryAddress": "123 imaginary street, fake town, madeupsville"}
},
"userId": "fakeUser"
}
从这个测试中,我们应该被告知“我们找不到您的购物车。”然后询问我们是否想要找到一个产品。
最后一种情况是成功的订单下单。这需要更多的工作,因为我们需要找到一个有效的购物车。为此,我们可以进入 AWS 并导航到 Dynamo。选择shopping-orders表,然后我们可以点击“Items”标签。这允许我们直接查看表中的项目,以便我们可以找到一个有效的购物车 ID。复制任何 ID,并将其粘贴为下一个测试用例中的值:
{
"currentIntent": {
"confirmationStatus": "None",
"slots": { "deliveryAddress": "123 imaginary street, fake town, madeupsville"}
},
"userId": ## paste your ID here
}
从这个测试中,我们期望得到一个响应,告诉我们我们已经成功下单。我们还可以在我们的shopping-orders表中检查是否有新的行。在运行此测试时,我们需要使用有效的购物车 ID。不幸的是,当我们创建订单时,我们会删除旧的购物车,这意味着 ID 不再有效,因此我们需要为每个测试获取一个新的 ID。
与迄今为止的所有 Lambda 和意图一样,最后我们需要做的是将这个经过测试的 Lambda 添加为意图的处理程序。导航到 Lex 聊天机器人,然后转到结账意图。在页面的“Fulfillment”部分,我们可以将履行方式更改为 Lambda 履行,并选择我们新的结账 Lambda 来履行。
保存我们的购物车
到目前为止,我们已经创建了完美对话的流程,即用户找到一到多个产品,将它们添加到购物车,然后立即结账。这是好的,但很多人会将东西添加到购物车,离开,然后回来结账。
我们需要创建一个意图,让用户保存他们的购物车,稍后回来结账。大多数购物网站都会有登录系统或使用 Web 缓存将购物车保存到用户,但我们将通过一个唯一的名称来保存购物车。
在我们这个聊天机器人的 Lex 控制台中,我们可以添加一个新的saveCart意图。在addToCart意图的末尾,我们询问用户他们是否想要添加另一个产品、保存购物车或结账。我们需要处理用户可能说的保存购物车的用语。添加诸如保存我的购物车和我想将我的购物车保存起来以后再用之类的用语。
cart和basket这两个词在意义上非常相似,所以为它们中的每一个添加一些用语:

saveCart 样本用语
对于这个意图,我们只需要一个槽位。添加一个名为cartName的槽位,槽位类型为 AMAZON.Musician,提示为你希望将你的购物车保存为什么名字?。使用 Musician 类型的槽位可能看起来很奇怪,但这个槽位类型允许接受任何值,从而使用户能够将购物车命名为他们想要的任何名字。我们可以将cartName槽位设置为必需的,因为我们总是需要一个名字来保存购物车。
创建 Lambda
在Lambdas文件夹中创建一个新的文件夹,命名为saveCart,并在其中创建一个index.js文件。在index.js文件中,我们将像往常一样,从 node 8.10 的异步处理程序开始。我们知道我们将使用 Lex 响应和访问 dynamo,因此我们添加这些文件并将它们引入到我们的index.js中:
const lex = require('./LexResponses');
const Lex = new lex();
const db = require('./DB');
const DB = new db();
exports.handler = async (event) => {
return handleSaveCart(event);
}
由于我们将使用 Dynamo,我们需要确保通过运行npm install --save aws-sdk来安装aws-sdk。
使用这个意图,我们永远不会对它执行confirmIntent,因此我们不需要处理任何确认状态。这意味着我们处理程序中唯一的函数是handleSaveCart(event)函数。
在handleSaveCart函数内部,我们需要从事件中获取userID和slots。然后我们可以从槽位中获取cartName:
const handleSaveCart = async event => {
let { slots } = event.currentIntent;
let { cartName } = slots;
}
我们首先需要检查是否存在cartName,因为总是需要。这个函数永远不会被调用,因为cartName槽位是必需的,但总是更安全地将它放在那里:
if (!cartName) {
let message = `You need to save your cart with a name. What do you want to call it?`;
let intentName = 'saveCart';
slots = { cartName: null };
let slotToElicit = 'cartName';
return Lex.elicitSlot({ intentName, slotToElicit, slots, message });
}
现在我们有一个有效的购物车名称,我们首先需要查看用户是否有可以结账的购物车。如果没有,我们就询问他们是否想要向他们的购物车中添加商品:
let [err, cart] = await to(DB.get('ID', event.userId, 'shopping-cart'));
if (err || !cart || !cart.Items) {
let message = `You don't have a cart. Would you like to find a product?`;
let intentName = 'productFind';
slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message });
}
接下来,我们可以检查是否已经存在具有该名称的购物车。为此,我们可以尝试获取具有该名称的购物车。如果我们找不到具有该名称的购物车,这意味着我们在保存时不会覆盖另一个购物车。如果我们找到了具有该名称的购物车,那么我们需要要求用户提供一个新的购物车名称:
let [getCartErr, getCarts] = await to(DB.getDifferent('cartName', cartName, 'shopping-cart'));
if (!getCarts || !getCarts[0] ) {
// No cart with that name so we can save the current cart to this name
return addNameToCart(cart, cartName);
}
let message = `Unfortunately you can't use that name. Please choose another name.`;
let intentName = 'saveCart';
let slotToElicit = 'cartName';
slots = { cartName: null };
return Lex.elicitSlot({ intentName, slots, slotToElicit, message });
为了使用该购物车名称保存他们的购物车,我们返回一个函数来将名称添加到购物车中。这个函数首先将购物车名称设置为传入的槽位值:
const addNameToCart = async (cart, cartName) => {
cart.cartName = cartName;
}
现在我们可以继续这个函数,通过将购物车写回表中。如果有错误,我们告诉用户我们无法保存他们的购物车,否则我们告诉他们已经保存,并说明下次如何访问它:
let [err, res] = await to(DB.write(cart.ID, cart, 'shopping-cart'));
if (err) {
console.log('err writing cart with name', err);
let message = `Unfortunately we cant save your cart`;
return Lex.close({ message });
}
let message = `Your cart has been saved. Type "find my cart" next time and enter "${cartName}" to get this cart.`;
return Lex.close({ message });
和往常一样,确保运行构建脚本来部署你的 Lambda。
Dynamo 更改
在这个 Lambda 中,我们使用cartName而不是ID进行DB.get。为了使这正常工作,我们需要按cartName索引我们的表。为键创建索引允许我们按值进行搜索。这也是为什么我们将购物车的默认名称设置为uuidv4()的原因。因为我们可以通过名称进行搜索,所以它需要是唯一的。
导航到 AWS 中的 Dynamo 服务并选择shopping-cart表。在这个部分的顶部有一行标签,我们将选择Indexes并点击创建索引。这将在一个弹出窗口中打开,我们需要输入我们想要索引的键,在这种情况下,是cartName。点击创建索引,索引将开始创建:

二级索引
当这个创建过程完成时,我们将能够通过cartName在这个表上执行getDifferent请求。
测试
测试我们的saveCart意图只需要处理四种情况:没有购物车名称,没有要保存的购物车,购物车名称已被占用,以及保存购物车。
当没有购物车名称时进行测试非常简单。我们期望这会要求我们为我们的购物车提供一个名称:
{
"currentIntent": {
"slots": {
"cartName": null
}
}
}
要测试在没有购物车时保存,我们可以使用一个无意义的userId,因为不可能有他们的购物车。现在我们需要传递一个有效的cartName,以便通过第一个检查:
{
"currentIntent": {
"slots": {
"cartName": "personalShopping"
}
},
"userId": "asdasdasdasdasd"
}
我们将不得不以不同的顺序测试最后两个。我们首先测试成功保存购物车。为此,我们需要一个带有购物车的userId。我们可以通过进入我们的 Dynamo 控制台并查看shopping-cart表上的“Items”标签来找到它。选择那里的任何 ID 并将其复制到测试对象中的userId值。
我们期望这次会成功,并且下次会被告知如何获取我们的购物车:
{
"currentIntent": {
"slots": {
"cartName": "testCartSave"
}
},
"userId": ## valid userId
}
现在我们已经保存了一个购物车,我们可以尝试用相同的名字保存另一个不同的购物车。我们必须为这次测试找到一个新的ID,但其余的请求保持不变。
这次,我们应该被告知我们不能使用那个购物车名称,并尝试一个不同的名称:
{
"currentIntent": {
"slots": {
"cartName": "testCartSave"
}
},
"userId": ## another valid userId
}
当所有的测试都按预期响应时,返回到这个机器人的 Lex 控制台,并将saveCart意图的履行改为saveCart Lambda。
获取已保存的购物车
现在用户可以保存他们的购物车了,我们需要给他们一个方法来获取他们保存的购物车。然后我们可以更改购物车,使其与他们的userId匹配,然后他们可以继续添加更多项目或结账。
在 Lex 中创建一个新的getSavedCart意图,我们只将询问他们保存的购物车的cartName。正如我们在saveCart意图中所做的那样,我们可以将cartName槽位类型设置为 AMAZON.Musician 以允许任何值通过。我们还可以将此槽位设置为必填,并有一个提示:“你保存购物车时用的名字是什么?”
与迄今为止的其他意图不同,我们可以允许用户将cartName作为语句的一部分输入。这是通过在语句中包含带有大括号的槽名称来完成的。这可以用于诸如"我想获取购物车 { cartName }"之类的语句。
除了包含cartName的语句之外,我们还将有正常的语句,例如"我想获取我的已保存购物车"或"获取我的购物车"。这个语句将使 Lex 提示用户输入cartName,使用我们提供的提示:

getSavedCart 的语句
创建 Lambda
我们首先在我们的Lambdas目录中创建一个getSavedCart文件夹,并在其中创建一个index.js文件,并将我们的DB.js和LexResponses.js文件从先前的 Lambda 中复制过来。我们的index.js文件将像往常一样开始,导入并初始化我们的DB和Lex类,并使用 node 8.10 处理器。我们需要确保在我们的文件夹中运行npm install --save aws-sdk。在这个意图上我们永远不会做确认意图,所以我们只需要处理一个getSavedCart事件:
const lex = require('./LexResponses');
const Lex = new lex();
const db = require('./DB');
const DB = new db();
exports.handler = async (event) => {
return handleGetSavedCart(event);
}
在我们的 Lambda 中,我们首先需要获取我们的userId、slots以及cartName槽。然后我们可以检查我们是否有cartName,如果没有,我们可以询问用户:
const handleGetSavedCart = event => {
let { userId, currentIntent: { slots } } = event;
let { cartName } = slots;
if (!cartName) {
let message = `What name did you save your cart as?`;
let intentName = 'getSavedCart';
let slotToElicit = 'cartName';
let slots = { cartName: null };
return Lex.elicitSlot({ intentName, slots, slotToElicit, message });
}
}
现在我们知道我们有cartName,我们可以尝试获取具有该名称的购物车。如果我们无法获取具有该名称的购物车,那么我们需要询问他们是否想要尝试另一个名称或开始一个新的购物车。这将必须是一个elicitIntent,因为他们可能选择两个意图中的任何一个:
let [err, carts] = await to(DB.getDifferent('cartName', cartName, 'shopping-cart'));
if (err || !carts || !carts[0]) {
let message = `We couldn't find a cart with that name. Would you like to try another name or start a new cart?`;
return Lex.elicitIntent({message});
}
为了使这个elicitIntent起作用,我们还需要将try another name的语句添加到getSavedCart中,并将start a new cart添加到productFind意图中。
DB.getDifferent获取一个匹配的购物车数组,这就是为什么我们在错误处理器之后寻找carts[0]。我们还需要通过添加以下行来提取我们的购物车:
let cart = carts[0];
如果我们找到了名为cartName的购物车,那么我们需要做两件事。我们需要为当前userId创建一个包含这些商品的购物车,然后我们需要删除旧的购物车。如果我们不删除旧的购物车,将会有两个同名购物车。
我们可以通过更改 ID 并更新旧购物车的 TTL 来创建新的购物车。我们还需要存储旧购物车的 ID,以便我们也可以删除它:
let cart = carts[0];
let oldCartID = cart.ID;
let newCart = { ...cart, ID: userId, TTL: Date.now() + 7 * 24 * 60 * 60 * 1000 };
当我们创建新的购物车并删除旧的购物车时,我们可以将它们都包裹在try/catch中,并以相同的方式处理任何错误。如果有任何错误,我们需要告诉用户我们无法恢复他们的购物车,并询问他们是否想要开始一个新的购物车。这可以在productFind意图上的confirmIntent中完成,这将让他们再次从流程的开始处开始。
如果没有错误,我们可以告诉他们我们已经获取了他们的购物车,并询问他们是否想要结账或获取另一个商品:
try {
await DB.write(userId, newCart, 'shopping-cart');
await DB.delete(oldCartID, 'shopping-cart');
} catch (createErr) {
let message = `Unfortunately we couldn't recover your cart. Would you like to create a new cart?`;
let intentName = 'productFind';
let slots = { type: null, size: null, colour: null, length: null, itemNumber: null };
return Lex.confirmIntent({ intentName, slots, message });
}
let message = `We have got your cart for you. Would you like to checkout or add another product?`;
return Lex.elicitIntent({ message });
这就是这个 Lambda 的结束,所以现在我们可以使用我们的脚本构建和部署,然后继续测试它。
测试
为了测试这个,我们需要测试三件事:
-
没有指定
cartName -
一个不存在的
cartName -
成功获取他们的购物车
测试没有购物车名称非常简单。我们期望得到一个要求输入购物车名称的响应:
{
"currentIntent": {
"slots": {
"cartName": null
}
}
}
为了测试一个不存在的购物车名称,我们需要使用一个别人不太可能使用的名称。我们期望的响应会说找不到具有该名称的购物车:
{
"currentIntent": {
"slots": {
"cartName": "nonsense"
}
}
}
最后的测试需要我们再次查看 Dynamo 表。这次,我们正在寻找一个具有有效名称的订单。如果我们完成了saveCart的测试,我们应该有一个名为testCartSave的购物车。这个请求应该得到一个响应,表示找到了购物车,并询问他们是否想要结账或寻找另一个产品:
{
"currentIntent": {
"slots": {
"cartName": "testCartSave"
}
}
}
一旦所有这些测试都通过,我们就可以将这个 Lambda 添加为我们的getSavedCart意图的履行方法。
我的购物车里有啥?
这是本章我们将要创建的最后一个意图。当用户询问他们购物车里的内容时,我们将给他们一个总结。这涉及到获取他们的购物车并将他们的项目编号与 S3 中的数据匹配起来。
当我们在 Lex 中创建这个意图时,我们不需要任何槽位——我们需要的唯一信息是他们的userId。话语将是关于他们购物车里的内容的问题,例如“我的购物车里有啥”和“我在篮子里有多少”。
创建 Lambda
我们在我们的 Lambda 目录中创建一个新的名为whatsInMyCart的文件夹,并在其中创建一个index.js文件,同时将DB.js和LexResponses.js复制到这个文件夹中。
这个函数将需要访问 Dynamo 以访问购物车,并访问 S3 以获取产品数据。我们通过在index.js文件中引入DB.js、LexResponses.js和aws-sdk,然后创建新的DB、Lex和S3类实例来开始index.js文件。我们没有对这个意图的任何确认,所以我们可以直接返回一个handleWhatsInMyCart函数:
const lex = require('./LexResponses');
const Lex = new lex();
const db = require('./DB');
const DB = new db();
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.handler = async (event) => {
return handleWhatsInMyCart(event);
}
当用户触发这个意图时,我们首先需要做的是获取他们的购物车。如果他们没有购物车,那么我们需要提醒他们他们可以恢复一个已保存的购物车或向现有的购物车添加新项目:
const handleWhatsInMyCart = async event => {
let [err, cart] = await to(DB.get('ID', event.userId, 'shopping-cart'));
if (err || !cart || cart.Items.length == 0) {
let message = `You don't appear to have a cart. If you have saved a cart then you can recover it by typing "Get my cart", or you can say "I want to buy something"`;
return Lex.elicitIntent({ message });
}
}
如果他们确实有一个购物车,那么我们可以将购物车中的商品重新格式化为更易于管理的格式。商品添加到购物车的方式是,相同商品的多个实例在数组中只是单独的项目。我们可以使用一些数组逻辑将此转换为以项目编号为键的对象,这些键指向包含数量的对象。此代码遍历每个项目,如果我们已经将该项目添加到items对象中,则将数量加 1。如果是该商品的第一个单位,则将数量设置为 1:
let items = {};
cart.Items.map(item => {
items[item] = (items[item] && items[item].quantity) ? { quantity: items[item].quantity + 1 } : { quantity: 1 };
});
对于我们的项目对象,我们需要将其映射到项目描述。为此,我们需要 S3 中的数据。我们可以将我们在productFind中使用的相同的getStock()函数复制到这个 Lambda 中。如果有错误或我们没有收到产品列表,我们需要告诉用户我们遇到了问题:
const [s3Err, products] = await to(getStock());
if (s3Err || !products) {
let message = `Unfortunately our system has had an error.`;
Lex.close({ message });
}
我们有我们的项目对象和所有产品。我们可以使用这个来扩展 items 对象中的数据。为此,我们可以映射每个 products,如果 itemNumber 在我们的 items 对象中,我们就将那些详细信息添加到那个项目的数据中:
products.forEach(product => {
if (items[product.itemNumber]){
items[product.itemNumber] = { ...product, ...items[product.itemNumber]};
}
});
我们有一个包含所有所需数据的对象。我们可以映射这个对象并创建一个描述项目和数量的字符串。我们可以使用 Object.values() 方法,它将一个对象转换成一个包含值的数组。以下是一个示例:
let data = {
name: { firstName: 'Tom', lastName: 'Jones' },
age: 25,
height: '178 cm'
};
console.log(Object.values(data));
// [ { firstName: 'Tom', lastName: 'Jones' }, 25 , '178cm' ]
我们可以用这个来获取每个项目的数据以创建 itemStrings,例如 2 blue jackets 或 1 long, black pair of trousers。我们可以使用在 productFind 中创建的 units() 函数来处理单位和裤子:
let itemStrings = Object.values(items).map(item => {
let { type, size, colour, length, quantity } = item;
return `${quantity} ${size}, ${length ? `${length}, ` : ''}${colour} ${units(type, quantity)}`;
});
我们现在可以将这个项目字符串数组合并成一个购物车总结。如果只有一个项目,我们只需说出这个项目。如果有两个项目,我们需要在它们之间添加 and,并且需要用逗号分隔三个或更多项目:
let message = `You have ${itemStrings.slice(0,-1).join(', ')}${itemStrings.length > 1 ? ` and `: ""}${itemStrings.pop()} in your cart. Would you like to checkout, save your cart or add another item?`;
消息创建完成后,我们剩下要做的就是返回我们的 Lex 响应,这将是一个 elicitIntent 响应:
return Lex.elicitIntent({ message });
Lambda 函数完成后,我们需要构建和部署它,然后继续测试。
测试
在这个 Lambda 中,我们只需要测试两种情况:
-
没有购物车
-
成功的购物车查找
要测试没有购物车的情况,我们可以提供一个不存在的 userId。我们应该得到一个响应告诉我们找不到我们的购物车:
{
"userId": "nonsense"
}
要测试成功的购物车查找,我们需要进入我们的 Dynamo 表并找到一个包含项目的购物车。我们应该得到一个描述购物车内项目的格式良好的句子,并会被问及我们是否想要结账、保存或添加另一个项目:
{
"userId": ## valid userID
}
测试完成后,我们可以进入 Lex 机器人并更改 whatsInMyCart 意图的履行方式为我们的 whatsInMyCart Lambda。
测试整个机器人
现在我们已经创建了所有的 Lambda 函数并测试了它们都能正常工作,我们可以将它们全部整合起来并构建我们的聊天机器人。在 Lex 控制台中为这个聊天机器人,逐个检查每个意图,确保它们都使用正确的 Lambda 函数来实现,然后我们可以在页面顶部点击“构建”。
一旦构建完成,我们就可以开始测试它。我们可以从查找一个产品开始。输入 I want to buy a shirt 会启动 productFind 意图流程,然后我们可以找到找到的项目库存水平:

测试 productFind
当我们看到一个产品时,我们也应该被问及是否想要将其添加到我们的购物车中。无论我们的回答如何,我们随后都应该被问及是否想要结账、添加另一个项目或保存购物车。我们需要尝试这些方法中的每一种,从添加另一个项目开始:

向购物车添加另一个项目
如预期,我们被发送到productFind流程的起点。在经历了该流程但未将新项目添加到购物车后,我们现在可以测试保存购物车。当我们到达产品查找的终点时,我们可以表示我们想要保存购物车。当我们提供购物车名称时,我们会被告知我们的购物车已被保存:

测试 saveCart
为了测试恢复购物车,我们可以在 Lex 中清除聊天并请求get my saved cart。我们应该被要求输入购物车名称,如果找到,它将为我们恢复:

测试 getSavedCart
现在我们已经恢复了我们的保存的购物车,我们可能想检查我们放进去的东西。我们只需要说what is in my cart,我们就应该得到我们产品的摘要。由于我们只添加了第一个产品,我们应该只有一个项目:

测试 whatsInMyCart
最后要测试的是结账。在我们的篮子里至少有一件商品的情况下,我们可以请求结账。我们应该被要求提供地址,然后被告知我们的订单已下单:

测试 checkout
在测试了所有这些之后,我们已经完成了我们的购物聊天机器人。如果你在这些测试中遇到了任何问题,请返回并确保你的所有代码都是正确的,并且 Lex 已经正确设置。如果你遇到进一步的问题,这本书的末尾有一组调试技巧。
摘要
本章涵盖了大量的内容。我们首先设计了一个复杂的聊天机器人流程,其中包含多个子流程。然后我们将这些流程构建成一组意图,使用户能够完成整个流程,或者只完成部分流程并在以后返回。这意味着我们需要直接从其他意图触发意图,预先填充一些槽位,并使用确认意图从一个意图切换到另一个意图。我们还学习了如何使用 DynamoDB 表来存储和检索有关用户进度的数据。
问题
-
复杂流程和简单流程之间的主要区别是什么?
-
我们如何使复杂的流程图更容易理解?
-
Lex 有哪五种满足类型?
-
我们可以使用 AWS SDK 的哪个部分来访问 DynamoDB 表?
进一步阅读
如果你想了解更多关于与 Dynamo 交互的不同方式,我建议查看 DocumentClient 文档。你可以学习如何实现扫描、查询和批量处理。你可以在docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html阅读该文档。
第七章:将您的聊天机器人发布到 Facebook、Slack、Twilio 和 HTTP
我们已经学习了如何使用 Amazon Lex 构建各种聊天机器人,但目前,其他人无法访问它们。在本章中,我们将学习如何将我们的聊天机器人部署到 Facebook、Slack 和 Twilio。我们还将学习如何将 Lex 与我们的前端集成,并创建一个 HTTP 端点,以实现更灵活的集成。
本章将涵盖以下主题:
-
将 Lex 聊天机器人部署到 Facebook Messenger、Slack 和 Twilio
-
创建一个 HTTP 端点以实现更灵活的集成
-
为我们的聊天机器人构建前端
技术要求
在本章中,我们将创建一个 Lambda 函数来为我们的 HTTP 端点提供动力,并且我们将使用我们在第二章,使用 AWS 和 Amazon CLI 入门中创建的本地开发环境来创建和部署它。
我们还将使用 Facebook 和 Slack,所以你需要有一个账户。如果你还没有,你可以免费创建账户。
本章所需的所有代码和数据都可以在bit.ly/chatbot-ch7找到。
集成
在构建了一个聊天机器人之后,你希望用户能够找到并使用它。许多用户已经拥有 Facebook 或 Slack,并且他们肯定有一个手机号码。能够通过这些现有的通信方式使用我们的聊天机器人,使得用户使用起来更加容易和自然。
为了允许聊天机器人进入他们的系统,Facebook、Slack 和 Twilio 创建了集成方法。这使得通过每个平台发送的消息都能到达我们的聊天机器人,使得我们的聊天机器人看起来像是系统的一部分。
Amazon Lex 使得我们与 Facebook、Slack、Twilio 和 Kik 的集成变得非常容易,在幕后隐藏了很多复杂的数据格式化。要访问 Lex 的集成,请点击“渠道”标签,你将可以选择配置 Facebook、Kik、Slack 或 Twilio SMS。
Facebook Messenger
截至 2018 年 4 月,Facebook Messenger 有 13 亿月活跃用户,并且这个数字每月都在增长。这是一个巨大的用户群,我们可以从中获取。
除了庞大的用户群之外,还有另一个非常适合聊天机器人开发者的特性。当你为一家公司、组织或其他任何东西创建一个 Facebook 页面时,它都有一个 Messenger 账户。这是为了让用户能够给公司发消息,但这意味着 Facebook 上的每家公司都可以从拥有聊天机器人中受益。这是一个巨大的目标市场。
要访问这些渠道,我们可以在 Lex 编辑器中点击“渠道”。我们可以先选择 Facebook 作为渠道,然后为这个渠道命名并添加描述。接下来,我们可以选择我们想要部署的别名。确保你已经将聊天机器人发布到一个别名,然后我们可以在下拉菜单中选择其中一个:

Lex 控制台中的渠道
我们现在能做的最后一件事是选择一个验证令牌。这是一个字符串,我们稍后会用它来帮助将 Facebook 连接到我们的 Lex 聊天机器人。这可以是您喜欢的任何字母和数字的字符串。
页面访问令牌和应用密钥是在创建 Facebook 应用后我们将获得的两个值,所以我们将接下来做这件事。
创建并连接 Facebook Messenger 应用
要将聊天机器人集成到 Facebook Messenger 中,我们首先需要创建一个 Facebook 应用。要开始,请访问developers.facebook.com/并点击“登录”。如果是您的第一个 Facebook 应用,那么您需要将此开发者账户链接到您的个人账户。登录后,您可以创建您的第一个应用。点击“我的应用”然后选择“创建新应用”。这将打开一个弹出窗口,我们可以命名该应用。
此应用名称不会显示给用户;它仅由 Facebook 页面管理员看到:

创建您的 Facebook 应用
Facebook 应用可以用于执行大量任务,但我们想构建一个聊天机器人,因此我们需要在“Messenger”下点击“设置”。
您现在应该在一个标题为“Messenger 平台”的页面上,在左侧,您应该看到“Messenger”在“产品”下。我们首先需要做的是创建一个令牌,这样 Lex 就可以访问这个应用。要生成令牌,我们可以进入“令牌生成”部分并点击“选择页面”下拉菜单:

生成您的页面令牌
如果您不是任何 Facebook 页面的管理员,那么您将不得不创建一个。在 Facebook 本身,您可以快速轻松地创建一个用于假店铺的页面,或者只是创建一个作为开发者的个人页面。
当您选择页面时,将生成一个令牌。这可以复制并粘贴到 Lex 通道配置中的“页面访问令牌”字段中。
我们还需要获取的是应用密钥,我们可以在应用的“设置 | 基本设置”页面下找到:

获取应用凭据
现在通道的所有细节都已完成,我们可以点击“激活”,我们将获得一个新的回调 URL。复制此 URL 并返回到我们的 Facebook 应用屏幕。返回到左侧菜单中的“Messenger 配置 | 设置”,然后滚动到“Webhooks”。Webhooks 是 Facebook 将通过它发送消息到我们的 Lex 聊天机器人的方式。
点击“设置 Webhooks”以打开一个弹出窗口,我们可以将我们从 Lex 获得的 URL 粘贴为“回调 URL”,然后是我们在 Lex 通道设置中指定的“验证令牌”。我们还需要订阅“消息”、“消息回调和”消息选择”。这些选项是选择 Facebook 将发送给 Lex 的消息类型:

Facebook Webhook 选项
点击验证和保存将会向 Lex 发送一个请求,并期望返回正确的验证令牌。通常,你将需要设置该端点,但 Lex 会处理所有这些。
我们最后需要设置的 Webhook 是选择我们可以订阅的页面。在 Webhooks 部分中,有一个“选择页面”下拉菜单,你需要设置并点击订阅。
现在,聊天机器人应该已经在你的页面上,但它只能由你自己和其他你添加到应用中的人访问。添加更多人进行测试或工作可以在左侧的“角色”菜单中完成。
在这个阶段,你可以通过访问你的 Facebook 页面并发送消息来测试你的聊天机器人。Lex 应该会接收到消息,并发送正确的响应,就像它在 Lex 控制台中做的那样。
在你可以设置你的新应用上线之前,你需要请求 Facebook 允许你进行页面消息发送。这需要在“消息者设置”页面底部滚动,并将 pages_messaging 添加到提交中。此时,你可能会被要求完成一些额外的事情,例如添加应用图标、设置隐私政策 URL 和类别:

提交要求
完成这些后,你可以提交你的应用以供审查。你将被要求提供示例命令及其自动响应。确保在提交之前测试过这些命令,因为验证一个应用可能需要长达一周的时间,所以第一次就做对是关键。
Facebook 最近更新了其政策,现在要激活你的聊天机器人,你需要有一个经过批准的 Facebook 商业账户。这涉及到注册你的商业详情并提供一些证明材料。
一旦你的应用和连接的商业账户得到验证,你就可以从“关闭”切换到“开启”,并允许每个人开始向你的聊天机器人发送消息。
Slack
Slack 是一个在软件开发者和科技公司中非常受欢迎的即时通讯平台,并且完全支持聊天机器人。
正如我们处理 Facebook 一样,我们需要选择一个频道名称和别名,如果你愿意的话,还可以提供频道描述。
创建和连接 Slack 应用
要开始设置我们的 Slack 应用,我们需要登录到 Slack API (api.slack.com/)。一旦我们登录,我们就可以创建一个新的应用:

创建 Slack 应用
接下来,我们可以设置应用的功能,对我们来说就是配置聊天机器人功能。我们需要为我们的应用提供一个显示名称和默认用户名,并将“始终开启”切换设置为开启。这意味着聊天机器人将始终显示为在线状态。
设置好之后,我们现在可以进入左侧菜单中的“基本信息”,在那里我们可以获取客户端 ID、客户端密钥和验证令牌,我们可以将这些粘贴到我们的 Lex 频道配置中。
当您激活 Lex 频道时,您应该会得到一个 Postback URL 和 OAuth URL。Postback URL 是监听来自 Slack 的消息的 URL,OAuth URL 用于验证您的聊天机器人。
使用 OAuth URL,我们可以回到api.Slack.com,并导航到我们的应用。从这里,我们可以导航到左侧菜单中的 OAuth & Permissions,并点击添加新的重定向 URL。现在我们可以粘贴从 Lex 获取的 OAuth URL:
我们还需要设置此应用将获得的权限范围。在 Scopes 部分,我们可以通过选择“选择权限范围”下拉菜单来添加权限。我们需要添加“发送消息作为...”(chat:write:bot)和“访问有关您工作空间的信息”(team: read),然后保存更改:

Slack 权限
下一步是允许 Lex 通过点击左侧菜单中的“交互组件”并开启交互性来与我们的 Slack 应用交互。然后我们可以设置请求 URL 为我们从 Lex 激活中获得的 Postback URL。
最后一步是启用事件订阅,它可以在左侧菜单中找到。开启它,并将我们的 Postback URL 作为请求 URL 粘贴,然后点击添加工作空间事件。向下滚动直到您看到 message.im,并添加它,然后保存更改。
要将我们的应用安装到您的 Slack 频道,我们需要进入“管理分发”并点击“添加到 Slack”。您应该会被重定向到您的 Slack 团队,并且应该在我们的直接消息中看到我们的聊天机器人。如果您看不到它,您可以使用+图标搜索它。
您现在可以通过 Slack 向聊天机器人发送消息,并且应该收到我们在 Lex 控制台中测试时得到的相同响应。
Twilio
Twilio是一个平台,允许您使用短信、电话和视频通话与用户互动。我们将使用它来允许用户通过短信文本消息与我们的聊天机器人互动。
就像我们处理前两个集成一样,我们可以给频道起一个名称并选择一个别名。Account SID 和 Authentication Token 需要从 Twilio 获取,所以我们现在就要这么做。
创建和连接 Twilio
要开始,我们需要访问www.twilio.com并注册或登录。登录后,在左侧菜单中进入设置,在 API 凭证下,您将看到 ACCOUNT SID 和 AUTH TOKEN。这些可以复制并粘贴到 Lex 频道设置中,然后我们可以点击激活。复制生成的端点 URL,然后返回到 Twilio 控制台。
在控制台中,我们需要进入可编程短信,并开始获取一个可以发送短信的号码:

获取号码
我们将获得一个随机电话号码,我们可以选择这个号码或者搜索一个不同的号码:

选择号码
现在我们有了可以使用的电话号码,我们可以从左侧菜单中选择消息服务。然后我们可以为我们的 Lex 聊天机器人添加一个服务。这个服务将允许我们接收文本消息,并在回复 Lex 响应之前将它们传递给我们的 Lex 聊天机器人。给这个服务起一个名字,并确保将用例设置为聊天机器人/交互式双向:

创建新服务
你应该被发送到“号码”子菜单,在那里我们可以向此服务添加一个现有号码。这选择了我们的聊天机器人将使用的号码。选择我们之前选择的号码,并将其添加到服务中。
在服务上设置了号码后,我们可以转到配置,添加我们从 Lex 获得的端点 URL。我们希望能够接收传入的消息,因此点击“处理传入消息”复选框,并将我们的 URL 粘贴到“请求 URL”框中。保存此服务,我们只剩下最后一件事要做:让我们的短信聊天机器人工作:

传入设置
我们需要做的最后一件事是允许我们的 Twilio 向我们所在地区的号码发送短信。在消息服务中,转到设置,然后是地理权限。这是一个所有可用的国家位置的列表;我们需要激活我们的地区,以便我们可以测试它。
搜索你的国家,通过勾选复选框来激活它。你可以激活你喜欢的任何地区。
你现在可以通过向为这项服务选择的号码发送文本来测试你的聊天机器人:

发送聊天机器人短信
如果你想要去掉来自 Twilio 试用账户的“发送自”消息,那么你需要升级。
HTTP 端点
Lex 使得将我们的聊天机器人集成到 Facebook、Slack 和 Twilio 变得非常容易,这真是太好了,但我们可能还希望我们的聊天机器人能够集成到没有内置集成的其他服务中。为此,我们可以为向我们的 Lex 聊天机器人发送消息创建一个 API 端点。
与 AWS 合作,我们很幸运,他们允许你使用 Lambdas 和 API 网关创建一个 API。这意味着我们不需要运行服务器,这意味着我们工作量更少。
创建 Lambda
我们首先在我们的 Lambdas 仓库中创建一个名为lex-shopping-api的新文件夹,并在其中创建一个index.js文件。在这个文件中,我们可以首先导出一个处理程序,该处理程序检查事件是否为POST请求,并调用sendToLex来生成回复。然后,这个回复被传递到done,它格式化数据,以便可以返回给 API 网关:
exports.handler = async (event) => {
if (event.httpMethod === "POST") {
let reply = await sendToLex(event);
return done(reply);
}
};
我们现在需要创建sendToLex函数。这个函数首先需要做的事情是将事件体映射到 Lex 所需的格式。我们将在稍后创建这个mapMessageToLex函数:
const sendToLex = async event => {
console.log('event', event);
let messageForLex = mapMessageToLex(JSON.parse(event.body));
}
此消息现在需要发送到 Lex。亚马逊通过创建 Lex 运行时使其变得简单,该运行时允许您向 Lex 聊天机器人发送消息。要访问 Lex 运行时,我们需要在lex-shopping-api文件夹中运行npm init和npm install --save aws-sdk来安装aws-sdk。然后我们可以在文件顶部添加此代码来引入它并创建 Lex 运行时类的新实例:
const AWS = require('aws-sdk');
const lexruntime = new AWS.LexRuntime();
要向 Lex 发送消息,我们需要调用lexruntime.postText(),传递messageForLex和处理程序回调。我们可以将整个操作包裹在一个new Promise中,以更好地控制async流程:
let lexPromise = new Promise((resolve, reject) => {
lexruntime.postText(messageForLex, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
})
});
我们现在可以使用错误处理程序await lexPromise以获取响应或错误。如果有错误,则可以返回该错误;如果收到响应,则可以将res设置为包含消息的对象:
let [err, res] = await to(lexPromise);
if (err) {
return { err }
}
console.log('lex response', res);
return { res: { message: res.message } }
这些返回值将一路返回以填充我们处理程序中的回复变量。这被传递给done,因此我们现在需要创建这个函数。API 网关期望以特定的格式获取响应,因此这个函数返回该格式:
const done = ({ err, res }) => {
console.log('res', res);
console.log('error', err);
return {
statusCode: err ? '404' : '200',
body: err ? JSON.stringify({ error: err }) : JSON.stringify(res),
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Origin': '*'
},
};
}
我们需要创建的最后一个函数是mapMessageToLex。Lex 运行时要求它得到一个包含botAlias、botName、inputText、userId和sessionAttributes的对象,因此我们将消息映射到这个格式。如果您想为不同的机器人创建 API,那么您只需要更改botName和botAlias:
const mapMessageToLex = message => {
return {
botAlias: 'prod',
botName: 'shoppingBot',
inputText: message.text,
userId: message.sessionID,
sessionAttributes: {}
};
}
测试
为了测试这个 Lambda 是否正常工作,我们可以在其上运行一些测试。需要传递给这个 Lambda 的唯一值是body和httpMethod。因为body是一个字符串,所以我们需要转义引号:
{
"body": "{\"text\":\"I want to buy a shirt\", \"sessionID\": \"abc123\"}",
"httpMethod": "POST"
}
运行此测试应该得到 API 网关期望的这种格式的响应:
{
"statusCode": "200",
"body": "{\"message\":\"What size of shirt are you looking for?\"}",
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Origin": "*"
}
}
连接 API 网关
API 网关是一项服务,允许我们创建可以接受所有正常 API 请求方法的 URL。首先,前往 AWS 中的 API 网关服务并点击开始。
在创建我们的第一个 API 时,我们应该选择新建 API,然后我们可以为此 API 命名和描述,然后点击创建 API:

新 API
您现在应该处于 API 的配置页面,但目前还没有创建任何端点。点击操作下拉菜单并选择创建资源。这样做允许您将所有 Lex 聊天机器人的 API 放在类似的 URL 上:

创建资源
将资源命名为shopping-bot并点击创建资源:

新资源
现在我们已经创建了一个资源,我们可以向其附加一个方法。在我们的 Lambda 中,我们检查httpMethod是否为POST,因此我们需要创建一个POST方法。点击我们的shopping-bot资源,然后点击操作 | 创建方法:

新方法
这将打开方法设置窗口,有很多人配置方法的方式,但我们将调用我们的 API Lambda。确保集成类型是 Lambda Function,并且已勾选 Use Lambda Proxy integration。这确保了所有请求数据都通过代理转发到 Lambda。
在方法设置中的下一件事是选择我们的lex-shopping-api作为 Lambda 函数,并保存方法:

方法设置
最后,我们需要在我们的 API 中添加跨源资源共享(CORS)。这允许我们从不同的互联网浏览器访问我们的 API。在下一节构建此 API 的前端时,这将非常重要。选择我们的 shopping-bot 资源,然后我们可以点击 Actions | Enable CORS。我们可以保留所有设置默认,并点击 Enable CORS,替换现有的 CORS 头部,确认我们想要替换现有值:

添加 CORS
测试
我们现在可以通过选择 POST 方法并点击 TEST 来测试我们的 Lambda 是否被正确调用:

方法 TEST
在这个屏幕上,我们可以设置查询字符串、头部和请求体。我们不需要发送任何查询字符串或头部,因此我们可以直接滚动到请求体部分。正如我们应该从 Lambda 的测试中记住的那样,我们只需要在体中传递text和sessionID,所以这就是我们可以作为请求体放入的内容:
{
"text":"I want to buy a shirt",
"sessionID": "abc123"
}
当我们点击 Test 时,API Gateway 会将我们的请求发送到我们的 Lambda。我们的 Lambda 会将它发送到我们的 Lex 聊天机器人,并将响应发送回来。我们的响应体应该返回如下:
{
"message": "What size of shirt are you looking for?"
}
构建 API
最后要做的事情是构建我们的 API。在我们处于 API 上时,我们可以选择 Actions | Deploy API。由于这是我们第一次部署此 API,我们需要创建一个新的阶段。给你的阶段起一个名字和描述,你还可以在点击 Deploy 之前添加一个部署描述:

创建一个阶段
当你的 API 部署时,你会得到一个用于它的 URL,它将是https://{unique-code}.execute-api.eu-west-1.amazonaws.com/{stage-name}。为了访问我们制作的端点,我们需要在末尾添加/shopping-bot。例如,https://acffds-4fnf8x-se54fws-s34d.execute-api.eu-west-1.amazonaws.com/production/shopping-bot。这意味着你现在可以使用这个 API 将 Lex 集成到更广泛的系统中。
网页用户界面
拥有自己的聊天机器人界面允许用户通过访问网页来访问它,但我们也可以将其集成到其他网站中,甚至为我们的聊天机器人创建移动应用。我们可以使用我们创建的 API 来轻松访问聊天机器人,而无需公开我们的 AWS 凭证。
HTML
首先,我们需要一个 HTML 页面来构建。我们需要开始的是三个组件:一个消息区域、一个输入框和一个发送按钮。创建一个包含index.html文件的文件夹,并将此代码添加到该文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="messageArea"></div>
<div id="inputDivs">
<input type="text" id="textInput">
<button id="sendButton">Send</button>
</div>
</body>
<script src="img/axios.min.js"></script>
<script src="img/script.js"></script>
</html>
这是一个简单的 HTML 文件,在标题中有一个 CSS 链接,这样我们就可以样式化我们的页面、消息区域、输入框和按钮,以及两个脚本。其中第一个脚本导入axios,这样我们就可以轻松地发出请求,第二个是我们的脚本。
由于我们已经包含了style.css和script.js文件,我们应该在我们的文件夹中创建这些文件。
创建我们的脚本
这个 UI 的所有功能都需要在这个脚本文件中处理。当所有 HTML 加载完成后,我们需要监听用户点击发送按钮。当发生这种情况时,我们需要从输入框获取文本并将其作为已发送消息写入,然后再将其发送到我们的 API。当我们的 API 回复时,我们可以将响应作为接收到的消息添加。
首先,我们需要确保文档已完全加载。我们可以检查文档是否已准备好,如果没有,则等待DOMContentLoaded事件:
if (document.readyState === 'complete') {
start();
} else {
document.addEventListener("DOMContentLoaded", start())
}
现在我们可以创建start函数并设置 API URL 和会话 ID。我们可以使用Math.random()技术生成一个随机的 16 位数字作为sessionID:
function start() {
const URL = 'YOUR-API-URL/production/shopping-bot';
// create unique code for this session
const sessionID = Math.random().toString().slice(-16);
}
在start()函数中,我们还需要使用document.querySelector访问消息区域、文本输入框和发送按钮:
let messageArea = document.querySelector('#messageArea');
let textArea = document.querySelector('#textInput');
let sendButton = document.querySelector('#sendButton');
接下来是sendButton,我们可以为用户点击发送时附加一个监听器。这将首先获取文本输入框的值。如果没有文本,则可以从函数中返回空值:
sendButton.addEventListener('click', async e => {
let text = textArea.value;
console.log(text);
if (!text) return;
}
如果有文本,则我们可以继续创建一个sendElement并将其添加到消息区域。我们需要确保为元素添加sendMessage和message类,以便我们稍后进行样式化:
// Add to sent messages
let sendElement = document.createElement('div');
sendElement.classList.add('sendMessage');
sendElement.classList.add('message');
sendElement.appendChild(document.createTextNode(text));
messageArea.appendChild(sendElement);
接下来,我们必须将消息发送到我们的 API。我们可以使用在 HTML 文件中导入的axios,通过text和sessionID作为正文传递。我们需要确保从我们的 Lambdas 函数中复制函数以进行错误处理:
// send to the API
let [err, response] = await to(axios.post(URL, { text, sessionID }));
如果响应中存在错误,则可以将消息设置为道歉;否则,它将是response.data.message:
let responseMessage;
if (err) {
responseMessage = 'Sorry I appear to have had an error';
} else {
responseMessage = response.data.message;
}
最后一件要做的事情是将接收到的消息添加到消息区域,以便用户可以看到它。别忘了添加receivedMessage和message类以便稍后进行样式化:
// adding the response to received messages
let receiveElement = document.createElement('div');
receiveElement.classList.add('receivedMessage');
receiveElement.classList.add('message');
receiveElement.appendChild(document.createTextNode(responseMessage));
messageArea.appendChild(receiveElement);
如果我们在浏览器中打开 HTML 文档,现在我们应该能够输入并发送消息到我们的 Lex 聊天机器人:

基本消息功能
前端样式化
我们创建了一个很棒的网页,允许用户与聊天机器人交谈,但,目前,它看起来很糟糕。我们可以通过使用我们的 CSS 文件来修复这个问题。在构建聊天时,我们一直在向元素添加类和 ID。这意味着我们可以设置这些类和 ID 的样式来美化整个聊天窗口。首先要做的是设置消息区域的大小。我们还可以添加一个浅色背景并将溢出设置为滚动:
#messageArea {
height: 93vh;
max-width: 450px;
background: #eee;
overflow-y: scroll;
}
接下来,我们可以美化消息。我们向message类添加了常见的样式,如padding、margin和max-width,而alignment、background和border-radius则在每个消息类型中定义:
.message {
padding: 3%;
margin: 2%;
position: relative;
max-width: 70%;
}
.sendMessage {
right: -20%;
background: blue;
color: white;
border-radius: 16px 16px 8px 16px
}
.receivedMessage {
background: #bbb;
left: 0;
border-radius: 16px 16px 16px 8px;
}
最后要美化的部分是输入文本框和发送按钮。我们可以在容器div上使用display: flex,并在文本输入上使用flex-grow: 2,使其填充按钮留下的宽度。我们可以通过不同的边框和背景来美化按钮:
#inputDivs {
width: 450px;
display: flex;
}
#textInput {
font-size: 15px;
flex-grow: 2;
}
#sendButton {
font-size: 15px;
border: 0px solid lightskyblue;
background: lightskyblue;
border-radius: 8px;
padding: 8px;
margin-left: 8px;
}
这比我们之前使用的纯文本提供了更好的用户体验。这就是你可以花时间定制你的界面外观,使其完全符合你想要的样子。
您甚至可以将此样式调整为与公司的品牌颜色相匹配:

美化后的聊天
摘要
在这一章中,我们学习了如何创建集成,使用户能够从 Facebook Messenger、Slack 和 Twilio 访问我们的聊天机器人。
我们还学会了如何创建一个 API,以便将我们的聊天机器人集成到亚马逊目前不支持的其他服务中。这个 API 使用 Lambda 函数来处理通过 API Gateway 发送的请求。
然后,我们使用这个 API 为我们的聊天机器人创建了一个前端网页。我们编写了一个简单的 HTML 文档,然后使用脚本与 API 通信并将消息添加到页面中。我们最后做的事情是为页面添加样式,使其看起来像真正的消息平台。
问题
-
为什么我们要将聊天机器人集成到其他平台和服务中?
-
你可以使用哪些服务来创建一个 API?
-
在 API 工作之前,我们需要添加哪两个东西?
-
为了使我们的 API 公开,我们需要最后做什么?
-
请命名我们聊天机器人网页的三个部分。
-
加载的脚本文件应该首先做什么?
第八章:提升您的聊天机器人的用户体验
在学习了如何创建 Alexa 技能和 Lex 聊天机器人之后,我们现在将学习如何提升用户体验。这非常重要,因为为 Lex 聊天机器人添加图片或为 Alexa 提供更好的语音模型,都会对用户是否享受与聊天机器人的互动产生巨大影响。添加这些功能也将使您的聊天机器人从基本的纯文本聊天机器人中脱颖而出。
本章将涵盖以下主题:
-
将响应卡片添加到 Lex 聊天机器人中
-
使用短语槽位为 Alexa 技能创建更精细的语音模型
-
使用 Amazon Lex 的语音监测来细化交互模型
技术要求
在本章中,我们将修改现有的 Lambda 函数,因此我们将使用我们创建的本地开发环境来部署它们第二章,AWS 和 Amazon CLI 入门。
本章所需的所有代码和数据都可以在bit.ly/chatbot-ch8找到。
Amazon Lex 中的响应卡片
卡片通过集成按钮、图片等,为您提供了比纯文本消息更丰富的对话体验。卡片可用于多种用途,例如显示产品信息、让消息接收者从预定的选项集中选择,以及显示搜索结果。如果您将卡片集成到 Slack 或 Facebook 中,它们将显示在这些平台上:

Facebook 中的示例卡片
创建卡片
要创建卡片,我们需要更改发送回 Lex 的响应格式。这意味着我们需要通过传递responseCard属性来更改LexResponses中的函数。然后我们可以将这个responseCard添加到dialogAction对象中。如果我们没有传递响应卡片参数,我们仍然希望函数能够工作,因此我们将其默认设置为null:
elicitIntent({ message, sessionAttributes = {}, responseCard = null }) {
return {
sessionAttributes,
dialogAction: {
type: 'ElicitIntent',
message: { contentType: 'PlainText', content: message },
responseCard
},
};
}
这需要对elicitSlot、close、elicitIntent和confirmIntent进行操作,但不包括delegate,因为该功能不会发送消息。
要添加响应卡片,我们需要确保响应也处于正确的格式。为了使这个过程更简单,我们可以在LexResponses内部创建一个新的函数,称为createCardFormat。这将接受卡片的一个属性,即包含title、subtitle、imageUrl、linkUrl和buttons的对象数组:
createCardFormat(cards) {
return {
version: 1,
contentType: "application/vnd.amazonaws.card.generic",
genericAttachments: cards.map(({ title, subtitle, imageUrl, linkUrl, buttons }) => {
return {
title,
subtitle,
imageUrl,
attachmentLinkUrl: linkUrl,
buttons: buttons.map(({ text, value }) => {
return { text, value };
})
};
})
}
}
在聊天中使用卡片
使用我们修改后的LexResponses类,我们现在可以开始向现有的 Lex Lambda 中添加卡片。一个明显的使用卡片的地方是在购物应用中显示基于用户搜索找到的商品。这意味着我们将修改我们的productFind Lambda。
在我们创建的消息中告诉用户我们有多少库存的商品(productFind/index.js的第 77 行)之后,我们可以创建我们的第一个卡片。
这将是一个带有项目标题的单个卡片,库存的副标题,图像,以及“添加到购物车”和“现在不”按钮:
let responseCard = Lex.createCardFormat([{
title: `${size}, ${colour}${type === 'trousers' ? ', ' + length : ''}${type}`,
subTitle: `${item.stock} in stock`,
imageUrl: item.imageUrl,
buttons: [
{ text: 'Add to Cart', value: 'Yes' },
{ text: 'Not Now', value: 'No' }]
}
]);
如你所见,我们正在给按钮赋予与文本不同的值。这使得我们收到的响应可以与用户点击的按钮不同。
你可能已经注意到我们使用 item.imageURL 添加了一个图像,但这个图像在我们的原始数据中并不存在。我们需要遍历并添加到库存数据中的每个项目。幸运的是,我们可以使用相同的图像来处理不同尺寸的衣物。带有图像的库存数据可在 bit.ly/chatbot-ch8 下载。
当我们部署这些更改时,我们可以在 Lex 聊天窗口中测试它们。我们可以通过正常的 productFind 流程,直到我们看到选定的产品。当我们被告知库存数量时,我们还会看到一个显示信息的卡片:

聊天卡片
如果我们从前几章中设置了 Facebook 或 Slack 集成,那么我们的新卡片也应该在那里工作。Lex 会进行很多聪明的逻辑转换,将卡片转换为每个平台所需的正确格式,然后在回复中使用它们。需要注意的是,Facebook 会将图像裁剪为 1:1.9 的比例,所以考虑到这一点选择你的图像是个好主意:

Facebook 卡片
Alexa 搜索查询
当你知道用户将要说的响应类型时,Alexa 是很棒的,但如果他们询问你意料之外的东西呢?即使使用自定义槽位类型也可能有限制,这可能导致用户的请求被错误处理。幸运的是,亚马逊引入了搜索查询槽位类型。
这种用于 Alexa 的槽位类型旨在能够接受更广泛的值范围,以便您能够处理更多请求。
我们将在现有的天气之神技能中添加一个新的意图,该意图使用搜索查询槽位类型,允许用户在某个城市中搜索地点。我们将使用 Google Maps API 来提供后端支持。
前往你的 Alexa 开发者控制台并打开 WeatherGods 技能。添加一个名为 searchIntent 的新意图,我们可以从创建我们将要使用的不同槽位开始。创建两个槽位,一个名为 query,另一个名为 city。我们的查询槽位可以指定为 AMAZON.SearchQuery,我们的城市槽位将是 AMAZON.US_CITY:

搜索槽位
完成槽位后,我们可以开始填充话语。不幸的是,我们不能在包含另一个槽位的话语中有一个搜索查询槽位,所以我们将不得不一次填充一个槽位。我们应该允许用户询问一个城市或提出一个查询以启动意图:

搜索查询的话语
现在我们已经完成了槽位和语句的设置,我们可以修改现有的 weatherGods Lambda 来处理新的意图。在您的编辑器中找到 Lambda,打开 index.js 文件,进入 handlers 对象。
在 handlers 对象内部,我们需要添加一个新的处理器,称为 searchIntent。这个处理器将首先获取城市和查询槽位值,并检查它们是否存在。如果它们不存在,我们将要求用户告诉我们缺失的信息。我们首先检查 cityValue,这样当我们请求查询时可以指定城市:
const SearchHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
handlerInput.requestEnvelope.request.intent.name === 'searchIntent';
},
async handle(handlerInput) {
const { slots } = handlerInput.requestEnvelope.request.intent;
let { city, query } = slots;
let cityValue = city.value;
let queryValue = query.value;
if (!cityValue) {
let slotToElicit = 'city';
let speechOutput = `What city are you looking in?`;
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
if (!queryValue) {
let slotToElicit = 'query';
let speechOutput = `What are you looking for in ${cityValue}`;
return handlerInput.responseBuilder
.speak(speechOutput)
.addElicitSlotDirective(slotToElicit)
.getResponse();
}
}
}
如果我们既有城市值又有查询值,那么我们可以使用这些值来向 Google 地图 API 发送请求。
Google Cloud Platform
要使用 Google Maps API,我们需要设置一个 Google Cloud Platform 开发者账户。我们可以通过访问 cloud.google.com 并点击“免费试用”来获取一个账户。您需要登录到一个 Google 账户,确认条款和条件,然后输入支付信息。不用担心;您在开始时将获得 300 美元的免费信用额度,所以您不会很快收到账单。
首先,我们需要通过点击左上角的“选择项目”并选择“新建项目”来创建一个项目。现在我们可以将我们的新项目命名为 WeatherGodsAPI 并点击“创建”。
在我们的项目创建后,我们需要检查它是否已选中页面左上角,然后我们就可以开始设置这个项目了。在搜索框中,我们可以搜索Places API并为此项目启用它:

Places API
一旦在这个项目中启用了 Places API,我们需要生成一个 API 密钥,以便我们能够从我们的 Lambda 中访问它。点击“凭证”,然后从“创建凭证”下拉菜单中选择“API 密钥”:

创建 API 密钥
您需要复制这个 API 密钥,因为我们将在我们的 Lambda 中使用它。
继续构建 Lambda
现在我们有一个可以用来调用 Google Places API 的 API 密钥。复制它并打开您的 Lambda 在 Lambda 控制台中。向下滚动到环境变量,创建一个键为 GOOGLE_API_KEY 的新变量,并将 API 密钥粘贴为值。确保不要删除其他 API 密钥,它是为 openWeatherMaps 使用的:

存储环境变量
将我们的 Google API 密钥存储为环境变量后,我们可以创建我们将要发送给 Google 的请求。API 请求 URL 的格式如下:
https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input={YOUR SEARCH}&inputtype=textquery&fields=formatted_address,name&key={YOUR API KEY}
为了让我们自己更容易操作,我们可以将这个文档的大部分内容存储为常量,甚至包括 Google API 密钥。在我们的 index.js 文件中,我们可以在文件顶部添加这些常量:
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
const googleURL = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=';
const queryString = '&inputtype=textquery&fields=formatted_address,name&key=';
在这些常量可访问的情况下,我们唯一需要生成的是我们的搜索。为此,我们可以将 queryValue 和 cityValue 转换为一个搜索短语。这可以通过将 {queryValue} 和 {cityValue} 组合成一个基本句子来完成。因为我们将其插入到 URL 中,所以需要使用 %20 而不是空格,然后我们可以构建我们的请求:
let completeURL = googleURL + [queryValue, 'in', cityValue].join('%20') + queryString + GOOGLE_API_KEY;
构建了请求 URL 后,我们可以向 Google 发送请求。为了捕捉任何错误,我们可以使用我们的 to 方法,然后检查是否存在错误以及是否存在 response.data 字段。如果我们没有得到预期的结果,我们可以告诉用户我们找不到该信息:
if (err || !res || !res.data) {
let apology = `unfortunately I couldn't find that for you`;
return handlerInput.responseBuilder
.speak(apology)
.getResponse();
}
如果我们的请求成功返回了一些数据,那么我们可以为用户构建一个响应。首先,我们可以告诉他们在该城市中有多少个查询,然后我们可以列出这些地点的名称:
let data = res.data;
let info = `There's ${data.candidates.length} ${query.value}${data.candidates.length === 1 ? "" : 's'} in ${city.value}.
${data.candidates.map(candidate => `the ${candidate.name}`)}`;
return handlerInput.responseBuilder
.speak(info)
.withShouldEndSession(false)
.getResponse();
我们现在已经完成了更新的 Lambda,可以使用第二章,《AWS 和 Amazon CLI 入门》中的构建脚本将其部署到 AWS。
重新构建技能并测试
在 Alexa 控制台中,我们可以检查修改后的技能,并确保保存它并重新构建。构建完成后,我们可以点击“测试”来尝试它。您可以测试我们在第四章,《将您的 Alexa 技能连接到外部 API》中构建的旧意图,并且它们应该像以前一样工作,但我们真正想测试的是我们的新意图。
您现在可以向天气之神询问曼彻斯特有什么,并说明您正在寻找一个天主教堂,您的技能将向 Google 查询曼彻斯特的天主教教堂。它应该告诉您有一个叫做索尔福德大教堂的教堂。
虽然这很好,但我们本可以使用自定义槽类型并列出用户可能询问的大量内容。这正是搜索查询真正有用之处:它们可以处理远不那么常见的请求。我们可以询问曼彻斯特的圣保罗小学,我们会得到一个结果。我们不可能创建一个足够大的自定义槽来包含每个学校的名称:

测试搜索查询
Lex 语句监控
当您创建意图并生成语句列表时,您尽力涵盖用户可能说的所有内容。不幸的是,人们经常想出一些独特的方式来表达您未曾想到的内容。在这种情况下,用户将从 Lex 获得一个“我不理解”的消息。这显然不利于提供良好的用户体验。
幸运的是,Lex 内置了监控功能,允许您查看用户所说的语句。要访问这些语句,我们需要点击 Lex 中的“监控”标签。Lex 语句存储在特定的聊天机器人版本中,因此我们需要从聊天机器人名称旁边的下拉菜单中选择一个值。
现在你应该有一个显示 Lex 使用情况的图表屏幕。这可能很有用,但我们正在寻找左侧菜单中的话语表:

话语监控
现在,你应该看到一个带有中心切换按钮的表格,用于检测/未检测。检测到的话语可以用来查看大多数用户是如何与聊天机器人互动的。这可以帮助你确定哪些领域可以开发以改善你的聊天机器人,以适应你最大部分的受众。
如果你没有看到任何话语,那么你需要检查几件事情。你需要确保在聊天机器人的常规设置中,COPPA 设置为否。接下来,你应该尝试更改聊天机器人的版本(位于聊天机器人名称旁边),因为话语保存到特定的版本。如果话语在 24 小时到 15 天之间,它们会出现在这些表格中。如果你仍然没有看到任何话语,那么你可能只需要等待直到你有这个范围内的话语。
当用户说了 Lex 无法匹配到你的示例话语时,话语监控非常有用。未识别话语给你一个列表,其中包含所有发生这种情况的话语。尽管其中一些可能是胡言乱语或打字错误,但其中一些可能是有效的、你可能没有考虑过的话语:

未识别话语
在查看未识别意图时,你可能会意识到用户输入了一个你没有想到的话语。你可以通过选择未识别话语,然后从位于表格上方下拉菜单中选择意图,轻松地将这个话语添加到意图中。这样可以节省手动复制和粘贴话语到意图中的时间。
一旦你将所有有效的未识别话语移动到正确意图中,你需要确保构建和部署你的更新版聊天机器人。
摘要
在本章中,我们探讨了三种改善 Alexa 技能和 Lex 聊天机器人用户体验的方法。
我们首先在 Lex 聊天中创建卡片,以增加提供给用户的视觉信息。这些卡片是使你的 Lex 聊天机器人区别于仅基于文本的聊天机器人的好方法。
然后,我们转向 Alexa,在那里我们学习了搜索查询槽位。这种槽位类型允许用户输入比使用自定义槽位类型更多的值来填充槽位。
我们最后学会使用的工具来改善用户体验是话语监控。看到你的用户真正对聊天机器人说了什么,可以帮助你增加每个意图的示例话语。这导致了一个能够成功处理更广泛用户话语的聊天机器人。
所有这些都会提供更多信息或减少聊天机器人无法处理用户请求的可能性。
在最后一章中,我们将讨论一些继续学习最佳方法。我们还将讨论聊天机器人的未来以及它们将如何成为我们日常生活的一部分。
问题
-
在 Lex 对话中使用卡片的好处是什么?
-
你需要在 Lex 卡片中使用图片吗?
-
为什么你会选择使用搜索查询槽位类型而不是默认或自定义槽位类型?
-
你能否从用户语句中填充搜索查询槽位?
-
你如何找出哪些 Lex 语句没有匹配到意图?
第九章:复习和持续开发
在本书中,我们涵盖了许多主题,并在许多不同的领域学习了技能。我们将这些技能结合起来,在 Alexa 和 Lex 平台上设计和创建复杂的聊天机器人。
本章将涵盖以下内容:
-
回顾本书中我们学到的技能
-
讨论如何继续你的聊天机器人开发探索
-
讨论聊天机器人的未来
我们学到了什么
本书涵盖了大量的技能,既有技术性的也有非技术性的。
对话设计
覆盖的第一个主题是对话设计。这是本书最重要的部分之一,因为每个好的聊天机器人都需要经过这个设计阶段。无论是 Alexa 技能、Lex 聊天机器人,还是使用不同技术构建的聊天机器人,都无关紧要。
在设计聊天机器人时,我们总是试图从一个完美的用户对话开始。从一个完美的对话开始意味着用户很可能会获得我们聊天机器人的最佳体验。
使用完美的对话,我们可以开始构建我们的流程图。这些图提供了更多技术结构,使我们能够指定我们正在保存哪些数据,我们正在调用哪些 API,以及从一个流程触发另一个流程。创建一系列相互链接的简短流程图非常强大,因为它提供了在聊天机器人成熟过程中连接新入口点和功能的灵活性。
亚马逊网络服务
接下来,我们介绍了亚马逊网络服务(AWS)以及我们可以使用的工具。我们首先通过 Lambda 控制台创建了一个 AWS Lambda,然后使用内置的测试功能进行了检查。
虽然在控制台中创建 Lambda 对于简单的函数来说很棒,但我们通常需要更多的功能和一个更可靠的体验。我们讨论了两种其他选项——Cloud9和本地编辑——并提到了它们的优点和局限性。
本地编辑有一些很好的优点,但缺乏轻松创建和更新 Lambda 的能力。为了解决这个问题,我们学习了aws-cli以及我们如何使用它来控制我们的 AWS 产品。使用aws-cli,我们创建了一个构建脚本,可以将我们的本地文件打包在一起,并将它们部署到 AWS。有了这个脚本,我们现在拥有了一个功能强大的开发环境,并且部署变得容易。
亚马逊 Alexa
然后是开始构建一些聊天机器人。我们从 Alexa 开始,了解了构成聊天机器人的组件。我们学习了如何使用意图、话语和槽位,以便用户能够与我们的聊天机器人进行交互。
为了使这个技能发挥作用,我们需要创建一个 Lambda 来处理请求。我们使用了alexa-sdk来使创建发送给用户的响应变得更加容易。
在一个可工作的 Alexa 技能的基础上,我们学习了如何使用内置的测试工具进行测试。这样,我们可以像用户一样测试它,但同时又能够看到我们的技能在后台是如何运作的。
一旦我们测试了聊天机器人,我们就准备好发布它。Alexa 技能需要发布到 Alexa 技能商店,我们学习了如何遵循这一流程,使我们的技能对公众可用。
Amazon S3
为了增加我们所有聊天机器人的实用性,我们需要能够访问大量存储的数据。为此,我们学习了如何创建一个 S3 存储桶,在其中存储数据,然后从我们的 Lambdas 中访问这些数据。有了这种数据访问,我们可以向用户提供他们请求的主题的更多信息。
使用 API
然后,我们学习了如何访问第三方 API,以进一步提高我们聊天机器人的实用性。我们以 openWeatherMaps API 为例,这使我们能够访问我们无法自己生成的实时信息。
为了做到这一点,我们还学习了 axios 和如何进行 API 请求。有了这些技能,你将能够向 API 发送请求,为你的聊天机器人添加新功能。我们还探讨了处理错误的最两种最佳方式——使用 try/catch 和 to() 方法。我们讨论了为什么你可能想使用其中一种而不是另一种,以及为什么错误处理很重要。
Amazon Lex
学习了 Amazon Lex 之后,我们开始构建基于文本的聊天机器人。我们看到了 Lex 和 Alexa 之间的相似之处和不同之处,并基于我们的现有知识创建了我们的第一个 Lex 聊天机器人。
我们了解到,每个意图都可以返回硬编码的响应或触发 Lambda。能够从每个意图触发不同的 Lambda 允许我们创建许多非常定制的 Lambda,以完成我们想要的确切操作。
当我们从 Lex 触发 Lambda 时,它期望一个非常明确的响应格式。不幸的是,目前还没有 lex-sdk,所以我们自己构建了一个。我们看到了五种不同的响应类型,并为每种类型创建了方法。这使得我们能够更容易地创建所需的响应。
Dynamo DB
虽然 S3 对于存储大量可能不会经常变化的数据来说很棒,但它并不是存储经常变化的数据的理想选择。为了存储这类数据,我们学习了 DynamoDB。这是亚马逊的非关系型数据库,它使我们能够轻松地存储、访问和更新信息。我们使用它来存储用于在线商店的购物车。
我们创建了一个 Dynamo 类,它具有获取、写入、更新和删除这些 Dynamo 表的方法,这样我们就不必每次都编写长而复杂的代码。
发布 Lex 聊天机器人
如果你的用户无法访问它,那么能够构建一个令人惊叹的聊天机器人是没有意义的。我们学习了如何通过将我们的聊天机器人集成到 Facebook、Slack 和 Twilio 等平台上,利用这些平台庞大的现有用户基础。
我们还构建了一个 API 服务,使我们能够将我们的聊天机器人集成到更广泛的应用中。基于这个 API,我们为我们的聊天机器人创建了自己的前端界面。这很好,因为它让我们能够使其看起来和工作方式完全符合我们的期望。
高级功能
本书的前七章教我们如何通过使用其他服务如 S3、DynamoDB 和外部 API 来创建强大的 Alexa 技能和 Lex 聊天机器人。在第八章“提升您的聊天机器人的用户体验”中,我们探讨了 Lex 和 Alexa 内置的一些高级功能。
我们首先学习了如何为 Lex 创建信息卡片。这使我们能够向用户发送比之前发送的基本消息更多的视觉信息。添加这些卡片极大地提升了用户体验。
然后我们学习了 Alexa 中的短语槽位以及它们如何用于捕获那些选项过多而无法创建自定义槽位类型的信息。能够将如此广泛范围的输入捕获到槽位中,使我们的技能更加可靠和健壮。
我们最后学习的是 Lex 中的话语监控。这是我们可以查看 Lex 检测到的话语和它错过的话语的地方,这让我们了解了用户如何与我们的聊天机器人互动。这也提供了一个机制,可以轻松地将我们错过的话语添加到现有的意图中。
继续您的学习
现在您已经完成了这本书,您对语音和基于文本的聊天机器人有了很好的理解。您能够构建复杂的、多流程的聊天机器人,并集成其他服务如 S3、DynamoDB 和外部 API。如果您喜欢学习如何构建这些系统,那么您正处于继续您旅程的绝佳位置。
您的学习方向有很多,我将为您概述一些可能性——一些专门针对 Alexa 或 Lex,还有一些非常适合两者共同学习。
Alexa
如果您非常喜欢为 Alexa 构建技能,那么在学习上我会选择两个方向。
Amazon Echo Spot 和 Amazon Echo Show
Amazon Echo Spot和Amazon Echo Show是带有屏幕的 Amazon Alexa 设备。这意味着您可以为用户提供视觉信息以及语音响应。与 Lex 上的卡片一样,额外的视觉信息可以使用户体验更加丰富。
Echo 设备屏幕的一大优势是您可以向用户提供图片。仅使用语音向用户介绍产品可能会非常困难,但有了图片,用户体验会更加流畅。您还可以播放视频,进行幻灯片放映,或创建包含大量不同信息的自定义显示。
构建函数库
如果你喜欢构建 Alexa 技能并且想要开始构建更多,那么在构建多个技能时,你可能会想要使用相同的方法。有两种选择——每次都复制代码,或者创建一个方法 Lambda 库。第一种方法适用于少量技能,但随着技能数量的增加,这会变得令人烦恼。
第二种方法设置起来会花费更多时间,但会使构建未来的技能变得更加容易。这种设置的架构类似于 Lex 的工作方式,其中每个意图触发一个单独的 Lambda。不幸的是,目前这还不被支持,但我们可以使用 Lambda 调用函数来创建相同的效果。这让我们可以从我们的handlers对象中触发 Lambda。
这种方法的优点是,常见的意图可以触发相同的 Lambda,减少重复代码,同时独特的意图仍然可以在主处理 Lambda 中构建。
Lex
如果你想要学习更多特定于 Lex 的技能,那么最好的方向是学习如何将其集成到更多的服务中。
Lex 原生不支持数百种消息服务,能够将你的聊天机器人集成到这些服务中将会是一项非常棒的技能。你可以尝试将你的聊天机器人集成到 Telegram、Twitter、微信或其他任何消息服务中。为此,你可能需要将消息映射到该特定服务的正确格式。格式之间的映射可能相当复杂,但学习这项技能是非常有用的。
一旦你构建了一个将 Lex 集成到这些消息平台的机制,你就可以宣传你的集成或你能够构建一个集成到公司自己的消息平台的机制。许多公司都希望能够在现有的消息平台上添加聊天机器人。
Alexa 和 Lex
通过使用既适用于 Alexa 也适用于 Lex 的技能来继续你的学习,可能是你时间的最佳利用方式,而且有很多不同的方向可以探索。
改进构建过程
随着你构建更多的 Alexa 技能和 Lex 聊天机器人,你可能会对不得不打开 Alexa 技能套件或 Lex 控制台来添加新的语音或更改意图感到沮丧。幸运的是,我们有aws-cli和ask-cli可以使用,这样我们就可以在不上网的情况下构建和更新我们的技能和聊天机器人。
你可能还记得第二章中提到的aws-cli,AWS 和 Amazon CLI 入门,我们用它来允许我们从本地机器构建 Lambda。你也可以使用aws-cli为 Lex 聊天机器人做类似的事情,而ask-cli为 Alexa 技能提供了类似的功能。对于这两个系统,学习曲线相当陡峭,你最终会阅读大量的文档,但能够在不使用浏览器的情况下构建新的聊天机器人或技能是非常有用的。
你可以选择将聊天机器人或技能的完整结构保存为电脑上的文件,或者你可以创建一个系统,根据更简单的配置文件生成这些文件。后者的优点是配置文件应该更容易阅读和理解,这使得确定需要更改的内容变得容易得多。
一旦你建立了这个系统,你应能在几分钟内为一个新的机器人创建一个新的配置文件。如果你使用这个系统,没有任何阻止你仍然使用在线的 Alexa 技能套件或 Lex 控制台来检查、编辑和更新你的技能和聊天机器人的。
如果你正在考虑将构建聊天机器人或 Alexa 技能作为一项工作,那么这个工具将非常有价值。
集成更多 AWS 服务
在这本书中,我们学习了如何使用 S3 和 DynamoDB 来提高我们的技能和聊天机器人的功能。目前有超过 100 个 AWS 服务,其中一些可以用来为你的聊天机器人添加更多功能。
这里有一些服务集成的想法:
-
对于数据库存储,可以使用 Amazon Redshift 或 Amazon ElastiCache,采用不同的方法。
-
Amazon Cognito,允许用户登录以访问现有的订单和聊天,或提供与用户信息相匹配的结果
-
Amazon Transcribe 和 Amazon Simple Email Service,在用户与 Alexa 聊天时发送包含他们所说内容的电子邮件
随着可用服务的数量,你可以用 Alexa 和 Lex 构建和做的事情仅限于你的想象力。
集成其他 API
在线可用的 API 数量令人难以置信!仅仅浏览一份顶级 API 列表就能给你一些惊人的想法。比如一个你可以询问特定产品的聊天机器人,它会搜索 eBay 上的这些产品,让你可以在聊天中出价,而无需离开聊天!还有一个人口普查 API,可以用来构建一个 Alexa 技能,你可以用它来了解美国任何地区的人口、就业统计、经济、新房数量等等。
如果你正在寻找自己聊天机器人的想法,我强烈建议查看可用的 API 以及你可以用它们做什么。你可能会在一个 API 上找到一个功能,它可以与另一个 API 上的功能结合,创建一个功能强大的聊天机器人。
聊天机器人的未来
聊天机器人在过去十年中取得了长足的进步,现在通常通过 Amazon Echo 和 Google Home 在家庭中使用。技术上,它们取得了飞跃性的进步,AI 和机器学习的改进带来了更好的语言理解,以及语音到文本技术,这些技术为 Echo 和 Google Home 设备提供了动力。
我预计聊天机器人的增长将继续,我们将开始看到它们在广泛的领域中应用,并通过各种设备使用。随着它们的改进,它们将被信任执行越来越复杂和重要的任务,并将彻底改变许多行业。例如,客户服务行业已经在改变,多个银行和零售网站以及电话系统上已经有了聊天机器人。
语言理解
要能够正确地回应某人,你需要理解他们在说什么,这样你才能构建正确的回应。随着机器学习的采用,这已经得到了很大的改善,但远非完美。
可能出现的一个问题是存在两个具有相似表述或相同关键词的意图。当用户说一个相似的表述时,它会与两个意图相似地匹配,因此无法选择触发哪一个。一个具有“什么时候足球比赛?”表述的意图和另一个具有“足球比赛在哪里?”意图的聊天机器人很可能感到困惑,无法处理请求。
另一个问题可能是拼写错误和打字错误。Lex 目前似乎能够很好地处理打字错误和拼写错误,但确实有几次这些错误导致了问题。
随着机器学习和语言理解的提高,我预计这些问题将会减少。
与语音交互工作
就像语言理解一样,能够响应用户的请求意味着能够理解他们所说的话。在语音系统中,这涉及到将语音声波转换为文本。虽然如果你说话清晰且口音中性,这可以工作得非常好,但当人们说话很快或带有浓重口音时,通常会有问题。
当文本是由口音浓重的用户生成时,它往往会被误解,产生的文本也没有意义。这意味着当它被传递到语言理解系统中时,语音无法与意图匹配。这对那些无法与这些设备交互的口音浓重的用户来说可能非常令人沮丧。这是在基于语音的聊天机器人成为商业应用中常见之前需要克服的一个重大障碍。
改进的设备交互
能够通过基于语音和文本的对话与越来越多的设备和系统交互,这是聊天机器人融入我们日常生活扩展的关键。好事是,可以将 Alexa 软件安装到 Raspberry Pi Zero 上,这是一个价值 10 美元的计算机芯片。这意味着将语音交互添加到任何设备都可以既便宜又相对简单。现在已经在汽车、智能镜子、智能桌面上看到了 Alexa 的集成,还有更多。
我认为聊天界面将在可穿戴设备领域显著增长的另一个领域是。蓝牙免提系统正变得越来越小,更加隐蔽,它们可以非常容易地集成语音聊天系统。在一天中的任何时刻,你都可以询问 Alexa 天气或当天的会议,并立即得到回应。这将克服一些人对语音系统将他们的回应投影给房间内每个人都能听到的安全担忧。
带内置语音和文本聊天机器人的智能手表将提供另一种我们将看到聊天机器人融入我们生活的方式。与耳塞相比,手表的优势在于它们有屏幕,允许聊天机器人显示视觉媒体或向用户展示信息,而无需全部说出来。查看天气比听下五天的天气预报要方便得多,而你只关心下周三的天气。
我能想象在不久的将来使用聊天机器人的最后一款可穿戴设备是智能眼镜。类似于谷歌眼镜的眼镜将允许你以与智能手表相同的方式接收视觉信息,但你甚至不需要低头看手腕。将聊天机器人添加到这种增强现实系统可能会非常强大。
聊天机器人能够集成到可穿戴设备中最强大的方式可能是多个系统的集成。使用耳塞进行基于语音的聊天,但使用智能手表或智能眼镜来显示视觉信息,将结合两者的优点。
连接的设备
在聊天机器人变得普遍之前,需要克服的第二个障碍是能够连接到这些智能家居系统或远程控制解决方案的设备数量。你现在可以得到智能开关、咖啡机和甚至可以通过聊天界面控制的门锁。
在未来,我预计将会有越来越多的设备配备类似的控制系统。我可以想象一台洗衣机,你只需通过和 Alexa 对话就能设置并启动它,最终甚至是一个厨房,其中每个设备和电器都可以通过语音控制。想象一下告诉 Alexa 将烤箱调至 180 度,并在达到正确的温度时通知你,而你正坐着看电视,然后准备烤火鸡并让 Alexa 打开烤箱,以便你直接放入。烤箱随后可以称重火鸡,并在准备烤土豆前 50 分钟设置提醒。
除了集成聊天机器人的家用电器之外,我预计在未来 10-20 年内,商业中的聊天机器人数量将增加。银行柜员可能会变成一个由基于语音的聊天机器人驱动的动画人物屏幕。你可以从控制机器人熟食店的聊天机器人那里购买新鲜肉类和奶酪。这些机器人可以以与当前人类工人完全相同的方式工作,询问布里奶酪轮是否足够大,同时保持刀的位置。
除了商业应用之外,我还预计公共服务信息将开始整合聊天机器人。你到达购物中心停车场,想要找到特定的商店。你只需走到一个信息标志前,询问商店在哪里,它就会在地图上显示方向,告诉你如何到达那里,甚至可以将方向发送到你的智能手表或智能眼镜。
独特的基于语音的系统
为了推动刚刚提到的集成和设备,需要改变基于语音的聊天机器人的构建方式。如果你想构建一个处理用户语音的系统,那么你的两个主要选择是构建一个 Alexa 技能或一个 Google Home 动作。这对于集成到已经运行这些系统的设备来说很棒,但公司不会希望将 Alexa 作为你的银行柜员聊天机器人系统。
随着能够创建无需在 Alexa 或 Google Home 上运行的自定义语音聊天机器人系统的出现,市场需要发生变化。目前,这通过 Amazon Lex 成为可能,因为它已被构建来处理语音交互,但我希望看到能够执行此操作的系统范围的扩大。
通用人工智能
当前聊天机器人存在的大多数问题将逐渐得到解决,它们的性能将逐步提高,但下一个重大进步将是通用人工智能的创建。
通用人工智能是一个概念,即一个系统可以处理任何请求。这听起来可能并不遥远,比如 IBM Watson 正在构建一个可以统治 Jeopardy 和其他问答游戏的系统,但能够回答简单问题只是挑战的一部分。
问题始于系统必须确定它需要哪些其他信息来满足请求以及如何请求这些信息。如果有人让你找到他们的班级毕业照片,你可能会问他们上了哪所学校以及哪一年毕业。你利用自己对班级照片的知识来决定你需要询问学校和年份才能准确找到他们的班级照片。我们的大脑在这些任务上非常擅长,但构建一个能够为现在或未来的每一个可能的请求解决这个问题的人工智能系统是一个艰巨的任务。为每一个可能的问题构建意图是不可能的,因此系统需要收集它关于该主题的信息,确定它还需要了解什么来回答问题,然后以人类的方式请求这些数据片段。
另一个问题是与外部系统的集成。在这本书中,我们使用 API 来访问第三方存储的数据。为了使用这些 API,我们需要一个 API 密钥,即使如此,我们也只能访问通过 API 提供的数据和功能。如果我们想创建一个聊天机器人,它能帮我们完成每周的超市购物,送货上门并付款,我们就需要获得一个允许我们完成所有这些的 API。创建这样的 API 是大多数超市不会考虑的事情。
在我的工作中,将聊天机器人集成到客户的现有系统中是一个主要的难题,以确保其功能正常。拥有一个可以访问世界上每个 API 的通用人工智能系统是不现实的,即使如此,也有一些系统没有通过 API 公开。
改善人们的看法
提高人们对聊天机器人的接受度的一个主要障碍是改善人们对聊天机器人的看法。当聊天机器人最初出现时,它们的功能非常有限,无法处理许多话语的变化,并且往往证明比有用更令人沮丧。现代聊天机器人已经取得了很大的进步,但仍然有许多老旧的系统使用起来非常令人沮丧。即使现代聊天机器人系统也有其局限性,正如我们之前讨论的,它们仍然可能因为意图识别错误或语音理解不当而让用户感到失望。
随着系统的改进,更好的系统将拥有更好的用户留存率,而老旧的系统将被取代。我预计人们关于聊天机器人的看法将持续改善。随着像 Alexa 和 Google Home 这样的系统在家庭中变得越来越普遍,年轻一代将在与聊天机器人的互动中成长,与之互动将变得像呼吸一样自然。
摘要
这本书通过构建越来越复杂的 Alexa 技能和 Lex 聊天机器人,为我们提供了一个实用的聊天机器人入门介绍。我们学习了从完美的对话开始,并创建流程图来可视化用户与聊天机器人的对话路径。利用这些流程图,我们通过使用语句和槽位构建了意图,这些语句和槽位在 Lambda 中处理。
我们通过使用 S3 存储、DynamoDB 数据库和外部 API 来改进了聊天机器人的功能和能力。为了提升用户体验,我们还学习了如何使用 SSML 来改变 Alexa 与我们的用户交流的方式,学习了如何创建卡片以提供更多视觉信息,以及学习了在 Alexa 中关于搜索查询槽位类型的内容,以便收集更广泛的槽位值。
最后,我们讨论了几种构建在本书所学内容之上的优秀方法,以及我们对聊天机器人未来发展的预期。
附录 A
第一章
-
意图、槽位和话语
-
以下任意两个:
-
它们都是亚马逊服务
-
它们都是聊天机器人
-
它们都使用自然语言理解/NLU/NLP
-
-
以下任意两个:
-
Alexa 使用语音交互,而 Lex 可以通过语音或文本触发
-
Alexa 使用技能,而 Lex 可以应用于许多应用
-
Lex 可以被其他服务触发,而 Alexa 只在 Alexa 设备上工作
-
-
你应该从一个完美的对话开始,然后在此基础上构建
-
语气是指你的聊天机器人使用的单词和短语,并确保它适合用户
-
错过的话语、外部 API 错误以及你代码中的错误
第二章
-
在 AWS 上使用 Lambda 控制台、使用 Cloud9、在本地开发环境中
-
亚马逊网络服务
-
没有简单的方法来部署或更新 Lambdas,并且难以在多台机器或作为团队的一部分工作
-
AWS-CLI、Bash/构建脚本、Git 和 GitHub/Bitbucket
第五章
-
alexa-sdk. -
所有三个:
-
将 Lambda ARN 复制到技能端点默认区域
-
将技能 ARN 复制到 Lambda 代码中的
exports.handler函数设置 -
将 Alexa 技能套件作为 Lambda 的触发器,并将技能 ARN 复制到设置中
-
-
s3.getObject(). -
你必须执行
JSON.parse(data.body),因为回复的正文是以缓冲区形式发送的,因此需要将其转换为可用的 JSON 格式。 -
点击 Lambda 控制台中 Test 旁边的下拉菜单,并选择配置测试事件。然后你可以创建一个新的测试或修改现有的测试。
第六章
-
API 是 应用程序编程接口,它允许从外部来源访问程序的功能。
-
axios是一个基于承诺的库,而HTTP是基于回调的。 -
使用
try/catch和to函数方法。 -
this.attributes.colour = colour. -
目前,只有字符串可以存储在会话属性中。
-
你可以使用 SSML 来改变 Alexa 说话的方式。这可以改变语调、强调、添加停顿或进行一些其他更改。
第五章
-
是的,你可以在 Lex 控制台的意图响应部分创建响应
-
你需要在响应中用大括号将槽位的名称括起来
-
Lex 可以为每个意图使用一个 Lambda,而 Alexa 使用单个 Lambda 处理所有请求
-
5
-
ElicitSlot、ElicitIntent、ConfirmIntent、Close、Delegate -
`S3.getObject()
第六章
-
一个复杂的流程由用户可以采取的许多不同路径组成,而一个简单的流程通常只有一个用户可以采取的路径。
-
我们可以将流程图分解成部分。这些部分可以连接到其他部分以创建完整的流程。
-
ElicitSlot、Close、ElicitIntent、ConfirmIntent和Delegate。 -
DynamoDB 文档客户端。
第七章
-
我们希望集成到其他平台和服务中,以便让我们的用户更容易访问我们的聊天机器人。与它们已经使用的服务集成,使访问我们的聊天机器人对他们来说更容易。
-
API 网关和 AWS Lambda。
-
资源和方法。
-
我们需要将 API 部署到预发布环境中。
-
HTML 文件、JavaScript 脚本和 CSS 文件。
-
当脚本文件加载时,它应该检查 DOM 是否已完全加载。如果没有,那么它应该在运行任何需要操作 DOM 的功能之前等待。
第八章
-
卡片是向用户提供更多信息的好方法,而不需要大量文本。添加卡片还允许你提供按钮,让用户有选择如何响应的选项。
-
不,所有属性都是可选的,但建议添加标题和副标题。
-
当用户可能输入大量不同值时,你会使用搜索查询槽位类型。创建一个包含所有值的槽位会花费太长时间,并且仍然会错过许多可能的值。
-
是的,但你不能在同一句话中填充搜索查询槽位和另一个槽位。
-
在 Lex 中,监控标签页有一个菜单,显示所有未识别的意图。如果需要,可以查看并将它们直接添加到意图中。
附录 B
调试
尝试找出为什么你的代码不起作用可能是一个非常令人沮丧的过程,并且可能存在大量不同的问题来源。在这本书中,我们主要有三个问题来源:Lambdas、Alexa 技能设置和 Lex 设置。
调试 Alexa 技能
如果你的技能没有起效,那么你需要检查以下几点:
-
你使用的是正确的表述
-
你的模型已保存和构建
-
你的端点设置正确
-
你已将 Alexa 添加为 Lambda 的触发器
-
你的 Lambda 运行正常
检查表述
如果 Alexa 回复“对不起,我不知道那个”,请确保你说的或输入的是正确的短语。应该是“Alexa,告诉你的技能调用 你的意图表述,”,所以可能是“Alexa,告诉我的技能你好”或“Alexa,询问汽车助手我应该买什么车。”请检查你是否已正确设置你的技能调用和意图表述。你可以添加更多表述到你的意图中,以便它能够与更多种类的短语一起工作。
你也可以在你的处理程序中添加控制台日志,以确保它们在你期望的时候被触发。
保存并构建你的模型
检查你是否已保存并构建了你的模型;保存模型和构建模型按钮应该是灰色,而不是蓝色。重新保存和重建你的模型以确保一切是最新的没有害处。
检查你的端点
在 Alexa 技能套件中最后要检查的是端点是否正确设置。确保默认区域框中有一个长的 ARN 代码。如果那里没有,那么打开你的 Lambda 控制台,找到你想处理这个技能的 Lambda。从屏幕右上角复制 ARN 号码,并将其粘贴到 Alexa 技能套件的默认区域框中。它应该具有以下格式:arn:aws:lambda:your-region:123456789012:function:your-lambda-name。
已将 Alexa 添加为 Lambda 的触发器
如果你的技能仍然不起作用,那么可能是因为你的 Lambda 存在问题。在 Lambda 编辑器中,确保 Alexa 在设计图中正好如以下所示。如果 Alexa 技能套件不在该图中,请从左侧的触发器列表中选择它,并遵循本章前面解释的设置步骤:

Alexa 触发
如果显示 Alexa 技能套件,但有一个完成配置或类似的消息,那么你需要完成它。点击 Alexa 技能套件符号并完成配置,如本章前面所述。
调试 Lex 聊天机器人
与 Alexa 技能一样,有几个不同的地方可能导致你的聊天机器人出现错误。以下是一些你需要检查的事项:
-
你使用的是正确的表述
-
你的意图有一个文本响应或正在触发正确的 Lambda
-
你的意图已保存,聊天机器人已构建和部署
-
你的连接平台已正确配置(Facebook、Slack、API)
-
你的 Lambdas 运行正常
检查你的表述
如果 Lex 表示它无法理解你所说的话,那么它就无法将你的语句与意图匹配。这种情况可能有两种发生方式:你没有足够接近你使用的语句的样本语句,或者有两个样本语句在不同的意图上匹配得很好。具有相似样本语句的不同意图通常会导致问题。
检查意图响应
你的意图可能被触发,但没有返回响应。在意图中向下滚动到“Fulfillment”,确保正在调用 Lambda 或你正在回复文本响应。如果你正在调用 Lambda,请检查 Lambda 日志以查看是否在触发该意图时 Lambda 正在运行。如果没有,你可能只需要保存、构建聊天机器人并再次部署。
保存意图,构建和部署
如果所有样本语句和履行都是正确的,那么你应该确保每个意图都已保存,构建你的聊天机器人,并将其部署到与之前相同的别名。很容易忘记这一步,然后 wonder 为什么所有的更改都没有产生任何影响。
检查你的连接平台
如果你正在尝试将你的聊天机器人连接到 Facebook、Slack 或你的 API 等平台,请确保它们已正确设置。如果你能在 Lex 控制台中成功测试你的聊天机器人,那么请回过头来检查平台的设置流程,确保你没有错过任何步骤。
检查你的 Lambda 是否正在运行
使用 Lambda 调试技巧确保你的 Lambdas 正确响应。
Lambda 调试
如果你发现问题不是出在你的 Alexa 技能或 Lex 聊天机器人上,那么可能是在你的 Lambda 上存在问题。在你开始更改代码之前,通常一个好的做法是在 Lambda 控制台中创建一个测试。这应该代表 Alexa 或 Lex 发送的请求。使用这个测试来查看是否是 Lambda 出了问题,或者它是否按预期工作。
如果你的测试失败,请在日志中查找错误消息。这通常可以指向问题的根源。
在尝试调试 Lambda 时,以下是一些需要检查的事项:
-
你的 Lambda 已安装所有必需的包
-
你的权限是正确的
-
所有你的变量都已被正确定义
-
你的 Lambda 代码是正确的
-
你 Lambda 调用的所有内容都在运行
安装所有必需的包
如果你的 Lambda 不工作,你需要检查所有包是否已安装。前往你的Lambdas文件夹并导航到你的问题 Lambda。在其中,应该有一个名为 archive.zip 的压缩文件。如果没有,再次运行构建脚本,应该会出现 archive.zip 文件——你的 Lambda 现在应该可以工作了。
如果有一个 archive.zip 文件,打开它并查看其内容。应该有一个 index.js 文件,一个 package.json 或 package-lock.json 文件,以及一个 node_modules 文件夹。如果这些内容中的任何一个缺失,那么重新运行构建脚本并确保它成功执行。
如果所有这些都正确,那么请检查 package.json 或 package-lock.json 文件,确保您正在 Lambda 代码中要求的所有包都有依赖项。检查 node_modules 中每个包是否都有一个文件夹。如果没有,您需要运行 npm install --save *PACKAGE-NAME* 将其添加到 package.json 文件中。
检查您的权限
进入您的 Lambda,向下滚动到执行角色,查看您选择了哪个角色。现在,转到 IAM 服务并选择您使用的角色。查看您添加到该角色的策略,并确保您拥有所有需要的权限。为该角色添加任何需要的策略。
修正您的 Lambda 代码
您还需要检查 Lambda 中的代码是否正确。随着您的 Lambda 变得越来越复杂,第一次使其工作变得更加困难。运行您创建的测试,并查找错误信息。
如果您不知道错误信息的意思或如何修复它,请在网上搜索,因为之前肯定有其他人遇到过这个错误。确保您阅读了关于如何修复错误的回复,并在将其用于代码之前理解为什么那个解决方案有效。这有助于您成为一名更好的开发者,并允许您在未来如果出现错误时进行修复。
如果错误信息中没有明显的错误,您可以使用 console.log('Some information') 在代码到达每个阶段时记录信息。使用这种方法来确定代码执行到了哪个阶段以及在该点可用的值。常常是变量或其他服务的响应格式与您预期的不同。在错误发生之前能够看到所有数据对于找出代码为何不工作非常有用。
检查外部服务
当与外部服务一起工作时,总有可能这些服务停止工作或更改它们的响应。每次您处理外部服务时,都应该处理它,如果它返回错误。如果服务看起来似乎在工作,但您的代码没有工作,那么请使用 console.log 输出响应以检查格式。


浙公网安备 33010602011771号