ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure OpenAI Service の Function Calling を .NET アプリケーションから使ってみた

まだプレビューですが、Azure OpenAI Service で Function Calling が使えるようになりました。今回は .NET エンジニアの視点で Function Calling を使ってみました。
techcommunity.microsoft.com

Function Calling とは

OpenAI を業務で活用しようとすると、外部システムの API と連携させたいケースが出てきます。チャットの会話でやりとりされる自然言語をもとに、呼び出したい API を特定したり、パラメータで渡すデータを生成したくなりますが、プロンプトエンジニアリングを試行錯誤する必要があり難易度が高い実装でした。

Function Calling を使うと、自然言語の中から API を呼び出すための情報を抽出してくれるので、OpenAI と外部システムを容易に連携させることができるようになります。
zenn.dev

Function Calling を使ってみる

紅葉の名所を探すのを手伝ってくれる AI アシスタントを作ります。この AI アシスタントは、「京都の紅葉」のように会話から指定された場所の名所を教えてくれます。

このシナリオを Azure OpenAI Client Library を使って実装してみます。.NET 7 のコンソールアプリを作る前準備は、以前の記事を参照してください。
モデルは、gpt-4 を使います。なお、SDK で Function Calling に対応しているバージョンは 1.0.0-beta.6 となります。
gooner.hateblo.jp
紅葉の名所を検索する API は、京都の清水寺で決め打ちの関数を定義します。

internal class ApiClient
{
    public static string SearchAutumnLeaves(string location)
    {
        // 本当は location を使って検索するが、今回は京都の名所を決め打ちで返す。
        return "{\n  \"result\": \"清水寺\",\n}";
    }
}

internal class AutumnLeavesParameter
{
    public required string Location { get; set; }
}

システムロールにアシスタントのコンテキストと Function を定義します。ChatCompletionsOptions クラスに Functions プロパティが追加されているので、FunctionDefinition クラスで Name、Description、Parameters を指定できます。

var chatCompletionsOptions = new ChatCompletionsOptions
{
    MaxTokens = 800,
    Messages =
    {
        new ChatMessage(ChatRole.System, """
            あなたは、ユーザーが紅葉の名所を検索するのを助けるために設計されたAIアシスタントです。
            ユーザーから紅葉の名所を質問されたら、search_autumn_leaves関数を呼び出します。
        """)
    },
    Functions =
    {
        new FunctionDefinition{
            Name = "search_autumn_leaves",
            Description = "引数で指定された場所の紅葉の名所を検索します。",
            Parameters = BinaryData.FromObjectAsJson(new
            {
                Type = "object",
                Properties = new
                {
                    location = new
                    {
                        Type = "string",
                        Description = "紅葉の名所を検索する場所、場所には都道府県や都市の名前を含めること、例:東京、山梨",
                    },
                },
                Required = new[] { "location" },
            },
            new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            }),
        }
    },
    FunctionCall = FunctionDefinition.Auto
};

Console.WriteLine("アシスタントのセットアップ中・・・");
var client = new OpenAIClient(new Uri(settings.Endpoint), new AzureKeyCredential(settings.ApiKey));
await client.GetChatCompletionsAsync(settings.DeploymentName, chatCompletionsOptions);

ユーザーロールには、こちらからの送信メッセージを定義します。

Console.WriteLine("チャットを開始する");
var userMessage = "今年は紅葉を見に行きたいけど、京都でお勧めの紅葉の名所を教えてください。";
Console.WriteLine($"{ChatRole.User}: {userMessage}");
chatCompletionsOptions.Messages.Add(new ChatMessage(ChatRole.User, userMessage));
var result = await client.GetChatCompletionsAsync(settings.DeploymentName, chatCompletionsOptions);
var choice = result.Value.Choices[0];

レスポンスの FinishReason が「function_call」の場合、紅葉の名所を検索する API を呼び出します。Function の Name と Arguments をいかに期待通りに取得できるかが肝となるので、

  • システムロールのコンテキスト
  • Functions や Parameter の Description

を明確かつ詳細に定義する必要があります。
Function Calling として必須ではありませんが、紅葉の名所は API から JSON 形式で返されるため、チャットで返すことに適した自然な文章を生成しています。

if (choice.FinishReason == "function_call")
{
    // API呼び出し
    var functionsResponse = String.Empty;
    switch (choice.Message.FunctionCall.Name)
    {
        case "search_autumn_leaves":
            var parameter = JsonConvert.DeserializeObject<AutumnLeavesParameter>(choice.Message.FunctionCall.Arguments);
            if (parameter == null)
            {
                Console.WriteLine("FunctionCall.Arguments is null");
            }
            else
            {
                functionsResponse = ApiClient.SearchAutumnLeaves(parameter.Location);
            }
            break;
        default:
            Console.WriteLine("function: not found");
            return;
    }

    // API のレスポンスを使ってメッセージを返す
    chatCompletionsOptions.Messages.Add(
        new ChatMessage
        {
            Role = choice.Message.Role,
            Name = choice.Message.FunctionCall.Name,
            Content = choice.Message.FunctionCall.Arguments
        });
    chatCompletionsOptions.Messages.Add(
        new ChatMessage
        {
            Role = ChatRole.Function,
            Name = choice.Message.FunctionCall.Name,
            Content = functionsResponse
        });
    var assistantResult = await client.GetChatCompletionsAsync(settings.DeploymentName, chatCompletionsOptions);
    var assistantChoice = assistantResult.Value.Choices[0];
    Console.WriteLine($"{assistantChoice.Message.Role}: {assistantChoice.Message.Content}");

    Console.WriteLine("---------------------------------");
    Console.WriteLine("Information");
    Console.WriteLine($"- FunctionCall.Name: {choice.Message.FunctionCall.Name}");
    Console.WriteLine($"- FunctionCall.Arguments: {choice.Message.FunctionCall.Arguments}");
}
else
{
    Console.WriteLine($"{choice.Message.Role}: {choice.Message.Content}");
}

チャットの質問に対して、期待通りの結果を取得できたことが分かります。

紅葉の名所を検索する場所が質問に含まれていないと場所を聞き返してくれるので、その後の会話で Funcion を呼び出すこともできます。
呼び出す Function が特定できないケースでは、FinishReason に「stop」が格納されています。

なお、まったく関係のない質問をすると、下記のように回答されます。


まとめ

Azure OpenAI Service で Function Calling を試してみました。チャットの会話でやりとりされる自然言語をもとに外部システムの API と連携させることが容易になるので、OpenAI 活用の幅が広がる機能ですし、プロンプトエンジニアリングの手法のひとつの ReAct をいい感じで実装できます。

Function Calling のリリースドキュメントに記載がありますが、関数の呼び出しだけでなく、JSON などの構造化されたモデルを生成する用途にも使えそうだと感じました。

今回のサンプルアプリは、こちらで公開しています。
github.com