处理用户中断

适用于:SDK v4

如何处理中断是判断机器人是否可靠的一个重要因素。 用户不总是按照定义的对话流逐步操作。 他们可能会在流程的中途提出问题,或者只是想要取消流程,而不要完成流程。 本文介绍一些在机器人中处理用户中断的常用方法。

注意

Bot Framework JavaScript、C# 和 Python SDK 将继续受支持,但 Java SDK 即将停用,最终长期支持将于 2023 年 11 月结束。

使用 Java SDK 构建的现有机器人将继续正常运行。

要生成新的机器人,请考虑使用 Microsoft Copilot Studio 并阅读选择正确的助理解决方案

有关详细信息,请参阅机器人构建的未来

先决条件

核心机器人示例使用语言理解 (LUIS) 来辨识用户意图;但是,辨识用户意图不是本文的重点。 有关辨识用户意图的信息,请参阅自然语言理解向机器人添加自然语言理解

注意

语言理解 (LUIS) 将于 2025 年 10 月 1 日停用。 从 2023 年 4 月 1 日开始,将无法创建新的 LUIS 资源。 语言理解的较新版本现已作为 Azure AI 语言的一部分提供。

对话语言理解(CLU)是 Azure AI 语言的一项功能,是 LUIS 的更新版本。 有关 Bot Framework SDK 中的语言理解支持的详细信息,请参阅自然语言理解

关于此示例

本文中使用的示例为某个航班预订机器人建模,该机器人使用对话从用户获取航班信息。 在与机器人聊天过程中的任何时候,用户都可以发出 helpcancel 命令来造成中断。 处理的中断有两种类型:

  • 轮次级别:绕过轮次级别的处理,但在堆栈中保留包含所提供信息的对话。 下一轮聊天从上次中断聊天的地方继续。
  • 对话级别:完全取消处理,使机器人能够从头开始。

定义和实现中断逻辑

首先,定义并实现 help 和 cancel 中断。

若要使用对话,请安装 Microsoft.Bot.Builder.Dialogs NuGet 包。

Dialogs\CancelAndHelpDialog.cs

实现 CancelAndHelpDialog 类来处理用户中断。 可取消的对话 BookingDialogDateResolverDialog 派生自此类。

public class CancelAndHelpDialog : ComponentDialog

CancelAndHelpDialog 类中,OnContinueDialogAsync 方法调用 InterruptAsync 方法来检查用户是否中断了正常的流。 如果该流已中断,则调用基类方法;否则返回 InterruptAsync 中的返回值。

protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
{
    var result = await InterruptAsync(innerDc, cancellationToken);
    if (result != null)
    {
        return result;
    }

    return await base.OnContinueDialogAsync(innerDc, cancellationToken);
}

如果用户键入“help”,InterruptAsync 方法将发送一条消息,然后调用 DialogTurnResult (DialogTurnStatus.Waiting) 来指示最前面的对话正在等待用户的响应。 这样只会中断某个轮次的聊天流,下一轮聊天会从上次中断聊天的地方继续。

如果用户键入“cancel”,该方法将对其内部对话上下文调用 CancelAllDialogsAsync,这会清除其对话堆栈,导致堆栈退出并返回 cancelled(已取消)状态,但不返回结果值。 MainDialog(稍后会介绍)将显示预订对话已结束并返回了 null,就如同用户选择不确认其预订一样。

private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken)
{
    if (innerDc.Context.Activity.Type == ActivityTypes.Message)
    {
        var text = innerDc.Context.Activity.Text.ToLowerInvariant();

        switch (text)
        {
            case "help":
            case "?":
                var helpMessage = MessageFactory.Text(HelpMsgText, HelpMsgText, InputHints.ExpectingInput);
                await innerDc.Context.SendActivityAsync(helpMessage, cancellationToken);
                return new DialogTurnResult(DialogTurnStatus.Waiting);

            case "cancel":
            case "quit":
                var cancelMessage = MessageFactory.Text(CancelMsgText, CancelMsgText, InputHints.IgnoringInput);
                await innerDc.Context.SendActivityAsync(cancelMessage, cancellationToken);
                return await innerDc.CancelAllDialogsAsync(cancellationToken);
        }
    }

    return null;
}

检查每个轮次的中断

实现中断处理类以后,请查看当此机器人收到来自用户的新消息时会发生什么情况。

Dialogs\MainDialog.cs

当新的消息活动抵达时,机器人将运行 MainDialogMainDialog 提示用户需要哪种帮助。 然后,机器人将通过调用 BeginDialogAsync 来启动 MainDialog.ActStepAsync 方法中的 BookingDialog,如下所示。

private async Task<DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if (!_luisRecognizer.IsConfigured)
    {
        // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
        return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken);
    }

    // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
    var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);
    switch (luisResult.TopIntent().intent)
    {
        case FlightBooking.Intent.BookFlight:
            await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken);

            // Initialize BookingDetails with any entities we may have found in the response.
            var bookingDetails = new BookingDetails()
            {
                // Get destination and origin from the composite entities arrays.
                Destination = luisResult.ToEntities.Airport,
                Origin = luisResult.FromEntities.Airport,
                TravelDate = luisResult.TravelDate,
            };

            // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder.
            return await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken);

        case FlightBooking.Intent.GetWeather:
            // We haven't implemented the GetWeatherDialog so we just display a TODO message.
            var getWeatherMessageText = "TODO: get weather flow here";
            var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
            break;

        default:
            // Catch all for unhandled intents
            var didntUnderstandMessageText = $"Sorry, I didn't get that. Please try asking in a different way (intent was {luisResult.TopIntent().intent})";
            var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput);
            await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken);
            break;
    }

    return await stepContext.NextAsync(null, cancellationToken);
}

接下来,在 MainDialog 类的 FinalStepAsync 方法中,预订对话将会结束,而预订被视为已完成或已取消。

private async Task<DialogTurnResult> FinalStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // If the child dialog ("BookingDialog") was cancelled, the user failed to confirm or if the intent wasn't BookFlight
    // the Result here will be null.
    if (stepContext.Result is BookingDetails result)
    {
        // Now we have all the booking details call the booking service.

        // If the call to the booking service was successful tell the user.

        var timeProperty = new TimexProperty(result.TravelDate);
        var travelDateMsg = timeProperty.ToNaturalLanguage(DateTime.Now);
        var messageText = $"I have you booked to {result.Destination} from {result.Origin} on {travelDateMsg}";
        var message = MessageFactory.Text(messageText, messageText, InputHints.IgnoringInput);
        await stepContext.Context.SendActivityAsync(message, cancellationToken);
    }

    // Restart the main dialog with a different message the second time around
    var promptMessage = "What else can I do for you?";
    return await stepContext.ReplaceDialogAsync(InitialDialogId, promptMessage, cancellationToken);
}

此处不显示 BookingDialog 中的代码,因为其不直接与中断处理相关。 其用于提示用户预订详细信息。 可以在 Dialogs\BookingDialogs.cs 中找到这些代码。

处理意外错误

适配器的错误处理程序处理机器人中未捕获到的任何异常。

AdapterWithErrorHandler.cs

在本示例中,适配器的 OnTurnError 处理程序接收机器人的轮次逻辑引发的任何异常。 如果引发了异常,该处理程序将删除当前聊天的聊天状态,以防止错误状态导致机器人陷入错误循环。

    {
        // Log any leaked exception from the application.
        // NOTE: In production environment, you should consider logging this to
        // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
        // to add telemetry capture to your bot.
        logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

        // Send a message to the user
        var errorMessageText = "The bot encountered an error or bug.";
        var errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.IgnoringInput);
        await turnContext.SendActivityAsync(errorMessage);

        errorMessageText = "To continue to run this bot, please fix the bot source code.";
        errorMessage = MessageFactory.Text(errorMessageText, errorMessageText, InputHints.ExpectingInput);
        await turnContext.SendActivityAsync(errorMessage);

        if (conversationState != null)
        {
            try
            {
                // Delete the conversationState for the current conversation to prevent the
                // bot from getting stuck in a error-loop caused by being in a bad state.
                // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
                await conversationState.DeleteAsync(turnContext);
            }
            catch (Exception e)
            {
                logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}");
            }
        }

        // Send a trace activity, which will be displayed in the Bot Framework Emulator
        await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
    };
}

注册服务

Startup.cs

最后,在 Startup.cs 中创建一个暂时性的机器人,并在每个轮次创建该机器人的新实例。


// Register the BookingDialog.

下面提供了在用于创建上述机器人的调用中使用的类定义供参考。

public class DialogAndWelcomeBot<T> : DialogBot<T>
public class DialogBot<T> : ActivityHandler
    where T : Dialog
public class MainDialog : ComponentDialog

测试机器人

  1. 安装 Bot Framework Emulator(如果尚未安装)。
  2. 在计算机本地运行示例。
  3. 按如下所示启动模拟器,连接到机器人,然后发送消息。

其他信息

  • 24.bot-authentication-msgraph 示例(用 C#JavaScriptPythonJava 编写)演示如何处理注销请求。 它使用类似于此处所示的模式来处理中断。

  • 应该发送默认响应,而非不采取任何措施,导致用户对当前情况感到困惑。 默认响应告诉用户机器人理解哪些命令,以便用户可以重新进行对话。

  • 在轮次中的任意时间点,轮次上下文的 responded 属性都会指示机器人在此轮次中是否向用户发送了消息。 在轮次结束之前,机器人应会向用户发送某条消息,即使该消息只是确认收到了用户的输入。