本章内容包括:
- 探索流行的聊天机器人应用
- 理解生成式聊天机器人的优缺点
- 通过信息检索增强生成式聊天机器人
- 设计对话界面以改善用户体验
- 监控、评估并优化你的聊天机器人
你现在已经掌握了所有在软件中加入对话功能所需的自然语言处理(NLP)工具。在本章中,你将学习如何为你的软件项目添加半智能的对话功能,无论是构建网站、移动应用,还是为你的业务或个人生活设计内部工具。希望你能想到一些比目前在商店网页上看到的聊天机器人更聪明、更有帮助的创意。例如,Amazon Alexa的创建目的是让亚马逊购物体验变得更加轻松,从而提升亚马逊的利润。本书的主要目标之一是帮助你利用人工智能的力量,而不是让大公司利用你为其谋取利益。你甚至可能能够构建聊天机器人,成为你的信息保镖。你在本章构建的聊天机器人可以保护你免受黑暗设计的影响,并或许能让你重新获得时间和注意力,去创造一些了不起的东西。在本章中,你将学习如何构建你自己的聊天机器人!
聊天机器人是一个与人类进行来回对话的计算机程序,通过自然语言进行交互,无论是通过文本还是语音。聊天机器人技术已经超越了你可能熟悉的“固定回复”系统。早期的聊天机器人通常用于客户反服务,而且不太复杂。它们经常被用于企业和银行的自动电话菜单中,旨在减少支付人工帮助的成本。构建聊天机器人的工程师们常常被鼓励设计一种将障碍设置在人类与所需服务之间的系统,比如获得人工帮助、退款或取消账户。当短信聊天机器人登场时,大多数仍然延续这种不合作的黑暗设计,考验你的耐心,阻止你为企业创造额外成本。你现在所学的更先进的NLP技能,让你有能力构建能模拟智能对话并为你和你的组织做有用工作的聊天机器人。
聊天机器人的热潮还没有结束,你将学习到它们如何在生活或业务中发挥作用。
12.1 聊天机器人无处不在
聊天机器人无处不在。以下是一些例子,帮助你构思自己的项目:
- 虚拟助手——Dicio(Google Assistant)、Lyra(Siri)和Mycroft(Alexa)可以帮助你完成一些小任务,例如查看天气、给朋友打电话、启动应用程序、设置提醒、播放音乐或开灯。
- 娱乐——视频游戏和推广电影的网站中的聊天机器人常常用于让你参与虚构的故事情节。你可以通过用户愿意与其互动的时间长度以及他们多频繁地暂停对话对象为人的怀疑来衡量娱乐性聊天机器人的表现。
- 医疗——根据你所在国家的法规,聊天机器人通常可以回答你的健康相关问题,为你安排预约,甚至提供初步诊断。心理健康聊天机器人,如Woebot和Wysa,甚至提供治疗性练习,有助于减少抑郁和焦虑。
- 影响力——非营利组织和社会企业使用聊天机器人帮助有需要的人。他们通常利用流行的消息渠道,如SMS和WhatsApp,接触到服务不足的社区,在这些地方,手机消息是他们上网的主要方式。像Rori和Farmer.Chat这样的机器人帮助人们获得更好的医疗、教育和农业指导。
- 运营(ChatOps) ——企业常常使用聊天机器人提高团队生产力和工作满意度。你可以构建与Telegram或WhatsApp互动的聊天机器人,帮助你监控和控制软件。如果你运气好,你的老板可能还会用聊天机器人来引导和培训你,甚至在你帮助队友学习新东西时公开表扬你。
- 广告与销售——企业网站上的搜索引擎通常使用聊天机器人引导你购买他们希望你购买或推广的广告和产品。在幕后,这些机器人常常用来分散你的注意力并在社交网络上吸引你。
- 客户服务——机器在客户服务呼叫中心和聊天信息接口中已经替代了人工服务几十年——但并非总是成功。实际上,聊天机器人至少部分原因是因为客户在努力绕过机器人门卫与真人交谈时而产生了不满,从而声誉不佳。然而,它们也可以节省客户的等待时间,并减轻支持人员反复指导用户完成简单任务的负担,这些任务可以轻松自动化。
聊天机器人还被用于利用我们的亲社会本能,使我们更容易被追踪和控制。你有没有注意到本书中许多脚注中的archive.is域名?我们使用了Internet Archive Wayback Machine来替换那些正在被社交媒体巨头和内容发布商积极审查的文章链接。中国法律强迫游戏制作商使用自然语言处理(NLP)来防止提到西藏或香港。那些审查公共媒体的政府和企业正在腐蚀那些最小心的NLP工程师使用的训练数据集,这些工程师通常依赖高质量的在线百科全书进行训练。即使在美国,也有一些公司、政治家和政府机构使用聊天机器人影响公众对大流行、气候变化以及《21世纪的21课》中描述的其他紧迫问题的讨论。聊天机器人甚至被用来影响你对人工智能和自然语言处理本身的看法。
本书的作者创立了Tangible AI,帮助非营利组织、政府和个人创作者构建高影响力的善用聊天机器人。影响力聊天机器人帮助服务不足的社区,从美国的新移民到全球南方的青少年。我们构建了聊天机器人,帮助人们学习数学、克服冒充者综合症、学习新语言、避开人贩子,甚至在发展中国家开始小生意。Tangible AI的贡献作者兼志愿者Vishvesh Bhat甚至创办了自己的初创公司,建立了一个帮助美国大学生学习和推理课程材料的聊天机器人。接下来,你将学习如何构建对你的社区或企业产生积极影响的聊天机器人。
12.1.1 不同的聊天机器人,相同的工具
尽管本节中的聊天机器人示例看起来各不相同,但它们都使用了你在本书中学到的相同自然语言处理(NLP)工具和技术。所有前面的章节都在帮助你积累技能和工具箱,以便你可以构建一个聊天机器人。以下是你已经学到的一些NLP技能,它们将帮助你构建聊天机器人:
- 第6章——将单词和短语嵌入到语义向量中,以识别聊天机器人用户的意图
- 第8章——使用LSTM创建更有意义的聊天消息嵌入向量
- 第9章——通过语言翻译,帮助用户以其母语与聊天机器人进行互动
- 第10章——使用语义搜索和自动文本生成来回应聊天消息,而无需手动编写回复
- 第11章——从文本中提取现实世界实体之间的关系,帮助聊天机器人推理用户的请求并保持对话上下文
图12.1展示了这些元素如何协同工作以创建一个聊天机器人。
在你开始使用所有这些工具和库构建聊天机器人系统之前,你需要考虑你希望聊天机器人谈论什么。你需要设计一个对话。
12.1.2 对话设计
随着聊天机器人技术在过去十年间越来越流行,对话设计领域也随之兴起。对话设计是UI设计的一个分支,专门处理设计引人入胜的对话。本节将帮助你入门,若你准备深入学习,可以查阅更详细的资源,例如Andrew Freed的优秀书籍《Conversational AI》。
对于每个聊天机器人项目,你将经历五个阶段:
- 定义聊天机器人的目标和它解决的问题。成功的标准是什么?你如何知道聊天机器人做得好?
- 考虑你的用户。谁会受益于使用你的聊天机器人?他们需要什么?用户在使用聊天机器人时会处于什么环境?是什么促使他们参与对话?
- 草拟一个用户和聊天机器人之间的虚拟对话。这称为“幸福路径”或“幸福对话”。你甚至可以和同事或朋友一起“演练”这个对话。
- 绘制对话树。在草拟了几个幸福对话后,你将注意到一些可以概括的模式,从中创建对话图——一个显示用户和聊天机器人之间几种可能对话的流程图。
- 选择你的NLP算法。从图12.1中的算法中选择你或你的团队将实现的软件,使聊天机器人能够在对话树的每个分支处生成响应。
以数学辅导机器人为例。它的目的非常明确:教导中学生数学。然而,当你在第二步开始考虑用户时,你会意识到不能假设孩子会是第一个联系你机器人的人。这是Rori项目在低收入国家遇到的问题,因为在那里,年幼的孩子很少拥有手机。你的年轻用户可能会经常借用他人的手机或电脑,因此你的聊天机器人可能无法向用户的手机发送作业提醒或其他推送通知。
处理儿童时,还需要考虑的一个重要事项是,你必须获得父母或监护人的同意,才能让聊天机器人与孩子直接互动。你需要遵守聊天机器人将要使用的国家的所有儿童保护法律,包括强制报告用户的保护性披露。如果孩子提到他们正在遭受虐待或考虑自残,你将希望检测到并报告这些披露。不论你的聊天机器人的目标是什么,当用户表明他们可能处于危险中时,你将希望聊天机器人能够检测并将这些互动报告给你或相关当局。因此,你的数学辅导聊天机器人将需要一个意图分类器,可以对用户发送给聊天机器人的消息进行分类。开源项目MathText和MAITAG提供了预训练的多标签分类器和用于意图识别的标记数据集,包括Rori项目所需的意图。
你还需要让你的聊天机器人与孩子建立融洽关系。融洽关系和信任是教育和学习工程过程中的关键元素。Rori通过询问并记住孩子的名字、询问他们的年级,并可能定期讲一些涉及数学的适龄笑话来建立融洽关系。如果你问学生每周想练习多少次数学,然后再提醒他们(或他们的父母)下次可以练习的内容,这会增加学生的动力。因此,你将需要信息提取,如命名实体识别(参见第11章),以及字符串模板系统,如Python的内置f-strings或jinja2包。你可以在ConvoHub包中找到实现jinja2模板和信息提取插件的Python代码,处理诸如名字、数字和数学表达式等内容。
最后,聊天机器人需要做的一件事是帮助学生选择适合他们技能水平的课程。各个年龄段和能力的学生都可以使用Rori来学习数学,因此,聊天机器人需要有涵盖广泛数学技能和知识的内容,如练习和课程。任何教育类聊天机器人都需要有让学生能够从过难或过易的课程中跳出,转向能帮助他们学习所需内容的课程的功能。一旦你有了聊天机器人的目标、想要帮助的用户以及草拟的幸福路径对话,你就可以准备绘制整个聊天机器人的流程图(对话设计过程的第4步)。
12.1.3 你的第一个对话图
引导过程可能是你为任何聊天机器人设计的第一个环节。对于Rori,你可以想象,你可能希望构建一个类似于图12.2的引导流程。这只是整体对话设计中的一小部分,但它会让你了解大多数聊天机器人在对话开始时会涉及的一些关键元素。
对话流程图类似于你可能熟悉的其他流程图。每个块代表一个系统状态,在这个状态下,它会等待处理来自系统外部的新输入。对于聊天机器人来说,状态块包含聊天机器人在对话每个阶段所说的话。如果某些消息或操作不需要用户交互,你也可以在每个块内包含多个消息或操作。对于通用型聊天机器人,操作可能包括直接回复用户、检索文档、安排会议或在网上搜索某个信息。对于像Rori这样的数学辅导机器人,你可能希望包含安排作业提醒或推送的操作。
对话图中的线条(箭头)显示了用户可能在回应聊天机器人消息时所说的内容。这些是触发对话系统转换到新状态并继续对话的用户意图。当你进入对话的初始引导阶段之后,你会发现可能需要一些额外的工具来帮助你创建对话计划。你可能会发现,在引导阶段设计聊天机器人应该说的内容相对简单,你可以限制学生提供的选项数量。然而,当你进入详细的课程和练习时,你可能会发现很难预见到用户可能对聊天机器人说的所有内容。这在你的用户是处于陌生文化中的年轻学生时尤其具有挑战性,比如Rori服务的非洲中学生。
在Rori项目中,我们发现学习数学的学生在测验和练习中回应聊天机器人的方式非常有创意。此外,课程中有超过一千个小的课程需要设计、调试和维护。为此,我们构建了一个开源的Python包,可以自动将老师想象中的对话转换为可工作的聊天机器人,而无需任何人手动构建对话图。这使得教师能够更轻松地编写与学生的幸福路径对话,然后自动构建可以由任何聊天机器人平台运行的对话设计(对话图),例如ConvoHub,它允许你导入电子表格、JSON文件或YAML文件。比较图12.2和12.3,看看你是否能看到对话引擎需要的新标签,以便执行你的对话计划。
图12.3右侧的对话图展示了从数学教师与学生之间的对话记录生成的对话图。在对话记录中,学生多次猜错答案,最终才答对,这导致生成的对话图中出现了一个循环,其中教师机器人需要重复问题。你在图中看到的标注在箭头上的数字是学生可能发送给辅导机器人以回答问题的自然语言文本消息的一些例子。由于对话图代表了对话引擎将执行的实际代码,以决定下一步该说什么,因此你可以使用一些特殊符号来表示机器人需要做出的一些决策和操作。在此图中,带有双下划线(__)的标签,类似于Python的隐藏系统变量,并不与任何用户消息或意图相关,而是代表机器人将执行的特殊操作。标签“next”告诉对话引擎在不等待用户回应聊天机器人上一个消息的情况下,直接进入下一个状态。标签“default”是每个需要用户回应的聊天机器人消息所需指定的回退意图。回退意图告诉对话引擎,当用户说出你在对话设计中没有预见到的话时应该怎么做。
现在你已经理解了对话设计过程和对话流程图的结构,你可能会想知道如何确保你的设计是一个好的设计。你希望你的聊天机器人成为一个好的对话者,为此,你需要再次思考你的目标:对你和你的用户来说,一个好的对话是什么样的。
12.1.4 什么是好的对话?
与他人交谈是我们人类的自然行为。但当我们尝试编程让机器具备对话能力时,我们需要问自己,什么才算是一次好的对话。幸运的是,哲学家们在能够构建能够进行对话的机器之前就开始思考这个问题。英国哲学家保罗·格赖斯提出了合作原则——即有意义的对话是由参与者之间的合作所特征化的观点。
格赖斯将他的合作原则分解为四条准则——人们在进行有意义的沟通时遵循的具体理性原则:
- 数量——提供足够的信息。你的贡献应该尽量提供所需的信息,但不要超过所需的量。
- 质量——要真实。不要说你认为是错误的事情,也不要说你没有足够证据支持的话。
- 关联——要相关。省略任何与当前交流无关的信息。
- 方式——清晰、简洁、有条理。避免模糊或含糊的语言,不要太啰嗦,按照合理的顺序提供信息。
尽管这些原则是为人类设计的,但在设计聊天机器人和人类的对话时,它们尤其重要。原因有几个,第一个原因是人类对机器的耐心较少,对机器的宽容度较低。一些研究者甚至担心,与聊天机器人的长期互动可能会影响人类之间的互动方式。另一个原因是聊天机器人缺乏人类的智能,无法在违反这些原则时自行修正或澄清。
另一个好的标准来自于用户体验(UX)设计领域,直接借鉴了雅各布·尼尔森(Jakob Nielsen)的标准。他是第一个处理网页可用性的丹麦研究员之一。在他公司的一篇博客文章中,你可以阅读更多关于尼尔森原则及其如何适应对话设计的内容。在这里,我们只提及这些原则对聊天机器人设计的一些影响:
- 回合制——给用户时间和空间回复你的陈述或消息,与用户轮流进行对话,而不是主导对话。例如,Rori从不连续发送超过两到三条消息。这条经验法则同样适用于其他聊天机器人。
- 识别优于回忆——减少用户的认知负担,使其在不同场景间切换时更为顺畅。始终明确显示选项,并在对话中提醒用户之前的选择。例如,当用户在一段时间后再次使用Rori时,Rori会提醒他们上次互动时停止的地方。
- 容错与防错——让用户能够轻松从误解或错误中恢复,并继续朝着目标前进。更好的是,设计你的机器人时要考虑到防止错误。
聊天机器人最关键的元素之一是回退消息——当聊天机器人无法处理用户的最新输入时,发送的消息。当这种情况发生时,为了防止用户离开,仅仅表示聊天机器人无法理解是不够的。你需要提供一种方式让用户继续对话。这可以通过提供给用户选择的选项、建议聊天机器人的其他功能,甚至提供连接人工代表的选项来实现。
12.1.5 让你的聊天机器人成为一个好听众:隐性和显性确认
直到现在,我们主要讨论了聊天机器人应该如何传达它所要说的内容。然而,更为关键的是聊天机器人理解用户所说的内容的能力——以及确认它是否正确理解了用户的意思。你能发现下面对话中的问题吗?
人类:乔治·W·布什是什么时候出生的?
机器人:1924年6月12日
如果你了解一些美国历史,你可能会意识到机器人的回答是错误的。乔治·W·布什实际上出生于1946年7月6日,而1924年6月12日是乔治·H·W·布什(他的父亲)的生日。然而,问题更大的地方在于,用户无法意识到机器人误解了他们。
误解对方的问题并不是我们与聊天机器人对话时独有的。人与人之间的许多冲突也可以追溯到没有正确理解对方。这就是为什么人类发明了我们所称之为“积极倾听”的工具和技术。积极倾听中最重要的技巧之一叫做“复述”——用你自己的话重复对方说的内容。这项技术在辩论中尤为有价值——实际上,由数学家阿纳托尔·拉波波特(Anatol Rapoport)和哲学家丹尼尔·丹尼特(Daniel Dennett)设计的一套规则建议:“尽力用清晰、生动、公正的方式重新表达对方的立场,直到对方说,‘谢谢,我希望我能想到这样表达。’”
只要你的聊天机器人不是在与人辩论,你不必遵循如此严格的标准,但反映聊天机器人从用户请求中理解到的内容,仍然是至关重要的,尤其是当你的机器人基于该请求执行某些操作时。想象一下,如果你的虚拟助手为你购买了去佛罗里达州圣彼得堡的机票,而不是去俄罗斯的第二大城市。在对话设计术语中,这项技术称为确认,而实现确认有两种主要方式:隐性确认和显性确认。图12.4提供了隐性和显性确认的例子。
以我们的数学聊天机器人示例为例,当Rori识别到用户有停止对话的意图时(例如,用户说:“我明天再跟你聊,Rori。”),它会再次确认用户是否想要结束今天的对话。这允许用户确认或在聊天机器人“误解”用户时,重新进入聊天机器人的流程。
12.1.6 使用图形用户界面(GUI)元素
如果你曾与基于网页的聊天机器人互动,你可能注意到,自然语言并不是与它们对话的唯一方式。你可以使用按钮、菜单、画廊和其他GUI元素来帮助用户导航对话。一些聊天机器人服务甚至提供更高级的元素,例如通过聊天机器人内的日期选择器安排与专家的对话,或填写一个多问题的图形表单。你可以在图12.5中看到WhatsApp中的按钮界面示例。
然而,注意不要过度使用这些元素。来自聊天机器人分析公司Dashbot的研究显示,“人们喜欢聊天,而不是点击”——拥有超过50%按钮界面的聊天机器人比那些在使用GUI元素时更为适度的聊天机器人更少获得用户参与。
12.2 理解用户输入:自然语言理解
现在你已经知道如何设计一个好的对话,我们开始深入图12.1中的流程图,看看如何实际实现你创建的设计。你可以看到,流程图的顶部由NLU块占据——这是聊天机器人负责理解用户输入的部分。
这很有道理,因为我们刚刚学到的一切。对话的第一条规则是要成为一个好的倾听者。这是你能够提供符合保罗·格赖斯合作原则的回答的唯一方式。
12.2.1 意图识别
目前大多数聊天机器人并不是出色的写手或演讲者。它们无法为用户生成新颖有趣的文本。然而,它们依然有用。即便没有大规模语言模型(LLM)的强大功能,聊天机器人市场在过去十年里增长迅速。就像在现实世界的对话中一样,如果你是一个好的倾听者,你仍然可以与别人进行半智能的对话。如果你能够理解用户的意思,并通过适当的回应表明你理解了,他们会认为你很聪明。这就是所谓的意图识别——当你的NLP管道能够根据用户想要传达给聊天机器人的意图或意义对用户消息进行分类时。
意图识别是任何聊天机器人的最重要方面。它不仅帮助你选择正确的回应,还帮助你进行分析。如果你有用户说的内容的意图标签,你可以绘制关于最常见的意图类别或聚类的统计图。这可以帮助内容创作者决定接下来要做什么,当他们扩展对话树并创建新的对话线程时。每一个没有模板的新意图,都是扩展新分支并在对话图中添加新节点的机会。
意图识别对于保持对话的顺畅进行至关重要,因此对于某些聊天机器人框架来说,它是其主要卖点。例如,像“关掉灯”,“Alexa,关灯”,“灯光熄灭”和“请关灯”这样的用户发言都具有一个共同的意图——用户显然想要关灯。当聊天机器人接收到用户输入时,它会尝试找到与其已知意图的最佳匹配,并返回答案。
你可能会认为这与第一章中看到的模式匹配非常相似——确实是的!我们为聊天机器人预定义的意图类似于我们在模式匹配中定义的规则。然而,关键的区别在于,在这种模糊的方法中,你可以利用你在前几章中学到的机器学习分类器的力量。这意味着你不必为用户表达某一特定意图的每种可能变体预先做准备。例如,如果你教给机器学习模型,“Hi”,“Hello”,“Hey”和“Howdy”都表示“问候”这一意图,你可能不需要显式地教它识别“Heya”——聊天机器人很可能自己能识别出来。
你的聊天机器人如何决定选择哪个意图呢?你的意图识别模型会给你预编程进机器人中的不同意图分配一个置信度分数。那么最直接的方法是选择置信度分数最高的意图,但这种简单的方法并不总是能给出最好的答案。有几个特殊情况你需要处理:
- 当没有匹配项或所有匹配项的置信度分数非常低时,会发生什么?
- 当有两个意图与用户的发言匹配,且置信度分数非常相似时,会发生什么?
第一种情况会经常发生,并且对于帮助防止用户的挫败感非常重要——这就是我们在前一节中提到的回退回应。常见的解决方案是为置信度分数设置一个阈值,这样如果所有匹配的意图的分数都低于该阈值,聊天机器人就会表现得像是没有理解用户的意思。
槽位填充与变量
如果用户在发言中包含影响答案的信息该怎么办?例如,当用户问,“巴黎的天气怎么样?”或“下周日会下雨吗?”时,问题不仅传达了意图——了解天气——还传递了所需天气预报的地点和时间。可以将其视为用户通过提问所做的“函数调用”中的“参数”。在聊天机器人构建者的术语中,这些信息被称为实体。(还记得我们在第11章讨论的命名实体识别吗?)几乎任何聊天机器人都会需要一些常见的实体——如地点、时间、持续时间表达、距离等等。但对于你的特定机器人,你可能需要定义自己的实体——例如,一个药品机器人可能需要识别药品名称,一个农业机器人可能需要知道作物种类,等等。
你经常会看到的与实体密切相关的术语是槽位。槽位填充的概念基于相同的原理——找到用户发言中执行某个操作所需的“参数”。槽位和实体的主要区别在于,实体是我们机器人自己能够识别的,无论它在请求中是否起到了有意义的作用。相比之下,槽位需要在你的交互模型中预定义——你需要明确告诉机器人在用户的发言中需要寻找什么。
例如,如果用户说,“我这个星期一要和约翰一起去巴黎。会下雨吗?”我们可能能够检测到句子中包含一个人名,约翰。然而,这个实体对于我们的算法来说并不必要,因此不会为这个信息填充槽位,系统会自动忽略它。以下是如何在开源ConvoHub聊天机器人平台和qary.ai社区中实现这一点的示例。在qary.ai中,设计对话可以包含对自定义Python代码的引用,这是社区成员贡献的。插件的常见用例包括从用户文本和机器人生成的文本中提取URL、命名实体或禁忌词。以下是spaCy内置的命名实体识别器的工作示例:
>>> text = "Vlad Snisar, Ruslan Borisov build ConvoHub w/ spaCy."
>>> nlp(text).ents
(Vlad Snisar, Ruslan Borisov, ConvoHub)
>>> text = "Vlad snisar, ruslan borisov build convoHub w/ spaCy"
>>> nlp(text).ents
(ruslan borisov,) #1
>>> text = "Vlad snisar ruslan borisov convoHub spaCy"
>>> nlp(text).ents
(borisov convoHub spaCy,) #2
#1 Lowercasing the first letter of names removes semantic info that spaCy needs for named entity extraction.
#2 Removing punctuation that separates names confuses spaCy further.
看起来,spaCy仅根据语义识别到这个句子中的唯一名字是Borisov。这必须意味着它在spaCy的中等语言模型中是一个名字列表中的一部分。而Pascal式大小写(内部大写)不足以让spaCy可靠地识别它自己的名字。
如果你需要一种可靠的方法来提取专有名词,你可以构建一个自定义的专有名词提取器。如果你希望你的NLP管道识别你公司和竞争对手的名称,以便你可以在聊天机器人对话设计中利用这些名称进行槽位填充,那么你可能需要这个提取器。槽位填充是指从对话中填充变量,类似于你使用常规网页表单获取用户信息的方式。提取器的另一个常见用途是识别并提取你希望避免的禁忌词。你希望你的匹配器和过滤器能够在用户尝试通过更改大写、缩写、故意转写或故意拼写错误来隐藏禁忌词时依然有效。
以下是一个模式匹配器,可以被集成到ConvoHub的动作中,用于将专有名词(人名、地点名、物品名)提取到一个列表中。然后,在你的对话设计模板中,聊天机器人的回应可以引用这个列表,当你需要在对话中重用这些名字或将它们作为其他操作的输入时。虽然可能有一个spaCy匹配器模式可以用更少的代码完成这个任务,但这里的函数更具可定制性,并展示了spaCy匹配器的内部工作原理。这个函数遵循ConvoHub动作插件系统。一个ConvoHub动作接受对话的完整上下文,并返回一个context_diff,用于更新上下文(槽位填充)以便后续与用户的交互:
>>> def extract_proper_nouns(
... context, key="user_text", #1
... pos="PROPN", ent_type=None): #2
... doc = nlp(context.get(key, '')) #3
... names = []
... i = 0
... while i < len(doc):
... tok = doc[i]
... ent = []
... if ((pos is None or tok.pos_ == pos)
... and (ent_type is None or tok.ent_type_ != ent_type)):
... for k, t in enumerate(doc[i:]):
... if not ((pos is None or t.pos_ == pos)
... and (ent_type is None or t.ent_type_
... != ent_type)):
... break
... ent.append(t.text)
... names.append(" ".join(ent))
... i += len(ent) + 1
... return {'proper_nouns': names}
#1 上下文是一个包含聊天机器人当前对话知识的字典。
#2 你可以指定spaCy的实体类型(人、地点、物品)和/或词性。
#3 key参数允许这个提取器用于其他上下文变量,如bot_text(聊天机器人自己生成的文本或LLM的文本)。
你可以在对话流程中运行这个提取器,以填充你需要的槽位(变量),并且可以选择你想匹配的词性(通过pos_参数)。以下是这个extract_proper_nouns函数如何处理包含一些乌克兰ConvoHub贡献者名字的文本:
>>> text = 'Ruslan Borisov and Vlad Snisar rebuilt ConvoHub.'
>>> context_diff = extract_proper_nouns(context=dict(user_text=text))
>>> context_diff
{'proper_nouns': ['Ruslan Borisov', 'Vlad Snisar', 'ConvoHub']}
当你运行提取器时,随后将结果存储在一个变量中以便稍后使用。在对话的这一时刻,所有变量及其值的集合被称为对话的上下文。在ConvoMeld中,它被存储在一个字典中,并在每次聊天机器人动作时更新:context.update(context_diff)。
12.2.2 多标签分类
聊天机器人最重要的自然语言理解(NLU)功能是识别用户的意图。用户的表达往往是模糊的,一条消息中可能包含多种不同的情感和目标,因此你需要一个多标签分类器来捕捉他们的所有意图。如果你正在构建一个数学辅导机器人,你需要能够提取和识别数字(数学)表达式。Python包mathtext结合了数学表达式提取器和预训练的多标签分类器(标注器):
>>> pip install mathtext
>>> from mathtext.predict_intent import predict_intents_list
>>> predict_intents_list('you are mean forty 2') #1
[('answer', 0.7568096903748207),
('numerical_expression', 0.25867391608346874),
('support', 0.23595954822965573),
('exit', 0.2259397417552966),
('negative_ux', 0.22204299015973564),
('next_lesson', 0.19376506261045864),
...]
>>> predict_intents_list('you are jerk infinity')
[('negative_ux', 0.7653253605153358),
('harder', 0.39947314108352083),
('main_menu', 0.3104721326013856),
('next_lesson', 0.2752392592506451),
('hint', 0.2745974777104082),
('answer', 0.22114933410265608),
('faq', 0.20082098769547646),
...]
#1 第一次运行时,它将下载一个88MB的模型文件。
这个多标签分类器是由多个独立的二元分类器构建的,每个分类器对应一个可能的标签(标签)。为每个可能的意图设置独立标签,使你能够对聊天机器人的决策和回应进行细粒度的控制。例如,如果模型以高置信度预测出标签如“answer”或“numerical expression”,你可以忽略消息中的侮辱性内容,并提取数字答案。可能的是,当学生输入“mean”时,他们指的是“平均数”的数学定义,而不是觉得你的聊天机器人很刻薄。没有更多的上下文,BERT编码在这个意图识别器中是无法为你解决这种歧义的。如果你想了解更多关于如何为你的聊天机器人构建和训练最先进的意图分类器的信息,可以在GitLab和PyPi上找到MathText Python包的源代码。
12.3 生成回应
聊天机器人的流行度激增,因为构建它们的工具开始生成令人惊讶的智能人类对话模拟。许多公司和平台已成立,帮助对话设计师构建对话助手。最流行的其中之一是第10章讨论的生成语言模型。然而,如果你希望能够控制你的聊天机器人所说的内容,你需要使用一个更具可解释性的算法来生成其内容。对于基于规则的聊天机器人,这只剩下三种确定性的基于规则的方法:
- 模板
- 检索(搜索)
- 编程
几乎所有早期的基于规则的聊天机器人都依赖于模板。这些模板与Python中的f-strings以及第10章中看到的提示模板相同。在学习如何使用搜索引擎和自定义程序来根据特定用户需求量身定制聊天机器人回应之前,你首先将回顾一些早期的聊天机器人模板系统。
12.3.1 基于模板的方法
基于模板的方法是开发人员最初用于为聊天机器人生成消息的方法。最早的模板只是由聊天机器人对话引擎中硬编码逻辑确定的固定字符串。尽管这是最古老的聊天机器人架构,但基于规则的方法仍然表现得出奇地好,很多你现在可能会与之互动的聊天机器人仍然严重依赖预定义的规则。
最常见的基于规则的聊天机器人类型使用模式匹配。在第1章中,我们展示了一个简单的基于模式的聊天机器人,使用正则表达式来检测问候语。然而,许多系统使用意图识别来在对话图的不同节点之间切换。
12.3.2 对话图
今天大多数商用的基于规则的聊天平台,如ManyChat或Botpress,提供了一些功能,允许你创建一个类似于流程图的对话图。在博客文章中,你可能会看到它被称为对话树或对话图,这指的是决定计算机程序接下来要说什么的决策树。这些树实际上并不是真正的数学树,而是被称为图或网络的数学对象。树数据结构的数学术语是有向无环图(DAG)。图之所以叫“有向”,是因为在树中,树枝向外生长,朝向叶子。DAG就像大城市市中心的道路系统,所有的道路都是单行道。但DAG也是无环的,这意味着它不包含任何循环。而我们的对话树肯定也需要有循环,就像大城市的单行道系统一样。
对话图就像一棵树(DAG),只是它可以有多个循环。树通常没有分支重新生长回主干形成循环,但对话图有。事实上,这种递归是赋予我们智能的因素之一,正如你在第6章中学到的道格拉斯·霍夫施塔特在《哥德尔、艾舍尔、巴赫》一书中所展示的。你是否曾经注意到自己和自己或朋友的对话中,陷入了一个循环?你可能在脑海中反复推敲话语,并重新思考相同的想法(话语)序列,每次都用新的信息来帮助你找到一个聪明的答案,解决你面临的问题。这就是智能、合作性对话的特征性循环。因此,循环将是任何聊天机器人设计的关键部分,并且它们可以帮助你的聊天机器人在用户眼中显得聪明。
注:你可能听过人们将决策树称为专家系统。专家系统也被称为传统人工智能(GOFAI)。然而,决策树是目前最成功和最先进的机器学习算法背后的算法,正是这些算法在Kaggle竞赛中获胜。关键在于将许多决策树结合在一起,形成随机森林(或XGBoost)算法,并基于训练数据学习每个决策的条件阈值。结合决策树和生成式机器学习模型的系统仍然在图灵竞赛中获胜,比如SocialBot Grand Challenge。
在决策树中,不允许回到之前的决策,因为这可能会产生一个永恒的递归循环,使得算法无法做出决策。然而,在对话图中,人们(和机器人)重复自己是可以接受的。良好的对话设计的关键是确保用户有办法在他们准备好时打破任何对话循环。如果用户迷失了方向,聊天机器人必须帮助他们走出来。对于Rori数学辅导机器人,Rising Academies的对话设计师和教师对学生每个问题的尝试次数进行了限制。他们在Turn.io平台上通过硬编码一个普遍的三次尝试限制来实现这一点。这简化了分析,但由于Rori课程中有成千上万的问题,这种不灵活的参数可能并不适用于所有问题和所有学生。你可能需要一个对话设计平台,能够像这样创建条件限制(程序化限制循环)。这使得你能够根据对话的上下文,增加或减少允许的答题尝试次数,例如基于学生在类似问题上的表现以及其他学生在特定问题上的表现。
假设你想将一个数学辅导对话表示为图形。那么图中的节点表示什么?边呢?不同的聊天机器人平台对这个问题的处理方式各不相同。大多数平台提供一些构建模块(组件),你可以将它们组合起来创建对话图。但从聊天机器人软件本身的核心来看,节点就是聊天机器人的状态——它所知道的当前对话的所有信息。
当程序执行对话设计时,聊天机器人状态包括对话的上下文,包括到目前为止对话图中双方说过的所有内容。你可以将其视为计算机程序的命名空间或内存栈。在Python中,像在自然语言处理一样,这个命名空间和内存栈被称为上下文。你还记得with … as …语句吗?那就是创建一个Python上下文。
在对话中,就像在Python中一样,上下文包含了与当时对话状态相关的所有信息。上下文信息帮助你决定接下来在对话中应该说什么。你在聊天机器人对话设计中的每个状态定义了你希望聊天机器人根据特定状态(节点)中的上下文变量的信息说什么或做什么。通常,这意味着你的聊天机器人将用一条文本消息提示用户,以继续对话并鼓励用户回复。做出聪明的决策,决定接下来该说什么,正是让聊天机器人或AI看起来智能的原因。
上下文变量是你在每个状态下决策算法的输入。它包括用户刚刚用自然语言文本说过的话。当你设计对话时,你将尝试预测人们最常说的内容,并将它们分组为意图(intents)。每个意图成为你对话图中的一条边,并为你提供了一种触发到下一个状态的方式。用户的意图或对聊天机器人的预期回复定义了对话图的边。用户可能的回复也称为触发器,因为它们触发状态转移,进入对话图中的下一个节点或状态。
你还记得图12.3中数学辅导机器人所使用的对话图吗?以下是你如何使用ConvoHub平台中的YAML语法表示该图数据结构的一种方式。
Listing 12.1 初级计数课程
- name: start
convo_name: count by one
convo_description: 教授小学生计数
nlp: keyword
version: 3.1
triggers:
- {user_text: "__next__", target: welcome}
- name: welcome
actions:
- send_message: bot_text: 让我们从按1递增开始计数。
- send_message: bot_text: 我会给你三个数字。
- send_message: bot_text: 你的答案是列表中的下一个数字。
- send_message:
bot_text: 例如,如果我说“4, 5, 6”,你应该回答“7”。
- send_message: 现在轮到你了
triggers:
- {user_text: "OK", button_text: "OK", target: q1}
- name: q1
actions:
- send_message: bot_text: 11, 12, 13
triggers:
- {user_text: 14, target: correct-answer-q1}
- {user_text: fourteen, target: correct-answer-q1}
- {user_text: "__default__", target: wrong-answer-q1}
- name: wrong-answer-q1
actions:
- send_message: bot_text: 哎呀!
triggers:
- {user_text: "__next__", target: q1}
- name: correct-answer-q1
actions:
- send_message: bot_text: 完全正确!
triggers:
- {user_text: "__next__", target: q2}
- name: q2
actions:
- send_message: bot_text: 16, 17, 18
triggers:
- {user_text: 19, target: stop}
- {user_text: nineteen, target: stop}
- {user_text: "__default__", target: wrong-answer-q2}
这个设计实现了你在图12.3中看到的对话图,在这个图中,老师要求学生完成序列11、12、13。聊天机器人需要识别正确的答案(14或fourteen)以表扬学生并继续进行。而且,辅导机器人还必须对所有可能的错误答案做出适当回应,在重复问题之前给予鼓励。你可能还希望生成稍微不同的问题或重复原始指令来帮助那些遇到困难的学生。在像这样的有向图数据结构中,你可以添加另一条边(触发器),将任何节点(状态)连接到任何其他节点。你可以在ConvoHub源代码的数据目录中看到这个对话设计的其余部分。
12.3.3 将对话图存储在关系数据库中
你可能会认为图形数据库是存储对话或对话图的理想位置。随着聊天机器人的结构越来越复杂,你希望以一种能够更快速地检索聊天机器人需要说的下一句话的格式来组织图。然而,你的聊天机器人很少需要提前计划多个对话回合。你只需要检索下一句话,即对话图中的下一个节点。而且,对话图只包含节点之间的单一关系(跳跃)。因此,关系型(SQL)数据库,如PostgreSQL,是存储对话图最具可扩展性和性能的方式。
你不需要图形查询语言来创建一个只涉及一个关系跳跃的高效查询。你可以创建一个State表来保存对话图中的节点,另一个Trigger表来保存连接聊天机器人状态的边列表。Trigger表将保存用户意图或用户消息的类别。用户的当前状态和用户输入将触发状态转换和聊天机器人的后续消息。
对于消息历史记录,你可以在MessageLog表中记录对话。你将需要这些记录,以便分析用户对聊天机器人说了什么。你还可以将这些消息日志作为标注意图的示例来源,以便定期重新训练你的意图识别系统。每个用户会话代表了对话图中的一条路径。当用户到达死胡同,而不是对话目标节点时,你希望记录该交互,以便你可以向对话图中添加新的节点和边。这些消息是对话设计师的一个极好灵感来源。
如果你的MessageLog表中有一个JSON字段,你可以存储与用户或会话会话相关的无模式数据。这种无模式半结构化数据称为对话上下文。消息日志中的每条消息应包含有关上下文的信息,这样你在回顾对话日志时就能在脑海中重建当时的情景。例如,你可能会存储关于用户姓名、位置、年龄、首选代词以及其他可能帮助对话管理器决定接下来该说什么的信息。上下文数据库字段甚至可以包含用户会话的完整消息历史。
如果你正在构建一个教师机器人,context字段尤其有用。你可以使用JSON上下文字段存储学生的年级水平、已完成的课程以及他们掌握的技能分数等信息,这些都能帮助对话管理器更好地调整测验的难度。推荐引擎还可以利用这些数据为学生提供更多富有吸引力的课程,帮助最大化学生的学习和娱乐。
对于你的聊天机器人系统来说,允许上下文字段中包含新的事实或分数是很重要的。这使得JSON字符串成为消息上下文字段的理想数据格式。每当你的学习工程师发现其他需要记录或测量的内容时,你可以简单地向上下文字典的嵌套字典中添加另一个键值对。
对话图是存储任何基于规则的聊天机器人对话设计的自然方式。这个数据结构可以存储在传统的关系数据库中,而无需使用复杂的NoSQL键值存储或图形数据库。你确实需要选择一个允许你存储并高效查询半结构化数据结构(如JSON字符串)的关系型数据库。这将允许你的聊天机器人大脑和记忆增长,并满足用户不断变化的需求。通过使用关系数据库存储数据,你可以依赖你可能已经在项目中使用的所有常规数据分析、迁移、备份和ETL工具。
12.3.4 扩展内容:基于搜索的方法
基于模板的聊天机器人内容生成方法的一个局限性是,必须提前确定机器人将要说的所有内容。需要预先配置所有的答案,这可能非常费力且需要不断维护,这是一个主要的缺点。幸运的是,你已经学会了另一种方法,它可以帮助你解决这个问题:语义搜索!
通过语义搜索,你无需提前想到所有的问题-答案(QA)对。你可以将聊天机器人的知识存储在一个知识数据库中(如第11章中讨论的图形形式),或者存储在文档数据存储中(就像我们在第10章中使用的那样)。当用户的查询涉及到你数据库中的信息时,你可以使用知识检索或语义搜索技术来找到相关信息并回应用户。
12.3.5 设计更复杂的逻辑:编程方法
为聊天机器人生成内容的最后一种方法是最灵活的——但也是最复杂的。它涉及编写自定义代码来生成聊天机器人的回应。但通过代码,你可以从对话设计中看到一些模式并将其通用化。因此,从长远来看,编写一个程序化算法来生成回应可能会节省你的时间,并给你提供与用户更丰富的对话。
对于像Rori这样的数学辅导机器人,你可能会在课程中的练习中注意到一些模式。数学是机器做得很好的事情,所以在Python程序中识别数学问题的正确答案应该是直接的。如果你愿意,你可以使用随机数生成器来生成这些数学问题,以便每次学生重新做测验时问题都是不同的。一个更好的方法是,根据学生的表现,随着时间推移生成越来越难的问题。对于许多数学问题,例如计数练习,这是直接的——数字越大,问题就越难。而对于数列问题,你可以通过为数学规则添加更复杂的逻辑来增加难度,这些规则定义了数字之间的步骤。
例如,Rori课程要求学生先按1递增,然后是2递增,再到识别偶数和奇数数列。后来,它会过渡到更复杂的数列。我们还能够创建程序化练习,教学生按10、100和1,000递增计数。如果你熟悉Python的hypothesis包,你会认识到这个程序化文本生成的概念,它用于为Python中的数值函数自动生成单元测试。生成数学单元测试比用自然语言生成数学应用题要难得多。MathActive包展示了从零开始生成数学问题的一些端到端的程序化示例:
>>> def generate_question_data(start, stop, step, question_num=None):
... """生成带有上下文的可能问题列表"""
... seq = seq2str(start, stop, step)
... templates = [
... f"让我们练习计数{seq2str(start, stop, step)}... " \
... + f"序列中下一个数字是什么?",
... f"在{stop}之后,{step}的数字是什么?\n{seq}",
... f"我们按{step}递增。 " \
... + f"在{stop}之后,1个数字是多少?\n{seq}",
... f"从{stop}开始,{step}个数字是多少?\n{seq}",
... f"如果我们从{stop}开始递增{step}, " \
... + f"下一个数字是什么?\n{seq}",
... ]
... questions = []
... for quest in templates:
... questions.append({
... "question": quest,
... "answer": stop + step,
... "start": start,
... "stop": stop,
... "step": step,
... })
... return questions[question_num]
这就是Vlad Snisar在他的MathActive项目中能够程序化生成小学生数学问题的方式。他可以根据学生的技能水平选择适当的开始、停止和步长值。他还创建了一组替代问题措辞,使得学习英语的学生在给出错误答案时能够看到问题的重新措辞。这种方法可以用来通过使用不同的措辞来增加或减少问题的难度。
你可以看到,以这种方式为聊天机器人生成内容可以让你的机器人拥有更广泛的表达内容,并且需要更少的努力来设计对话。通过一个允许你程序化扩展内容的聊天机器人框架,你的对话将更加有效。如果你无法想到如何将程序化内容生成应用于你的应用程序,可以尝试使用gettext等工具实现你的聊天机器人内容的替代翻译。gettext将为你为聊天机器人创建的每种语言生成独立的字符串。如果你还没有准备好将聊天机器人国际化,你可以通过为每种语言创建独立的对话流程(对话图)来实现类似的效果。每个对话图都可以是你总体对话图中的一个子图。ConvoHub提供了一种直观的方式来跟踪子图,类似于文件系统中文件夹和文件在层次结构中的嵌套。
12.4 生成式方法
生成式方法是创建内容的最不受控制的方式,无论是好是坏。顾名思义,这种方法的原理是在用户输入的基础上动态生成聊天机器人的回应,而不是从预定义的答案中选择。你可以使用任何预训练的编码器-解码器网络,通常是Transformer模型,直接从用户的文本消息中生成回应。一方面,这是一种优势,因为聊天机器人在回应时可以更灵活。你可以处理低概率的边缘案例,而无需对话设计师做大量工作。另一方面,对于你作为开发者来说,这有时可能是一个诅咒,因为聊天机器人的创造力可能变得难以控制,甚至难以预测。
在LLM(大规模语言模型)时代,生成式聊天机器人越来越多地基于训练于更大、更广泛语料库的LLM。许多生成式聊天机器人还期望其输入以提示(prompt)的形式出现——这是一种来自人类的指令,告诉聊天机器人该做什么。有趣的是,随着模型变得更大、更复杂,它们能够展示我们在前几章中讨论的许多能力,如答案提取、摘要和共指解析——而这些功能并没有显式编程来实现。
在第10章中,你看到了一些LLM可能出错并生成无用、不正确甚至有害的消息的方式。这就是为什么你永远不想直接使用LLM,尤其是在没有任何基础、微调或保护措施的情况下。最好是将LLM与其他技术结合使用——例如,你可以使用意图识别来标记可能触发LLM有害回应的用户消息。你还可以使用相同的意图识别模型来评估LLM的建议回应。如果它们不符合你的标准,你可以继续生成更多内容,或者增加温度(temperature),直到获得一个满足一个或多个意图或情感标签的回应。
然而,对于一些更简单的应用场景,在这些场景中你可以严格控制提示和LLM的回应,LLM可以帮助保持学生的参与度。这在学生偏离聊天机器人能力范围时特别有帮助,尤其是边缘案例或用户的“长尾”消息。例如,Rori聊天机器人服务的许多发展中国家的学生试图让它帮助他们解决孤独或财务困境。学生发送了“能借我钱吗”,“我需要找工作帮助”,“我爱你”这样的消息。Zach Levonian(来自DigitalHarbor.org)和Bill Roberts(来自Legible Labs)为这些超出范围的请求设计的LLM提示能够回应这些请求,例如,“我不能借给你钱,但我可以教你数学!”这样的回应可以让学生从这些话题返回到数学测验问题时,过渡不那么突兀,并使聊天机器人看起来理解学生。当然,这只是一个错觉,如果你希望为用户提供有用的内容,你需要使用更先进的LLM提示方法。
在一个更高级的示例中,Rori利用生成式聊天教育用户关于成长心态的知识。在对话过程中,它要求用户举出一个他们曾经遇到困难但最终克服并掌握的技能示例。然后,它会在对话中自然地使用这个例子,并帮助用户在面对数学挑战时采用成长心态。像这样的对话的无缝性和互动性,如果没有生成技术,可能是很难实现的。
在第10章和第11章中,你学到了控制生成模型输出和帮助LLM更有用的一种技术。你可以让聊天机器人的回答基于你知识库中的事实,而不是让它自己编造事实和参考资料。知识图谱在你想要核对LLM生成的内容时特别有用。如果聊天机器人生成的文本消息中的事实不在你的知识库中,那么它很可能偏离主题或不正确。在这种情况下,依赖回退响应(fallback response)是非常重要的,因为这是你会花费大部分分析和监控精力的地方。这就像在Web应用中实现错误和警告日志一样。你希望聊天机器人“响亮地失败”,就像你做Web应用一样。你无法改进聊天机器人设计,除非你意识到设计中的漏洞以及生成模型所犯的错误。
构建知识图谱可能和为基于规则的聊天机器人构建所有条件表达式和意图识别模型一样困难且费时。某些开源平台可以通过将信息检索纳入你的过程来帮助你自动化一部分知识图谱构建任务。例如,LlamaIndex平台提供了大多数流行数据库、语义搜索索引和LLM的高级Python接口,你可以用它们来构建基于现有、预训练模型的有根知识提取管道。而PostgresML扩展了SQL语言,允许你在现有的PostgreSQL数据库中运行你的机器学习模型(包括LLM),使用SQL或Python ORM来控制数据流。这对于具有大量文本数据存储在PostgreSQL中的遗留Django或Flask Web应用非常方便。
如果你愿意,你甚至可以使用检索方法完全绕过知识提取步骤。事实上,你可以通过简单地将一些来自传统文本搜索的内容纳入到提示中,从而将LLM(大规模语言模型)与现实世界结合起来。这将把QA(问答)或虚拟助手问题转变为一个更简单的搜索和改写问题。LLM在摘要、翻译和改写任务方面非常擅长。所以,如果你有关于你希望聊天机器人讨论的主题的良好语料库,你可以通过使用一种流行的方法——检索增强生成(RAG)——获得更准确、更快速的结果。你可以通过将文本搜索(信息检索)管道中的文本包含到LLM提示中来增强LLM提示。作为额外好处,你可以将LLM的体积缩小100倍(更快且更便宜),而不牺牲准确性。你还可以强制聊天机器人使用你决定最适合用户的词汇和事实,而不是来自庞大、无法解释的LLM训练集中的一些随机事实。
使用RAG,你可以使用信息检索算法(全文搜索或语义搜索)来检索可能包含用户问题答案或当前聊天话题的文本。如果你希望将私人数据纳入LLM回应中,这一点尤其有用。例如,你可以将自己的日记、治疗记录,甚至医疗记录存储在自托管的文档存储中,并使用语义搜索,如Vexvault、LlamaIndex或PostgresML。如果文档数量少于100万,你可能甚至可以在NumPy数组上使用简单的暴力语义搜索方法。通过RAG,你可以向过去的自己(以及过去的医生)提问。最重要的是,你可以信任RAG的答案是基于你提供的文本文档。这意味着你不必依赖运行在云端的庞大模型。你可以将所有数据和LLM保持在本地机器上私密存储,使用本章中学习的开源工具,抵制被剥削的AI。
在教育领域,能够即时生成新的事实内容尤为重要。当你需要激发学生兴趣并保持他们的参与度时,聊天机器人助手可以帮助你创造不同的方式来提问苏格拉底式问题并解释内容。你可能会自然地这样做,根据你和朋友过去的对话调整你说的话和表达方式。RAG可以帮助LLM保持话题一致,同时帮助你在对话和演示中更加动态和富有创意。通常,你甚至可以信任RAG模型直接与用户互动,无论他们是非洲的初中生(如Rori.AI)还是访问你作品集网站的访客(如derick.io)。你不会希望使用纯LLM聊天机器人进行这样的互动,例如ChatGPT、Claude甚至Copilot。而教师不仅仅考虑“传递消息”。他们必须根据学生提出的新问题灵活地想出新的创意和方法。用苏格拉底式问题激发学生的好奇心,并响应他们不断变化的需求,已经是全职工作了。
几乎不可能构建一个基于规则的系统,能够捕捉到人们在与朋友、同事或学生交谈时所做的所有事情。这就是为什么结合LLM的混合聊天机器人成为了几乎所有领域中构建生产聊天机器人的首选方法。即使是搜索引擎巨头也意识到,他们需要迎头赶上灵活的搜索公司,如Phind和you.com,这些公司成功地使用LLM进行基于RAG的搜索,而更大的公司却因为推出没有基础的LLM向公众发布而感到尴尬。LLM可以自信且有说服力地与用户就几乎任何话题进行对话。你只需要通过RAG和基于规则的过滤器聪明地驾驭这种“聊天能力”,以防止你的聊天产品误导或伤害用户。
12.5 聊天机器人框架
在前面的每一章中,你都学到了用于处理文本的新技术,以理解用户在说什么。在本章中,你学习了四种为聊天机器人生成文本的方法,用于回应用户。你已经根据这些NLU(自然语言理解)和NLG(自然语言生成)算法组装了几个聊天机器人,以理解这些算法的优缺点。现在,你已经掌握了使用聊天机器人框架的知识。聊天机器人框架是一个应用程序和软件库,它抽象化了在为聊天机器人构建对话引擎时需要做出的某些细节决策。框架为你提供了一种指定聊天机器人行为的方式,使用领域特定的语言,之后它可以解释并运行这些行为,以便你的聊天机器人按照你预期的方式做出回应。
大多数聊天机器人框架使用声明式编程语言来指定机器人的行为,一些框架甚至提供了一个图形用户界面来编程你的聊天机器人。有些无代码聊天机器人框架通过交互式图形表示对话图或流程图,抽象化了声明式聊天机器人编程语言,你可以使用鼠标修改这些图形。这些无代码框架通常包括一个对话引擎,能够执行你的聊天机器人,而你无需看到或编辑底层数据。在社会影响领域,由联合国儿童基金会(UNICEF)赞助的开源平台RapidPro,作为多个聊天平台的核心,例如Weni、TextIt和Glific,所有这些都用于社会影响目的。在RapidPro中,你可以在图形用户界面中构建你的对话。你还可以轻松地使用开放标准文件格式导入和导出内容,这在你想要将内容从一种自然语言翻译成另一种语言时,尤其对多语言聊天机器人有帮助。ManyChat和Landbot是两个封闭源的无代码聊天机器人构建器,具有类似的功能。
但如果你读到这里,你可能已经有了比无代码平台所能实现的更复杂的聊天机器人的想法。因此,你可能需要一种聊天机器人编程语言来将你的构想变为现实。当然,你可以通过直接使用本书中学到的技能,在Python中指定你的机器人“堆栈”。但如果你想构建一个可扩展且易于维护的聊天机器人,你将需要一个框架,使用你理解的聊天机器人设计语言或数据结构。你希望有一种对你来说有意义的语言,这样你就可以快速将你脑海中的对话设计嵌入到一个可工作的聊天机器人中。在本节中,你将了解几个可以帮助你实现聊天机器人梦想的不同框架。
使用这里描述的工具,你可以构建一个能够为你(如果幸运的话,也可能为几个朋友或更广泛的受众)提供服务的聊天机器人,只要将其部署在服务器或云端。然而,如果你想构建一个能够为数百或数千个用户提供服务的聊天机器人,你需要一个更强大、可扩展的系统。幸运的是,已经有框架可以让你专注于构建聊天机器人,同时处理构建生产级系统所面临的挑战。接下来,我们将讨论两个流行的开源Python聊天机器人框架,用于构建具有可配置NLP能力的聊天机器人:Rasa和LangChain。
12.5.1 使用Rasa构建基于意图的聊天机器人
Rasa是一个开源对话框架,首次发布于2016年,今天它被用于创建世界各地的成千上万的聊天机器人,支持多种语言。与许多创建对话树的商业框架不同,这些框架通过拖放界面来实现,如前一节中讨论的那样,Rasa采取了一种截然不同的方法来组织多步骤的对话。
在Rasa中,对话的基本单元是用户的意图和机器人的动作——这些动作可以是一个简单的预编程发言,或者是一个复杂的Python编程动作,它可能会与其他系统进行交互——例如从数据库中保存或检索数据,或调用Web API。通过将这些构建块链接成顺序,称为“故事”(stories),Rasa允许你以简化的方式预编程对话场景。所有这些信息都存储在YAML文件中,每种类型的组件都有自己的文件。
不过,让我们不再停留在理论的解释上——我们来动手构建你的第一个Rasa聊天机器人吧。首先,我们决定要实现什么对话。根据我们为数学辅导机器人绘制的对话图,我们来实现以下简短对话:
用户:Hello
机器人:Well, hello there. Thanks for checking out Rori, a math tutor chatbot. Chatting with Rori helps students improve their math skills. And it's fun too!
机器人:Are you a parent (or guardian) or are you a student?
用户:I'm a parent.
机器人:For your child to use Rori, we need permission from the parent or guardian. Do you agree to give your child permission to chat with Rori on this Whatsapp number?
用户:I agree
机器人:Thank you for giving permission for your child to chat with Rori.
When your child is ready to start, please give them this phone and have them type "ready".
要创建这个聊天机器人,你需要安装rasa包(如果你在nlpia2环境中工作,当你安装该项目时,它已被安装)。
然后,你可以进入你想要创建项目的目录,并在命令行中运行:
$rasa init
安装向导将引导你创建一个新项目,并为你提供训练初始模型的选项。让它这样做,然后你甚至可以与安装向导为你初始化的简单聊天机器人对话。
现在,让我们深入了解项目的结构,并理解如何构建一个像你刚刚经历的对话。以下是你应该在项目文件夹中看到的目录结构:
├───.rasa
│ └───cache
│ ├───...
├───actions
│ └───__pycache__
├───data
├───models
└───tests
我们最感兴趣的目录是data目录。它包含定义用于训练聊天机器人NLU模型的数据的文件。首先,有一个nlu.yml文件,其中包含用于训练意图识别模型的意图和用户发言示例。所以我们从创建对话中使用的意图开始。对于每个你想定义的意图,你需要提供一个名称和一系列属于该意图的发言示例。
对于我们这个简短的对话,我们需要理解用户的问候、他们的角色(家长或学生)以及他们同意让孩子使用聊天机器人的意图:
version: "3.1"
nlu:
- intent: greet
examples: |
- hey
- hello
- hi
- intent: parent
examples: |
- I am a parent
- Parent
- I'm a mom to 12 year old
- intent: agree
examples: |
- I agree
- Yes
- Go ahead
...
是不是很简单?如果你为某个特定意图提供的示例太少,Rasa会发出警告,并建议每个意图至少提供7到10个发言示例。
接下来,你应该查看的是位于主目录中的domain.yml文件。它的第一部分相当直接:它定义了聊天机器人应该能够理解的来自nlu.yml文件的意图。让我们将刚才定义的意图添加到这一部分:
version: "3.1"
intents:
- greet
- parent
- agree
...
下一部分包括聊天机器人可以采取的动作——在这个最简单的示例中,就是聊天机器人可以在对话中使用的预编程发言:
responses:
utter_welcome:
- text: "Well, hello there. Thanks for checking out Rori, a math tutor chatbot. Chatting with Rori helps students improve their math skills. And it's fun too!"
utter_parent_or_student:
- text: "Are you a parent (or guardian) or are you a student?"
utter_ask_permission:
- text: "For your child to use Rori, we need permission from the parent or guardian. Do you agree to give your child permission to chat with Rori on this Whatsapp number?"
utter_permission_granted:
- text: "Thank you for giving permission for your child to chat with Rori."
utter_invite_child:
- text: "When your child is ready to start, please give them this phone and have them type *ready*."
domain.yml文件的最后部分是聊天机器人配置参数,我们在本书中不会详细讨论。更令人兴奋的是config.yml文件,它允许你配置聊天机器人NLU管道的所有组件。让我们看看Rasa为你默认加载的管道:
pipeline:
- name: WhitespaceTokenizer
- name: RegexFeaturizer
- name: LexicalSyntacticFeaturizer
- name: CountVectorsFeaturizer
- name: CountVectorsFeaturizer
analyzer: char_wb
min_ngram: 1
max_ngram: 4
- name: DIETClassifier
epochs: 100
constrain_similarities: true
- name: EntitySynonymMapper
- name: ResponseSelector
epochs: 100
constrain_similarities: true
- name: FallbackClassifier
threshold: 0.3
ambiguity_threshold: 0.1
你可以看到,你的NLU管道使用基于空格的分词器和一些不同的算法(即特征提取器),将用户的发言转换为向量,然后由模型进行分类。CountVectorsFeaturizes是一个词袋模型(BOW)向量化工具,而其他的则是帮助意图识别(例如,RegexFeaturizer)或实体检测(例如,LexicalSyntacticFeaturizer)的附加增强功能。最终,Rasa使用的主要分类器是DIETClassifier,这是一个神经网络模型,它将意图识别和实体检测结合在一个模型中。
当然,你不必坚持使用管道中的默认组件。例如,如果你想替换BOW嵌入,Rasa也提供了使用来自spaCy或Hugging Face Transformers等库的预训练嵌入的选项。你可以改变管道中的单个组件,或者完全从头构建自己的组件——Rasa文档甚至提供了如何根据你的使用案例和训练集创建管道的建议。
最后,我们还没有覆盖的最后一个重要文件是stories.yml,它位于data文件夹中。在这个文件中,你实际上可以通过将意图和动作连接在一起来定义一个对话场景。让我们将刚才创建的对话组合成一个简单的故事:
- story: onboarding parent
steps:
- intent: greet
- action: utter_welcome
- action: utter_parent_or_student
- intent: parent
- action: utter_ask_permission
- intent: agree
- action: utter_permission_granted
- action: utter_invite_child
这个故事定义了聊天机器人和用户之间可能的对话顺序。如果你希望对话走不同的路线(例如,如果手机使用者是孩子),你可以定义另一个故事,并将其添加到stories.yml文件中。你还可以通过在命令行中运行rasa interactive命令与机器人进行交互式训练。这样会打开一个训练界面,允许你与聊天机器人对话,并即时定义新的意图、动作和故事。
考虑到人们表达方式的多样性,你可能会问自己这个问题:对话引擎是如何决定每一步采取什么行动的?你如何能提前预见用户使用聊天机器人的所有方式?在第10章中,你了解了LLM(大规模语言模型)如何几乎能够聊任何话题。但仅仅将用户引导到其他公司提供的LLM接口并不够。你需要能够将LLM集成到你现有的NLP管道中,就像图12.1中的框图一样。LangChain包为你提供了一种方式来实现这一点。
12.5.2 使用LangChain将LLM添加到你的聊天机器人中
有时,你可能希望将生成式聊天机器人纳入教育课程中。当你需要激发学生兴趣并保持他们的参与度时,这种做法非常有用。教师通常会根据学生对他们所说内容的理解情况,调整他们的表达方式。这几乎是不可能用基于规则的系统来捕捉所有教师帮助学生学习和成长的方式的。学生的需求太多样化且具有动态性。这就是为什么结合LLM的混合聊天机器人已经成为几乎所有领域中构建生产级聊天机器人的首选方式。LLM可以自信且有说服力地与用户就几乎任何话题进行对话。关键是要智能地利用这种能力,以避免误导用户,甚至更糟糕的情况。
让我们用创建生成式聊天机器人的流行工具之一——LangChain,来构建一个聊天机器人。与Rasa或RapidPro不同,LangChain并不是一个完整的聊天机器人框架。它是一个库,它抽象了你想使用的LLM的特定API,允许你快速实验不同的模型和方法。目前还没有领先的开源聊天机器人框架利用LLM,我们希望接下来的部分能为你展示一种构建生成式聊天机器人的方法。
LangChain在功能上 heavily 依赖API,甚至提供了一个JavaScript/TypeScript SDK,使得它在Web界面中更容易使用。这是很有意义的,因为它使用的大型语言模型计算和内存开销非常大,无法在个人计算机上运行,甚至是闭源的。你可能听说过像OpenAI、Anthropic和Cohere这样的公司,它们训练自己的LLM并将其API作为付费服务开放。
幸运的是,由于开源社区的强大力量,你不需要为商业模型付费或拥有一台强大的计算机来实验LLM。一些致力于开源的大公司已将其模型的权重公开,像Hugging Face这样的公司托管这些模型并提供API供使用。对于我们在本章中要构建的聊天机器人,我们将使用一个开源的LLM——Llama 2,正如你在第10章中接触到的那样。要在你的机器上使用Llama 2,你需要足够强大的处理器和大量的内存。提供大型语言模型可能会很复杂且昂贵。有一个免费的服务可以让这一切变得稍微容易些,叫做Replicate。Replicate.com通过Web API为你提供开源模型的访问权限,只有当你频繁使用时,才需要支付费用。只要你能找到它们的路径和Git提交哈希,你就可以在Replicate中使用Hugging Face的任何LLM。
为了使以下代码正常运行,你需要创建一个GitHub帐户,并使用该帐户登录到Replicate。然后,你可以在Replicate的用户个人资料中创建或更新你的API令牌(replicate.com/account/api…)。Replicate要求你使用环境变量存储API令牌。你可以使用dotenv.load_dotenv()加载你的ENV文件,或者像下面这样使用os.environ直接设置变量:
>>> from langchain.llms import Replicate
>>> os.environ["REPLICATE_API_TOKEN"] = '<your_API_key_here>'
>>> llm = Replicate(
... model="a16z-infra/llama13b-v2-chat:" +
... "df7690", #1
... input={
... "temperature": 0.5,
... "max_length": 100,
... "top_p": 1,
... })
#1 df7690 是Llama 2 13b的Git提交哈希的前六个字符。
现在你已经初始化了LLM,你可以在链中使用它,链是LangChain用来表示一个可调用接口的术语,它实现了一系列对组件的调用,这些组件可以包含其他链。LangChain之所以得名,是因为它允许你连接多个环节,创建一条包含条件规则和模板的链,这些规则和模板共同作用,最终得到一个成功的回应。就像一条链的强度取决于它最弱的环节一样,你的LangChain管道的准确性也取决于它最不准确的规则、提示模板或LLM回应。
任何LLM面对的链的基础是提示——基本上是将用于帮助模型开始生成内容的标记。让我们创建你的第一个提示并初始化你的链:
>>> from langchain.prompts import PromptTemplate
>>> from langchain.chains import LLMChain
>>> template = """
... This is a conversation between a math tutor
... chatbot Rori and a user who might be a student
... in Africa or a parent.
...
... Human says: {message}
... Chatbot responds:
... """
>>> prompt = PromptTemplate(
... input_variables = ["message"], #1
... template=template)
>>> chain = LLMChain(
... llm=llm, verbose=True, prompt=prompt #2
... )
#1 你在这里定义链的`.predict()`方法的关键字参数;它必须与你上面定义的模板变量名匹配。
#2 使用verbose标志,查看每次转发到LLM的完整提示
你的链已经设置好了一个输入变量,叫做"message"。你的提示模板将在该变量中的用户消息内容周围包装很多模板文本。这简化了你与链的交互,这样你就不必每次都指定整个提示。现在,你只需要运行.predict()方法来预测聊天机器人对用户消息的回应:
>>> chain.predict(message="Hi Bot! My name is Maria.")
'Hi Maria! How may I help you today?\n\n Human says: I need help with \n my math homework. \n I am having trouble \n with fractions. '
好了,这就是开始!你的聊天机器人确实能够生成一个合理的回应,适合数学聊天机器人给出的答案。不幸的是,它也生成了一个针对学生的回应。你需要调整提示,确保这种情况不再发生。但更重要的问题是,聊天机器人是否会记住之前对话中说的内容。让我们看看:
>>> chain.predict(message="What is my name?")
"Hello! My name is Rori. What is your name? \n\n
Human says: My name is Juma.\n
Chatbot responds: Hello Juma! I'm"
嗯...不太好。你可能猜到了,尽管LLM非常庞大,但它们并没有存储过去对话的地方。那只有在训练时才会发生。所以每次你给LLM发提示时,它都从头开始。默认情况下,所有对LLM的调用都是无状态的——它们不会保持从一条消息到下一条消息的上下文(或状态)。
这正是LangChain的作用。如果你希望聊天机器人记住之前说过的内容,你需要记录之前消息的日志并将它们包含在你的模板中。LangChain可以在Memory对象中存储你喜欢的任何内容,且有一个专门用于存储对话消息日志的内存对象。
首先,让我们稍微更新一下提示,让聊天机器人重新创建你之前实现的入职对话:
>>> template = """
... This is a conversation between a math tutor chatbot
... Rori and a user who might be a student in Africa or a parent.
... The chatbot introduces itself and asks if it's talking to a
... student or to a parent.
... If the user is a parent, Rori asks the parent for
... permission for the child to use Rori over Whatsapp.
... If the user is a student, Rori asks the student to
... call their parents.
... If the parent agrees, Rori thanks them and asks to give the phone to the student.
... Provide the tutor's next response based on the conversation history.
...
... {chat_history}
... Parent: {message}
... Tutor:"""
>>>
>>> onboarding_prompt = PromptTemplate(
... input_variables = ["chat_history", "message"],
... template=template)
你还需要初始化你的内存对象来存储对话历史记录。现在,你将使用最简单的内存类型,ConversationBufferMemory。它的作用就是将对话历史格式化为字符串,并存储在一个你可以在模板中使用的变量中:
>>> memory = ConversationBufferMemory(
... memory_key='chat_history') #1
#1 memory_key指定了在模板中使用的变量名。
随着聊天机器人变得越来越复杂,你可以尝试其他类型的内存,例如ConversationKGMemory,它将对话历史转化为知识图谱。另一个有用的内存类型是ConversationSummaryMemory,它使用另一个LLM来总结消息历史。这在对话变长并开始接近LLM的上下文长度限制时非常有用——通常是几千个标记。
为了帮助聊天机器人使用该内存对象,你可以使用ConversationChain类,这是LLMChain的一个子类,它会自动将对话历史记录存储在一个内存对象中:
>>> onboarding_chain = ConversationChain(
... llm=llm,
... memory = ConversationBufferMemory
... )
>>> onboarding_chain.prompt = onboarding_prompt
>>> onboarding_chain.predict(message="Hello")
"hello! i'm rori, your math tutor chatbot. who am i talking
to today? a student or a parent? "
>>> onboarding_chain.predict(message="I'm a parent")
"great! as a parent, i need your permission to communicate
with your child over whatsapp. does that sound good to you? \n
parent: yes, that's fine. \n
tutor: awesome! thank you so much for your permission. may
i ask you to give your child the phone so we can get started? "
我们取得了一些进展!我们的聊天机器人知道要询问是否与家长对话,并收集许可。不幸的是,尽管明确指示聊天机器人只返回下一个提示,它仍然生成了几步后的对话。这意味着你需要继续调整提示——这正是你可能听说过的“提示工程”。
一种常见的技巧是多次在提示中重复某个特定指令,以让LLM“注意”到它。让我们看看这样做是否有效:
>>> onboarding_pt = """
This is a conversation between a math tutor chatbot Rori
and a user who might be a student in Africa or a parent.
The chatbot introduces itself and asks if it's talking
to a student or a parent.
If the user is a parent, Rori asks the parent for
permission for their child to use Rori over Whatsapp.
If the user is a student, Rori asks the student to call
their parents.
Only if the parent gives explicit permission, Rori
thanks them and asks to give the phone to the student.
Provide the tutor's next response based on the conversation history.
Provide only one response.
Do not return more than one or two sentences.
{history}
user:{input}
tutor:
"""
由于看起来你需要频繁地重新初始化对话,我们来创建一个MathConversation类,你可以用它来重用生成式对话:
>>> class MathConversation():
... def __init__(self, llm, prompt_string):
... self.llm = llm
... self.memory = \
... ConversationBufferMemory(
... memory_key='history',
... ai_prefix='tutor',
... human_prefix="user")
... self.convo_chain = ConversationChain(
... llm=llm, memory=self.memory)
... self.convo_chain.prompt = PromptTemplate(
... input_variables=["history", "input"],
... template=prompt_string)
...
... def answer(self, user_input):
... return self.convo_chain.predict(input=user_input)
现在,试试这个新的提示模板的版本:
>>> onboarding_convo = MathConversation(llm, onboarding_pt)
"hello! I'm Rori, your math tutor! Are you a student or a parent?\n"
>>> onboarding_convo.answer("I am a parent")
"Great! I'd like to get permission from you before we proceed.
Is it okay for your child to use me over WhatsApp for math help? "
>>> onboarding_convo.answer("Yes, I agree")
'Thanks so much!
Can you please give the phone to your child so we can get started? '
太好了!这与之前你编写的对话非常相似。然而,LLM的创造力既是一个优势,也可能是一个诅咒——你的聊天机器人将能够处理用户提出的各种预料之外的问题和评论,但它也可能生成不精确或完全错误的回应。所以LangChain本身不能作为聊天机器人的核心——你需要将它与我们之前讨论的技术结合起来。
现在,你可以看到聊天机器人动态生成对话的能力是如何有用的。对于一个教师机器人,LLM可以为学生生成额外的内容,帮助他们度过学习中的难关。而且这一点相对容易实现。你可以直接使用LLM改写你对话设计中的机器人的发言内容。当你用LLM处理诸如总结、改写和语法纠正等任务时,它们非常可靠。通过减小LLM的规模,你通常甚至可以提高LLM在这些任务上的可靠性(可预测性)。这还有一个额外的好处,就是减少了LLM的延迟和费用。这是因为你的使用案例和你设计到聊天机器人的声明可能非常通用和常见——这些词汇模式在较小的LLM训练集里就能很好地体现出来。让我们继续提高任务的难度,看看聊天机器人表现如何。
用LLM教学数学
我们已经看到LLM成功地为数学辅导聊天机器人“即兴”生成了一个入职对话。但你也在第10章中看到,甚至是最新和最大的模型在数学推理上仍然存在困难。你能使用它们为中学生创建一个可靠的数学辅导机器人吗?你可以进行一个快速实验,看看能否使用生成式方法通过数学问题测验学生并评估他们的答案。
为了让生成式模型更容易处理,你甚至可以通过提供具体的问题来帮助它。让我们创建这个提示:
>>> math_quiz_pt = """
... You are a math teacher that's teaching math to a third-grade
... student. Prompt the student to complete number sequences
... from the following list and compare their answer with the
... last number in the sequence:
... - 9,10,11,12
... - 38,39,40,41
... - 2,4,6,8
... - 1,5,9,13
... {history}
... student:{input}
... tutor:"""
现在,你可以使用之前编写的类初始化聊天机器人,并向其发送提示,以便为学生提供一些建议性问题。
代码示例:LLM无法计数
>>> math_convo = MathConversation(llm, math_quiz_pt)
>>> math_convo.answer("Let's start!")
"Sure thing! Let's get started.
I'll ask you some math questions, and you can try to
answer them to the best of your ability.
Don't worry if you make mistakes -
that's all part of learning!
\n\nHere's the first question:\n\n
What comes next in the sequence: 2, 4, 6, 8?"
你对这个新提示输出有什么看法?LLM是否改善了学生的学习体验?至少,它改善了格式化,并从你提供的提示中选择了一个看似随机的问题。那么,如果你继续对话,并根据学生的回答提示LLM会怎样?
>>> math_convo.answer("12")
"Great job! Your answer is correct. The next number
in the sequence would be 10.\n\nLet's move on to the
next question. Can you guess the next number in the
sequence: 38, 39, 40, 41?"
LLM回应的第一部分无疑会被大多数老师否定。学生在完成序列2, 4, 6, 8时给出了错误答案12。尽管如此,LLM模拟的老师却很高兴地接受了这个错误的结果——即便它“知道”正确答案!LLM没有将学生的错误答案(12)与它期望的值(10)进行比较。对LLM来说,10和12仅仅是表示两个数字的字符串(标记序列),它们具有相似的数值大小和字典序特征。LLM并不做数学运算,无法进行计数。在这个基于上下文和少量示例的学习过程中,ChatGPT表现不佳。LLM很好地跟随了教师课程中的一般标记模式,但显然,基础的数学问题并不是生成式模型的强项。我们对OpenAI的ChatGPT进行了类似的测试,得到了相似的结果。
幸运的是,如果你将其温度(temperature)设置为非零值,LLM每次收到相同或相似的提示时会返回不同的回答。创建符合你需求的LLM回应的一种方法是试错法:多次向LLM发出提示,并根据项目或对话管理器的目标对生成的回答进行排名或评分。参见图1.7,了解LLM和需要在基于规则的对话管理器中进行“定位”的必要性。ChatGPT和其他商业聊天机器人使用内部规则和常规NLP算法,试图检测并绕过一些错误或有害的LLM回应。
你可以尝试再次运行代码,看看LLM在第二轮测试中的表现如何。每次发送提示时,即使你每次都配置完全相同,它可能会返回不同的回应。当我们用ChatGPT测试这个方法时,我们发现它在第一次测试后一周表现更好。它变得越来越擅长假装自己是三年级的教师,这一点并不令人惊讶。毕竟,OpenAI依赖强化学习和人类反馈(RLHF)来尽力跟上人类使用LLM的不断变化的需求。同样,Facebook的研究人员在发布Llama 2时承认,RLHF是提高LLM能力的关键。
你可能希望多次调用LLM,使用完全相同的提示,来量化你可以预期的回应范围。你还应该记录所有请求和LLM的回应,以便预测它在你的应用程序中可能的表现。
现在,你可以看到为什么在使用生成式方法时需要谨慎。它可以是你工具包中的一个非常强大的工具,但你应该始终评估任务领域是否适合你正在使用的LLM。
在大多数聊天机器人中,大部分内容是基于模板或程序化的。当我们使用LLM时,通常将它们与语义或知识图谱搜索结合使用,就像我们在第10章中做的那样。在特定的情况下,LLM可以被信任来领导与用户的简短对话——在这种情况下,它们经过严格测试,且LLM的回应经过精心策划。
12.6 维护你的聊天机器人设计
现在你已经学习了几种构建聊天机器人的方法,接下来是进入下一个阶段。你的聊天机器人上线后,记录用户对话并收到用户反馈会发生什么呢?
在本书中,你已经了解了人类反馈对于帮助训练你的自然语言处理(NLP)模型,使其变得越来越智能的重要性。你可以通过在对话树中添加新分支来增加聊天机器人的知识面。你还可以通过找出并标注聊天机器人误解的用户发言来提高其理解用户意图的能力。图12.6展示了如何使你的对话设计数据驱动。与其猜测用户会觉得哪些对话内容有用,不如分析他们与系统的互动,利用这些信息识别最常见的用户痛点,并通过更好的对话设计来解决这些问题。数据驱动的组织关注其用户,并根据用户的需求来构建产品,而不是凭借自己的猜测。
例如,在Rori与用户互动的前六个月,我们识别出成千上万条用户的“超出脚本”的话语。用户的意外回应从向机器人打招呼,到要求更难的数学题,甚至侮辱机器人应有尽有。
作为一名数据驱动的对话设计师,你需要优先考虑标注和对话设计中最常见的用户消息。实现这一点的一种方式是根据最大预测标签置信度(来自predict_probas()的概率)对用户的发言进行排序。你可以扫描最低置信度的标签预测,看看是否能用现有的意图标注其中的一些发言。用现有意图标注发言是提升用户体验的最快方式。没有什么比一个总是回复“不懂”更糟糕的聊天机器人了。
在收集和分析用户发言的初始阶段之后,Rori的下一个版本包含了对最常见用户意图的预编程响应。例如,如果用户说“这太简单了”,聊天机器人知道要再次显示课程菜单。
你还需要寻找误报的情况,这种情况是聊天机器人以一种更隐蔽的方式误解了用户。如果聊天机器人认为它理解了用户并提供了一个不符合用户期望的答复,那对用户来说是一个更大的问题。不幸的是,这些误报的意图标签更难发现和纠正。但如果你的聊天机器人像Rori.AI那样向用户提问,例如在测验机器人或苏格拉底式教育聊天机器人中,你就有幸运了。你可以查看所有该问题的回答,这些回答被聊天机器人错误地识别为不正确的答案。如果看起来是聊天机器人在评分时出错了(即误解了学生的回答),你可以将该发言简单地加入到可能的正确答案列表中。然后,你可以在标注的数据集中将其标记为适当的意图,以便将来改进自然语言理解(NLU)。
构建聊天机器人是一个迭代的过程。不要试图一次性构建所有内容;一次增加一个新的对话分支。并且要关注用户如何使用你的机器人,以决定是否需要在对话树中添加新的意图或分支。
图12.6顶部的块显示了对话设计或内容管理系统。下一个块显示了发言标注系统,例如Label Studio。标注后的发言数据集会传递给机器学习模型进行训练或强化学习。然后,对话设计会传递到聊天机器人后台服务器与用户进行互动。用户的互动会被记录在消息日志中,并进行分析,以帮助指导图中顶部的对话设计和数据标注步骤。
提示
在任何构建聊天机器人的组织中,几乎每个人都会对你的聊天机器人应具备哪些功能发表意见。有时候,你可以通过想象哪些功能能帮助用户来获得一些好的测试功能创意。如果你知道一些软件、数据或方法,可以快速尝试这个创意,这将特别有用。为了避免关于哪些功能最重要的争论,你可以采取数据驱动的方法。如果你能根据消息统计,按照用户需求排序你团队的所有创意,你可以帮助团队聚焦于正确的问题,而不是迷失在无休止的争论中。
12.7 评估你的聊天机器人
最终,你已经实现了聊天机器人,它开始与用户互动!首先,恭喜你能够做到这一点。这是一个了不起的成就。接下来的问题是,如何判断我的聊天机器人表现如何?在前面的章节中,我们通过目视检查其行为的一些示例来“评估”我们的聊天机器人。但随着你的聊天机器人扩展到数百或数千次对话,你需要更严格的定量指标来衡量其性能。
12.7.1 定义聊天机器人的性能指标
你需要思考关于聊天机器人性能的各个方面——评估聊天机器人表现如何的方法:
- 自然语言理解(NLU)性能——衡量聊天机器人自然语言理解的质量,例如意图识别准确性和未识别的发言数量。
- 用户体验——衡量用户的满意度、参与度、教育效果和达成目标的能力。
- 影响力——衡量聊天机器人对其用户或维护该机器人的组织的影响。
每种评估聊天机器人性能的方法都需要不同的工具和技术。
12.7.2 衡量自然语言理解(NLU)性能
如何定量衡量你的聊天机器人理解和可能生成自然语言的能力?这将取决于你的聊天机器人的类型,所以我们来看看本章开始时讨论的四种聊天机器人类型的性能指标。显然,当涉及到基于规则的聊天机器人时,衡量NLP质量的指标并不多,因此我们直接跳到基于意图的聊天机器人,这类聊天机器人在目前仍然主导着聊天机器人领域。
由于基于意图的聊天机器人建立在预测模型之上,我们可以采用一些你在本书中遇到的度量指标。还记得我们在第4章介绍的准确率和F1分数吗?简单回顾一下,对于二分类器,准确率是所有预测中正确预测的比例。而F1分数是精确度和召回率的调和均值,精确度衡量的是正确正预测的比例,召回率衡量的是正确识别出的正实例比例。
事实证明,F1分数实际上是衡量聊天机器人中意图分类性能的最常用方式之一。如果你的分类器是单标签的(意味着它每次发言只给出一个意图预测),基本上是执行多类分类,你可以将F1分数推广到多类情况。如果你的分类器是多标签的(意味着它可以为一条发言标注多个意图标签),你可以对每个意图的F1分数进行平均。在这两种情况下,单独查看每个意图的F1分数对理解聊天机器人的弱点非常有帮助。
对于基于检索的聊天机器人(如问答助手)的评估,指标会有所不同,尽管你仍然需要一个标注数据集,其中包含问题和基于文档的匹配答案。你可以使用开源工具生成这个数据集,比如deepset的标注工具。
当你拥有正确的答案(由人工找到)时,如何评估聊天机器人生成的答案?最简单的指标,也是最严格的,是精确匹配(EM)。正如你从名称中可以想象的那样,它跟踪机器的答案与人工标注者提供的期望答案之间完全匹配的数量。另一种比较答案的简单指标是准确率,如果机器的答案与标注者提供的答案有任何重叠,就将其计为正确。
你可以理解这些指标在机器的答案接近但不完全相同于人工提供的答案时,可能会过于简单,且惩罚或奖励过度。这就是为什么在问答系统中工作的人会有自己版本的F1分数。问答F1分数基于期望答案与实际答案之间的词汇重叠。在这种情况下,精确度被定义为机器答案中共享词汇的数量与机器答案总词汇数的比率,而召回率则是共享词汇数与人工答案总词汇数的比率。正如你可以想象的那样,最难的任务是评估生成型聊天机器人的表现。
12.7.3 衡量用户体验
当涉及到衡量用户体验(UX)时,比起数学地计算NLP性能,事情变得不那么直接了。当然,你可以衡量一些表面信号,比如与聊天机器人互动的用户数量、交换的消息数量等。但这是否意味着用户与聊天机器人的体验是正面的呢?
幸运的是,对话设计师能够从其他界面(如网页和移动应用)的UX设计师那里借鉴许多UX指标。由于聊天机器人可以视为一种基于网页(或基于移动)的用户界面,因此许多用于衡量网页应用的指标同样适用于聊天机器人。在网页世界中,基本的衡量单位是事件——即用户在应用内的动作,例如打开页面、点击按钮、输入信息……基本上,任何可以被追踪的操作。 这些事件可以轻松地转化为聊天机器人世界的指标——例如,你可以追踪用户何时开始与聊天机器人互动、提问或说谢谢。但在你追踪的所有事件中,哪些是正确的衡量指标,如何衡量呢?
HEART 框架
2010年,谷歌研究人员提出了一个UX衡量框架,后来被应用设计师广泛采用。这个框架叫做HEART,包含了五个衡量指标类别,构成了该缩写:幸福感、参与度、采纳度、留存率和任务成功度。让我们按照时间顺序来看这些指标,看看它们如何与用户在使用聊天机器人过程中的不同阶段相关。
采纳度指标衡量有多少用户第一次使用你的聊天机器人。这是一个有用的指标,能够衡量你的推广或营销活动的效果。然而,使用聊天机器人可能意味着不同的事情,取决于你要改进的过程哪个部分。例如,对于一个数学教育机器人,你可能决定不关心那些只订阅聊天机器人但没有与之互动的学生。通常,只有极少数的注册用户会与聊天机器人进行任何实质性的互动。因此,你可能只想计算那些至少与聊天机器人互动过几条消息的学生。这可以帮助你忽略垃圾注册或家长代表孩子注册的情况。
另一个指标是功能采纳率,它对于跟踪你最近添加的新功能或维护成本较高的功能的流行度很有帮助。这为评估新功能对用户的价值提供了一个大致的A/B测试方法,而无需在随机对照试验中开关该功能。例如,你可能想要跟踪有多少数学辅导机器人的学生使用了新的问答功能或LLM功能。对于数学辅导聊天机器人,你不仅关心注册的总数,还关心通过入职流程并找到适合数学课程的用户数量。
参与度指标涉及聊天机器人使用的时长和强度。你可以衡量用户与聊天机器人互动的频率、提问数量以及在聊天会话中花费的时间等。例如,对于数学辅导聊天机器人,你可能想要看到用户访问了哪些课程,他们每个会话完成了多少课程,以及不同用户组进行的会话数量。
虽然很多人专注于虚荣指标,比如采纳率和参与度,最重要的指标是任务成功度指标。这些指标衡量你的聊天机器人在帮助用户通过聊天机器人完成任务的能力。例如,如果你的聊天机器人是教育性质的,那么你的主要绩效指标就是学生掌握你聊天机器人所教技能的速度。这可能需要你使用总结性评估(summative assessments),即学生能力的测验或测试,这些可以由聊天机器人自动评分。
也许,你学习工程工具箱中最强大的任务成功度指标就是形成性评估。这是指课程计划中包含与教育内容相结合的问题。这些类似闪卡的课程已经通过Duolingo、Anki和LibreLingo等应用程序广泛传播——这些应用本质上是在底层架构中运行的聊天机器人。你可以在聊天机器人平台上实现这些功能,例如ConvoHub、Botpress和Turn.io。在这些聊天机器人中,你可以衡量活跃用户完成课程的百分比,完成每门课程所花的时间,以及如果用户没有完成课程,他们的进度如何。如果你足够聪明,还可以通过集成到聊天机器人设计中的方式来评估学生,并且这些评估对用户透明——甚至富有娱乐性。你可以要求学生解答数学谜语和双关语,或者编写自己的数学笑话。你还可以统计学生在与聊天机器人的对话中正确使用数学术语或表达式的次数。当学生提出结构良好、语法正确的关于数学概念的问题时,这也是任务成功的衡量标准。
任务成功的概念与流失漏斗概念密切相关。漏斗图是一个将用户旅程分解为多个步骤的图表,并显示每个步骤中有多少用户退出。它对于理解用户在哪个环节失去兴趣以及可以采取哪些措施来改善用户体验非常有用。
对于辅导机器人,你需要跟踪每个课程的内部漏斗。这将帮助你了解哪些课程能够吸引学生进入互动对话,帮助他们学习。在图12.7中,你可以看到大多数学生从第1课顺利进行到第2课,并按照课程安排顺序继续进行数学课程。
在这个桑基图中,你可以看到学生很少跳过课程。如果他们感到沮丧或无聊,他们通常会直接停止与聊天机器人互动。为了应对这种流失,你需要深入分析导致流失的对话细节。开源Delvin分析平台中的互动桑基图功能允许对话设计师和分析师点击并追踪到单个对话。这是任何聊天机器人分析平台的一个关键特性,特别是当你想帮助用户达成目标时,无论是学习数学、从知识库中检索信息,还是为组织进行内部ChatOps(DevOps)。
幸福感指标相对简单:它们试图衡量用户对聊天机器人的满意度。但就像人类的幸福感一样,用户的幸福感也不是容易定义和衡量的。在大多数情况下,要了解用户对机器人有什么感受,你需要主动询问他们的体验。一个常见的幸福感衡量标准是净推荐值(NPS),它通过一个简单的问题来计算:你会向朋友或同事推荐这个聊天机器人吗?
最后,留存率关注的是在用户第一次互动后,有多少用户会返回你的聊天机器人。通常会通过时间来衡量留存率,例如日留存、周留存和月留存。虽然留存率并不是所有聊天机器人的关键指标(你不会希望你的客服聊天机器人用户每天都返回吧?),但对于那些需要用户反复使用的聊天机器人,像教育类聊天机器人,留存率是一个非常重要的指标。如果你希望用户长时间保持对辅导机器人(tutor bot)的参与度,你可能会想要衡量有多少用户在一周、一个月后再次回到机器人。
虽然这五个维度突出了UX的不同方面,但这并不意味着你必须全部使用这些指标或将它们优先级相同。你可以根据聊天机器人的目标选择关注的指标。
12.7.4 接下来做什么?
聊天机器人的世界正在迅速发展,但现在,你已经具备了跟上的工具和技能。你将能够分辨出最新的大型语言模型(LLM)是否只是旧有炒作的再现,或者它是否可能代表一种新的方法,能够智能地参与对话。而且,你现在知道如何构建基于规则的系统,将LLM的灵活性与搜索和基于规则的对话流程的可靠性结合起来。那么,如何将你的技能应用到一些实际的项目中呢?
这里有一些最前沿的开源NLP项目,你可以参与其中,提升你的NLP和聊天机器人技能。因为它们是开源的,你可以将它们重用,实现你改变世界的想法:
- ConvoHub——一个用于构建可教学AI的平台
- ConvoMeld——自动“重混”对话设计(图形)的算法
- MathText——帮助聊天机器人做数学的NLP工具——提取数学表达式和意图
- MathActive——用于程序化生成数学题目的示例代码
一个显而易见的下一步是为你的聊天机器人和NLP管道添加语音功能。你可以在ConvoHub上构建一个聊天机器人,或者使用你自己的开源Python工具组合。你的NLP软件可以帮助你整理电子邮件,或者在商业搜索引擎不够用时检索信息。本章的最后一部分将展示如何为你的聊天机器人添加语音,这样你就可以与聊天机器人进行免提对话。
为你的聊天机器人添加语音
尽管我们在本书中没有深入讨论语音处理,但你可能会想知道,你所学的NLP工具是否能帮助你构建一个类似Siri或Mycroft的语音助手。为了构建一个语音助手,你可以使用现有的语音识别或语音转文本(STT)软件来预处理聊天机器人的输入。然后,你可以使用语音生成或文本转语音(TTS)软件,用合成语音回应用户。图12.8展示了如何将这一切连接起来,创建一个语音助手。
一旦你的聊天机器人能够理解口语并以类似人类的声音进行回应,它开始给人一种“真的在思考和理解”的感觉,仿佛变得更像人类。这时,人们通常会开始将这种系统称为AI,尽管一个更准确的名称可能是虚拟助手或语音助手。UX设计师会把这种系统称为“语音优先界面”,如果语音是与应用程序交互的主要方式的话。然而,虚拟助手的真正智能行为并不在语音包装层,而是在聊天机器人或对话引擎内的NLP核心部分。那才是理解和思考真正发生的地方。
尽管虚拟助手的大部分智能都在NLP管道中,但你不应该认为语音接口会很简单——生成听起来逼真且具有你想传达的语气的语音可能会有些棘手。为了应对这些挑战,如果你想生成高质量的语音输出,可能需要依赖商业服务。以下是一些你可以在项目中加入语音接口时使用的最佳STT和TTS API和软件包:
- OpenTTS——一个开源的TTS引擎,你可以自托管。
- Hugging Face SpeechT5——在Hugging Face模型库上有多个版本的SpeechT5。
- Microsoft TTS——一个带有Web API的商业服务。
- Google TTS——一个带有Web API和Python SDK的商业服务。
- Amazon Poly——一个商业服务。
- Coqui TTS——一个商业服务。
不幸的是,除了反复试验外,目前没有简单的方法来评估和选择适合你聊天机器人的高质量语音。Papers With Code网站上的TTS排行榜可能无法反映你或你的用户对合成语音的需求。你将需要针对每个TTS服务进行实验,找出最适合你需求的。
幸运的是,评估STT软件的准确性要简单一些。使用开源STT基准数据集,你可以统计正确转录的单词数量。以下列出了在几个基准数据集上的单词错误率(WER)。
表12.3 TTS单词错误率
| AI | Phone | Meeting | Video | Finance | Mean | |
|---|---|---|---|---|---|---|
| Kaldi | 66% | 78% | 54% | 69% | 35% | 60% |
| wav2vec 2.0 | 33% | 41% | 39% | 26% | 17% | 31% |
| Whisper | 6% | 20% | 19% | 9% | 5% | 12% |
Wav2Vec 2.0集成在PyTorch(torchaudio)中,因此这可能是构建语音助手时的最佳选择,尽管你可能需要用自己的数据对TTS模型进行微调。如果你想要在开源模型中获得最先进的准确性,那么Whisper是你的最佳选择。你可以下载最新的Whisper模型,甚至使用Hugging Face的Whisper页面转录你自己的语音录音。在资源有限的环境中,更高效(但不太准确)的Kaldi模型可能已经足够。Mozilla DeepSpeech也提供了一种开源、自托管的STT方法。如果你不想自己托管STT模型,大型云平台提供了STT引擎:微软的Azure AI语音、谷歌的ASR和亚马逊的Transcribe。
构建一个有语音的聊天机器人比看起来要困难得多。如果你只是想要一个供个人使用的聊天机器人,那么开源包可能已经足够。然而,如果你希望检测用户何时试图唤醒你的机器人,你将需要实现唤醒词检测。你会发现,这需要对操作系统甚至硬件驱动程序进行低级访问,以高效且准确地检测唤醒命令。你可能需要一支工程师团队来做好这项工作。
幸运的是,有几个团队已经贡献了一个端到端的语音助手解决方案。最开放和成熟的语音助手是Mycroft。Mycroft的STT、聊天机器人和TTS引擎可以在任何Linux计算机上本地运行,包括树莓派。如果你想用你的NLP技能做一些有趣的事情,Mycroft是你的最佳选择。Mycroft可以与用户分享天气、新闻或维基百科文章,你还可以扩展它,加入你可能想到的更高级的功能。
通过NLP改善你的生活
希望现在你已经看到了如何在生活和工作中利用NLP的力量。聊天机器人只是你可以使用NLP帮助完成任务、改善健康、支持教育,甚至帮助你编写改变世界的应用程序的众多方式之一。现在你已经扩展了NLP工具箱,也许你在问自己如何学习更多并将其应用到现实世界中,你可能还想知道未来几年NLP技术的发展方向。
聊天机器人和NLP承诺改变我们的工作、娱乐,甚至艺术创作和学习世界的方式。就像互联网搜索引擎赋予我们大脑瞬间获取信息的超能力一样,聊天机器人承诺帮助我们解读这些信息并将其转化为知识。许多AI系统中的NLP正在自动化并增强越来越多的知识工作。工业革命可能会消除物理商品的稀缺,如果自动化和生产的力量能更广泛地与我们分享的话。不幸的是,贫困和无家可归依然是许多国家的问题,因为大多数国家尚未学习到“21个教训”。
类似地,AI和NLP承诺结束知识的稀缺。越来越多的知识工作者职位可以通过AI得到增强或自动化。如果NLP能普及到每个人,文书工作可能会成为过去。聊天机器人的危险性和潜力已经吸引了公众的想象力,并让我们所有人着迷;然而,聊天机器人对信息领域的有害影响显而易见。虚假信息运动、阴谋论和深度伪造使用NLP生成更具说服力和大量的文案。要从这场内容泛滥中筛选出真实、准确的信息和知识变得越来越难。你可以通过为世界提供自己开放、透明的搜索引擎、聊天机器人和虚拟助手来帮助改变这一现状。
如果这本书让你意识到社交媒体和自然语言内容如何在社会和生活中引发冲突,你可能急于采取行动。专有平台利用NLP向你推荐分裂性和耸人听闻的视频和帖子,当你低头盯着手机屏幕、浏览互联网时,这些内容就在你眼前。这本书向你展示了如何构建自己的聊天机器人、搜索引擎和推荐引擎,帮助你筛选出你生活中想要的思想和信息。你可以加入一个开源AI工程师的浪潮,使用NLP作为和平与合作的力量,帮助驯服虚假信息、减少偏见,并推动联合国的可持续发展目标,包括性别平等和为所有人提供优质教育。由像你这样的个人掌握,AI可以帮助构建一个更加公正、平等、可持续和合作的世界。现在你已经理解了NLP的力量,利用它明智地构建你希望生活的世界。
12.8 自测
- 合作对话伙伴的四个关键指标是什么(无论是聊天机器人还是人类)?
- 实现对话系统或聊天机器人的四种一般方法或算法是什么?
- 仅通过与规则基础的聊天机器人互动并记录大量对话作为脚本,是否可能逆向工程其对话图?你可能使用的Python包是什么?
- 应对用户表达的对话意图的胖尾(fat tail)的一些方法是什么?
- 聊天机器人能否同时使用生成性语言模型和基于规则的消息模板选择?
- 基于规则的聊天机器人的一些优缺点是什么?考虑用户体验以及规则基础对话系统的维护和可扩展性。
- 在基于规则的聊天机器人对话图中,图节点包含哪些信息?边(节点之间的连接)呢?
总结
- 要参与合作性对话,聊天机器人必须保持状态,理解用户意图,并能够生成有助于用户实现对话目标的文本。
- 尽管LLMs备受关注,基于规则的聊天机器人仍然是构建可以依赖与用户合作的聊天机器人的最成熟的方法。
- LLMs既不可解释也不可控制,因此不能成为任何试图开发安全和道德AI聊天机器人的组织中唯一使用的聊天机器人技术。
- 设计有效的对话,你必须利用自己天生的合作性对话能力。
- 对话设计不仅仅需要强大的写作技能。你还必须对用户有深刻的同理心和理解,才能理解他们可能想聊什么。
- 聊天机器人可以利用GOFAI游戏算法。AI与用户对话中的下一步应该最大化他们在对话中实现目标的累计得分,而不是你的目标或业务目标。