在技能中使用对话

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

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

关于此示例

skills skillDialog 示例包含两个机器人的项目:

  • 对话根机器人,它使用 技能对话 类来调用技能。
  • 对话技能机器人,它使用对话来处理来自技能调用方的活动。 此技能是核心机器人示例的改编版本。 (有关核心机器人的详细信息,请参阅如何向机器人添加自然语言理解。)

本文重点介绍如何在技能机器人中使用对话来管理多个操作。

有关技能使用者机器人的信息,请参阅如何通过对话使用技能

资源

对于部署的机器人,机器人到机器人身份验证要求每个参与的机器人都有有效的标识。 但是,可以使用 Bot Framework Emulator 在本地测试技能和技能使用者,而无需提供标识信息。

若要使技能可供面向用户的机器人使用,请在 Azure 中注册该技能。 有关详细信息,请参阅如何使用 Azure AI 机器人服务注册机器人

此外,技能机器人也可以使用航班预订 LUIS 模型。 若要使用此模型,请使用 CognitiveModels/FlightBooking.json 文件来创建、训练和发布 LUIS 模型。

应用程序配置

  1. (可选)将技能的标识信息添加到技能的配置文件。

    (如果技能或技能使用方其中一方指定了身份,则双方都必须指定身份。)

  2. 如果使用的是 LUIS 模型,请添加 LUIS 应用 ID、API 密钥和 API 主机名。

DialogSkillBot\appsettings.json

{
  "MicrosoftAppType": "",
  "MicrosoftAppId": "",
  "MicrosoftAppPassword": "",
  "MicrosoftAppTenantId": "",
  "ConnectionName": "",

  "LuisAppId": "",
  "LuisAPIKey": "",
  "LuisAPIHostName": "",

  // This is a comma separate list with the App IDs that will have access to the skill.
  // This setting is used in AllowedCallersClaimsValidator.
  // Examples: 
  //    [ "*" ] allows all callers.
  //    [ "AppId1", "AppId2" ] only allows access to parent bots with "AppId1" and "AppId2".
  "AllowedCallers": [ "*" ]
}

活动路由逻辑

该技能支持几项不同的功能。 它可以预订航班或获取城市的天气状况。 此外,如果它收到这些上下文之外的消息,则可以使用 LUIS 来尝试解释消息。 技能清单介绍了这些操作及其输入和输出参数以及技能的终结点。 请注意,该技能可以处理“BookFlight”或“GetWeather”事件。 它还可以处理消息活动。

技能定义活动路由对话,该对话用于根据技能使用者中的初始传入活动选择要启动的操作。 如果已提供,LUIS 模型可以识别初始消息中的预订航班和获取天气信息意向。

预订航班操作是一个多步骤过程,作为单独的对话实现。 操作开始后,传入活动由该对话处理。 get-weather 操作包含占位逻辑,在完整实现的机器人中,这些逻辑会被替换。

活动路由对话包括用来执行以下操作的代码:

技能中使用的对话继承自组件对话类。 有关组件对话的详细信息,请参阅如何管理对话复杂性

初始化对话

活动路由对话框包含一个用于预订航班的子对话框。 主瀑布对话包含一个步骤,该步骤将根据收到的初始活动启动操作。

它还接受 LUIS 识别器。 如果已初始化此识别器,则对话将使用它来解释初始消息活动的意向。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private readonly DialogSkillBotRecognizer _luisRecognizer;

public ActivityRouterDialog(DialogSkillBotRecognizer luisRecognizer)
    : base(nameof(ActivityRouterDialog))
{
    _luisRecognizer = luisRecognizer;

    AddDialog(new BookingDialog());
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] { ProcessActivityAsync }));

    // The initial child Dialog to run.
    InitialDialogId = nameof(WaterfallDialog);
}

处理初始活动

在主瀑布对话的第一个(也是唯一一个)步骤中,该技能会检查传入的活动类型。

  • 事件活动将转发到事件活动处理程序,该处理程序根据事件的名称启动相应的操作。
  • 消息活动将转发到消息活动处理程序,该处理程序在确定要执行的操作之前执行其他处理。

如果技能无法识别传入活动的类型或事件的名称,则将发送错误消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> ProcessActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    // A skill can send trace activities, if needed.
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.ProcessActivityAsync()", label: $"Got ActivityType: {stepContext.Context.Activity.Type}", cancellationToken: cancellationToken);

    switch (stepContext.Context.Activity.Type)
    {
        case ActivityTypes.Event:
            return await OnEventActivityAsync(stepContext, cancellationToken);

        case ActivityTypes.Message:
            return await OnMessageActivityAsync(stepContext, cancellationToken);

        default:
            // We didn't get an activity type we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized ActivityType: \"{stepContext.Context.Activity.Type}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}
// This method performs different tasks based on the event name.
private async Task<DialogTurnResult> OnEventActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnEventActivityAsync()", label: $"Name: {activity.Name}. Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    // Resolve what to execute based on the event name.
    switch (activity.Name)
    {
        case "BookFlight":
            return await BeginBookFlight(stepContext, cancellationToken);

        case "GetWeather":
            return await BeginGetWeather(stepContext, cancellationToken);

        default:
            // We didn't get an event name we can handle.
            await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Unrecognized EventName: \"{activity.Name}\".", inputHint: InputHints.IgnoringInput), cancellationToken);
            return new DialogTurnResult(DialogTurnStatus.Complete);
    }
}

处理消息活动

如果已配置 LUIS 识别器,则技能将调用 LUIS,然后基于意向启动操作。 如果未配置 LUIS 识别器或意向不受支持,则技能将发送错误消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

// This method just gets a message activity and runs it through LUIS. 
private async Task<DialogTurnResult> OnMessageActivityAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    await stepContext.Context.TraceActivityAsync($"{GetType().Name}.OnMessageActivityAsync()", label: $"Text: \"{activity.Text}\". Value: {GetObjectAsJsonString(activity.Value)}", cancellationToken: cancellationToken);

    if (!_luisRecognizer.IsConfigured)
    {
        await stepContext.Context.SendActivityAsync(MessageFactory.Text("NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and 'LuisAPIHostName' to the appsettings.json file.", inputHint: InputHints.IgnoringInput), cancellationToken);
    }
    else
    {
        // Call LUIS with the utterance.
        var luisResult = await _luisRecognizer.RecognizeAsync<FlightBooking>(stepContext.Context, cancellationToken);

        // Create a message showing the LUIS results.
        var sb = new StringBuilder();
        sb.AppendLine($"LUIS results for \"{activity.Text}\":");
        var (intent, intentScore) = luisResult.Intents.FirstOrDefault(x => x.Value.Equals(luisResult.Intents.Values.Max()));
        sb.AppendLine($"Intent: \"{intent}\" Score: {intentScore.Score}");

        await stepContext.Context.SendActivityAsync(MessageFactory.Text(sb.ToString(), inputHint: InputHints.IgnoringInput), cancellationToken);

        // Start a dialog if we recognize the intent.
        switch (luisResult.TopIntent().intent)
        {
            case FlightBooking.Intent.BookFlight:
                return await BeginBookFlight(stepContext, cancellationToken);

            case FlightBooking.Intent.GetWeather:
                return await BeginGetWeather(stepContext, cancellationToken);

            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 new DialogTurnResult(DialogTurnStatus.Complete);
}

开始多步骤操作

预订航班操作启动多步骤对话,以获取用户的预订详细信息。

get-weather 操作尚未实现。 目前,它将发送占位符消息并结束。

DialogSkillBot\Dialogs\ActivityRouterDialog.cs

private async Task<DialogTurnResult> BeginBookFlight(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var bookingDetails = new BookingDetails();
    if (activity.Value != null)
    {
        bookingDetails = JsonConvert.DeserializeObject<BookingDetails>(JsonConvert.SerializeObject(activity.Value));
    }

    // Start the booking dialog.
    var bookingDialog = FindDialog(nameof(BookingDialog));
    return await stepContext.BeginDialogAsync(bookingDialog.Id, bookingDetails, cancellationToken);
}
private static async Task<DialogTurnResult> BeginGetWeather(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var activity = stepContext.Context.Activity;
    var location = new Location();
    if (activity.Value != null)
    {
        location = JsonConvert.DeserializeObject<Location>(JsonConvert.SerializeObject(activity.Value));
    }

    // We haven't implemented the GetWeatherDialog so we just display a TODO message.
    var getWeatherMessageText = $"TODO: get weather for here (lat: {location.Latitude}, long: {location.Longitude}";
    var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput);
    await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken);
    return new DialogTurnResult(DialogTurnStatus.Complete);
}

返回结果

该技能为“预订航班”操作启动预订对话。 由于活动路由对话只包含一个步骤,因此当预订对话结束时,活动路由对话也会结束,而预订对话的对话结果将成为活动路由对话的对话结果。

无需设置返回值,获取天气信息操作即可结束。

取消多步骤操作

预订对话框及其子级日期解析器对话框均派生自基础的“取消和帮助”对话框,后者用于检查来自用户的消息。

  • 单击“帮助”或“?”时,它将显示一条帮助消息,然后在下一轮继续对话流。
  • 单击“取消”或“退出”时,它将取消所有对话,这会结束技能。

有关详细信息,请参阅如何处理用户中断

服务注册

此技能所需的服务与技能机器人通常所需的服务相同。 有关所需服务的讨论,请参阅如何实现技能

技能清单

技能清单是一个 JSON 文件,用于描述技能可以执行的活动、其输入和输出参数以及技能的终结点。 清单包含你从另一个机器人访问该技能所需的信息。

DialogSkillBot\wwwroot\manifest\dialogchildbot-manifest-1.0.json

{
  "$schema": "https://schemas.botframework.azure.cn/schemas/skills/skill-manifest-2.0.0.json",
  "$id": "DialogSkillBot",
  "name": "Skill bot with dialogs",
  "version": "1.0",
  "description": "This is a sample skill definition for multiple activity types.",
  "publisherName": "Microsoft",
  "privacyUrl": "https://dialogskillbot.contoso.com/privacy.html",
  "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
  "license": "",
  "iconUrl": "https://dialogskillbot.contoso.com/icon.png",
  "tags": [
    "sample",
    "travel",
    "weather",
    "luis"
  ],
  "endpoints": [
    {
      "name": "default",
      "protocol": "BotFrameworkV3",
      "description": "Default endpoint for the skill.",
      "endpointUrl": "https://dialogskillbot.contoso.com/api/messages",
      "msAppId": "00000000-0000-0000-0000-000000000000"
    }
  ],
  "activities": {
    "bookFlight": {
      "description": "Books a flight (multi turn).",
      "type": "event",
      "name": "BookFlight",
      "value": {
        "$ref": "#/definitions/bookingInfo"
      },
      "resultValue": {
        "$ref": "#/definitions/bookingInfo"
      }
    },
    "getWeather": {
      "description": "Retrieves and returns the weather for the user's location.",
      "type": "event",
      "name": "GetWeather",
      "value": {
        "$ref": "#/definitions/location"
      },
      "resultValue": {
        "$ref": "#/definitions/weatherReport"
      }
    },
    "passthroughMessage": {
      "type": "message",
      "description": "Receives the user's utterance and attempts to resolve it using the skill's LUIS models.",
      "value": {
        "type": "object"
      }
    }
  },
  "definitions": {
    "bookingInfo": {
      "type": "object",
      "required": [
        "origin"
      ],
      "properties": {
        "origin": {
          "type": "string",
          "description": "This is the origin city for the flight."
        },
        "destination": {
          "type": "string",
          "description": "This is the destination city for the flight."
        },
        "travelDate": {
          "type": "string",
          "description": "The date for the flight in YYYY-MM-DD format."
        }
      }
    },
    "weatherReport": {
      "type": "array",
      "description": "Array of forecasts for the next week.",
      "items": [
        {
          "type": "string"
        }
      ]
    },
    "location": {
      "type": "object",
      "description": "Location metadata.",
      "properties": {
        "latitude": {
          "type": "number",
          "title": "Latitude"
        },
        "longitude": {
          "type": "number",
          "title": "Longitude"
        },
        "postalCode": {
          "type": "string",
          "title": "Postal code"
        }
      }
    }
  }
}

技能清单架构是一个 JSON 文件,用于描述技能清单的架构。 最新架构版本为 v2.1

测试技能机器人

您可以使用 skill consumer 在 Emulator 中测试该技能。 为此,需要同时运行技能和技能使用者机器人。 有关如何配置技能的信息,请参阅如何通过对话使用技能

下载并安装最新的 Bot Framework Emulator

  1. 在计算机上以本地方式运行对话技能机器人和对话根机器人。 如果需要相关说明,请参阅该示例的 README 文件:C#JavaScriptJavaPython

  2. 使用模拟器测试机器人。

    • 第一次加入对话时,机器人会显示欢迎消息,并询问你要调用的技能。 此示例的技能机器人只包含一项技能。
    • 选择“DialogSkillBot”。
  3. 接下来,机器人会要求你为技能选择一项操作。 选择“BookFlight”。

    1. 该技能开始预订航班流程;请根据提示作答。
    2. 技能完成后,根机器人会显示预订详细信息,然后会再次提示你要调用的技能。
  4. 再次选择“DialogSkillBot”和“BookFlight”。

    1. 回答第一个提示,然后输入“cancel”以取消操作。
    2. 技能机器人在未完成该操作的情况下结束,系统会提示用户选择要调用的技能。

有关调试的更多信息

由于技能与技能使用方之间的流量已通过身份验证,因此在调试此类机器人时需要执行额外的步骤。

  • 技能调用方及其直接或间接调用的所有技能都必须处于运行状态。
  • 如果机器人在本地运行,并且任何机器人有应用 ID 和密码,则所有机器人都必须具有有效的 ID 和密码。
  • 如果所有机器人均已部署,请参阅如何使用 devtunnel 从任何通道调试机器人
  • 如果某些机器人在本地运行,而另一些机器人已部署,请参阅如何调试技能或技能使用方

否则,你可以像调试其他机器人一样调试技能使用方或技能。 有关详细信息,请参阅调试机器人使用 Bot Framework Emulator 执行调试

其他信息