🚀 原文地址:https://rasa.com/docs/action-server/knowledge-bases

在 Rasa 框架中使用ActionQueryKnowledgeBase在对话中利用知识库的信息,知识库操作可以使得我们能够处理以下类型的对话:
image.png
对话式AI中的一个常见问题是,用户不仅通过名称来引用某些对象,而且还使用诸如“第一个”或“它”之类的引用术语。我们需要跟踪提供的信息,这些信息帮助我们将这些提及解析为正确的对象。

此外,用户可能希望在对话期间获得有关对象的详细信息,例如餐厅是否有户外座位,或者它有多贵。为了响应这些用户请求,需要有关餐厅领域的知识。由于信息可能会发生变化,因此硬编码信息不是解决方案。

为了应对上述挑战,Rasa 可以与知识库集成。要使用此集成,我们可以创建一个从ActionQueryKnowledgeBase继承的自定义操作,这是一个预先编写的自定义操作,其中包含查询知识库中对象及其属性的逻辑。

我们可以在 examples/knowledgebasebot 中找到完整的示例,以及在下面实现此自定义操作的说明。

1. 使用ActionQueryKnowledgeBase

1.1 创建知识库

用于回答用户请求的数据将存储在知识库中,知识库可以用来存储复杂的数据结构,建议开始时使用InMemoryKnowledgeBase。一旦你想开始处理大量数据,可以切换到一个定制的知识库。

要初始化InMemoryKnowledgeBase,需要在 JSON 文件中提供数据。以下示例包含有关餐厅和酒店的数据,JSON 结构应该包含每个对象类型的键,例如resuauranthotel。每个对象类型都映射到一个对象列表,这里我们有一个包含 3 家餐厅和 3 家酒店的列表。

  1. {
  2. "restaurant": [
  3. {
  4. "id": 0,
  5. "name": "Donath",
  6. "cuisine": "Italian",
  7. "outside-seating": true,
  8. "price-range": "mid-range"
  9. },
  10. {
  11. "id": 1,
  12. "name": "Berlin Burrito Company",
  13. "cuisine": "Mexican",
  14. "outside-seating": false,
  15. "price-range": "cheap"
  16. },
  17. {
  18. "id": 2,
  19. "name": "I due forni",
  20. "cuisine": "Italian",
  21. "outside-seating": true,
  22. "price-range": "mid-range"
  23. }
  24. ],
  25. "hotel": [
  26. {
  27. "id": 0,
  28. "name": "Hilton",
  29. "price-range": "expensive",
  30. "breakfast-included": true,
  31. "city": "Berlin",
  32. "free-wifi": true,
  33. "star-rating": 5,
  34. "swimming-pool": true
  35. },
  36. {
  37. "id": 1,
  38. "name": "Hilton",
  39. "price-range": "expensive",
  40. "breakfast-included": true,
  41. "city": "Frankfurt am Main",
  42. "free-wifi": true,
  43. "star-rating": 4,
  44. "swimming-pool": false
  45. },
  46. {
  47. "id": 2,
  48. "name": "B&B",
  49. "price-range": "mid-range",
  50. "breakfast-included": false,
  51. "city": "Berlin",
  52. "free-wifi": false,
  53. "star-rating": 1,
  54. "swimming-pool": false
  55. },
  56. ]
  57. }

一旦在 JSON 文件中定义了数据,例如 data.json,我们可以使用此数据文件创建InMemoryKnowledgeBase,它将被传递给查询知识库的操作。知识库中的每个对象都应该至少有nameid字段以使用默认实现。如果没有,则必须自定义InMemoryKnowledgeBase

1.2 定义NLU数据

在本章节中:

  • 我们将引入一个新的意图query_knowledge_base
  • 我们将标注mention实体,以便模型检测到像“第一个”这样间接提及的对象
  • 我们将广泛使用同义词

为了让机器人理解用户想要从知识库中检索信息,我们需要定义一个新的意图,这里称之为query_knowledge_base。我们可以将ActionQueryKnowledgeBase可以处理的请求分为两类:

  1. 用户希望获得特定类型的对象列表
  2. 用户希望了解对象的某个属性

意图应该包含着两个请求的多个变化:

  1. nlu:
  2. - intent: query_knowledge_base
  3. examples: |
  4. - what [restaurants]{"entity": "object_type", "value": "restaurant"} can you recommend?
  5. - list some [restaurants]{"entity": "object_type", "value": "restaurant"}
  6. - can you name some [restaurants]{"entity": "object_type", "value": "restaurant"} please?
  7. - can you show me some [restaurants]{"entity": "object_type", "value": "restaurant"} options
  8. - list [German](cuisine) [restaurants]{"entity": "sobject_type", "value": "restaurant"}
  9. - do you have any [mexican](cuisine) [restaurants]{"entity": "object_type", "value": "restaurant"}?
  10. - do you know the [price range]{"entity": "attribute", "value": "price-range"} of [that one](mention)?
  11. - what [cuisine](attribute) is [it](mention)?
  12. - do you know what [cuisine](attribute) the [last one]{"entity": "mention", "value": "LAST"} has?
  13. - does the [first one]{"entity": "mention", "value": "1"} have [outside seating]{"entity": "attribute", "value": "outside-seating"}?
  14. - what is the [price range]{"entity": "attribute", "value": "price-range"} of [Berlin Burrito Company](restaurant)?
  15. - what about [I due forni](restaurant)?
  16. - can you tell me the [price range](attribute) of [that restaurant](mention)?
  17. - what [cuisine](attribute) do [they](mention) have?

上述示例仅仅是为了展示与餐厅领域相关的例子,我们应该将知识库中存在的每种对象类型的示例添加到 query_knowledge_base意图中。

除了为每种查询类型添加各种训练示例外,还需要在训练示例中指定和注释以下实体:

  • obejct_type:每当训练样本引用知识库中的特定对象类型时,该对象应该标记为实体。使用同义词将restaurants映射到restaurant,正确的对象类型为知识库中的键。
  • mention:如果用户通过“第一个”、“那个”或“它”引用对象,则应该将这些术语标记为mention,我们还使用同义词将一些提及映射到符号。
  • attribute:知识库中定义的所有属性名称都应该在 NLU 数据中标识为属性。同样,使用同义词将属性名称的变体映射到知识库中使用的名称。

请记住将这些实体添加到域文件中(作为实体和插槽):

  1. entities:
  2. - object_type
  3. - mention
  4. - attribute
  5. slots:
  6. object_type:
  7. type: any
  8. influence_conversation: false
  9. mappings:
  10. - type: from_entity
  11. entity: object_type
  12. mention:
  13. type: any
  14. influence_conversation: false
  15. mappings:
  16. - type: from_entity
  17. entity: mention
  18. attribute:
  19. type: any
  20. influence_conversation: false
  21. mappings:
  22. - type: from_entity
  23. entity: attribute

1.3 创建查询知识库的操作

要创建自己的知识库操作,需要继承ActionQueryKnowledgeBase,并将知识库传递给ActionQueryKnowledgeBase的构造函数。

  1. from rasa_sdk.knowledge_base.storage import InMemoryKnowledgeBase
  2. from rasa_sdk.knowledge_base.actions import ActionQueryKnowledgeBase
  3. class MyKnowledgeBaseAction(ActionQueryKnowledgeBase):
  4. def __init__(self):
  5. knowledge_base = InMemoryKnowledgeBase("data.json")
  6. super().__init__(knowledge_base)

无论何时创建ActionQueryKnowledgeBase,都需要将知识库传递给构造函数,它可以是InMemoryKnowledgeBase类或者自己实现的KnowledgeBase类。因为不支持同时使用多个知识库,我们只能从一个知识库中提取信息。

以下是此操作的全部代码,操作的名称是action_query_knowledge_base,不要忘记将其添加到域文件中。

  1. actions:
  2. - action_query_knowledge_base

:::info 📌 备注
——————————
如果我们重写了默认操作action_query_knowledge_base,那么需要将以下三个未特征化的插槽添加到域文件中。这些插槽会在ActionQueryKnowledgeBase内部进行使用,如果我们保留了默认操作名称,则会自动添加这些插槽:

  • knowledge_base_objects
  • knowledge_base_last_object
  • knowledge_base_last_object_type :::

我们需要确保将故事添加到故事文件中,其中包括query_knowledge_base意图和action_query_knowledge_base操作。例如:

  1. stories:
  2. - story: knowledge base happy path
  3. steps:
  4. - intent: greet
  5. - action: utter_greet
  6. - intent: query_knowledge_base
  7. - action: action_query_knowledge_base
  8. - intent: goodbye
  9. - action: utter_goodbye

我们需要做的最后一件事是在域文件中定义响应utter_ask_rephrase,如果该操作不知道如何处理用户的请求,它将使用此响应来要求用户重新措辞:

  1. responses:
  2. utter_ask_rephrase:
  3. - text: "Sorry, I'm not sure I understand. Could you rephrase it?"
  4. - text: "Could you please rephrase your message? I didn't quite get that."

添加所有相关部分后,该操作便可以查询数据库了。

2. 如何工作

ActionQueryKnowledgeBase会查看在请求中提取的实体,以及之前设置的插槽,以决定查询什么。

2.1 查询知识库中的对象

为了查询任何类型对象的知识库,用户的请求需要包含对象类型。

有这样一个例子“Can you please name some restaurants?”,此问题包含了感兴趣的对象类型restaurant。机器人需要获取该实体来指定查询,否则该操作将不知道用户对哪些对象感兴趣。

当用户说了这样的内容“What Italian restaurant options in Berlin do I have?”,用户想要获取的餐厅列表要包含两个条件,一是有意大利美食,二是坐落于 Berlin。如果命名实体识别在用户的请求种检测到这些属性,则操作将使用这些属性来过滤在知识库中找到的餐厅。

为了让那个机器人检测这些属性,我们需要在 NLU 数据中将 “Italian”和“Berlin”标记为实体:

  1. intents:
  2. - intent: query_knowledge_base
  3. examples: |
  4. - What [Italian](cuisine) [restaurant](object_type) options in [Berlin](city) do I have?.

属性的名称cuisinecity应该与知识库中使用的名称相同,我们需要将他们作为实体和插槽添加到域文件中。

2.2 查询知识库中的对象属性

如果用户想要获取关于某个对象的特定信息,则请求应该包括感兴趣的对象和属性。

例如,如果用户问了这样的内容“What is the cuisine of Berlin Burrito Company?”,用户想要获得 Berlin Burrito Company 餐厅(感兴趣的对象)的 “cuisine”(感兴趣的属性)。应该将感兴趣的属性和对象标记为 NLU 训练数据中的实体:

  1. intents:
  2. - intent: query_knowledge_base
  3. examples: |
  4. - What is the [cuisine](attribute) of [Berlin Burrito Company](restaurant)?

确保将对象类型restaurant作为实体和插槽添加到域文件中。

2.3 解决提及

按照上面的例子,用户可能并不总是用他们的名字来指代餐馆。用户可以通过名称引用感兴趣的对象,例如“Berlin Burrito Company”(对象的标识字符串),或者他们可能通过提及引用先前列出的对象,例如“What is the cuisine of the second restaurant you mentioned?”,我们的操作能够将这些提及解析为知识库中的实际对象。更具体地说,它可以解析两种提及和类型:序数(例如“第一个”)和指代(例如“它”或“那个”)。

1)序数提及

当用户通过它在列表中的位置来引用一个对象时,它被称为序数提及。以下是一个示例:

  1. User: What restaurants in Berlin do you know?
  2. Bot: Found the following objects of type 'restaurant': 1: I due forni 2: PastaBar 3: Berlin Burrito Company
  3. User: Does the first one have outside seating?

用户使用“the first one”一词来指代“I due forni”,其他序数提及可能包含“第二个”、“最后一个”、“任何”或“3”。

当向用户呈现对象列表时,通常使用序号提及,为了将这些提及解析为实际对象,我们可以使用在KnowledgeBase类中设置的序数提及映射。默认映射如下所示:

  1. {
  2. "1": lambda l: l[0],
  3. "2": lambda l: l[1],
  4. "3": lambda l: l[2],
  5. "4": lambda l: l[3],
  6. "5": lambda l: l[4],
  7. "6": lambda l: l[5],
  8. "7": lambda l: l[6],
  9. "8": lambda l: l[7],
  10. "9": lambda l: l[8],
  11. "10": lambda l: l[9],
  12. "ANY": lambda l: random.choice(l),
  13. "LAST": lambda l: l[-1],
  14. }

序数提及映射将字符串(例如“1”)映射到列表中的对象(例如lambda l: l[0]表示索引为 0 的对象)。例如,由于序数提及映射不包含“the first one”条目,因此使用实体同义词将 NLU 数据中的“the first one”映射到“1”很重要:

  1. intents:
  2. - intent: query_knowledge_base
  3. examples: |
  4. - Does the [first one]{entity: "mention", value": 1} have [outside seating]{entity: "attribute", value": "outside-seating"}

命名实体识别将“first one”检测为提及实体,但将“1”放入mention插槽。因此,我们的操作可以将mention槽与序数提及映射一起使用,从而将“first one”解析为实际对象“I due forni”。

我们可以通过KnowledgeBase实现的set_oridinal_mention_mapping函数来覆写序数提及。

2)指代提及

我们来看一段下面的对话:

  1. User: What is the cuisine of PastaBar?
  2. Bot: PastaBar has an Italian cuisine.
  3. User: Does it have wifi?
  4. Bot: Yes.
  5. User: Can you give me an address?

在“Does it have wifi?”问题中,用户通过“it”这个词来指代“PastBar”。如果命名实体识别检测到“it”作为实体提及,知识库操作会将其解析为对话中最后提到的对象“PastBar”。

在下一个输入中,用户间接引用对象“PastBar”,而不是明确提及它。知识库操作将检测用户想要获取特定属性的值,在上例中为地址。如果命名实体识别未检测到提及或对象,则该操作假定用户指的是最近提及的对象“PastBar”。

我们可以在初始化操作时,将user_last_object_mention设置为False来禁用此行为。

3. 自定义

3.1 创建ActionQueryKnowledgeBase

如果想自定义机器人对用户说的内容,我们可以覆写ActionQueryKnowledgeBase的两个函数:utter_objectsutter_attribute_value

当用户请求对象列表时使用utter_objects,一旦机器人从知识库中检索到对象,它将默认使用一条消息响应用户,格式如下:

  1. Found the following objects of type 'restaurant'
  2. 1. I due forni
  3. 2. PastaBar
  4. 3. Berlin Burrito Company

或者如果没有对象被发现:

  1. I could not find any objects of type 'restaurant'.

如果想要更改话语格式,我们可以在操作中覆写utter_objects方法。

当用户询问有关对象的特定信息时,utter_atrribute_value函数决定说什么。如果在知识库中找到感兴趣的属性,机器人将用以下话语进行响应:

  1. 'Berlin Burrito Company' has the value 'Mexican' for attribute 'cuisine'.

如果没有找到请求属性的值,机器人将响应:

  1. Did not find a valid value for attribute 'cuisine' for object 'Berlin Burrito Company'.

如果要更改机器人的话语,我们可以覆写utter_attribute_value函数。

:::info ⌛ 备注
——————————
在此博客上有一个关于如何在自定义操作中使用知识库的教程,这里主要详细解释ActionQueryKnowledgeBase背后的实现。 :::

3.2 创建自己的知识库操作

借助于ActionQueryKnowledgeBase,我们可以轻松地将知识库集成到操作中。但是,该操作只能处理两种用户请求:

  • 用户想要从知识库中获取对象列表
  • 用户想要获取特定对象的属性值

该操作无法在我们的知识库中比较对象,或考虑对象之间的关系。此外,解决任何提及都采用对话中最后提及的对象,这种方式不总是最优的结果。

如果想要处理更复杂的用例,我们可以编写自己的自定义操作。Rasa SDK 提供了一些助手函数[rasa_sdk.knowledge_base.utils](https://github.com/RasaHQ/rasa-sdk/tree/main/rasa_sdk/knowledge_base/)可以帮助我们实现自己的解决方案。建议使用KnowledgeBase接口,这样在新自定义操作中仍然可以使用ActionQueryKnowledgeBase

3.3 自定义InMemoryKnowledgeBase

InMemoryKnowledgeBase继承了KnowledgeBase,我们可以通过覆写以下函数来自定义InMemoryKnowledgeBase

  • get_key_attribute_of_object:为了跟踪用户最后谈论的对象,我们将重要属性的值存储在特定的插槽中。每个对象都应该有一个唯一的主键属性,类似于关系数据库中的主键。默认情况下,每个对象类型的主键属性的名称都设置为id。通过调用set_key_attribute_of_object,覆写特定对象类型的主键属性的名称。
  • get_representation_function_of_object:我们看一个示例
    1. {
    2. "id": 0,
    3. "name": "Donath",
    4. "cuisine": "Italian",
    5. "outside-seating": true,
    6. "price-range": "mid-range"
    7. }

当用户要求机器人列出任何意大利餐厅时,它不需要餐厅的所有细节。相反地,他可能希望能提供一个有意义的名称来标识餐厅,在大多数情况下,对象的名称就可以了。get_representation_function_of_object函数返回一个lambda函数,该函数将上述餐厅对象映射到其名称。

  1. lambda obj: obj["name"]

每当机器人谈论特定对象时,都会使用此功能,以便为用户提供一个有意义的对象名称。默认情况下,lambda函数返回对象name属性的值。如果对象没有name属性,或者对象的name不明确,则应该通过调用set_representation_function_of_object为该对象类型设置新的lambda函数。

  • set_ordinal_mention_mapping:要将序数提及(例如“second one”)解析为列表中的对象,需要使用序数提及映射。默认情况下,序数提及映射如下所示:
    1. {
    2. "1": lambda l: l[0],
    3. "2": lambda l: l[1],
    4. "3": lambda l: l[2],
    5. "4": lambda l: l[3],
    6. "5": lambda l: l[4],
    7. "6": lambda l: l[5],
    8. "7": lambda l: l[6],
    9. "8": lambda l: l[7],
    10. "9": lambda l: l[8],
    11. "10": lambda l: l[9],
    12. "ANY": lambda l: random.choice(l),
    13. "LAST": lambda l: l[-1],
    14. }
    我们可以通过调用set_ordinal_mention_mapping函数来覆写。

有关InMemoryKnowledgeBase的示例实现,请参见示例,它使用了set_representation_function_of_object方法覆写对象类型为hotel的默认表示。

3.4 创建自己的知识库

如果有更多的数据,或者想使用更复杂的数据结构(例如涉及不同对象之间的关系),我们可以创建自己的知识库实现。只需要继承KnowledgeBase并实现get_objectsget_object以及get_attribute_of_object知识库代码提供了关于这些方法应该做什么的更多信息。

我们还可以通过调整自定义InMemoryKnowledgeBase中的方法,进一步定制知识库,这里有一篇博客解释如何建立自己的知识库。