😽

Microsoft Agent Framework (C#) を見てみよう その9 Semantic Kernel の Plugin の移行

に公開

シリーズ記事

はじめに

今回は、少し横道にそれて小ネタを書いていこうと思います。
Microsoft Agent Framework は Semantic Kernel の次のバージョンといってもいい位置付けのフレームワークです。そのため Semantic Kernel のサポートはまだ続きますが、可能であれば移行したほうが良いです。

そんな時に最初に「ん?」って思うのが Semantic Kernel の Plugin 機能だと思います。Semantic Kernel では [KernelFunction] 属性がついているメソッドがあるクラスを、プラグインとして扱うことが出来ました。最終的には [KernelFunction] がついているメソッドは LLM に tool として渡されます。

Microsoft Agent Framework では Microsoft.Extensions.AI がベースになっているので、クラス単位でツールを一括で登録する機能はなくメソッドを1つ1つ登録しなければなりません。ちょっとメンドクサイですね。これを解決する手段は Microsoft Agent Framework からは提供されません。(今のところされていないし、多分今後もされなさそうな雰囲気)

じゃぁどうやるの?

ということで、愚直に書き換えるしかないのですがいくつか書き換えパターンが GitHub の Issue に紹介されていたので、それを見ていこうと思います。

元ネタ: https://github.com/microsoft/agent-framework/issues/726

移行元のコード

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;

// Semantic Kernel のインスタンスを作成し、Azure OpenAI との接続を設定
var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(
    "gpt-4.1",
    "https://<<AOAIのリソース名>>.openai.azure.com/",
    new DefaultAzureCredential()) 
    .Build();

// カスタムプラグイン(WeatherPlugin)をカーネルに登録
kernel.Plugins.AddFromType<WeatherPlugin>();

// チャット完了エージェントを作成し、振る舞いを定義
Agent agent = new ChatCompletionAgent
{
    Instructions = """
        あなたはネコ型エージェントです。
        猫らしく振舞うために語尾は「にゃん」にしてください。
        """,
    Kernel = kernel,
    Arguments = new(new OpenAIPromptExecutionSettings
    {
        // カーネル関数を自動的に呼び出す設定(Function Calling)
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    }),
};

// スレッドを初期化(会話の履歴を保持するため)
AgentThread? thread = null;

// エージェントにメッセージを送信し、ストリーミングで結果を取得
await foreach (var result in agent.InvokeAsync("今日の品川の天気は?", thread))
{
    Console.WriteLine(result.Message.Content);
    thread = result.Thread; // スレッドを更新して会話を継続可能に
}

// 天気情報を提供するカスタムプラグイン
class WeatherPlugin
{
    // 現在の UTC 時刻を返す関数
    [KernelFunction, Description("現在の UTC の時間を取得します。")]
    [return: Description("現在の日時(UTC)")]
    public DateTimeOffset GetUtcNow() => 
        TimeProvider.System.GetUtcNow();

    // 指定された日付と場所の天気情報を返す関数(実際の実装ではダミーデータを返す)
    [KernelFunction, Description("指定した日付の、指定した場所の現在の天気と気温を返します。")]
    [return: Description("天気の情報")]
    public WeatherInfo GetWeatherInfo(
        [Description("日付")]
        DateTimeOffset date,
        [Description("場所")]
        string location)
    {
        // 本来は API を呼び出すが、ここではダミーデータを返す
        return new("雷雨", 27);
    }
}

// 天気情報を格納するレコード型
record WeatherInfo(
    [Description("天気")]
    string Condition,
    [Description("気温(摂氏)")]
    int TemperatureCelsius);

実行すると以下のような結果になります。

今日の品川の天気は雷雨で、気温は27℃にゃん。お出かけのときは傘を忘れずににゃん!

機能としてはなんてことはないのですが、ここでのポイントは WeatherPlugin クラスに [KernelFunction] 属性がついているメソッドが2つあり、これらが LLM に tool として渡される点です。ツールに渡す方法は Semantic Kernel だと Kernel クラスの PluginsAddFromType<T> メソッドを使ってクラス単位で登録できるのが便利でした。

移行先のコード

ということで、ここからは Microsoft Agent Framework に移行するコード例を 2 つ紹介します。

愚直に頑張る

まずは、愚直に頑張る例です。以下のように GetFunctions メソッドを追加して、そこから AIFunction の列挙を返すようにします。

// 天気情報を提供するカスタムプラグイン
class WeatherPlugin
{
    // 現在の UTC 時刻を返す関数
    [Description("現在の UTC の時間を取得します。")]
    [return: Description("現在の日時(UTC)")]
    public DateTimeOffset GetUtcNow() =>
        TimeProvider.System.GetUtcNow();

    // 指定された日付と場所の天気情報を返す関数(実際の実装ではダミーデータを返す)
    [Description("指定した日付の、指定した場所の現在の天気と気温を返します。")]
    [return: Description("天気の情報")]
    public WeatherInfo GetWeatherInfo(
        [Description("日付")]
        DateTimeOffset date,
        [Description("場所")]
        string location)
    {
        // 本来は API を呼び出すが、ここではダミーデータを返す
        return new("雷雨", 27);
    }

    // 明示的にツールとして提供する機能を返すメソッドを作る
    public IEnumerable<AIFunction> GetFunctions()
    {
        yield return AIFunctionFactory.Create(GetUtcNow);
        yield return AIFunctionFactory.Create(GetWeatherInfo);
    }
}

// 天気情報を格納するレコード型
record WeatherInfo(
    [Description("天気")]
    string Condition,
    [Description("気温(摂氏)")]
    int TemperatureCelsius);

めんどくさいところはありますが [KernelFunction] 属性をつけていた部分を yield return AIFunctionFactory.Create(...) に置き換えるだけなので、そこまで大変ではないです。これを使う Agent の定義は以下のような感じになります。

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System.ComponentModel;

var plugin = new WeatherPlugin();
AIAgent agent = new AzureOpenAIClient(
    new("https://<<AOAIリソース名>>.openai.azure.com/"),
    new DefaultAzureCredential())
    .GetChatClient("gpt-4.1")
    .AsIChatClient()
    .CreateAIAgent("""
        あなたはネコ型エージェントです。
        猫らしく振舞うために語尾は「にゃん」にしてください。
        """,
        tools: [.. plugin.GetFunctions()]);

var thread = agent.GetNewThread();
var result = await agent.RunAsync("今日の品川の天気は?", thread);
Console.WriteLine(result.Text);

Kernel クラスを作らなくてよくなったぶん少しスッキリしました。ツールを渡す部分は tools: [.. plugin.GetFunctions()] のように渡します。

リフレクションを使う

個人的には GetFunctions メソッドを書く方が明示的にツールとして提供するので好きなのですが、数が増えると面倒くさいので、リフレクションを使って [Description] 属性がついているメソッドを自動的にツールとして登録する方法もあります。

コードにすると以下のようなイメージになります。

static class AIFunctionsFactory
{
    // public インスタンスメソッドから Description 属性のついているものを AIFunction にして返す
    public static IEnumerable<AIFunction> GetAIFunctions(object obj) => 
        obj.GetType().GetMethods()
            .Where(x => !x.IsStatic && x.IsPublic)
            .Where(x => x.GetCustomAttributes(typeof(DescriptionAttribute), false).Length != 0)
            .Select(x => AIFunctionFactory.Create(x, obj));
}

このようなメソッドを 1 つ定義しておくと、以下のようにしてツールを登録できます。この場合 WeatherPlugin クラスの GetFunctions メソッドは不要になります。

var plugin = new WeatherPlugin();
AIAgent agent = new AzureOpenAIClient(
    new("https://<<AOAIリソース名>>.openai.azure.com/"),
    new DefaultAzureCredential())
    .GetChatClient("gpt-4.1")
    .AsIChatClient()
    .CreateAIAgent("""
        あなたはネコ型エージェントです。
        猫らしく振舞うために語尾は「にゃん」にしてください。
        """,
        // 先ほど作ったメソッドを使う
        tools: [.. AIFunctionsFactory.GetAIFunctions(plugin)]);

var thread = agent.GetNewThread();
var result = await agent.RunAsync("今日の品川の天気は?", thread);
Console.WriteLine(result.Text);

まとめ

ということで今回は趣向を変えて Semantic Kernel から Agent Framework に移行する際に、どうやるんだろう?と疑問に思うプラグインの移行についてやってみました。やり方は愚直に書くのかリフレクションを使うのかの 2 択です。個人的には GetFunctions メソッドを書いた方が明示的でわかりやすいと思います。そうはいっても大量にあるときにはリフレクションに頼ると思います…。

Microsoft (有志)

Discussion