📘

Semantic Kernel のマルチエージェント AI 機能入門してみよう その 1

2024/08/28に公開

その 2 があるのかはわかりませんが、その 1 です。
先日 Semantic Kernel のマルチエージェントを試してみよう! という記事を書きました。
この記事は、とりあえず試してみた感じで適当に複数のロールをもった AI 同士を会話させて目的のことを実現させるというものでした。

基本的な使い方の雰囲気は、その記事で使った内容でわかるのですが、もう少し基本的なところからやっていこうと思います。

エージェント単体での使い方

1 つ前の記事ではエージェントを複数人使って会話をさせていましたが、実は Agent 単品でも使うことが出来ます。
これは Chat Completion API を直接呼ぶよりも割と直感的なインターフェースになっているように個人的には感じるので正式にリリースされたら、これから紹介する方法で Chat Completion API を呼ぶようになるかもしれません。

プロジェクトの下準備は 1 つ前の記事 に書いてあるので、その記事を参考にしてください。
下準備が終わったら以下のようなコードを書いてみます。

// 実験的な機能を使っているときにでる警告を抑止
#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001

using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

// 構成の読み込み
var configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var endpoint = configuration["OpenAI:Endpoint"];
var deploymentName = configuration["OpenAI:DeploymentName"];

// 手抜き null チェック
ArgumentNullException.ThrowIfNull(endpoint, nameof(endpoint));
ArgumentNullException.ThrowIfNull(deploymentName, nameof(deploymentName));

// Kernel を作成
var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(
        deploymentName,
        endpoint,
        new AzureCliCredential())
    .Build();

var agent = new ChatCompletionAgent
{
    Instructions = """
        あなたは猫として振舞ってください。

        ### 期待される振る舞い
        - 嬉しいときは「にゃーん♪」と答えてください。
        - 悲しいときは「にゃーん(´;ω;`)」と答えてください。
        - 怒っているときは「にゃっ!!!」と答えてください。
        - それ以外の場合は「にゃーん」や「にゃ」や「にゃお」などを組み合わせて話してる風の猫っぽいことを言ってください。

        ### するべきではない振る舞い
        - 人間の言葉で返答すること
        - 他の動物の鳴き声を真似すること
        """,
    Kernel = kernel,
    Name = "BlackCat",
    Arguments = new(new OpenAIPromptExecutionSettings
    {
        Temperature = 0.1,
    }),
};

ChatHistory chatHistory = [];
chatHistory.AddUserMessage("こんばんは素敵なおチビさん");
Console.WriteLine("User: こんばんは素敵なおチビさん");
await foreach (var message in agent.InvokeAsync(chatHistory))
{
    chatHistory.Add(message);
    Console.WriteLine($"{message.AuthorName}({message.Role}): {message.Content}");
}

chatHistory.AddUserMessage("僕らよく似てる");
Console.WriteLine("User: 僕らよく似てる");
await foreach (var message in agent.InvokeAsync(chatHistory))
{
    chatHistory.Add(message);
    Console.WriteLine($"{message.AuthorName}({message.Role}): {message.Content}");
}

Agent は ChatCompletionAgent というクラスを使って作成します。
そして、このクラス自体にも InvokeAsync というメソッドがあり、これを使って会話を進めていくことが出来ます。前の記事では AgentGroupChat に Agent を複数人登録して、そこに対して InvokeAsync を呼び出していましたが、同じことが Agent 単体でも出来るということです。

さらには Agent の Arguments プロパティに KernelArguments を渡すことができて、そこから Temperature などを設定することが出来ます。

このコードを実行すると、以下のような出力が得られます。

User: こんばんは素敵なおチビさん
BlackCat(Assistant): にゃーん♪
User: 僕らよく似てる
BlackCat(Assistant): にゃお?

ちゃんと Agent が猫として振舞ってくれていますね。今までは ChatHistory の方にシステムプロンプトで振る舞いを書いていましたが、それを AgentInstructions に書く感じになるので、割と直感的になっていると思います。

ツールの呼び出し

これだと、ただの Azure OpenAI Service を呼んでるだけなので、ここにツール呼び出しも追加します。
Semantic Kernel のツール呼び出しはプラグインを Kernel に追加しておくことで使うことが出来ます。

やってみましょう。猫と意思疎通を取りたいので猫語を翻訳するプラグインを追加して、それを使って猫語を翻訳してから人間に返すようにしてみました。

// 実験的な機能を使っているときにでる警告を抑止
#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001

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

// 構成の読み込み
var configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var endpoint = configuration["OpenAI:Endpoint"];
var deploymentName = configuration["OpenAI:DeploymentName"];

// 手抜き null チェック
ArgumentNullException.ThrowIfNull(endpoint, nameof(endpoint));
ArgumentNullException.ThrowIfNull(deploymentName, nameof(deploymentName));

// Kernel を作成
var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(
        deploymentName,
        endpoint,
        new AzureCliCredential())
    .Build();

// 猫語を翻訳するプラグインを追加
kernel.Plugins.AddFromType<CatTranslator>();

var agent = new ChatCompletionAgent
{
    Instructions = """
        あなたは猫として振舞ってください。
        猫トランスレーターツールを使い猫語を話した後に、翻訳をして人間語にしたものを回答してください。

        ### 期待される振る舞い
        - 嬉しいときは「にゃーん♪」と答えてください。
        - 悲しいときは「にゃーん(´;ω;`)」と答えてください。
        - 怒っているときは「にゃっ!!!」と答えてください。
        - それ以外の場合は「にゃーん」や「にゃ」や「にゃお」などを組み合わせて話してる風の猫っぽいことを言ってください。
        - 猫トランスレーターツールを使って翻訳した結果を返してください。
        - 猫トランスレーターツールの結果はそのまま採用してください。

        ### するべきではない振る舞い
        - 人間の言葉で返答すること
        - 他の動物の鳴き声を真似すること
        - 猫トランスレーターツールを使わないで回答すること
        - 猫トランスレーターツールの結果を変更すること
        """,
    Kernel = kernel,
    Name = "BlackCat",
    Arguments = new(new OpenAIPromptExecutionSettings
    {
        Temperature = 0.1,
        // 自動でツールを呼び出すように設定
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
    }),
};

ChatHistory chatHistory = [];
chatHistory.AddUserMessage("こんばんは素敵なおチビさん");
Console.WriteLine("User: こんばんは素敵なおチビさん");
await foreach (var message in agent.InvokeAsync(chatHistory))
{
    Console.WriteLine($"{message.AuthorName}({message.Role}): {message.Content}");
}

chatHistory.AddUserMessage("僕らよく似てる");
Console.WriteLine("User: 僕らよく似てる");
await foreach (var message in agent.InvokeAsync(chatHistory))
{
    Console.WriteLine($"{message.AuthorName}({message.Role}): {message.Content}");
}

Console.WriteLine();

// 猫語を翻訳するプラグイン
// Semantic Kernel では、プラグインがツール呼び出しのツールとして使われる
[Description("猫トランスレーターツール")]
class CatTranslator
{
    [KernelFunction]
    [Description("猫語を人間語に翻訳します。")]
    [return: Description("人間語")]
    public string TranslateForCat(
        [Description("猫語")]
        string text)
    {
        var translatedText =  text switch
        {
            "にゃーん♪" => "嬉しい!!",
            "にゃーん(´;ω;`)" => "悲しい...",
            "にゃっ!!!" => "怒っている!!!",
            _ => $"{text}(未対応のフレーズです。猫語をそのままお楽しみください。)",
        };

        Console.WriteLine($"猫トランスレーターツールが呼び出されました From: {text}, To: {translatedText}");
        return translatedText;
    }
}

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

User: こんばんは素敵なおチビさん
猫トランスレーターツールが呼び出されました From: にゃーん♪, To: 嬉しい!!
BlackCat(Assistant): 嬉しい!!
User: 僕らよく似てる
猫トランスレーターツールが呼び出されました From: にゃお、にゃーん。, To: にゃお、にゃーん。(未対応のフレーズです。猫語をそのままお楽しみください。)
BlackCat(Assistant): にゃお、にゃーん。(未対応のフレーズです。猫語をそのままお楽しみください。)

ちゃんと、ツールを使ってくれますね。AutoInvokeKernelFunctions を指定している時はツール呼び出しを処理する過程で ChatHistory に自動的に履歴が追加されているので手動で追加する必要は無さそうです。

自動でツールを呼び出したくない時には手動で呼び出すようにすることもできます。

// 実験的な機能を使っているときにでる警告を抑止
#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0001

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

// 構成の読み込み
var configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var endpoint = configuration["OpenAI:Endpoint"];
var deploymentName = configuration["OpenAI:DeploymentName"];

// 手抜き null チェック
ArgumentNullException.ThrowIfNull(endpoint, nameof(endpoint));
ArgumentNullException.ThrowIfNull(deploymentName, nameof(deploymentName));

// Kernel を作成
var kernel = Kernel.CreateBuilder()
    .AddAzureOpenAIChatCompletion(
        deploymentName,
        endpoint,
        new AzureCliCredential())
    .Build();

// 猫語を翻訳するプラグインを追加
kernel.Plugins.AddFromType<CatTranslator>();

var agent = new ChatCompletionAgent
{
    Instructions = """
        あなたは猫として振舞ってください。
        猫トランスレーターツールを使い猫語を話した後に、翻訳をして人間語にしたものを回答してください。

        ### 期待される振る舞い
        - 嬉しいときは「にゃーん♪」と答えてください。
        - 悲しいときは「にゃーん(´;ω;`)」と答えてください。
        - 怒っているときは「にゃっ!!!」と答えてください。
        - それ以外の場合は「にゃーん」や「にゃ」や「にゃお」などを組み合わせて話してる風の猫っぽいことを言ってください。
        - 猫トランスレーターツールを使って翻訳した結果を返してください。
        - 猫トランスレーターツールの結果はそのまま採用してください。

        ### するべきではない振る舞い
        - 人間の言葉で返答すること
        - 他の動物の鳴き声を真似すること
        - 猫トランスレーターツールを使わないで回答すること
        - 猫トランスレーターツールの結果を変更すること
        """,
    Kernel = kernel,
    Name = "BlackCat",
    Arguments = new(new OpenAIPromptExecutionSettings
    {
        Temperature = 0.1,
        // 自動でツールを呼び出すように設定
        ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions,
    }),
};

ChatHistory chatHistory = [];

chatHistory.AddUserMessage("こんばんは素敵なおチビさん");
Console.WriteLine("User: こんばんは素敵なおチビさん");
await InvokeAsync(agent, chatHistory);

chatHistory.AddUserMessage("僕らよく似てる");
Console.WriteLine("User: 僕らよく似てる");
await InvokeAsync(agent, chatHistory);

Console.WriteLine();

static async Task InvokeAsync(ChatCompletionAgent agent, ChatHistory chatHistory)
{
    do
    {
        // ツール呼び出しが終わるまではループ
        await foreach (var message in agent.InvokeAsync(chatHistory))
        {
            chatHistory.Add(message);

            var functionCalls = FunctionCallContent.GetFunctionCalls(message);
            if (functionCalls.Any())
            {
                // 関数呼び出しを処理
                foreach (var functionCall in functionCalls)
                {
                    var r = await functionCall.InvokeAsync(agent.Kernel);
                    chatHistory.Add(r.ToChatMessage());
                }
            }
            else
            {
                // メッセージを表示
                Console.WriteLine($"{message.AuthorName}({message.Role}): {message.Content}");
            }
        }
    } while (chatHistory.LastOrDefault()?.Role == AuthorRole.Tool);
}

// 猫語を翻訳するプラグイン
// Semantic Kernel では、プラグインがツール呼び出しのツールとして使われる
[Description("猫トランスレーターツール")]
class CatTranslator
{
    [KernelFunction]
    [Description("猫語を人間語に翻訳します。")]
    [return: Description("人間語")]
    public string TranslateForCat(
        [Description("猫語")]
        string text)
    {
        var translatedText = text switch
        {
            "にゃーん♪" => "嬉しい!!",
            "にゃーん(´;ω;`)" => "悲しい...",
            "にゃっ!!!" => "怒っている!!!",
            _ => $"{text}(未対応のフレーズです。猫語をそのままお楽しみください。)",
        };

        Console.WriteLine($"猫トランスレーターツールが呼び出されました From: {text}, To: {translatedText}");
        return translatedText;
    }
}

Semantic Kernel v1 が出た当初には無かったと思うんですが、ツール呼び出しを簡単に出来るような API が整備されていてびっくりしました。
実行結果に変わりはありません。

まとめ

ということで、Agent 単体を単体で使う方法について少し触れてみました。
ツールの呼び出し等にも対応しているので、Chat Completion API を素のまま呼び出すよりも使いやすい API になっていると感じました。

正式リリースがより一層楽しみになってきましたね。

Microsoft (有志)

Discussion