实现顺序聊天流

适用于:SDK v4

通过发布问题来收集信息是机器人与用户交互的主要方式之一。 对话库提供有用的内置功能(如 prompt 类),以轻松提问和验证答复,从而确保它与特定数据类型匹配或符合自定义验证规则 。

可以使用对话库来管理线性的和较复杂的对话流。 在线性的交互中,机器人将按固定的顺序运行一组步骤,直到对话完成。 当机器人需要从用户收集信息时,对话就非常有用。

本文介绍如何通过创建提示并从瀑布对话调用这些提示来实现线性的对话流。 有关如何在不使用对话库的情况下编写自己的提示的示例,请参阅创建自己的提示来收集用户输入一文。

重要

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

先决条件

关于此示例

在多轮次提示示例中,我们将使用一个瀑布对话、几个提示和一个组件对话来创建线性的交互,向用户提出一系列问题。 代码使用对话来循环执行以下步骤:

步骤 提示类型
请求用户提供其交通方式 选择提示
请求用户输入其姓名 文本提示词
询问用户是否愿意提供其年龄 确认提示
如果他们回答“是”,则请求他们提供年龄 带验证的数字输入提示,只接受大于 0 且小于 150 的年龄值
如果他们不使用 Microsoft Teams,则请求他们提供个人资料图片 带有允许附件缺失验证的附件提示
询问收集的信息是否“正确” 重复使用确认提示

最后,如果用户回答“是”,则显示收集的信息;否则,告知用户他们的信息不会保留。

创建主对话

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

机器人通过 UserProfileDialog 来与用户交互。 创建机器人的 DialogBot 类时,UserProfileDialog 将设置为其主对话。 然后,机器人使用 Run 帮助器方法来访问该对话。

C# 示例的类图。

Dialogs\UserProfileDialog.cs

首先创建派生自 UserProfileDialog 类的 ComponentDialog,这包括 7 个步骤。

UserProfileDialog 构造函数中创建瀑布步骤、提示和瀑布对话,然后将其添加到对话集。 这些提示需要处于使用它们的同一对话集中。

public UserProfileDialog(UserState userState)
    : base(nameof(UserProfileDialog))
{
    _userProfileAccessor = userState.CreateProperty<UserProfile>("UserProfile");

    // This array defines how the Waterfall will execute.
    var waterfallSteps = new WaterfallStep[]
    {
        TransportStepAsync,
        NameStepAsync,
        NameConfirmStepAsync,
        AgeStepAsync,
        PictureStepAsync,
        SummaryStepAsync,
        ConfirmStepAsync,
    };

    // Add named dialogs to the DialogSet. These names are saved in the dialog state.
    AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
    AddDialog(new TextPrompt(nameof(TextPrompt)));
    AddDialog(new NumberPrompt<int>(nameof(NumberPrompt<int>), AgePromptValidatorAsync));
    AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    AddDialog(new AttachmentPrompt(nameof(AttachmentPrompt), PicturePromptValidatorAsync));

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

接下来,添加对话框用来提示用户输入的步骤。 若要使用提示,请从对话中的某个步骤调用它,然后使用 stepContext.Result 检索下一步骤中的提示结果。 在幕后,提示是由两个步骤组成的对话。 首先,提示要求提供输入。 然后它返回有效值,或者使用重新提示从头开始,直到收到有效的输入为止。

应始终在瀑布步骤中返回非空的 DialogTurnResult。 否则,对话可能不按设计意图运行。 下面显示了瀑布对话中 NameStepAsync 的实现。

private static async Task<DialogTurnResult> NameStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["transport"] = ((FoundChoice)stepContext.Result).Value;

    return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions { Prompt = MessageFactory.Text("Please enter your name.") }, cancellationToken);
}

AgeStepAsync 中,指定当用户输入采用的是提示无法分析的格式,或者不符合验证条件,因而无法验证时,要使用的重试提示。 在这种情况下,如果未提供重试提示,则提示将使用初始提示文本来重新提示用户输入。

private async Task<DialogTurnResult> AgeStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    if ((bool)stepContext.Result)
    {
        // User said "yes" so we will be prompting for the age.
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        var promptOptions = new PromptOptions
        {
            Prompt = MessageFactory.Text("Please enter your age."),
            RetryPrompt = MessageFactory.Text("The value entered must be greater than 0 and less than 150."),
        };

        return await stepContext.PromptAsync(nameof(NumberPrompt<int>), promptOptions, cancellationToken);
    }
    else
    {
        // User said "no" so we will skip the next step. Give -1 as the age.
        return await stepContext.NextAsync(-1, cancellationToken);
    }
}

UserProfile.cs

用户的交通方式、姓名和年龄将保存在 UserProfile 类的实例中。

public class UserProfile
{
    public string Transport { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

    public Attachment Picture { get; set; }
}

Dialogs\UserProfileDialog.cs

在最后一步,检查上一个瀑布步骤中调用的对话框所返回的 stepContext.Result。 如果返回值为 true,则个人资料访问器将获取并更新用户个人资料。 若要获取用户个人资料,请调用 GetAsync,然后设置 userProfile.TransportuserProfile.NameuserProfile.AgeuserProfile.Picture 属性的值。 最后,在调用用于结束对话的 EndDialogAsync 之前汇总用户的信息。 结束该对话会将其从对话堆栈中弹出,并向其父对话返回一个可选结果。 父级是启动了刚刚结束的对话的对话或方法。

    else
    {
        msg += $" Your profile will not be kept.";
    }

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}

private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    stepContext.Values["picture"] = ((IList<Attachment>)stepContext.Result)?.FirstOrDefault();

    // Get the current profile object from user state.
    var userProfile = await _userProfileAccessor.GetAsync(stepContext.Context, () => new UserProfile(), cancellationToken);

    userProfile.Transport = (string)stepContext.Values["transport"];
    userProfile.Name = (string)stepContext.Values["name"];
    userProfile.Age = (int)stepContext.Values["age"];
    userProfile.Picture = (Attachment)stepContext.Values["picture"];

    var msg = $"I have your mode of transport as {userProfile.Transport} and your name as {userProfile.Name}";

    if (userProfile.Age != -1)
    {
        msg += $" and your age as {userProfile.Age}";
    }

    msg += ".";

    await stepContext.Context.SendActivityAsync(MessageFactory.Text(msg), cancellationToken);

    if (userProfile.Picture != null)
    {
        try
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Attachment(userProfile.Picture, "This is your profile picture."), cancellationToken);
        }
        catch
        {
            await stepContext.Context.SendActivityAsync(MessageFactory.Text("A profile picture was saved but could not be displayed here."), cancellationToken);

运行对话

Bots\DialogBot.cs

OnMessageActivityAsync 处理程序使用 RunAsync 方法来启动或继续对话。 OnTurnAsync 使用机器人的状态管理对象将所有状态更改保存到存储中。 ActivityHandler.OnTurnAsync 方法调用各种活动处理程序方法,例如 OnMessageActivityAsync。 这样,在消息处理程序完成之后、轮次本身完成之前,状态会被保存。

public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
    await base.OnTurnAsync(turnContext, cancellationToken);

    // Save any state changes that might have occurred during the turn.
    await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    Logger.LogInformation("Running dialog with Message Activity.");

    // Run the Dialog with the new message Activity.
    await Dialog.RunAsync(turnContext, ConversationState.CreateProperty<DialogState>(nameof(DialogState)), cancellationToken);
}

为机器人注册服务

此机器人使用以下服务:

  • 机器人的基本服务:凭据提供程序、适配器和机器人实现。
  • 用于管理状态的服务:存储、用户状态和聊天状态。
  • 机器人使用的对话。

Startup.cs

Startup 中为机器人注册服务。 可以通过依赖项注入将这些服务用于其他代码部分。

{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient().AddControllers().AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth;
        });

        // Create the Bot Framework Authentication to be used with the Bot Adapter.
        services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

        // Create the Bot Adapter with error handling enabled.
        services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

        // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.)
        services.AddSingleton<IStorage, MemoryStorage>();

        // Create the User state. (Used in this bot's Dialog implementation.)
        services.AddSingleton<UserState>();

        // Create the Conversation state. (Used by the Dialog system itself.)
        services.AddSingleton<ConversationState>();

注意

内存存储仅用于测试,不用于生产。 请务必对生产用机器人使用持久型存储。

测试你的机器人

  1. 安装 Bot Framework Emulator(如果尚未安装)。

  2. 在计算机本地运行示例。

  3. 按如下所示启动 Emulator,连接到机器人,然后发送消息。

与多轮提示机器人对话的示例记录。

其他信息

关于对话和机器人状态

在此机器人中,定义了两个状态属性访问器:

  • 在会话状态中为对话状态属性创建了一个。 对话状态跟踪用户在对话集的对话中所处的位置,由对话上下文更新,例如,在调用 begin dialog 或 continue dialog 方法时,会更新对话状态。
  • 其中一个是在用户状态中为用户配置文件属性创建的。 机器人使用它来跟踪其所掌握的用户信息,而你必须在对话代码中显式管理该状态。

状态属性访问器的 getset 方法在状态管理对象的缓存中获取和设置属性值。 首次在某个轮次中请求状态属性的值时,将填充该缓存,但是,必须显式持久保存该值。 为了持久保存对这两个状态属性所做的更改,我们将调用相应状态管理对象的 save changes 方法。

此示例从对话内部更新用户配置文件状态。 这种做法适用于某些机器人;如果你想要在多个机器人中重复使用某个对话,则此做法不适用。

有多种选项可将对话步骤与机器人状态相分离。 例如,在对话收集完整信息后,你可以:

  • 使用 end dialog 方法将收集的数据作为返回值返回给父上下文。 此上下文可能是机器人的轮次处理程序,或对话堆栈中以前的某个活动对话,这就是提示类的设计方式。
  • 向相应的服务生成请求。 如果你的机器人作为一个更大型服务的前端,这种方式可能会很有效。

提示验证程序方法的定义

UserProfileDialog.cs

下面是 AgePromptValidatorAsync 方法定义的验证程序代码示例。 promptContext.Recognized.Value 包含已解析的值,此值是一个整数,用于数值提示。 promptContext.Recognized.Succeeded 指示提示是否能够解析用户的输入。 验证程序应返回 false 以指示未接受该值,且提示对话应重新提示用户;否则,返回 true 以指示接受输入并从提示对话返回。 可以根据方案更改验证程序中的值。

    }

    // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
    return await stepContext.PromptAsync(nameof(ConfirmPrompt), new PromptOptions { Prompt = MessageFactory.Text("Is this ok?") }, cancellationToken);
}

后续步骤