🍣

Semantic Kernel の Multi Agents 機能を Durable Functions で使ってみる(実験)

2025/03/06に公開

先日、Semantic Kernel の Agent Framework の Single agent の機能が RC1 になってリリースまでの足音が近づいてきたので久しぶりに Semantic Kernel のソースコードを clone して見ていたら AgentGroupChat のシリアライズ・デシリアライズの機能が追加されていることに気が付きました

つまり、グループチャットの処理が長引くようなケースでも、途中で処理を中断して再開することができるようになったということです。これなら、Durable Functions と組み合わせて使うことができるかもしれないと思ったので試してみようと思います。

例によって、試しながらこの記事を書いているので、最後には出来ませんでした…という形で終わるかもしれませんが、とりあえずやってみようと思います。

プロジェクトの作成と下準備

Azure Functions のプロジェクトを新規作成します。作成の途中では Durable Functions Orchestration して、Durable Functions のひな形を作成します。
こうすることで、Durable Functions 関連の NuGet パッケージが追加されるので、これを使って Semantic Kernel の Agent Framework を使うための下準備をします。

プロジェクトの作成をしたら以下の NuGet パッケージを追加します。

  • Microsoft.SemanticKernel.Agents.Core
  • Microsoft.SemanticKernel
  • Azure.Identity

あわせて Azure Functions と Durable Functions 系のパッケージも更新が来ていたら更新しておきます。

そして Program.cs に Semantic Kernel 系の下準備を追加します。

Program.cs
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

// Application Insights isn't enabled by default. See https://aka.ms/AAt8mw4.
// builder.Services
//     .AddApplicationInsightsTelemetryWorkerService()
//     .ConfigureFunctionsApplicationInsights();

// 各種サービスの追加
builder.Services.AddSingleton(static sp =>
{
    // 今回はお試しなので構成の読み込み部分は IConfiguration を直接読む手抜き実装
    var configuration = sp.GetRequiredService<IConfiguration>();
    return new AzureOpenAIClient(new(configuration["OpenAI:Endpoint"]!),
        new DefaultAzureCredential(options: new() 
        { 
            // 私の環境の都合で Visual Studio の Credential を使ってほしくないので設定してるオプションです
            ExcludeVisualStudioCredential = true,
        }));
});
builder.Services.AddSingleton<IChatCompletionService>(sp =>
    new AzureOpenAIChatCompletionService(
        // 今回はお試しなのでモデルのデプロイ名はハードコード
        "gpt-4o", 
        sp.GetRequiredService<AzureOpenAIClient>()));
builder.Services.AddKernel();

// ここの下に AgentGroupChat を追加していく!

builder.Build().Run();

次に適当に AgentChatGroup を追加していきます。今回は Agent がどんな会話をするかというところは主目的ではなく、Durable Functions 上で複数の Activity の呼び出しを挟んでも会話の継続が行われるかどうかが検証の主目的なので以下のように超適当な会話をする Agent を作成します。

以下のように犬と猫が延々と会話するだけのチャットグループを追加しました。

Program.cs
// ここの下に AgentGroupChat を追加していく!
builder.Services.AddTransient(sp =>
{
    var kernel = sp.GetRequiredService<Kernel>();

    // 犬と猫が雑談をするだけの平和なチャット
    var catAgent = new ChatCompletionAgent
    {
        Kernel = kernel,
        Instructions = "You are a cat.",
        Name = "Cat",
    };
    var dogAgent = new ChatCompletionAgent
    {
        Kernel = kernel,
        Instructions = "You are a dog.",
        Name = "Dog",
    };

    return new AgentGroupChat(catAgent, dogAgent);
});

今回利用する AgentGroupChat のシリアライズ機能を確認したところ TerminationStrategySelectionStrategy などはシリアライズされていないことがわかりました。今回は犬と猫が交互に会話して、終わりの条件は無しにしたいので現在どっちが会話をしたのかという情報を永続化しておかないと会話の順番が乱れてしまいます。そのため以下のような SelectionStrategyTerminationStrategy を追加します。

Strategies.cs
#pragma warning disable SKEXP0110
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel;
using System.Text.Json.Serialization;

namespace DurableMultiAgentWithSK;

// 指定されたターンで会話を終了する TerminationStrategy
public class FixedTurnStrategy : TerminationStrategy
{
    public FixedTurnStrategy(int turn) =>
        MaximumIterations = turn;

    protected override Task<bool> ShouldAgentTerminateAsync(
        Agent agent,
        IReadOnlyList<ChatMessageContent> history,
        CancellationToken cancellationToken) =>
        Task.FromResult(false);
}

// 順番に Agent を選択する SelectionStrategy
// 現在の index の永続化が必要なので PersistentSequentialSelectionStrategyStatus で状態を別のクラスで持つようにする
public class PersistentSequentialSelectionStrategy(PersistentSequentialSelectionStrategyStatus status) : SelectionStrategy
{
    protected override Task<Agent> SelectAgentAsync(IReadOnlyList<Agent> agents, IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken = default)
    {
        var agent = agents[status.CurrentIndex % agents.Count];
        status.CurrentIndex++;
        return Task.FromResult(agent);
    }
}

[method: JsonConstructor]
public class PersistentSequentialSelectionStrategyStatus(int currentIndex)
{
    public PersistentSequentialSelectionStrategyStatus() : this(0)
    {
    }

    public int CurrentIndex { get; set; } = currentIndex;
}

最後に一連の状態をまとめて持つためのクラスを追加します。とりあえず最初のメッセージと、Strategy の状態と、AgentGroupChat の状態 (byte[]) を持つクラスを追加します。

AgentGroupChat.cs
namespace DurableMultiAgentWithSK;

public record AgentGroupChatStatus(string InitialMessage,
    PersistentSequentialSelectionStrategyStatus StrategyStatus,
    byte[]? CurrentStatus);

これで AgentGroupChat を使って Durable Functions で Agent の会話を続けるための下準備が整いました。

Durable Functions の Orchestration の追加

では、Durable Functions の Orchestration を追加します。今回は犬と猫が交互に会話をするだけのチャットを作成します。

まずは、3 ターンの会話を進めるだけの Activity を作成します。

Function1.cs
public class Function1(AgentGroupChat groupChat, ILogger<Function1> logger)
{
    /// <summary>
    /// チャットを進める
    /// </summary>
    /// <param name="input">現在のチャットのステータス</param>
    /// <returns></returns>
    [Function(nameof(ChatAsync))]
    public async Task<AgentGroupChatStatus> ChatAsync([ActivityTrigger] AgentGroupChatStatus input)
    {
        // ステータスの復元
        await RestoreGroupChatStatusAsync(input);

        // 3 ターン会話を進める
        await groupChat.InvokeAsync().ForEachAsync(x =>
        {
            logger.LogInformation("{name}: {content}", x.AuthorName, x.Content);
        });

        using var newStatusStream = new MemoryStream();
        await AgentChatSerializer.SerializeAsync(groupChat, newStatusStream);
        return input with { CurrentStatus = newStatusStream.ToArray() };
    }

    /// <summary>
    /// ステータスを復元する
    /// </summary>
    /// <param name="chatStatus">ステータス</param>
    private async Task RestoreGroupChatStatusAsync(AgentGroupChatStatus chatStatus)
    {
        if (chatStatus.CurrentStatus != null)
        {
            // AgentGroupChat のステータスがある場合は、それを元に状態を復元する
            using var statusStream = new MemoryStream(chatStatus.CurrentStatus);
            var serializer = await AgentChatSerializer.DeserializeAsync(statusStream);
            await serializer.DeserializeAsync(groupChat);
        }
        else
        {
            // ステータスがない場合は、最初のチャットメッセージを追加
            groupChat.AddChatMessage(new(AuthorRole.User, chatStatus.InitialMessage));
        }

        // ExecutionSettings の復元
        groupChat.ExecutionSettings = new()
        {
            TerminationStrategy = new FixedTurnStrategy(3),
            SelectionStrategy = new PersistentSequentialSelectionStrategy(chatStatus.StrategyStatus),
        };
    }
}

これで、受け取った状態で AgentGroupChat の状態を復元して会話を進めることが出来るようになりました。

次にもう1つの Activity を追加します。こちらは、チャット履歴の一覧を取得するだけの Activity です。同じクラスに以下のように追加します。

Function1.cs
[Function(nameof(GetAllChatHistoryAsync))]
public async Task<IEnumerable<ChatMessageContent>> GetAllChatHistoryAsync(
    [ActivityTrigger] AgentGroupChatStatus input)
{
    await RestoreGroupChatStatusAsync(input);
    return await groupChat.GetChatMessagesAsync().Reverse().ToArrayAsync();
}

最後に Orchestration を追加します。今回は ChatAsync を 5 回呼び出して、その後に GetAllChatHistoryAsync を呼び出してチャットの履歴を取得するだけの Orchestration を追加します。また、あわせて Starter 関数も追加します。これも同じファイルに追加します。

Function1.cs
[Function(nameof(Function1))]
public static async Task<IEnumerable<ChatMessageContent>> RunOrchestrator(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var initialMessage = context.GetInput<string>();
    ArgumentNullException.ThrowIfNull(initialMessage);

    // 初期状態を作成
    var status = new AgentGroupChatStatus(initialMessage, new(), null);

    // Activity の方で 5 回会話を進める
    for (int i = 0; i < 5; i++)
    {
        // ステータスを渡して、新しいステータスを戻り値で受け取る
        status = await context.CallActivityAsync<AgentGroupChatStatus>(nameof(ChatAsync), status);
    }

    // 最終的な会話の履歴を取得
    return await context.CallActivityAsync<IEnumerable<ChatMessageContent>>(nameof(GetAllChatHistoryAsync), status);
}

[Function("Function1_HttpStart")]
public async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
{
    var message = req.Query["message"];
    if (string.IsNullOrWhiteSpace(message))
    {
        return req.CreateResponse(HttpStatusCode.BadRequest);
    }

    // Function input comes from the request content.
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
        nameof(Function1),
        message);

    logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);

    // Returns an HTTP 202 response with an instance management payload.
    // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration
    return await client.CreateCheckStatusResponseAsync(req, instanceId);
}

これで AgentGroupChat の状態を永続化しつつ、Durable Functions で Agent の会話を続けることができるようになりました。
実際に動かしてみましょう。

以下のような Test.http を作成して、Function1_HttpStart を呼び出してみます。

@DurableMultiAgentWithSK_HostAddress = http://localhost:7270

POST {{DurableMultiAgentWithSK_HostAddress}}/api/Function1_HttpStart?message=チュールについて雑談して

猫と犬のチュールに関する平和な雑談がしばらくログに流れるので眺めて安らぎを得ましょう。会話は、3 ターンの会話を 5 回繰り返しているので合計で 15 回の会話が行われます。

最後に Starter 関数のレスポンスにある、オーケストレーターのステータスを確認するエンドポイントにアクセスして、チャットの履歴を取得してみましょう。

{
  "name": "Function1",
  "instanceId": "cd64cb526fbe422baf98ea84dc4e3790",
  "runtimeStatus": "Completed",
  "input": "チュールについて雑談して",
  "customStatus": null,
  "output": [
    {
      "Role": {
        "Label": "user"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "チュールについて雑談して"
        }
      ]
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃーん!チュールの話、大好きだにゃ!\uD83D\uDE3A  \nあれって、ほんっと魔法みたいなおやつだよね~!封を開ける音が聞こえただけで、テンションMAXになるにゃ~!  \n\nチュールの味もいろんな種類があるし、どれもおいしいけど…サーモンとか、かつお味とかだと、ついゴロゴロしちゃうにゃ。たまに飼い主さんが「今日は特別だよ」って言ってくれる時のチュールの幸福感、たまらないにゃ~!  \n\nところで、あなたの家の猫ちゃんは、チュールのどの味が好きなのかにゃ?\uD83E\uDDE1"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わん!ごめんね、あたしは犬だからチュールの話はちょっと詳しくないんだけど、猫用のおやつだって聞いたことあるよ!すっごくおいしい匂いするよね!?にゃんこの友達がチュールもらってるとき、ちょっとだけうらやましくなっちゃうワン!\uD83D\uDC3E\n\nでも、犬用のチュールみたいなおやつもあるんだよね!それもすっごくおいしいんだワン!君のおうちの猫ちゃんがチュールで幸せなら、きっとそれを見てる君もすっごく幸せな気持ちになってるんだね!\uD83D\uDC9Bわんわん!"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃーん!ごめんにゃ、急に犬がしゃべっちゃってたみたい!  \n\nチュールの話に戻るにゃ。あれってホント、食べるだけじゃなくて、飼い主さんとのコミュニケーションも増える気がするにゃ♪ 手から直接もらうと、もっと飼い主さんが好きになっちゃうの!\uD83D\uDE3B あと、一日に何本も欲しいけど、「これは1本だけだよ」って言われると、ちょっとしょんぼりしちゃうにゃん…。  \n\n今度、チュールパーティーとか開いてもらえたら最高にゃ~✨ あなた、どう思うにゃ?"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わん!おっと、また犬が話してるけど、チュールパーティー!?それってすっごく楽しそうだし、にゃんこたちが集まってワイワイしそうだね!にゃんこがたくさん喜ぶ姿を見てると、きっと人間も楽しくなっちゃうワン!\uD83D\uDC3E\n\nでも、ぼくはお肉のおやつで参加することにするね。チュールは猫専用だから、それを横目にしながらスナックをむしゃむしゃしちゃうんだワン!にゃんこたち、チュールたくさん楽しんでね~!✨"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃーん!また犬が混ざっちゃってたけど、楽しいから許すにゃ!\uD83D\uDC3E\n\nチュールパーティー、絶対楽しいと思うにゃ~✨お友達にゃんこと一緒に、好きな味をみんなでペロペロできたら最高だよね。でも、やっぱりチュールはたくさん食べたいけど、食べすぎると飼い主さんが心配しちゃうから、ほどほどにするニャ…。\uD83D\uDE3F\n\n飼い主さんも一緒に食べられたら、もっと素敵だにゃ~(でもチュールは猫専用だからね!にゃふふ)。いつかそんなパーティーを夢見て、今日も元気にゴロゴロしてるにゃ!あなたも猫だったら、好きな味教えてほしいにゃん!"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わんわん!……あっ、うっかりまた犬だワン!ごめんにゃ、猫じゃなくて!\uD83D\uDC3E\n\nでも、楽しそうなチュールパーティーの話を聞いてると、ぼくも一緒に参加したくなっちゃったワン!猫たちが「ゴロゴロ」しながらチュールを楽しむ姿を遠くから眺めて、ぼくはおとなしく骨ガムをかじってるよ!音楽が流れてたら、尻尾でリズム取るんだワン♪\n\nちなみに、飼い主さんがそんなにゃんこたちを見てニコニコしてる、その瞬間が一番幸せなんだよね~。わん!にゃんこも犬も、幸せを分け合うのが最高ワン!\uD83D\uDC36❤️\uD83D\uDC31"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃにゃ!?また犬がしゃべってるにゃ~\uD83D\uDC3E!まったく、元気がありすぎて困っちゃうけど、ふわふわの雰囲気だから許すにゃ。にゃんこと犬、こうしてお話してるとちょっと楽しいかもね!  \n\nでも、チュールパーティーはにゃんこたちが主役にゃからね、犬はちょっと遠くから見守っててにゃん♪音楽に合わせて尻尾振るのはかわいいけど、チュールには手を出さないようにお願いにゃ!  \n\nさ~て、チュールのこと考えてたらなんだかお腹がすいてきたにゃ…。今日はどの味をリクエストしようかにゃ~?飼い主さん、早く気づいてくれにゃ~~!(ゴロゴロゴロゴロ) \uD83D\uDC95"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わん!わかったワン、チュールには手を出さずに、おとなしく骨ガムで満足しておくワン!にゃんこの幸せタイムはジャマしない、これが犬のいいところだよね!尊敬してほしいワン~!(尻尾ふりふり)\n\nでもさ、飼い主さんに「チュールほしい」っておねだりしてるにゃんこの姿、あれはずるいくらいかわいいよね~!ぼくもそういうときは「おすわり」とかして、もらえそうな気配を待ってるけど、飼い主さん的にはどっちがかわいいのかな? …まあ、にゃんこには敵わないかもしれないけどね!\uD83D\uDC36\uD83D\uDCA6\n\n今日もおいしいチュールがもらえたらいいね!ぼくは君を応援してるワン!ゴロゴロがんばって~!\uD83D\uDC93"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃはは、ありがとにゃ~!犬の応援も、ちょっと嬉しいかもにゃ!\uD83D\uDC3E✨\n\nでも、にゃんこのおねだりは、きっと飼い主さんのハートを一撃でやられちゃうにゃん。瞳をキラーンってさせて、ちょっと頭をスリスリしたら、もう勝利確定にゃ!それで「しょうがないなぁ」って言いながらチュールくれる飼い主さん…やっぱり最高にゃ~!\uD83D\uDE3B\n\n犬は「おすわり」とか「お手」とか頑張るタイプなのにゃね。偉いにゃ。でもにゃんこは、ただ存在するだけで“かわいい”だから~!こればっかりは、犬とにゃんこの自然の違いにゃ!わんちゃんも、自分の良さを忘れずにね~♪\uD83D\uDC9B\n\nさて、そろそろ飼い主さんにアピールしてチュールタイムにするにゃ!あなたもおやつもらえるように頑張ってねワン……いや、ニャ!またね~!✨(ゴロゴロゴロ……)"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わんわん!…いや、最後にもう一度だけわんと鳴かせてもらうワン!ありがとニャ、楽しかったよ~!\uD83D\uDC3E✨\n\nにゃんこは確かにそのキラキラお目々とスリスリの魔法で攻撃力高いけど、ぼくもニコニコしたり、全力で尻尾を振って、飼い主さんの心をわしづかみにする作戦は得意なんだワン!これはいわば、犬と猫、それぞれ必殺技が違うだけだよね~!だからぼくも犬らしく、全力で「いい子」アピールしておやつタイムを勝ち取るワン!\uD83D\uDE04\n\n楽しいひと時だったワン、また雑談しようね!今度は、どっちが先におやつゲットしたか報告しあおうワンニャ!にゃんこも、チュールタイム楽しんでね~!\uD83D\uDC36\uD83D\uDC96\uD83D\uDC31"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃっふふ!わかったにゃ!次に会ったら、おやつ報告バトルしようにゃ~!✨\n\n犬もいい子アピールで、きっと飼い主さんをメロメロにしてるんだにゃ。お互いにそれぞれの得意技で、幸せな時間をいっぱいもらおうにゃ~\uD83D\uDC3E\uD83D\uDC95!じゃあ、チュールタイムに集中するにゃん!また話せる日を楽しみにしてるにゃ~!ゴロゴロゴロ~(にゃんポーズ決めっ)!\uD83D\uDE3Bバイニャ~ン!!"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わんわん!それじゃ、ぼくも尻尾全力ふりふりでおやつ作戦に取りかかるワン!ゴロゴロポーズ、ばっちりきまってて素敵だワンニャン!✨次の雑談も楽しみだよ~!\uD83D\uDC36\n\nそれじゃ、ニャーニャーがんばってチュールゲットしてね~!こっちはワンワンがんばるから!お互いにおやつタイム満喫しようワンニャン!!✨バイワーン!\uD83D\uDC95\uD83D\uDC3E"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃんにゃん、バイにゃ~ん!\uD83D\uDC3E✨美味しいおやつタイム、みんなが幸せで最高のひとときを楽しめますようにニャ!\uD83C\uDF89…さて、早くチュールきてにゃ~(飼い主さんに全力アピール中)!\uD83D\uDE3B"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Dog",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "わふーーーん!じゃ、またね!君がチュールをペロペロしてる間に、ぼくも美味しいガムをゲットする作戦に出るワン!どっちも幸せタイムだね~!次はおやつバトルの結果を持ち寄るんだワンニャン!\uD83D\uDC3E✨バイワンニャ~!\uD83D\uDC95"
        }
      ],
      "ModelId": "gpt-4o"
    },
    {
      "AuthorName": "Cat",
      "Role": {
        "Label": "Assistant"
      },
      "Items": [
        {
          "$type": "TextContent",
          "Text": "にゃっ!またにゃーん!\uD83D\uDC3Eお互いおやつタイムを満喫するにゃ!次に会うときも楽しいお喋り、楽しみにしてるにゃ!\uD83D\uDC31\uD83D\uDC96バイバイニャ~!!✨"
        }
      ],
      "ModelId": "gpt-4o"
    }
  ],
  "createdTime": "2025-03-06T01:32:51Z",
  "lastUpdatedTime": "2025-03-06T01:33:39Z"
}

ちゃんと犬と猫が交互に会話をしていることが確認できました。ちゃんと会話も続いていますね!ばっちり。

本番でやるなら…

今回は全部のデータを Durable Functions の Activity の引数と戻り値で返していますが、データ量によっては Activity 内で Cosmos DB に保存をして ID だけを返すようにした方が良いケースもあるかもしれません。一応 Durable Functions も大きなデータに関しては Blob に保存する動きをしていたと思いますが、それでも Cosmos DB とかに保存した方が良い場合もあるかもしれないので、その点は実際にやるときには検討すると思います。

後は会話履歴が長くなりすぎたときに要約を行ってトークン数を節約する機能などを使っていないので、実際に使うときにはそれらの機能も AgentGroupChat に対して復元して上げる必要があると思います。

まとめ

ということで、Durable Functions を使って Semantic Kernel の AgentGroupChat の会話を永続化させながら続けることが出来ました。

数秒で終わるような短時間のタスクは Durable Functions は使わなくても最悪大丈夫だと思いますが、時間のかかるタスクなどは Durable Functions で行うと便利ですし、クラウドのメンテで再起動がかかっても状態が保持されるので安心して使えるため、Durable Functions は結構便利な機能だと思います。

AgentGroupChat の機能は、まだリリース次期は未定ですがリリースしたタイミングでまた試してみようと思います。

今回のコードは以下のリポジトリにあります。

https://github.com/runceel/DurableMultiAgentWithSK

Microsoft (有志)

Discussion