人机对话(1)-基于规则的Task-DM系统
以目标为中心的DM
对于任务型对话来讲,我们如果想要进行复杂的对话管理,首先要进行复杂场景的抽象。什么是对话过程中一直不变的?
流程不是,虽然它能快速解决问题
对话有顺序,但不必然,有的需要有先后,有的无所谓先后
信息呢,似乎有的必要,有的没有必要
但是所有的任务型对话都有终点:达成目标,而且与目标有关的信息是必要的,与目标有关的顺序也需要先后。
IBM-Watson Assistant是典型的这类设计,前半部分就以该产品来探索基于目标的DM系统
对话节点
看一下最简单的例子
对话节点用户输入一句话经过NLU模块解析后得知用户目的,从而bot通过判断后给一个响应
我们将处理同个目标的模块叫做一个节点;节点的基本构造是条件→响应;只要满足节点条件,就可以进入节点,bot根据内部逻辑判断给予响应
槽节点
再来看一个相对复杂的例子
槽节点
对于有的节点来讲,为了实现目标,必须要依赖一些信息;这些信息与目标的完成相关,这些信息我们称之为槽位,识别这些信息的过程称为填槽;
填槽所需要的信息可以来自于用户表述,也可以来自于外部接口(比如出发地可以取用户位置)
有的槽位信息没有不行,缺失就会进行追问(澄清),所以槽有必填与非必填之分
实现这样的槽位信息管理的过程本质上也是通过if-then的基本节点组成
为了方便管理对话,我们把因为节点信息依赖而嵌套多个子节点就叫做槽节点,基本上所有复杂对话场景中都会有槽节点;
上下文
在槽节点中,我们通过槽填充的过程捕捉了实体信息,信息值会一同存储在上下文中。
上下文在DM系统中就像是一个全局变量,可以被多个节点进行引用。并且值可以被更新。
有了上下文,再次进入节点时,已经填写的上下文槽值仍然存在,除非用户主动提供新的槽值,新值会覆盖旧值。而且对话也有了记忆功能,已经完成信息在节点之间的传递,带来更好的用户体验;
另一方面,上下文作为变量实现节点之间的逻辑判断(见对话流部分)
节点信息修改
对于槽节点来说,节点维护了一个上下文信息表,既然存在了填写信息的过程,那么用户就有修改的可能性;我们需要在该节点的触发条件上加上修改该节点信息的意图;
比如上文中订票节点,我们需要增加一个or关系的意图条件,从而当用户表达修改意图时,可以进入该节点(这里就能明显感受到基于目标的节点设计思路);后续用户表达中的对应实体部分会作为新的值覆盖原来存储在上下文中的槽值,从而对信息进行修改
修改示例
如上图所示,已经填写的槽值被替换,并可以返回给用户更改成功的回应;
节点总结
节点结构节点作为建立在用户目标层级上的元组件;通过以下三个内置逻辑来完成节点目标:
1、触发条件:节点的触发条件不仅仅是意图,同样包括对话过程中的信息与值,并且条件之间可以作为组合进行判断
2、信息依赖:为实现节点目标所需要的信息,即填槽的过程
3、对用户的响应不仅仅可能是文案,也有可能是其他形式的富消息(比如选项list);也有可能节点响应会输出复杂的逻辑:上下文更新、影响下个节点的触发等
对话流
在实际对话业务中由于业务的复杂性,想要实现某一个目标需要分步完成。就像现实生活中我们将目标分为一个个小目标,在某个阶段可能出现分叉口,每一步的选择可能造成不同的发展方向;整体来说对话流是一个树结构,节点是树的基石,节点之间的流转推动对话的进行。管理节点流转是DM的核心逻辑,需要解决以下问题
1、意图识别后基于系统业务主导下,从根节点到子节点的节点流转
2、在对话流程中,基于用户主导,在对话分支中或者对话分支之间进行跳转
3、节点的触发条件可能相似(比如intent:sayyes),如何避免歧义
因此,对话树的处理应该具有以下规则:
对话流按照顺序处理
服务沿着树一路向下,如果发现符合的条件,就会触发相应节点。然后用户进行下一步输入后,服务将检查子节点,遍历第一个子节点到最后一个子节点检查触发条件。以此遍历规则直至到达分支末尾的最后一个节点。从而推动业务流的发生,由于服务优先检查依赖节点,缩小了歧义的范围。
通过节点响应声明下一步的节点
在某个节点响应中,通过声明 jump to 可以定义服务下一个需要处理的节点。从而中断遍历顺序。比如:
1、下文中两个推荐节点处于平级节点,触发条件都是 yes or no;可以通过节点跳转实现触发正确的目标节点,而不是从第一个遍历
2、当用户答应为他推荐酒店时,我们定义响应跳转到另一个树的【酒店订购】节点,结合上下文设置进行触发。
跳转规则同样在一定程度避免了歧义
通过反向遍历寻找离题节点
服务到达分支末尾,或者在当前所求值的子节点集内找不到求值为 true 的条件时,会跳回至树的基本节点。随后,服务将再一次从第一个根节点到最后一个根节点进行处理。
所以服务路线支持了用户发起了话题转移,用户可以在当前节点通过声明遍历到前序节点或者其他树节点。
触发不触发依靠节点触发条件,但能不能触发还要依靠服务焦点
平行与依赖节点
我们可以把相互独立的一组信息在同一层级的节点处理,依赖这部分信息的放入后续节点处理,同一分支上不同层级的信息往往也带有不同的小目标,但是最终的大目标是相同的。
仍然拿订火车票举例
依赖节点
在完成车票订购这个目标上,【确定订票信息】节点与【下单确认】节点是依次进行的,在节点层级上后续节点需在前序节点基础上进行,属于依赖关系。同样的在下单完成后,后续的推荐节点因前序某些槽值信息从而带有不同的走向,也是依赖关系;依赖节点存在先后的执行关系。
平行节点
而对于【天气询问】【酒店订购】相对于【火车预定】是相互独立的,推荐的两个节点也是相互独立的,信息互不影响。平行节点不存在先后的执行关系。槽节点内部的槽之间也属于平行节点
基于服务的遍历规则,推动依赖节点精准完成目标,平行节点也不用顾虑顺序问题
离题
服务支持用户在对话过程中进行话题转移;当用户输入无法在当前节点以及后续节点找到匹配条件时,会进行前向遍历寻找匹配项。
1、跳转至其他节点进行对话流
在一次对话树进行中,用户突然转移话题至另一个对话树并进行下去。比如用户订票过程中突然放弃,转而询问附近游玩攻略
2、修改前序节点
前文提到过对节点槽值信息的修改,用户可以在当前节点修改已经填写过的槽值
用户也可以在后续节点比如下单确认中突然觉着不对想要修改,由于服务无法在后续节点找到true条件,前向遍历回到确定订票信息节点,从新进入节点后,上下文中新的信息覆盖旧的信息。
这里特别说明下:对于依赖节点,前序节点信息的修改会对后续节点进行影响,就比如修改火车票场景,如果已经完成了确认订单节点,再回去修改的话后续节点比如订单确认应该再走一遍。所以说对于这类依赖节点的修改应该自修改点重新开始,而且修改的同时清空后续的上下文,重新填写依赖槽位。
实际业务中,更有可能的是有些信息是不允许修改的,像订车票场景更应该的是前往改签节点。所以需要对节点的准入条件进行更多的限制;
3、跳转至其他节点后回到原来的节点
正常的对话交流中,更多出现的是因为某些因素终止对话但是后面会再回来继续。比如订票过程中填写目的地槽位时,用户突然询问了目的地的天气情况,发现下暴雨,然后用户回来后更改至隔壁城市。
对话节点支持设置离题后返回的内置逻辑;示例:
离题示例
总结
总结基于目标设计的对话流是一个树结构,由节点组成
NLU的意图与实体模块提供了树的初始起点与后续的条件与信息的来源。
节点实现了最基本的逻辑:触发条件-依赖信息获取-响应动作;节点通过槽填充获取必要信息,并将上下文作为全局信息进行维护;
顺序遍历、响应跳转、离题跳转实现节点流转,推动对话完成目标;解决了对话过程中复杂的引导、顺序、修改、歧义、直接触发、话题更改后返回等复杂问题
以意图为中心的DM(主流)
虽然以目标为中心的bot具有强大的业务支撑能力,但目前市场主流的产品都是另一种模式的设计:以意图为中心; 比如谷歌的dialogflow、百度unit、小爱同学等
下文主要以Google dialogflow 为参考
基于意图与基于目标有什么区别?
下面以订票节点为例
分离目标为任务执行意图任务本质上是将节点的触发条件与响应进行了拆分。系统的目标是完成当下用户的指令;
那么就会造成
1、依赖信息的冗余,订票与修改订票就都需要建立所依赖的槽位,虽然是完全一样的;
2、意图将不仅仅是语义更是任务,比如 “好的” 本身仅代表同意,但是意图作为任务时,订酒店的同意与订车票的同意就与任务耦合。其语料作为触发条件需要在各自的任务中都出现,虽然是完全一样的
3、歧义问题。意图之间不加以限制,识别会是平级,容易发生歧义。比如“好的”
但是,这样的设计非常好懂,符合人的理解;另一方面对于大多数业务来讲,应用频率最高的是意图任务层级;因为复杂的业务更适合GUI去处理,也就是说意图设计完全够用
意图设计结构
设计结构1、训练语料
作为触发意图的主要条件
2、参数
意图所依赖的信息,比如槽位等
3、响应
静态文案
富文本
事件响应(直接调用意图)
上下文更新
4、Fulfillment
DM的核心部分,dialog可以通过Fulfillment实现后端数据交互以及复杂的响应逻辑
5、上下文
DM的核心部分,作为条件作用于意图识别,也作为信息承载槽位值在意图之间的引用
区别于IBM的设计是:每次进入意图中,都是重新开始,上下文不会主动填写已填写槽位;
上下文需要声明才会存在,否则意图结束后,信息不会保留下来
6、事件
DM的核心部分,系统通过Fulfillment可以调用意图
基于意图任务的DM模块是与意图整合在一起的;其中事件、上下文以及Fulfillment交互工具是DM模块的主要组件,最重要的就是上下文。
谷歌dialogflow的上下文设计
总体来说,谷歌的对话管理相对其他竞品更完善
输出上下文:
1、作为载体承载信息,在存续期间可以被被其他意图引用
2、上下文存在激活与未激活的状态,可作为条件
备注:上下文中同一槽位的值是会被覆盖的
输入上下文:
作为准入条件(限制)实现对意图的精准识别;解决意图间的依赖关系
上下文作为准入条件是是且的关系,且关系的条件限制会更精确
仍以订票为例
dialogflow context
在【订票】意图输出 output_context,然后以该context作为input连接意图之间的识别;后续三个意图依赖于前序意图;避免了误识别的情况(比如别的意图也会包含是否的场景或者正常交谈过程中口语歧义)
同时,intent【订票】与intent【修改订票】之间存在槽信息依赖,需要上下文在意图之间传递信息,避免了不必要的追问。槽位信息的继承不仅会应用在相关意图,也可以应用在不同意图之间,只要上下文续存,信息就可以被引用;
在dialogflow 中,意图可以设计所谓的后续意图
后续意图其实就是输入输出上下文的自动创建,交互上更便捷
Fulfillment与事件
在谷歌对话系统中,Fulfillment作为与后端服务交互的路径,不仅接收接口信息或下达业务指令,还会在意图响应中设计动态响应(比如根据槽值不同)、更新上下文、通过事件直接调用意图。能够实现比较复杂的以系统主导的意图管理逻辑。这些管理逻辑在IBM系统中多数是抽象成功能放在节点管理页面上。
问题
谷歌的设计中,context必须在意图响应时生成,意味着如果在意图处理过程中进行跳转,已经填写的槽位是无法保留的,这种场景如果发生离题再通过意图声明回来时,相当于从新开始;在和谷歌客服的邮件沟通过程,他们的回复似乎更偏向于教怎样不能跳出,也就是说他们的设计本身就不鼓励没有完成任务就去做另一件事儿,如果跳出相当于放弃。
小爱、猎户星空等上下文设计
其他几个产品的上下文设计区别于谷歌,主要分为 连续对话语料 +前置意图
连续对话语料
连续对话语料截图连续对话语料应用于用户在同一个意图中进行的多轮对话信息之间的传递
本质上仍然是利用context对在意图之间传递,只不过在设计上偏向用户理解的层级
通过输入连续的语料,将【北京明天天气如何】与【那后天呢】这样的意图交给系统,这些意图都会触发天气意图,每一个都维护了槽位信息,再次触发时将槽位信息进行继承,只不过用户输入的新值会进行覆盖。
前置意图
前置意图截图前置意图解决的是在不同意图之间传递槽位的问题
当一个意图需要应用另一个意图的信息时,对前者开启前置意图的功能,对人来说很好理解。但是只要当前意图可能用的到的其他意图,都需要录入进去
总的来说这类前置意图的设计是不如context设计抽象的,因为这类信息依赖的问题不在于意图本身,而在于信息。因为这些意图都涉及到了某组信息,所以才导致意图之间存在前后的依赖;
context的设计是对信息进行了抽象,避免了前置意图这种设计在多场景信息依赖时意图维护的繁杂。
基于规则的DM设计就说到这里,这类DM的好处就是可控、可靠。当然有多少智能背后也就有多少人工。下一篇谈谈基于Data-drive的DM系统
(笔者正寻求NLP、人机对话方向产品职位 微信:cheng390552021)