处理用户中断

适用于:SDK v4

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

重要

Bot Framework SDK 和 Bot Framework Emulator 已在 GitHub 上存档。 项目不再更新或维护。 自 2025 年 12 月 31 日起,Bot Framework SDK 的支持票证将不再提供服务。

若要使用所选的 AI 服务、业务流程和知识生成代理,请考虑使用 Microsoft 365 代理 SDK。 代理 SDK 对 C#、JavaScript 或 Python 具有语言支持。 可以在 aka.ms/agents 了解有关代理 SDK 的详细信息。 如果现有的机器人是使用 Bot Framework SDK 生成的,则可以将机器人更新到代理 SDK。 查看 Bot Framework SDK 到代理 SDK 迁移指南的核心更改和更新。

如果要构建设计为在 Microsoft Teams 中工作的协作代理,请考虑使用 Teams SDK。 它为在 Teams 环境中运行的代理提供 Teams 特定的 API、自适应卡支持和内置 AI 协同调度功能。 可以在 Teams SDK(Teams AI 库)中了解详细信息。

如果要查找基于 SaaS 的代理平台,请考虑 Microsoft Copilot Studio

先决条件

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

注意

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

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

关于此示例

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

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

定义和实现中断逻辑

首先,定义并实现“帮助”“取消”中断。

若要使用对话,请安装 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,这会清空其对话堆栈,并使其以已取消状态退出,且不返回结果值。 对 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 会询问用户需要它提供什么帮助。 然后,它在 MainDialog.ActStepAsync 方法中启动 BookingDialog,如下面所示,通过调用 BeginDialogAsync

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);
}

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

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 属性都会指示机器人在此轮次中是否向用户发送了消息。 在轮次结束之前,机器人应会向用户发送某条消息,即使该消息只是确认收到了用户的输入。