作者 | 无名之辈FTER

责编 | 夕颜

出品 | 程序人生(ID:coder_life)

本文翻译自Rasa官方文档,并融合了自己的理解和项目实战,同时对文档中涉及到的技术点进行了一定程度的扩展,目的是为了更好的理解Rasa工作机制。与本文配套的项目GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,欢迎star和issues,我们共同讨论、学习!

对话管理

1.1 多轮对话

多轮对话是相对于单轮对话而言的,单轮对话侧重于一问一答,即直接根据用户的问题给出精准的答案。问答更接近一个信息检索的过程,虽然也可能涉及简单的上下文处理,但通常是通过指代消解和 query 补全来完成的,而多轮对话侧重于需要维护一个用户目标状态的表示和一个决策过程来完成任务,具体来说就是用户带着明确的目的而来,希望得到满足特定限制条件的信息或服务,例如:订餐,订票,寻找音乐、电影或某种商品等。因为用户的需求可以比较复杂,可能需要分多轮进行陈述,用户也可能在对话过程中不断修改或完善自己的需求。此外,当用户的陈述的需求不够具体或明确的时候,机器也可以通过询问、澄清或确认来帮助用户找到满意的结果。

因此,任务驱动的多轮对话不是一个简单的自然语言理解加信息检索的过程,而是一个决策过程,需要机器在对话过程中不断根据当前的状态决策下一步应该采取的最优动作(如:提供结果,询问特定限制条件,澄清或确认需求,等等)从而最有效的辅助用户完成信息或服务获取的任务。

注:任务驱动的多轮对话系统通常为封闭域(domain)(闲聊系统为开放域),而特定限制条件对应于槽(Slot),也就是说,用户在满足特定限制条件时就是一次槽值填充的过程,如果用户能够在一次会话中,满足全部的限制条件,那么就不必进行多轮对话,即可直接使用户得到满意的信息或服务。

1.2 对话管理

对话管理,即Dialog Management(DM),它控制着人机对话的过程,是人机对话系统的重要组成部分。DM会根据NLU模块输出的语义表示执行对话状态的更新和追踪,并根据一定策略选择相应的候选动作。简单来说,就是DM会根据对话历史信息,决定此刻对用户的反应,比如在任务驱动的多轮对话系统中,用户带着明确的目的如订餐、订票等,用户需求比较复杂,有很多限制条件,可能需要分多轮进行陈述,一方面,用户在对话过程中可以不断修改或完善自己的需求,另一方面,当用户的陈述的需求不够具体或明确的时候,机器也可以通过询问、澄清或确认来帮助用户找到满意的结果。如下图所示,DM 的输入就是用户输入的语义表达(或者说是用户行为,是 NLU 的输出)和当前对话状态,输出就是下一步的系统行为和更新的对话状态。这是一个循环往复不断流转直至完成任务的过程。

从本质上来说,**任务驱动的对话管理实际就是一个决策过程,系统在对话过程中不断根据当前状态决定下一步应该采取的最优动作(如:提供结果,询问特定限制条件,澄清或确认需求等),从而最有效的辅助用户完成信息或服务获取的任务。**对话管理的任务大致有:

  • 对话状态维护(dialog state tracking, DST)

对话状态是指记录了哪些槽位已经被填充、下一步该做什么、填充什么槽位,还是进行何种操作。用数学形式表达为,t+1 时刻的对话状态S(t+1),依赖于之前时刻 t 的状态St,和之前时刻 t 的系统行为At,以及当前时刻 t+1 对应的用户行为O(t+1)。可以写成S(t+1)←St+At+O(t+1)。

  • 生成系统决策(dialog policy)

根据 DST 中的对话状态(DS),产生系统行为(dialog act),决定下一步做什么 dialog act 可以表示观测到的用户输入(用户输入 -> DA,就是 NLU 的过程),以及系统的反馈行为(DA -> 系统反馈,就是 NLG 的过程)。

  • 作为接口与后端/任务模型进行交互

Rasa Core

Rasa Core是Rasa框架提供的对话管理模块,它类似于聊天机器人的大脑,主要的任务是维护更新对话状态和动作选择,然后对用户的输入作出响应。所谓对话状态是一种机器能够处理的对聊天数据的表征,对话状态中包含所有可能会影响下一步决策的信息,如自然语言理解模块的输出、用户的特征等;所谓动作选择,是指基于当前的对话状态,选择接下来合适的动作,例如向用户追问需补充的信息、执行用户要求的动作等。举一个具体的例子,用户说“帮我妈妈预定一束花”,此时对话状态包括自然语言理解模块的输出、用户的位置、历史行为等特征。在这个状态下,系统接下来的动作可能是:

  • 向用户询问可接受的价格,如“请问预期价位是多少?”;

  • 向用户确认可接受的价格,如“像上次一样买价值200的花可以吗?”

  • 直接为用户预订

2.1 Stories

Rasa的故事是一种训练数据的形式,用来训练Rasa的对话管理模型。故事是用户和人工智能助手之间的对话的表示,转换为特定的格式,其中用户输入表示为相应的意图(和必要的实体),而助手的响应表示为相应的操作名称。Rasa核心对话系统的一个训练示例称为一个故事。这是一个故事数据格式的指南。两段对话样本示例:

<!-- ##表示story的描述,没有实际作用 -->
## greet + location/price + cuisine + num people
* greet
   - utter_greet
* inform{"location": "rome", "price": "cheap"}
   - action_on_it
   - action_ask_cuisine
* inform{"cuisine": "spanish"}
   - action_ask_numpeople    
* inform{"people": "six"}
   - action_ack_dosearch


<!-- Form Action-->
## happy path 
* request_weather
   - weather_form
   - form{"name": "weather_form"}
   - form{"name": null}

Story格式大致包含三个部分:

  • 1. 用户输入(User Messages)

使用*开头的语句表示用户的输入消息,我们无需使用包含某个具体内容的输入,而是使用NLU管道输出的intent和entities来表示可能的输入。需要注意的是,如果用户的输入可能包含entities,建议将其包括在内,将有助于policies预测下一步action。这部分大致包含三种形式,示例如下:

(1)* greet 表示用户输入没有entity情况;

(2)* inform{"people": "six"} 表示用户输入包含entity情况,响应这类intent为普通action;

(3)* request_weather 表示用户输入Message对应的intent为form action情况;

  • 2. 动作(Actions)

使用-开头的语句表示要执行动作(Action),可分为utterance actions和custom actions,其中,前者在domain.yaml中定义以utter_为前缀,比如名为greet的意图,它的回复应为utter_greet;后者为自定义动作,具体逻辑由我们自己实现,虽然在定义action名称的时候没有限制,但是还是建议以action_为前缀,比如名为inform的意图fetch_profile的意图,它的response可为action_fetch_profile。

  • 3. 事件(Events)

Events也是使用-开头,主要包含槽值设置(SlotSet)和激活/注销表单(Form),它是是Story的一部分,并且必须显示的写出来。Slot Events和Form Events的作用如下:

(1)Slot Events

Slot Events的作用当我们在自定义Action中设置了某个槽值,那么我们就需要在Story中Action执行之后显著的将这个SlotSet事件标注出来,格式为- slot{"slot_name": "value"}。比如,我们在action_fetch_profile中设置了Slot名为account_type的值,代码如下:

from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests




class FetchProfileAction(Action):
    def name(self):
        return "fetch_profile"




    def run(self, dispatcher, tracker, domain):
        url = "http://myprofileurl.com"
        data = requests.get(url).json
        return [SlotSet("account_type", data["account_type"])]

那么,就需要在Story中执行action_fetch_profile之后,添加- slot{"account_type" : "premium"}。虽然,这么做看起来有点多余,但是Rasa规定这么做必须的,目的是提高训练时准确度。

## fetch_profile
* fetch_profile
   - action_fetch_profile
   - slot{"account_type" : "premium"}
   - utter_welcome_premium

当然,如果您的自定义Action中将槽值重置为None,则对应的事件为-slot{"slot_name": null}。

(2)Form Events

在Story中主要存在三种形式的表单事件(Form Events),它们可表述为:

  • Form Action事件

Form Action即表单动作事件,是自定义Action的一种,用于一个表单操作。示例如下:

- restaurant_form
  • Form activation事件

form activation即激活表单事件,当form action事件执行后,会立马执行该事件。示例如下:

- form{"name": "restaurant_form"}
  • Form deactivation事件

form deactivation即注销表单事件,作用与form activation相反。示例如下:

- form{"name": null}

总之,我们在构建Story时,可以说是多种多样的,因为设计的故事情节是多种多样的,这就意味着上述三种内容的组合也是非常灵活的。另外,在设计Story时Rasa还提供了Checkpoints 和OR statements两种功能,来提升构建Story的灵活度,但是需要注意的是,东西虽好,但是不要太贪了,过多的使用不仅增加了复杂度,同时也会拖慢训练的速度。其中,Checkpoints用于模块化和简化训练数据,示例如下:

## first story
* greet
   - action_ask_user_question
> check_asked_question




## user affirms question
> check_asked_question
* affirm
  - action_handle_affirmation
> check_handled_affirmation




## user denies question
> check_asked_question
* deny
  - action_handle_denial
> check_handled_denial




## user leaves
> check_handled_denial
> check_handled_affirmation
* goodbye
  - utter_goodbye

在上面的例子中,可以使用> check_asked_question表示first story,这样在其他story中,如果有相同的first story部分,可以直接用> check_asked_question代替。而OR Statements主要用于实现某一个action可同时响应多个意图的情况,比如下面的例子:

## story
* affirm OR thankyou
  - action_handle_affirmation

2.2 Domain

Domain,译为**“领域”**,它描述了对话机器人应知道的所有信息,类似于“人的大脑”,存储了意图intents、实体entities、插槽slots以及动作actions等信息,其中,intents、entities在NLU训练样本中定义,slots对应于entities类型,只是表现形式不同。domain.yml文件组成结构如下:

具体介绍如下:

1. intents

intents:
  - affirm
  - deny
  - greet
  - request_weather
  - request_number
  - inform
  - inform_business
  - stop
  - chitchat

intents,即意图,是指我们输入一段文本,希望Bot能够明白这段文本是什么意思。在Rasa框架中,意图的定义是在NLU样本中实现的,并且在每个意图下面我们需要枚举尽可多的样本用于训练,以达到Bot能够准确识别出我们输入的一句话到底想要干什么。

2. session_config

session_config:
  carry_over_slots_to_new_session: true
  session_expiration_time: 60

session_config,即会话配置,这部分的作用为配置一次会话(conversation session)是否有超时限制。上例演示的是,每次会话的超时时间为60s,如果用户开始一段会话后,在60s内没有输入任何信息,那么这次会话将被结束,然后Bot又会开启一次新的会话,并将上一次会话的Slot值拷贝过来。当然,我们希望舍弃上一次会话Slot的值,可以将carry_over_slots_to_new_session设置为false。另外,当session_expiration_time被设置为0时,Bot永远不会结束当前会话并一直等待用户输入(注:执行action_session_start仍然可以开始一次新的会话,在设置为0的情况下)。

3. slots

slots:
  date_time:
    type: unfeaturized
    auto_fill: false
  address:
    type: unfeaturized
    auto_fill: false

Slots,即插槽,它就像对话机器人的内存,它通过键值对的形式可用来收集存储用户输入的信息(实体)或者查询数据库的数据等。关于Slots的设计与使用,详情请见本文2.6小节。

4. entities

entities:
  - date_time
  - address

entities,即实体,类似于输入文本中的关键字,需要在NLU样本中进行标注,然后Bot进行实体识别,并将其填充到Slot槽中,便于后续进行相关的业务操作。

5. actions

actions:
  - utter_answer_affirm     # utter_开头的均为utter actions
  - utter_answer_deny
  - utter_answer_greet
  - utter_answer_goodbye
  - utter_answer_thanks
  - utter_answer_whoareyou
  - utter_answer_whattodo
  - utter_ask_date_time
  - utter_ask_address
  - utter_ask_number
  - utter_ask_business
  - utter_ask_type
  - action_default_fallback # default actions

当Rasa NLU识别到用户输入Message的意图后,Rasa Core对话管理模块就会对其作出回应,而完成这个回应的模块就是action。Rasa Core支持三种action,即default actions、utter actions以及 custom actions。关于如何实现Actions和处理业务逻辑,我们在一篇文章中详谈,这里仅作简单了解。

6. forms

forms:
  - weather_form

forms,即表单,该部分列举了在NLU样本中定义了哪些Form Actions。关于Form Actions的相关知识,请移步至本文的2.7小节。

7. responses

responses:
  utter_answer_greet:
    - text: "您好!请问我可以帮到您吗?"
    - text: "您好!很高兴为您服务。请说出您要查询的功能?"


  utter_ask_date_time:
    - text: "请问您要查询哪一天的天气?"




  utter_ask_address:
    - text: "请问您要查下哪里的天气?"


  utter_default:
    - text: "没听懂,请换种说法吧~"

responses部分就是描述UtterActions具体的回复内容,并且每个UtterAction下可以定义多条信息,当用户发起一个意图,比如 “你好!”,就触发utter_answer_greet操作,Rasa Core会从该action的模板中自动选择其中的一条信息作为结果反馈给用户。

2.3 Responses

Responses的作用就是自动响应用户输入的信息,因此我们需要管理这些响应(Responses)。Rasa框架提供了三种方式来管理Responses,它们是:

  • 在domain.yaml文件中存储Responses;

  • 在训练数据中存储Responses;

  • 自定义一个NLG服务来生成Responses。

由于第一种我们在本文2.2(7)小节有过介绍,而创建NLG服务是这样的:

nlg:
  url: http://localhost:5055/nlg    # url of the nlg endpoint
  # you can also specify additional parameters, if you need them:
  # headers:
  #   my-custom-header: value
  # token: "my_authentication_token"    # will be passed as a get parameter
  # basic_auth:
  #   username: user
  #   password: pass
# example of redis external tracker store config
tracker_store:
  type: redis
  url: localhost
  port: 6379
  db: 0
  password: password
  record_exp: 30000
# example of mongoDB external tracker store config
#tracker_store:
  #type: mongod
  #url: mongodb://localhost:27017
  #db: rasa
  #user: username
  #password: password

2.4 Actions

当Rasa NLU识别到用户输入Message的意图后,Rasa Core对话管理模块就会对其作出回应,而完成这个回应的模块就是action。Rasa Core支持三种action,即default actions、utter actions以及 custom actions。关于如何实现Actions和处理业务逻辑,我们在一篇文章中详谈,这里仅作简单了解。

1. default actions

DefaultAction是Rasa Core默认的一组actions,我们无需定义它们,直接可以story和domain中使用。包括以下三种action:

  • action_listen:监听action,Rasa Core在会话过程中通常会自动调用该action;

  • action_restart:重置状态,比初始化Slots(插槽)的值等;

  • action_default_fallback:当Rasa Core得到的置信度低于设置的阈值时,默认执行该action;

2. utter actions

UtterAction是以utter_为开头,仅仅用于向用户发送一条消息作为反馈的一类actions。定义一个UtterAction很简单,只需要在domain.yml文件中的actions:字段定义以utter_为开头的action即可,而具体回复内容将被定义在templates:部分,这个我们下面有专门讲解。定义utter actions示例如下:

actions:
  - utter_answer_greet
  - utter_answer_goodbye
  - utter_answer_thanks
  - utter_introduce_self
  - utter_introduce_selfcando
  - utter_introduce_selffrom

3. custom actions

CustomAction,即自定义action,允许开发者执行任何操作并反馈给用户,比如简单的返回一串字符串,或者控制家电、检查银行账户余额等等。它与DefaultAction不同,自定义action需要我们在domain.yml文件中的actions部分先进行定义,然后在指定的webserver中实现它,其中,这个webserver的url地址在endpoint.yml文件中指定,并且这个webserver可以通过任何语言实现,当然这里首先推荐python来做,毕竟Rasa Core为我们封装好了一个rasa-core-sdk专门用来处理自定义action。关于action web的搭建和action的具体实现,我们在后面详细讲解,这里我们看下在在Rasa Core项目中需要做什么。假如我们在天气资讯的人机对话系统需提供查询天气和空气质量两个业务,那么我们就需要在domain.yml文件中定义查询天气和空气质量的action,即:

actions:
  ...  
  - action_search_weather

另外,FormAction也是自定义actions,但是需要在domainl.yaml文件的forms字段声明。

forms:
  - weather_form

2.5 Policies

Policies是Rasa Core中的策略模块,对应类rasa_core.policies.Policy,它的作用就是使用合适的策略(Policy)来预测一次对话后要执行的行为(Actions)。预测的原理是衡量命中的哪些Policies哪个置信度高,由置信度高的Policy选择合适的Action执行。假如出现不同的Policy拥有相同的置信度,那么就由它们的优先级决定,即选择优先级高的Policy。Rasa对提供的Policies进行了优先级排序,具体如下表:

它们的描述与作用如下:

  • Memoization Policy

MemoizationPolicy只记住(memorizes)训练数据中的对话。如果训练数据中存在这样的对话,那么它将以置信度为1.0预测下一个动作,否则将预测为None,此时置信度为0.0。下面演示了如何在策略配置文件config.yml文件中,配置MemoizationPlicy策略,其中,max_history(超参数)决定了模型查看多少个对话历史以决定下一个执行的action。

 policies:
    - name: "MemoizationPolicy"
    max_history: 5

注:max_history值越大训练得到的模型就越大并且训练时间会变长,关于该值到底该设置多少,我们可以举这么个例子,比如有这么一个Intent:out_of_scope来描述用户输入的消息off-topic(离题),当用户连续三次触发out_of_scope意图,这时候我们就需要主动告知用户需要向其提供帮助,如果要Rasa Core能够学习这种模型,max_history应该至少为3。story.md中表现如下:

* out_of_scope
   - utter_default
* out_of_scope
   - utter_default
* out_of_scope
   - utter_help_message
  • Keras Policy

KerasPolicy策略是Keras框架中实现的神经网络来预测选择执行下一个action,它默认的框架使用LSTM(Long Short-Term Memory,长短期记忆网络)算法,但是我们也可以重写KerasPolicy.model_architecture函数来实现自己的框架(architecture)。KerasPolicy的模型很简单,只是单一的LSTM+Dense+softmax,这就需要我们不断地完善自己的story来把各种情况下的story进行补充。下面演示了如何在策略配置文件config.yml文件中,配置KerasPolicy策略,其中,epochs表示训练的次数,max_history同上。

policies:
  - name: KerasPolicy
    epochs: 100
    max_history: 5
  • Embedding Policy

基于机器学习的对话管理能够学习复杂的行为以完成任务,但是将其功能扩展到新领域并不简单,尤其是不同策略处理不合作用户行为的能力,以及在学习新任务(如预订酒店)时,如何将完成一项任务(如餐厅预订)重新应用于该任务时的情况。EmbeddingPolicy,即循环嵌入式对话策略(Recurrent Embedding Dialogue Policy,REDP),它通过将actions和对话状态嵌入到相同的向量空间(vector space)能够获得较好的效果,REDP包含一个基于改进的Neural Turing Machine的记忆组件和注意机制,在该任务上显著优于基线LSTM分类器。

EmbeddingPolicy效果上优于KerasPolicy,但是它有个问题是耗时,因为它没有使用GPU、没有充分利用CPU资源。KerasPolicy和EmbeddingPolicy比较示意图如下:

配置EmbeddingPolicy参数:

policies:
  - name: EmbeddingPolicy
    epochs: 100
    featurizer:
    - name: FullDialogueTrackerFeaturizer
      state_featurizer:
        - name: LabelTokenizerSingleStateFeaturizer

注:新版的Rasa将EmbeddingPolicy重命名为TEDPolicy,但是我在config.yml配置文件中将其替换后,提示无法找到TEDPolicy异常,具体原因不明,暂还未涉及源码分析。

  • Form Policy

FormPolicy是MemoizationPolicy的扩展,用于处理(form)表单的填充事项。当一个FormAction被调用时,FormPolicy将持续预测表单动作,直到表单中的所有槽都被填满,然后再执行对应的FormAction。如果在Bot系统中使用了FormActions,就需要在config.yml配置文件中进行配置。

policies:
  - name: FormPolicy
  • Mapping Policy

MappingPolicy可用于直接将意图映射到要执行的action,从而实现被映射的action总会被执行,其中,这种映射是通过triggers属性实现的。举个栗子(domain.yml文件中):

intents:
 - greet: {triggers: utter_goodbye}

其中,greet是意图;utter_goodbye是action。一个意图最多只能映射到一个action,我们的机器人一旦收到映射意图的消息,它将执行对应的action。然后,继续监听下一条message。需要注意的是,对于上述映射,我们还需要要在story.md文件中添加如下样本,否则,任何机器学习策略都可能被预测的action_greet在dialouge历史中突然出现而混淆。

  • Fallback Policy

如果意图识别的置信度低于nlu_threshold,或者没有任何对话策略预测的action置信度高于core_threshold,FallbackPolicy将执行fallback action。通俗来说,就是我们的对话机器人意图识别和action预测的置信度没有满足对应的阈值,该策略将使机器人执行指定的默认action。configs.yml配置如下:

policies:
  - name: "FallbackPolicy"
    # 意图理解置信度阈值
    nlu_threshold: 0.3
    # action预测置信度阈值
    core_threshold: 0.3
    # fallback action
    fallback_action_name: 'action_default_fallback'

其中,action_default_fallback是Rasa Core中的一个默认操作,它将向用户发送utter_default模板消息,因此我们需要确保在domain.yml文件中指定此模板。当然,我们也可以在fallback_action_name字段自定义默认回复的action,比如my_fallback_cation,就可以这么改:

policies:
  - name: "FallbackPolicy"
    nlu_threshold: 0.4
    core_threshold: 0.3
    fallback_action_name: "my_fallback_action"

2.6 Slots

Slots,槽值,相当于机器人的内存(memory),它们以键值对的形式存在,用于存储用户输入时消息时比较重要的信息,而这些信息将为Action的执行提供关键数据。Slots的定义位于domain.yaml文件中,它们通常与Entities相对应,即Entities有哪些,Slots就有哪些,并且Slots存储的值就是NLU模型提取的Entities的值。

2.6.1 Slots Type

1. Text类型

示例:

# domain.yaml
slots:
   cuisine:
      type: text

2. Boolean类型

示例:

slots:
   is_authenticated:
      type: bool

3. categorical类型

示例:

slots:
   risk_level:
      type: categorical
      values:
      - low
      - medium
      - high

4. Float类型

示例:

slots:
   temperature:
      type: float
      min_value: -100.0
      max_value:  100.0

5. List类型

示例:

slots:
   shopping_items:
      type: list

6. Unfeaturized 类型

示例:

slots:
   internal_user_id:
      type: unfeaturized

2.6.2 Slots Set

Slots值填充有多种方式,它们的操作方式如下:

1. Slots Initial

# domain.yaml
slots:
  name:
    type: text
    initial_value: "human"

在domain.yaml文件中声明slots时,可以通过initial_value字段为当前slot提供一个初始值,也就是说,当会话开始时,被设定初始值的slot已经被填充好。当然,这个操作不是必须的。

2. Slots Set from NLU

# stories.md
# story_01
* greet{"name": "Ali"}
  - slot{"name": "Ali"}
  - utter_greet

假如在stories.md文件添加一个包含-slot{}的story,这就意味着当NLU模型提取到一个名为name的实体且这个实体有在domain.yaml中定义,那么NLU模型提取到的实体值会被自动填充到name槽中。实际上,对于Rasa来说,就算你不添加-slot{}字段,这个实体值也会被提取并自动填充到name槽中。当然,如果你希望禁止这种自动填充行为,改为添加-slot{}字段填充,可以在domain.yaml定义slot时,设置auto_fill的值为False,即:

# domain.yaml
slots:
  name:
    type: text
    auto_fill: False

3. Slots Set By Clicking Buttons

# domain.yaml
utter_ask_color:
- text: "what color would you like?"
  buttons:
  - title: "blue"                           
    payload: '/choose{"color": "blue"}'  # 格式 '/intent{"entity":"value",...}' 
  - title: "red"
    payload: '/choose{"color": "red"}'   

在点击Button时填充Slots的值,是指当我们的Bot(Rasa Core)在回复用户时,可以在回复的消息中附加Button信息,这种Button类似于快捷键,用户获取到之后,可以直接将其发送给Rasa Core,它会直接进行解析以识别intent和提取entity,并将entity的值填充到slot中。比如你让用户通过点击一个按钮来选择一种颜色,那么可以在domain.yaml中utter_ask_color的回复中添加buttons:/choose{"color": "blue"}和/choose{"color": "red"}。注:通常每个button由title和payload字段组成。

4. Slots Set by Actions

from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests




class FetchProfileAction(Action):
    def name(self):
        return "fetch_profile"




    def run(self, dispatcher, tracker, domain):
        url = "http://myprofileurl.com"
        data = requests.get(url).json
        return [SlotSet("account_type", data["account_type"])]

该示例演示了如何在Custom Action中通过返回事件来填充Slots的值,即调用SlotSet事件函数并将该事件return。需要注意的是,为了达到这个目的,我们在编写Story时必须包含该Slot,即使用-slot{}实现,只有这样Rasa Core就会从提供的信息中进行学习,并决定执行正确的action。Story.md示例如下:

# story_01
* greet
  - action_fetch_profile
  - slot{"account_type" : "premium"}
  - utter_welcome_premium




# story_02
* greet
  - action_fetch_profile
  - slot{"account_type" : "basic"}
  - utter_welcome_basic

其中,account_type在domain.yaml中定义如下:

slots:
   account_type:
      type: categorical
      values:
      - premium
      - basic

2.6.3 Slots Get

目前主要有两种获取Slots值方式:

1. Get Slot in responses

responses:
  utter_greet:
  - text: "Hey, {name}. How are you?"

在domain.yaml的responses部分,可以通过{slotname}的形式获取槽值。

2. Get Slot in Custom Action

from rasa_sdk.actions import Action
from rasa_sdk.events import SlotSet
import requests




class FetchProfileAction(Action):
    def name(self):
        return "fetch_profile"




    def run(self, dispatcher, tracker, domain):
        # 获取slot account_type的值
        account_type = tracker.get_slot('account_type')
        return []

Tracker,可理解为跟踪器,作用是以会话会话的形式维护助手和用户之间的对话状态。通过Tracker,能够轻松获取整个对话信息,其中就包括Slot的值。

2.7 Form

在Rasa Core中,当我们执行一个action需要同时填充多个slot时,可以使用FormAction来实现,因为FormAction会遍历监管的所有slot,当发现相关的slot未被填充时,就会向用户主动发起询问,直到所有slot被填充完毕,才会执行接下来的业务逻辑。使用步骤如下:

(1)构造story

在story中,不仅需要考虑用户按照我们的设计准确的提供有效信息,而且还要考虑用户在中间过程改变要执行的意图情况或称输入无效信息,因为对于FormAction来说,如果无法获得预期的信息就会报错,这里我们分别称这两种情况为happy path、unhappy path。示例如下:

## happy path
* request_weather
    - weather_form
    - form{"name": "weather_form"}  激活form
    - form{"name": null}  使form无效
## unhappy path
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* stop
    - utter_ask_continue
* deny
    - action_deactivate_form
    - form{"name": null}

注:* request_restaurant为意图;- restaurant_form为form action;- form{"name": "restaurant_form"}为激活form;- form{"name": null}为注销form;- action_deactivate_form为默认的action,它的作用是用户可能在表单操作过程中改变主意,决定不继续最初的请求,我们使用这个default action来禁止(取消)表单,同时重置要请求的所有slots。

构建stroy最好使用官方提供的Interactive Learning,防止漏掉信息,详细见本文2.8小节。

(2)添加form字段到Domain

在doamin文件下新增forms:部分,并将所有用到的form名称添加到该字段下:

intents:
  - request_weather


forms:
  - weather_form

(3)配置FormPolicy

 在工程的配置文件configs.yml中,新增FormPolicy策略:

policies:
  - name: EmbeddingPolicy
    epochs: 100
    max_history: 5
  - name: FallbackPolicy
    fallback_action_name: 'action_default_fallback'
  - name: MemoizationPolicy
    max_history: 5
  - name: FormPolicy

(4)Form Action实现

class WeatherForm(FormAction):




    def name(self) -> Text:
        """Unique identifier of the form"""




        return "weather_form"




    @staticmethod
    def required_slots(tracker: Tracker) -> List[Text]:
        """A list of required slots that the form has to fill"""




        return ["date_time", "address"]




    def submit(
            self,
            dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict[Text, Any],
    ) -> List[Dict]:
        """Define what the form has to do
            after all required slots are filled"""
        address = tracker.get_slot('address')
        date_time = tracker.get_slot('date_time')




        return []

当form action第一被调用时,form就会被激活并进入FormPolicy策略模式。每次执行form action,required_slots会被调用,当发现某个还未被填充时,会主动去调用形式为uter_ask_{slotname}的模板(注:定义在domain.yml的templates字段中);当所有slot被填充完毕,submit方法就会被调用,此时本次form操作完毕被取消激活。

2.8 Interactive Learning

虽然我们可以容易的人工构建story样本数据,但是往往会出现一些考虑不全,甚至出错等问题,基于此,Rasa Core框架为我们提供了一种交互式学习(Interactive Learning)来获得所需的样本数据。在互动学习模式中,当你与机器人交谈时,你会向它提供反馈,这是一种强大的方法来探索您的机器人可以做什么,也是修复它所犯错误的最简单的方法。基于机器学习的对话的一个优点是,当你的机器人还不知道如何做某事时,你可以直接教它。

(1)开启Action Server

python -m rasa run actions --port 5055 --actions actions --debug

(2)开启Interactive Learning

python -m rasa interactive -m models/20200313-101055.tar.gz --endpoints configs/endpoints.yml --config configs/config.yml 
# 或者(没有已训练模型情况)
# rasa会先训练好模型,再开启交互式学习会话
python -m rasa interactive --data /data --domain configs/domain.yml --endpoints configs/endpoints.yml --config configs/config.yml 

分别执行(1)、(2)命令后,我们可以预设一个交互场景根据终端的提示操作即可。如果一个交互场景所有流程执行完毕,按Ctrl+C结束并选择Start Fresh进入下一个场景即可。当然Rasa还提供了可视化界面,以帮助你了解每个Story样本构建的过程,网址:http://localhost:5005/visualization.html。

执行流程大致如下:

Bot loaded. Visualisation at http://localhost:5006/visualization.html .
Type a message and press enter (press 'Ctr-c' to exit).
? Your input -> 查询身份证439912199008071234
? Is the intent 'request_idcard' correct for '查询身份证[439912199008071234](id_number)' and are all entities labeled correctly?  Yes
------
Chat History




 #    Bot                                               You       
───────────────────────────────────────────────────────────────────
 1    action_listen
───────────────────────────────────────────────────────────────────
 2                       查询身份证[439912199008071234](id_number)
                                       intent: request_idcard 1.00








Current slots:
        address: None, business: None, date-time: None, id_number: None, requested_slot: None




------
? The bot wants to run 'number_form', correct?  Yes
Chat History




 #    Bot                                                                                                                              You       
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 1    action_listen
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 2                                                                                                      查询身份证[439912199008071234](id_number)
                                                                                                                      intent: request_idcard 1.00
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 3    number_form 1.00
      您要查询的身份证号码439912199008071234所属人为张三,湖南长沙人,现在就职于地球村物业。
      form{"name": "number_form"}
      slot{"id_number": "439912199008071234"}
      form{"name": null}
      slot{"requested_slot": null}








Current slots:
        address: None, business: None, date-time: None, id_number: 439912199008071234, requested_slot: None




------
? The bot wants to run 'action_listen', correct?  Yes

生成的一个Story示例如下:

## interactive_story_10
# unhappy path:chitchat stop but continue path
* greet
    - utter_answer_greet
* request_number{"type": "身份证号码"}
    - number_form
    - form{"name": "number_form"}
    - slot{"type": "身份证号码"}
    - slot{"number": null}
    - slot{"business": null}
    - slot{"requested_slot": "number"}
* chitchat
    - utter_chitchat
    - number_form
    - slot{"requested_slot": "number"}
* stop
    - utter_ask_continue
* affirm
    - number_form
    - slot{"requested_slot": "number"}
* form: request_number{"number": "440123199087233467"}
    - form: number_form
    - slot{"number": "440123199087233467"}
    - slot{"type": "身份证号码"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_noworries

改进ChitChatAssistant项目

3.1 config.yml

# zh_jieba_mitie_embeddings_config.yml




language: "zh"




pipeline:
- name: "MitieNLP"
  model: "data/total_word_feature_extractor_zh.dat"
- name: "JiebaTokenizer"
  dictionary_path: "data/dict"
- name: "MitieEntityExtractor"
- name: "EntitySynonymMapper"
- name: "RegexFeaturizer"
- name: "MitieFeaturizer"
- name: "EmbeddingIntentClassifier"




policies:
  - name: FallbackPolicy
    nlu_threshold: 0.5
    ambiguity_threshold: 0.1
    core_threshold: 0.5
    fallback_action_name: 'action_default_fallback'
  - name: MemoizationPolicy
    max_history: 5
  - name: FormPolicy
  - name: MappingPolicy
  - name: EmbeddingPolicy
    epochs: 500

考虑到目前项目的样本较少,这里使用MITIE+EmbeddingPolicy组合,虽然训练时慢了点,但是能够保证实体提取的准确性,同时又能够提高意图识别的命中率。

3.2 weather_stories.md

## happy path
* request_weather
    - weather_form
    - form{"name": "weather_form"}
    - form{"name": null}


## happy path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
    - form{"name": null}
* thanks
    - utter_noworries




## unhappy path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* chitchat
    - utter_chitchat
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## very unhappy path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* chitchat
    - utter_chitchat
    - weather_form
* chitchat
    - utter_chitchat
    - weather_form
* chitchat
    - utter_chitchat
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## stop but continue path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* stop
    - utter_ask_continue
* affirm
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## stop and really stop path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* stop
    - utter_ask_continue
* deny
    - action_deactivate_form
    - form{"name": null}




## chitchat stop but continue path
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* chitchat
    - utter_chitchat
    - weather_form
* stop
    - utter_ask_continue
* affirm
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## stop but continue and chitchat path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* stop
    - utter_ask_continue
* affirm
    - weather_form
* chitchat
    - utter_chitchat
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## chitchat stop but continue and chitchat path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* chitchat
    - utter_chitchat
    - weather_form
* stop
    - utter_ask_continue
* affirm
    - weather_form
* chitchat
    - utter_chitchat
    - weather_form
    - form{"name": null}
* thanks
    - utter_noworries




## chitchat, stop and really stop path
* greet
    - utter_answer_greet
* request_weather
    - weather_form
    - form{"name": "weather_form"}
* chitchat
    - utter_chitchat
    - weather_form
* stop
    - utter_ask_continue
* deny
    - action_deactivate_form
    - form{"name": null}


## interactive_story_1
## 天气 + 时间 + 地点 + 地点
* request_weather
    - weather_form
    - form{"name": "weather_form"}
    - slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
    - form: weather_form
    - slot{"date_time": "明天"}
    - slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
    - form: weather_form
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "广州"}
    - slot{"date_time": "后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_1
## 天气 + 时间 + 地点 + 时间
* request_weather
    - weather_form
    - form{"name": "weather_form"}
    - slot{"requested_slot": "date_time"}
* form: inform{"date_time": "明天"}
    - form: weather_form
    - slot{"date_time": "明天"}
    - slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
    - form: weather_form
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "上海"} OR request_weather{"address": "深圳"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "广州"}
    - slot{"address": "上海"}
    - form{"name": null}
    - slot{"requested_slot": null}
* affirm
    - utter_answer_affirm




## interactive_story_2
## 天气/时间/地点 + 地点
* request_weather{"date_time": "明天", "address": "上海"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "上海"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "广州"} OR request_weather{"address": "广州"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "上海"}
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_3
## 天气/时间/地点 + 时间
* request_weather{"address": "深圳", "date_time": "后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "深圳"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "深圳"}
    - slot{"date_time": "大后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_2
## 天气/时间/地点 + 地点 + 时间
* request_weather{"date_time": "明天", "address": "上海"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "上海"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "北京"} OR request_weather{"address": "北京"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "上海"}
    - slot{"address": "北京"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "北京"}
    - slot{"date_time": "后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* affirm
    - utter_answer_affirm




## interactive_story_3
## 天气/时间/地点 + 地点 + 地点
* request_weather{"date_time": "后天", "address": "北京"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "北京"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "深圳"} OR request_weather{"address": "深圳"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "北京"}
    - slot{"address": "深圳"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "南京"} OR request_weather{"address": "南京"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "深圳"}
    - slot{"address": "南京"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_4
## 天气/时间/地点 + 时间 + 地点
* request_weather{"date_time": "明天", "address": "长沙"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "长沙"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "长沙"}
    - slot{"date_time": "后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "大后天"} OR request_weather{"date_time": "大后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "长沙"}
    - slot{"date_time": "大后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* affirm
    - utter_answer_affirm




## interactive_story_5
## 天气/时间/地点 + 时间 + 时间
* request_weather{"date_time": "后天", "address": "深圳"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "深圳"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "深圳"}
    - slot{"date_time": "明天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"address": "广州"} OR request_weather{"address": "广州"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "深圳"}
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_4
## 天气/时间 + 地点 + 时间
* request_weather{"date_time": "明天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
    - form: weather_form
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "后天"} OR request_weather{"date_time": "后天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "明天"}
    - slot{"address": "广州"}
    - slot{"date_time": "后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks




## interactive_story_5
## 天气/地点 + 时间 + 时间
* request_weather{"address": "广州"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"address": "广州"}
    - slot{"requested_slot": "date_time"}
* form: inform{"date_time": "后天"}
    - form: weather_form
    - slot{"date_time": "后天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* inform{"date_time": "明天"} OR request_weather{"date_time": "明天"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "后天"}
    - slot{"address": "广州"}
    - slot{"date_time": "明天"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks


## interactive_story_1
## 天气/时间/地点 + chit + chit(restart)+询问天气
* request_weather{"date_time": "今天", "address": "广州"}
    - weather_form
    - form{"name": "weather_form"}
    - slot{"date_time": "今天"}
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* chitchat
    - utter_chitchat
* chitchat
    - utter_chitchat
    - action_restart
* request_weather
    - weather_form
    - form{"name": "weather_form"}
    - slot{"requested_slot": "date_time"}
* form: inform{"date_time": "今天"}
    - form: weather_form
    - slot{"date_time": "今天"}
    - slot{"requested_slot": "address"}
* form: inform{"address": "广州"}
    - form: weather_form
    - slot{"address": "广州"}
    - form{"name": null}
    - slot{"requested_slot": null}
* thanks
    - utter_answer_thanks

在构建Story样本时,主要是使用Interactive Learning工具实现,以确保枚举尽可能多的unhappy story,同时又能够防止在构建样本时出现信息遗漏的情况。此外,本版本中除了查询天气这个案例,还新增了其他案例,并列举了如何使用同义词、自定义字典以及正则表达式的使用方法,详细见最新版项目。

GitHub地址:ChitChatAssistant https://github.com/jiangdongguo/ChitChatAssistant,欢迎star和issues,我们共同讨论、学习!

原文链接:

https://blog.csdn.net/AndrExpert/article/details/105434136

【END】


更多精彩推荐

没有监控和日志咋整?老程序员来支招

朱广权李佳琦直播掉线,1.2 亿人在线等

RPC 的超时设置,一不小心就是线上事故!

拿下 Gartner 容器产品第一,阿里云打赢云原生关键一战!

深聊 Solidity 的测试场景、方法和实践,太详细了,必须收藏!

万字干货:一步步教你如何在容器上构建持续部署!

☞据说,这是当代极客们的【技术风向标】...

今日福利:评论区留言入选,可获得价值299元的「2020 AI开发者万人大会」在线直播门票一张。  快来动动手指,写下你想说的话吧。

点击阅读原文,精彩继续!

你点的每个“在看”,我都认真当成了喜欢

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐