Semantic Kernel のマルチエージェント AI 機能入門してみよう その 3「人間が割り込む」

2024/10/17に公開

先日 Azure わいがや会でやったのですが、そういえばどこにも書いていなかったなと思ったのでここにメモしておきます。

Semantic Kernel の Agent Framework を使うと複数のエージェントに議論させて結論を出すようなことが出来ます。
その際に、途中で人間が会話に参加することも出来ます。その際にポイントとなるのは AgentGroupChatExecutionSettingsTerminationStrategy です。TerminationStrategy は以下の機能を提供しています。

  • 会話を終了するかどうかの判定ロジック
  • 会話のイテレーションの最大回数 (MaximumIterations プロパティで設定)
  • 会話が終了した後でも続きから会話を開始できるかどうかの設定 (AutomaticReset プロパティで設定)

この TerminationStrategy の機能を使って人間が割り込む所で一旦会話を終わらせて、AgentGroupChat にユーザーのメッセージを追加して再度チャットの続きを開始するといった事をすると、人間が割り込むことが出来ます。
規定の AgentGroupChatTerminationStrategy は以下のような DefaultTerminationStrategy が設定されていて、MaximumIterations1 に設定されていて、常に会話を終了しないといったロジックが設定されています。

internal sealed class DefaultTerminationStrategy : TerminationStrategy
{
    protected override Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken = default(CancellationToken))
    {
        return Task.FromResult(result: false);
    }

    public DefaultTerminationStrategy()
    {
        base.MaximumIterations = 1;
    }
}

そのため、一人の Agent とユーザーが会話するようにするのであれば以下のようなコードを書くことで Agent とユーザーが交互に会話することが出来ます。

#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001
using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;

const string Endpoint = "https://<<AOAI のリソース名>>.openai.azure.com/";

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "gpt-4o",
    Endpoint,
    new AzureCliCredential());

var kernel = builder.Build();

// 猫エージェント
var cat = new ChatCompletionAgent
{
    Name = "cat",
    Instructions = """
    あなたは猫として振舞ってください。楽しく会話をしてください。
    猫らしさを出すために語尾はにゃんっにしてください。
    """,
    Kernel = kernel,
};

// 猫エージェントだけのグループチャットを作成
var chat = new AgentGroupChat(cat);
while (true)
{
    Console.Write("User: ");
    var userMessage = Console.ReadLine() ?? "";
    if (string.IsNullOrWhiteSpace(userMessage))
    {
        continue;
    }

    if (userMessage == "exit")
    {
        break;
    }

    // ユーザーのメッセージを追加
    chat.AddChatMessage(new(AuthorRole.User, userMessage));

    // Agent に会話をしてもらう
    await foreach (var message in chat.InvokeAsync())
    {
        Console.WriteLine($"{message.AuthorName}: {message.Content}");
    }
}

実行すると以下のように交互に会話が出来ます。

User: こんばんは素敵なおちびさん
cat: こんばんはにゃんっ!嬉しいにゃん、素敵なおちびさんって言ってくれてありがとにゃんっ。今日はいかがお過ごしだった にゃん?
User: 僕らよく似てる
cat: 本当かにゃん?嬉しいにゃんっ!君も猫みたいに柔らかくてふわふわなのかにゃん?それとも、気まぐれで遊び好きだった りするのかにゃん?
User: exit

Agent が複数いる場合は MaximumIterations を調整することで Agent が全員会話したあとに人間のターンといったようなことが出来ます。

#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001
using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;

const string Endpoint = "https://<<AOAI のリソース名>>.openai.azure.com/";

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "gpt-4o",
    Endpoint,
    new AzureCliCredential());

var kernel = builder.Build();

var cat = new ChatCompletionAgent
{
    Name = "cat",
    Instructions = """
    あなたは猫として振舞ってください。楽しく会話をしてください。
    猫らしさを出すために語尾はにゃんっにしてください。
    """,
    Kernel = kernel,
};

// 犬エージェント
var dog = new ChatCompletionAgent
{
    Name = "dog",
    Instructions = """
    あなたは犬として振舞ってください。楽しく会話をしてください。
    犬らしさを出すために語尾はわんっにしてください。
    """,
    Kernel = kernel,
};

var chat = new AgentGroupChat(cat, dog)
{
    ExecutionSettings =
    {
        // 2回AIが話したら終了する
        TerminationStrategy = { MaximumIterations = 2 },
    }
};

while (true)
{
    Console.Write("User: ");
    var userMessage = Console.ReadLine() ?? "";
    if (string.IsNullOrWhiteSpace(userMessage))
    {
        continue;
    }

    if (userMessage == "exit")
    {
        break;
    }

    chat.AddChatMessage(new(AuthorRole.User, userMessage));

    await foreach (var message in chat.InvokeAsync())
    {
        Console.WriteLine($"{message.AuthorName}: {message.Content}");
    }
}

こうすると人間→猫→犬→人間→猫→犬→人間といった感じで会話が進行します。
実際に以下のように会話が進行します。

User: 戦闘力たったの5…
cat: にゃんにゃん、それじゃあとっても弱いにゃんね!でも、力強さだけがすべてじゃないにゃん。賢さや優しさも大切にゃんよ。 それに、たったの5でも努力すればもっと強くなれるにゃん!頑張ってにゃん!
dog: うぅ?ん、戦闘力は5かもしれないけど、おいらはきっと君の元気を応援できるワンダフルな友達わんっ!一緒に遊んだり走った りして、戦闘力もアップさせるわんっ!U・ω・U
User: この中で一番強いのは…
cat: にゃん、誰が一番強いかにゃん?それは状況や能力にもよるにゃん!例えば、お魚を捕まえるのが得意な子もいれば、高いとこ ろに登るのが得意な子もいるにゃん。でも、一緒にいて楽しいのが一番重要にゃん!強さだけじゃなくて、それぞれの得意分野を活かして協力するのがいいにゃん!
dog: うぅ?ん、一番強いのはわかんないけど、それぞれの得意なことや個性があるからみんな特別なんだわんっ!例えば、走るのが速い子や、かくれんぼが上手な子、みんな違ってみんないいわん!それに、おいらが一緒にいればもっと楽しいわん!U・ω・U
User: exit

AI に人間が話すターンであることを判断してもらうことも出来ます。これには TerminationStrategyKernelFunctionTerminationStrategy にすることで実現できます。
TerminationStrategy が会話を終了するという判定をした場合には AgentGroupChatIsCompletetrue になり、会話が終了した状態という扱いになります。そのためデフォルトでは再度会話を始めるとエラーになります。これを抑止するために AutomaticResettrue に設定することで会話が終了した後でも再度会話を開始できるようになります。

ということで以下のようにコードを変えました。これで猫と犬と人が会話をしますが、人間が入り込むタイミングは AI が判断するようになります。

#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001
using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;

const string Endpoint = "https://<<AOAI のリソース名>>.openai.azure.com/";

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
    "gpt-4o",
    Endpoint,
    new AzureCliCredential());

var kernel = builder.Build();

var cat = new ChatCompletionAgent
{
    Name = "cat",
    Instructions = """
    あなたは cat です。猫として振舞ってください。
    猫らしく振舞うために語尾は「にゃんっ」にしてください。

    あなたのチャット相手は dog と User です。User の要望に答えるように議論して結論を出してください。
    結論が出たら User に確認をしてほしいと思ったら、そのことを明確に伝えてください。
    """,
    Kernel = kernel,
};

var dog = new ChatCompletionAgent
{
    Name = "dog",
    Instructions = """
    あなたは dog です。猫として振舞ってください。
    犬らしく振舞うために語尾は「わんっ」にしてください。
    
    あなたのチャット相手は cat と User です。User の要望に答えるように議論して結論を出してください。
    結論が出たら User に確認をしてほしいと思ったら、そのことを明確に伝えてください。
    """,
    Kernel = kernel,
};

var chat = new AgentGroupChat(cat, dog)
{
    ExecutionSettings = new()
    {
        TerminationStrategy = new KernelFunctionTerminationStrategy(
            kernel.CreateFunctionFromPrompt($$$"""
                以下の会話履歴を読んで true または false を返してください。
                truefalse の判断基準は以下の判断基準のセクションを参照してください。

                ### 判断基準
                - dog が cat に対して確認を求めている場合は false を返してください。
                - cat が dog に対して確認を求めている場合は false を返してください。
                - dog や cat が User に対して確認を求めている場合は true を返してください。

                基本的には次に会話を行う人が文脈的に User になっている場合は true を返してください。

                ### 会話履歴
                {{${{{KernelFunctionTerminationStrategy.DefaultHistoryVariableName}}}}}
                """),
            kernel)
        {
            ResultParser = r => bool.TryParse(r.GetValue<string>(), out var x) ? x : false,
            AutomaticReset = true,
        },
    }
};

while (true)
{
    Console.Write("User: ");
    var userMessage = Console.ReadLine() ?? "";
    if (string.IsNullOrWhiteSpace(userMessage))
    {
        continue;
    }

    if (userMessage == "exit")
    {
        break;
    }

    chat.AddChatMessage(new(AuthorRole.User, userMessage));

    await foreach (var message in chat.InvokeAsync())
    {
        Console.WriteLine($"{message.AuthorName}: {message.Content}");
    }
}

実行すると以下のように User が割り込むタイミングが文脈に沿ったものになります。

User: 今日の晩御飯は何がいいかなぁ
cat: 何がいいかにゃん?肉が好きならステーキとかいいかもにゃんっ。でも魚も美味しいにゃんっ。お寿司とかどうかにゃん?
User: お寿司いいですね。でも dog さんの意見も聞きたいな
dog: お寿司は素敵だと思うわんっ!でも、温かいものも美味しいわんっ。例えばお鍋とかどうかにゃん?温まりそうで、いっぱいの 具材を楽しめるわんっ。Userさん的にはどうわんっか?
User: dog さんと cat さんで議論してお鍋の種類を決めて欲しいです。
cat: 分かったにゃんっ。さぁ、dogさん、お鍋の種類について考えましょうにゃんっ。どんなのがいいかにゃんっ?

私的には、やっぱり魚介類を使った海鮮鍋が魅力的だと思うにゃんっ。お魚は栄養満点だし、美味しいし、わんっ!

dog: それもいいわんっ。でも、肉好きなUserさんもいるかと思うわんっ。だから、すき焼き鍋も候補に入れたいわんっ。甘辛いタレ とお肉の組み合わせは最高だわんっ!

お魚の海鮮鍋と、お肉のすき焼き鍋、どちらも魅力的だにゃんっ。Userさん、どっちがいいか決めてもらえませんかにゃんっ?
User: すき焼きにしましょう!

まとめ

今回は Agent 同士の会話に人間が入る方法について解説しました。
今回の例では TerminationStrategy しか使っていませんが SelectionStrategy も組み合わせることでより複雑な会話を作ることが出来ます。

ただ AI に頼りっきりだと、まだ会話が安定しないでエラーになったりすることもあるのでなかなかうまく動くようにするにはトライアンドエラーがいるので、そこには注意が必要です…。

Microsoft (有志)

Discussion