🎉

Semantic Kernelの自動関数呼び出しをしつつ会話履歴に基づく動作をさせる

2024/09/05に公開

はじめに

通常、Semantic Kernelの自動関数呼び出しのサンプルをみるとLLMに生成してもらっているところは以下のようになっていることが多い。

var result = await kernel.InvokePromptAsync(input);

この場合、通常inputに自分で会話履歴を作り込んであげないと会話が成り立たない。

IChatCompletionServiceを使用する

IChatCompletionServiceとChatHistoryを組み合わせて使用すると会話履歴を保ったまま簡単に自動関数呼び出しによるネイティブプラグイン呼び出しも可能になっている。
Semantic Kernelが出た当初はこのIChatCompletionServiceは会話を実装するのにすごく便利でしたがそれ以上の発展性がなくネイティブプラグインを呼び出すためには、KernelをInvokeしてあげる必要がありました。

実装例
プラグイン側

WeatherPlugin.cs
/// <summary>
/// 天気を取得するプラグイン
/// </summary>
/// <param name="client"></param>
public class WeatherPlugin(HttpClient client)
{
    private readonly HttpClient _client = client;

    /// <summary>
    /// 天気を取得する場所コードを取得します
    /// </summary>
    /// <param name="place">天気を取得する場所</param>
    /// <returns>天気コード</returns>
    /// <exception cref="ArgumentException">対応してない地域の場合</exception>
    [KernelFunction, Description("""
        天気を取得する場所コードを取得します。
        対応している場所コードは下記のとおりです。
        下記に含まれない場所の場合は近くの場所の天気を代わりに取得してください。
        東京
        群馬
        埼玉
        千葉
        横浜
        名古屋
        京都
        静岡
        福井
        新潟
        富山
        金沢
        岐阜
        長野
        高山
        松本
        大津
        大阪
        札幌
        仙台
        福岡
        那覇
        ----
        以上、ここに含まれない場合は近くの場所の代わりに取得してください。
        """)]
    public static int GetPlaceId([Description("天気を取得する場所")] string place)
    {
        var res = place switch
        {
            "札幌" => 016000,
            "群馬" => 100000,
            "埼玉" => 110000,
            "千葉" => 120000,
            "東京" => 130000,
            "横浜" => 140000,
            "名古屋" => 230000,
            "京都" => 260000,
            "静岡" => 220000,
            "福井" => 180000,
            "新潟" => 150000,
            "富山" => 160000,
            "金沢" => 170000,
            "岐阜" => 210000,
            "長野" => 200000,
            "高山" => 190000,
            "松本" => 200000,
            "大津" => 250000,
            "大阪" => 270000,
            "仙台" => 040000,
            "福岡" => 400000,
            "那覇" => 471000,
            _ => throw new ArgumentException("対応していない地域です。")
        };
        return res;
    }

    /// <summary>
    /// 場所コードの地域の天気を返す
    /// </summary>
    /// <param name="place">場所コード</param>
    /// <returns>天気情報</returns>
    [KernelFunction, Description("場所コードの地域の天気を返す")]
    public async Task<string> Weather([Description("場所コード")] int place)
    {
        var response = await _client.GetAsync($"https://www.jma.go.jp/bosai/forecast/data/forecast/{place}.json");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

}

上のプラグインはプライマリーコンストラクタでHttpClientを受け取るようにしてあります。
これの呼び出し側でHttpClientを使えるように以下のように実装していきます。

Program.cs
IConfigurationRoot config = new ConfigurationBuilder()
    .AddEnvironmentVariables()
    .AddUserSecrets<Program>()
    .Build();

string deploymentName = config["OpenAI:DeploymentName"] ?? throw new InvalidOperationException("OpenAI:DeploymentName is not set.");
string endpoint = config["OpenAI:Endpoint"] ?? throw new InvalidOperationException("OpenAI:BaseUrl is not set.");

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
   deploymentName,
   endpoint,
   new DefaultAzureCredential()).Build();

builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(LogLevel.Trace));
builder.Services.AddSingleton<HttpClient>();
builder.Plugins.AddFromType<WeatherPlugin>();
Kernel kernel = builder.Build();

ChatHistory chatHistory = new();
var chat = kernel.GetRequiredService<IChatCompletionService>();

OpenAIPromptExecutionSettings? setting = new()
{
    ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    MaxTokens = 2000,
};

while (true)
{
    Console.Write("User > ");
    string input = Console.ReadLine()!;

    chatHistory.AddUserMessage(input);

    if (input == "exit")
    {
        break;
    }
    else
    {
        //var result = await kernel.InvokePromptAsync(input, new(setting));
        var result = await chat.GetChatMessageContentAsync(chatHistory, setting, kernel);
        chatHistory.AddAssistantMessage(result.ToString());
        Console.WriteLine($"Assistant > {result}");
    }
}

ポイントその1:IKernelBuilderのサービスにシングルトンでHttpClientを追加しています。
ポイントその2:IChatCompletionServiceのGetChatMessageContentAsyncでLLMから文書生成をしています。この際に第3引数にkernelを渡しています。
このkernelにネイティブプラグインの情報が含まれていますのでここがポイントです。

動作確認

①まずは「大阪城について教えてください」と投げかけてみます。
ここでの答えはまあLLMの持っている知識でよい感じに答えてくれるはずです。この段階ではネイティブプラグインの呼び出しもありません。
②「天気はどうでしょうか」と投げかけてみます。
どこの天気とは聞いていませんので会話の履歴をもってLLMが判断してネイティブプラグインに大阪の天気を問い合わせる動作をする必要があります。
ここで、会話の履歴を保持してるのか?
正常にネイティブプラグインを呼び出しできるのか?
2つの観点の正常性が求められます。

ではやってみました。

結果、正常に動作している様子です。

参考

https://learn.microsoft.com/ja-jp/semantic-kernel/concepts/plugins/adding-native-plugins?pivots=programming-language-csharp&WT.mc_id=%3Fwt.mc_id%3DDT-MVP-5004827

Discussion