Skip to main content

基于Azure Bot Framework SDK开发的聊天机器人如何管理状态

分类:  Azure机器人 标签:  #Azure Bot Framework SDK #Azure Bot Service #机器人 发布于: 2023-08-07 22:52:46

我们这一节来学习如何管理状态,开始之前建议先了解如下的内容:

另外本站的实例代码是构建在文章: https://www.azuredeveloper.cn/article-create-a-full-function-chat-bot-project 中已经建立好的项目之上,如果要练习本章的代码,请先使用本章内容创建一个项目。

我们之前已经讨论过了Azure Bot Framework SDK的应用是构建于Web API的应用之上,同样也是基于Asp.net Core的框架,这样带来一个问题就是每一个Bot应用实际上是无状态的应用,因为HTTP协议天然就是无状态,为了在Web应用上实现状态的管理,每个开发的框架都有实现自己的会话管理机制(Session), 大多数Web应用会基于Cookie或者是Header或者是查询字符串在每次请求的时候带上唯一的id, 用这个ID来标识每一个会话。基于这样的构想,Azure Bot Framework SDK也同样使用了类似的原理来管理状态,和Web应用不同,Bot的状态管理因为场景的差异,加入了更多的设计。

Bot应用中状态范围(State Scope)

我们之前多次的讨论过,在聊天机器人应用中有如下一些逻辑的设计:

  • Channel
  • Conversation
  • Turn
  • User and Bot, 实际上UserBot在某些场景下,都是User

Bot应用预先为状态设计了三个应用范围(Scope):

  • 基于Conversation的应用范围
  • 基于User数据的应用范围
  • 基于Private Conversation的应用范围。

所谓的应用范围实际也就是数据在哪些地方可以存取,哪些场景下可以一直保存。

当用户使用某个channel连接上Bot应用之后,这个用户就有了一批表示这个场景的数据:用户的ID, ConversationIdChannelIdTurnID, 还有ActivityId, 关于TurnActivityChannel, 以及Conversation这些概念,可以翻一下之前的文章,我们有详细介绍微软基于聊天机器人的详细设计。

当用户拥有了这些数据之后,自然就需要创建一些数据使用的范围,例如用户自己的数据:用户名,性别等等这些数据,只要在用户ID是一致的情况下,那么这些数据应该就是一致的,再比如在某一个Conversation里,只要Conversation Id是一致的,那么我们就应该认为这个范围下的数据都是同一个Conversation的,这就是状态范围的概念。

Bot的应用也确实是根据这些场景来使用数据的:

  • UserState: 即是用户数据的范围: 只要一个用户的用户Id和他使用的ChannelID不变,那么这个范畴就表示是用户数据的范围, 也即UserState, 同一个用户在使用同一个Channel的情况下,开启多个Conversation, 但是在这些Conversation里访问存储在UserState状态里的数据是一致的。
  • Conversation: 即会话的数据范围,只要一个Conversation的Id不变,那么就是表示数据在同一个Conversation
  • PrivateConversation: 这个使用的场景,例如在聊天室里,可能存在私人会话的场景。它的唯一要素是:Channel IdConversation idUser Id, 三者一致,则在同一个数据存取范围。

Bot中状态管理的组件

为了实现Bot中的状态管理,SDK提供一个基类: BotState, 同时根据预定义的三个状态范围,定义了三个基于BotState的子类:

  • ConversationState
  • UserState
  • PrivateConversationState

以上这些类是用于状态管理,同时为了状态中的数据可以持久保存,SDK有提供存储的基础接口和默认实现:

  • IStore: 该接口是用于表示存储的接口。
  • MemoryStorageSDK的默认实现,数据保存在内存中。
  • AzureStorageSDK基于Azure Storage的实现。

状态管理组件的使用方法

为了使用状态管理组件,首先需要在DI中注入服务:

services.addSingleton<IStore, MemoryStorage>
services.addSingleton<UserState>()
services.addSingleton<Conversation>()

在服务里注册好这些服务了之后,如果需要在Bot类里引用,我们需要从构造函数中注入:

private readonly BotState _conversationState;
private readonly BotState _userState;
public TestBot(ConversationState conversationState, UserState userState)
{
    _userState = userState;
    _conversationState = conversationState;
}

在构造函数里注入之后,如果我们需要使用不同的状态,我们需要属性存取器:

var conversationPropertyAccessor = _conversationState.CreateProperty<string>("mydatakey1");
var userName = await conversationPropetyAccessor.getAsync(turnContext, () => new string("myusername"));

var userPropertyAccessor = _userState.CreateProperty<string>("mydatakey2");
var password = await userPropertyAccessor.getAsync(turnContext, ()=> new string("mypassword"))

上述例子我们创建了Conversation状态和User状态的存取器,存储了里面存放的对象。

注意
我们上面虽然实现了在不同会话里存储了状态,但是这些状态仅仅是放置在缓存里,如果需要将这些状态写回到Storage里,我们必须调用状态管理的SaveChangeAsync, 需要考虑的时是什么时候来保存状态。

持久会话数据

我们有两种方式来持久化数据:

  • Bot类的OnTurnAsync方法中保存。
  • 使用Adapter的中间件自动保存会话数据。

我们打开Bot的定义类,添加如下的方法:

 public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
    {

        await base.OnTurnAsync(turnContext, cancellationToken);

        await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
        await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
    }

这样就可以持久化会话数据了。需要注意的是调用顺序:
必须在await base.OnTurnAsync(turnContext, cancellationToken);之后调用会话保存数据。

如果是需要在Adapter类中使用中间件,打开文件Adapter\AdapterWithErrorHandler.cs在构造函数结束之前添加一行:

Use(new AutoSaveStateMiddleware(ConversationState, UserState));

注意
需要提前在构造函数里注入需要自动保存的状态对象。