處理用戶中斷

適用於: SDK v4

處理中斷是健全 Bot 的重要層面。 使用者不會一律遵循您定義的交談流程,逐步執行。 他們可能會嘗試在過程中提出問題,或只是想取消它,而不是完成它。 本文說明處理 Bot 中用戶中斷的一些常見方式。

注意

Bot Framework JavaScript、C# 和 Python SDK 將會繼續受到支援,不過,Java SDK 即將淘汰,最終長期支援將於 2023 年 11 月結束。

使用 Java SDK 建置的現有 Bot 將繼續運作。

針對新的 Bot 建置,請考慮使用 Power Virtual Agents ,並閱讀 選擇正確的聊天機器人解決方案

如需詳細資訊,請參閱 Bot 建置的未來。

必要條件

核心 Bot 範例會使用 Language Understanding (LUIS) 來識別用戶意圖;不過,識別使用者意圖並不是本文的重點。 如需識別使用者意圖的相關信息,請參閱 自然語言理解將自然語言理解新增至 Bot

注意

Language Understanding (LUIS) 將於 2025 年 10 月 1 日淘汰。 從 2023 年 4 月 1 日起,您將無法建立新的 LUIS 資源。 新版的語言理解現在已提供作為 Azure AI 語言的一部分。

對話式語言理解(CLU)是 Azure AI 語言的一項功能,是 LUIS 的更新版本。 如需 Bot Framework SDK 中語言理解支援的詳細資訊,請參閱 自然語言理解

關於此範例

本文中使用的範例會建立航班預約 Bot 的模型,該 Bot 會使用對話從使用者取得航班資訊。 在與 Bot 交談期間,用戶隨時都可以發出 說明取消 命令,以造成中斷。 有兩種類型的中斷處理:

  • 回合層級:略過回合層級的處理,但保留堆棧上的對話框,並提供的資訊。 在下一個回合中,繼續離開交談的位置。
  • 對話框層級:完全取消處理,讓 Bot 可以重新開始。

定義及實作中斷邏輯

首先,定義並實作 說明取消 中斷。

若要使用對話框,請安裝 Microsoft.Bot.Builder.Dialogs NuGet 套件。

Dialogs\CancelAndHelpDialog.cs

實作 類別 CancelAndHelpDialog 來處理用戶中斷。 可取消的對話框, BookingDialogDateResolverDialog 衍生自這個類別。

public class CancelAndHelpDialog : ComponentDialog

在類別中CancelAndHelpDialogOnContinueDialogAsync,方法會呼叫 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);
}

如果使用者輸入「說明」,則 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;
}

檢查每個回合的中斷

實作中斷處理類別之後,請檢閱當此 Bot 收到來自使用者的新訊息時會發生什麼情況。

Dialogs\MainDialog.cs

當新的訊息活動送達時,Bot 會執行 MainDialog。 會 MainDialog 提示使用者提供其可協助的內容。 然後它會在方法中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);
}

接下來,在類別的 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 中找到該程式代碼。

處理非預期的錯誤

配接器的錯誤處理程式會處理 Bot 中未攔截的任何例外狀況。

AdapterWithErrorHandler.cs

在範例中,配接器處理程式 OnTurnError 會收到 Bot 回合邏輯擲回的任何例外狀況。 如果擲回例外狀況,處理程式會刪除目前交談的交談狀態,以防止 Bot 卡在錯誤迴圈中,因為狀態不正確。

    {
        // 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中,Bot 會建立為暫時性,並在每一回合建立 Bot 的新實例。


// Register the BookingDialog.

如需參考,以下是呼叫中用來建立上述 Bot 的類別定義。

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

測試聊天機器人

  1. 如果您尚未這麼做,請安裝 Bot Framework 模擬器
  2. 在本機電腦上執行範例。
  3. 啟動模擬器、連線至 Bot,並傳送訊息,如下所示。

其他資訊

  • C#JavaScriptPythonJava 中的 24.bot-authentication-msgraph 範例會示範如何處理註銷要求。 它會使用類似此處所示的模式來處理中斷。

  • 您應該傳送預設回應,而不是不執行任何動作,讓使用者想知道發生了什麼事。 默認回應應該告訴使用者 Bot 瞭解哪些命令,讓使用者可以回到正軌。

  • 在回合內容的任何時間點,回合內容的 響應 屬性會指出 Bot 是否已在此回合傳送訊息給使用者。 回合結束之前,您的 Bot 應該傳送一些訊息給使用者,即使它是對其輸入的簡單通知。