🙆

Semantic Kernel の Function calling サポートを試してみよう

2023/10/10に公開

2023年の9月末くらいに以下のPull RequestsでFunction callingのサポートが追加されていました。

https://github.com/microsoft/semantic-kernel/pull/2842

そして、本日 .NET 版の Semantic Kernel の v1.0.0-beta1 が出ていて、上記の Pull Requests が取り込まれたバージョンがリリースされています。ということで早速ためしてみようと思います。

https://github.com/microsoft/semantic-kernel/releases/tag/dotnet-1.0.0-beta1

以前、Function calling が出たころに Semantic Kernel で Function calling をなんとか使えないか試した時に、FunctionView クラスの Parameters に IsRequired プロパティが無いので Function calling に渡す関数の必須パラメーターの設定がどうやっても出来ないので諦めていたのですが、今回の更新でちゃんと IsRequired プロパティも追加されていました。やったね。

やってみよう

下準備

ということで、コンソールアプリのプロジェクトを作って以下のパッケージを追加して準備します。

  • Azure.Identity
  • Microsoft.SemanticKernel (v1.0.0-beta1)

自分の Azure CLI などでログインしているユーザーに対して、Azure Portal で Cognitive Services OpenAI User を追加して Managed ID 認証で呼び出せるように準備もしておきます。

呼び出される関数の定義

Semantic Kernel には、大まかに分類して AI を呼び出す Semantic Function とネイティブの関数を呼び出す Native Funtion があります。
今回は簡単に試すために、Native Function を 2 つ定義して、それを呼び分けるようにしてみようと思います。ということで以下のレストランのレコメンドをしてくれる関数と、ショッピングセンターのレコメンドをしてくれる関数を作りました。Function calling で関数を選択してくれることが今回の主目的なのでレコメンド結果は定型文です。

Semantic Kernel のコードを確認したところ関数のパラメーターの IsRequired についてはデフォルトの値が設定されているかどうかで見てるっぽいので category パラメーターにはデフォルト値を設定しています。

class NativeFunctions
{
    [SKFunction]
    [Description("お勧めのレストランの名前を返します。")]
    public string RecommendRestaurant(
        [Description("地名")] string location, 
        [Description("レストランの食事のカテゴリ")]string? category = null)
    {
        return $"うまい {location}{(string.IsNullOrEmpty(category) ? "" : $"{category} の ")}レストラン";
    }

    [SKFunction]
    [Description("お勧めのショッピングセンターの名前を返します。")]
    public string RecommendShop(
        [Description("地名")] string location, 
        [Description("ショッピングセンターのカテゴリ")] string? category = null)
    {
        return $"お勧めの {location}{(string.IsNullOrEmpty(category) ? "" : $"{category} の ")}ショッピングセンター";
    }
}

これを Semantic Kernel に読み込んで FunctionView を表示してみましょう。

using Microsoft.SemanticKernel;
using System.ComponentModel;
using System.Runtime.InteropServices;

var kernel = Kernel.Builder.Build();

kernel.ImportFunctions(new NativeFunctions(), "Recommender");
var fv = kernel.Functions.GetFunctionViews();
foreach (var f in fv)
{
    Console.WriteLine("----------");
    Console.WriteLine($"{f.Name}({f.Description}))");
    foreach (var p in f.Parameters)
    {
        Console.WriteLine($"  {p.Name}({p.Description}), IsRequired: {p.IsRequired}");
    }
}

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

----------
RecommendShop(お勧めのショッピングセンターの名前を返します。))
  location(地名), IsRequired: True
  category(ショッピングセンターのカテゴリ), IsRequired: False
----------
RecommendRestaurant(お勧めのレストランの名前を返します。))
  location(地名), IsRequired: True
  category(レストランの食事のカテゴリ), IsRequired: False

いい感じですね。

Function calling に渡してみよう

Function calling を使った Planner とかは無いので Semantic Kernel が提供する IChatCompletion インターフェースを使って利用する形になります。
チャットを呼び出すときに一緒に渡す OpenAIRequestSettingsFunctionsFunctionView から Function calling の形式に変換したものと、FunctionCallOpenAIRequestSettings.FunctionCallAuto を渡してあげることで Function calling が有効になります。

チャットが関数呼び出しで終わった場合には GetFunctionResponse に値が入っているので、それで関数を呼び出す必要があるのかどうか判定が出来ます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk;
using System.ComponentModel;

// エンドポイントとモデルのデプロイ名
const string Endpoint = "https://ai-kaota-gpt4.openai.azure.com/";
const string ModelDeployName = "gpt-4-32k";

var kernel = Kernel.Builder
    // Azure OpenAI のチャットを使うための設定
    .WithAzureChatCompletionService(ModelDeployName, Endpoint, new AzureCliCredential())
    .Build();

kernel.ImportFunctions(new NativeFunctions(), "Recommender");

var chatCompletion = kernel.GetService<IChatCompletion>();

// Kernel に登録されている関数を OpenAI の関数に変換しておく
var requestSettings = new OpenAIRequestSettings()
{
    Functions = kernel.Functions.GetFunctionViews().Select(x => x.ToOpenAIFunction()).ToArray(),
    FunctionCall = OpenAIRequestSettings.FunctionCallAuto,
};

// チャットメッセージのを組み立てて
var chatHistory = chatCompletion.CreateNewChat();
chatHistory.AddUserMessage("東京のお勧めのレストランについて教えてください。");

// OpenAI を呼び出して、結果の最初だけ取得
var chatResult = (await chatCompletion.GetChatCompletionsAsync(chatHistory, requestSettings))[0];
// チャットメッセージのレスポンスを確認
var chatMessage = await chatResult.GetChatMessageAsync();
Console.WriteLine($"Role: {chatMessage.Role}");
Console.WriteLine($"Content: {chatMessage.Content}");

// 関数呼び出しがあるかどうかは FunctionResponse が null かどうかで判断する
var functionResult = chatResult.GetFunctionResponse();
Console.WriteLine($"Plugin name: {functionResult?.PluginName}");
Console.WriteLine($"Function name: {functionResult?.FunctionName}");
foreach (var p in functionResult?.Parameters ?? [])
{
    Console.WriteLine($"  Param name: {p.Key}, value: {p.Value}");
}

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

Role: assistant
Content:
Plugin name: Recommender
Function name: RecommendRestaurant
  Param name: location, value: 東京

Function calling が有効になっているので、チャットの結果が RecommendRestaurant という関数の呼び出しになっていますね。
パラメーターの location にも、しっかり東京が入っています。category は特に元の文章に入っていなかったので、パラメーター自体が無いのもちゃんとなってます。

戻ってきた関数を呼び出そう

Function calling で呼び出すべき関数が返されることがわかったので、次は関数を呼び出してみましょう。
一応、そのための機能も提供されていて kernel.Functions.TryGetFunctionAndContext を使うことで呼び出すべき関数を取得できます。
関数が取得できたら kernelRunAsync で呼び出せます。

using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk;
using System.ComponentModel;

// エンドポイントとモデルのデプロイ名
const string Endpoint = "https://ai-kaota-gpt4.openai.azure.com/";
const string ModelDeployName = "gpt-4-32k";

var kernel = Kernel.Builder
    // Azure OpenAI のチャットを使うための設定
    .WithAzureChatCompletionService(ModelDeployName, Endpoint, new AzureCliCredential())
    .Build();

kernel.ImportFunctions(new NativeFunctions(), "Recommender");

var chatCompletion = kernel.GetService<IChatCompletion>();

// Kernel に登録されている関数を OpenAI の関数に変換しておく
var requestSettings = new OpenAIRequestSettings()
{
    Functions = kernel.Functions.GetFunctionViews().Select(x => x.ToOpenAIFunction()).ToArray(),
    FunctionCall = OpenAIRequestSettings.FunctionCallAuto,
};

// チャットメッセージのを組み立てて
var chatHistory = chatCompletion.CreateNewChat();
chatHistory.AddUserMessage("東京のお勧めのレストランについて教えてください。");

// OpenAI を呼び出して、結果の最初だけ取得
var chatResult = (await chatCompletion.GetChatCompletionsAsync(chatHistory, requestSettings))[0];

// 関数呼び出しがあるかどうかは FunctionResponse が null かどうかで判断する
var functionResult = chatResult.GetFunctionResponse();
// 今回は関数呼び出しが目的なので関数が無い場合はおしまい。
if (functionResult is null) return;

// ISKFunction と ContextVariable が取れる
if (kernel.Functions.TryGetFunctionAndContext(functionResult, out var function, out var context))
{
    // 関数を実行して結果を取得
    var kernelResult = await kernel.RunAsync(function, context);
    var responseValue = kernelResult.GetValue<string>();
    Console.WriteLine(responseValue);
}

class NativeFunctions
{
    [SKFunction]
    [Description("お勧めのレストランの名前を返します。")]
    public string RecommendRestaurant(
        [Description("地名")] string location, 
        [Description("レストランの食事のカテゴリ")]string? category = null)
    {
        return $"うまい {location}{(string.IsNullOrEmpty(category) ? "" : $"{category} の ")}レストラン";
    }

    [SKFunction]
    [Description("お勧めのショッピングセンターの名前を返します。")]
    public string RecommendShop(
        [Description("地名")] string location, 
        [Description("ショッピングセンターのカテゴリ")]string? category = null)
    {
        return $"お勧めの {location}{(string.IsNullOrEmpty(category) ? "" : $"{category} の ")}ショッピングセンター";
    }
}

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

うまい 東京 の レストラン

最初の文字列を "おすすめの東京の和食のレストランを教えてください" にすると以下のような結果になります。

うまい 東京 の 和食 の レストラン

ばっちりですね。

関数の呼び出し結果を元にチャットを続けたい

最後のフェーズです。AI に関数を選んでもらえて、選んでもらった関数が呼び出せたので、その結果を使ってチャットを継続するというのが割とよくあるシナリオだと思います。素の OpenAI の SDK の場合は RoleFunction を設定して Name に関数名、Content に関数の結果を渡してあげることで AI に、これは関数の結果だよ!ということを伝えることが出来ます。

残念なことに、現時点の Semantic Kernel は、この部分が実装されておらず以下の Issue で v1.0 candidate ラベル付きで管理されています。多分来るでしょう。というか来ないと v1.0 で片手落ちみたいな感じになるので…。

https://github.com/microsoft/semantic-kernel/issues/3017

まとめ

ということで Semantic Kernel の v1.0.0-beta1 が出てきて、それに Function calling のサポートが入っていたので試してみました。
まだ実装途中といった感じなので今後に期待ですが、個人的にはタイプセーフに Function calling の関数を定義できるのが気に入ったので、Azure.OpenAI の SDK を生で使うのではなく Semantic Kernel を使おうかなと思っています。

v1.0.0 のリリース楽しみだなぁ。

因みに Azure.OpenAI のパッケージを生で使った場合の Function calling については以下の記事で試しています。もし興味があれば見てみてください。

https://zenn.dev/microsoft/articles/dotnet-sdk-openai-functioncalling

Microsoft (有志)

Discussion