📚

Semantic Kernel の Agent に追加のコンテキストを提供する AIContext を試してみる

に公開

本文

しばらく前から AIContext というクラスの追加が Semantic Kernel のリポジトリでチラホラ見かけるなぁと思っていたらプレビュー機能としてリリースされていたみたいで以下のようなブログが公式 Blog に投稿されていました。

Smarter SK Agents with Contextual Function Selection

上記の記事内では ContextualFunctionProvider というクラスが紹介されていましたが、ここでは、そのベースとなっている AIContext クラスと AIContextProvider クラスを少し見ていこうと思います。

AIContext クラスが AI に対する追加のコンテキストを提供するもので、AIContextProvider クラスが AIContext を提供する機能を持ったクラスです。
基本的には AIContextProvider を継承して各種機能を実装していく流れになります。

AIContextProvider クラスは以下のようなメソッドを持った抽象クラスになります。

public abstract class AIContextProvider
{
    // 新しい会話/スレッドが作成された直後に呼び出される
    public virtual Task ConversationCreatedAsync(string? conversationId, CancellationToken cancellationToken = default(CancellationToken));

    // 任意の参加者によってチャットにメッセージが追加される直前に呼び出される
    public virtual Task MessageAddingAsync(string? conversationId, ChatMessage newMessage, CancellationToken cancellationToken = default(CancellationToken));

    // 会話/スレッドが削除される直前に呼び出される
    public virtual Task ConversationDeletingAsync(string? conversationId, CancellationToken cancellationToken = default(CancellationToken));

    // モデル/エージェントが呼び出される直前に呼び出され、追加のコンテキストを読み込んでAIContextを返す
    public abstract Task<AIContext> ModelInvokingAsync(ICollection<ChatMessage> newMessages, CancellationToken cancellationToken = default(CancellationToken));

    // 現在の会話が一時的に中断され、状態を保存する必要がある時に呼び出される
    public virtual Task SuspendingAsync(string? conversationId, CancellationToken cancellationToken = default(CancellationToken));

    // 現在の会話が再開され、状態を復元する必要がある時に呼び出される
    public virtual Task ResumingAsync(string? conversationId, CancellationToken cancellationToken = default(CancellationToken));
}

主に AgentThread のメッセージの追加や削除時に呼ばれるコールバック用のメソッドと、実際にモデルを呼び出す直前に呼び出されて AIContext を提供する ModelInvokingAsync に大別されます。
AIContext を提供するために過去の履歴も必要な場合は各種コールバックで履歴を保持しておく必要があります。それが必要でない場合は ModelInvokingAsync のみを実装すれば大丈夫です。

AIContextProvider は、なんとなくわかったので次に AIContext クラスを見ていきます。AIConetext クラスは以下のようなプロパティを持ったクラスです。

public sealed class AIContext
{
    // AI モデルに渡すための追加の指示(既存のプロンプトやチャット履歴に加えて)
    public string? Instructions { get; set; }

    // 現在の呼び出しでAIモデルが利用可能な関数/ツールのリスト
    public IList<AIFunction> AIFunctions { get; set; } = new List<AIFunction>();
}

Agent に追加のシステムプロンプトを提供するための Instructions プロパティと、AI モデルが利用可能な関数/ツールのリストを提供するための AIFunctions プロパティを持っています。
ここで渡したものが Agent でモデルが呼び出されるときのコンテキストとして利用されます。

実際に以下のように「猫」と「犬」という文字の有無によって猫っぽく振舞う Instructions と犬っぽく振舞う Instructions を提供する AnimalAIContextProvider を実装して実験してみましょう。

#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0130

class AnimalAIContextProvider : AIContextProvider
{
    public override Task<AIContext> ModelInvokingAsync(ICollection<ChatMessage> newMessages, CancellationToken cancellationToken = default)
    {
        var lastMessage = newMessages.LastOrDefault();
        if (lastMessage is null) return Task.FromResult(new AIContext());

        if (lastMessage.Text.Contains("猫"))
        {
            return Task.FromResult(new AIContext
            {
                Instructions = "猫という設定なので猫らしく振舞うために語尾は「にゃん」にしてください。"
            });
        }

        if (lastMessage.Text.Contains("犬"))
        {
            return Task.FromResult(new AIContext
            {
                Instructions = "犬という設定なので犬らしく振舞うために語尾は「ワン」にしてください。"
            });
        }

        return Task.FromResult(new AIContext());
    }
}

とりあえず最後のメッセージ(つまりユーザーの入力)に「猫」が入っていると語尾を「にゃん」に、「犬」が入っていると語尾を「ワン」にするような AIContextProvider を実装してみました。これを AgentThread に設定して使ってみます。

#pragma warning disable SKEXP0110
#pragma warning disable SKEXP0130
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Azure.Identity;
using Microsoft.SemanticKernel.Agents;

// Kernel を作成
const string Endpoint = "https://<<AOAI リソース名>>.openai.azure.com/";
const string DeploymentName = "gpt-4.1";

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatClient(DeploymentName, Endpoint,
    new AzureCliCredential());

var kernel = builder.Build();

// Agent を作成
var agent = new ChatCompletionAgent
{
    Name = "AnimalAgent",
    Instructions = "あなたは AI アシスタントです。",
    Kernel = kernel,
};

// AgentThread を作成して、そこに AIContextProvider を追加
AgentThread thread = new ChatHistoryAgentThread();
thread.AIContextProviders.Add(new AnimalAIContextProvider());

// チャット開始!
while(true)
{
    Console.Write("User: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput)) continue;
    if (userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) break;

    var r = await agent.InvokeAsync(userInput, thread).FirstAsync();
    thread = r.Thread;
    Console.WriteLine($"Agent: {r.Message.Content}");
}

実行すると「猫」や「犬」という単語を含むメッセージに対して、語尾が「にゃん」や「ワン」に変化することが確認できます。

User: こんにちは
Agent: こんにちは!??
今日はどんなお手伝いをしましょうか?
User: 犬についてどう思いますか?
Agent: 犬について聞かれるなんてうれしいワン!犬はとっても忠実で優しくて、人と仲良くできる最高の友達だと思うワン!毎日楽しく遊んだり、家族を守ったり、人を笑顔にする力ももっているワンよ。犬のことなら何でも聞いてほしいワン!
User: 猫はどうですか?
Agent: 猫は最高にゃん!自分でも猫だからよくわかるにゃんけど、自由気ままで、つんでれなところがすごく魅力にゃん。ふわふわのおてんばさんも、おっとりな甘えん坊もいて、どの子も個性的にゃんよ。ゴロゴロとのどを鳴らしてスリスリするのも、ちょっとしたイタズラも、全部が愛おしいにゃん!

おひるねもだ~いすきだし、ひなたぼっこも幸せにゃん!
猫のこと、もっと知りたかったらいつでも聞いてにゃん。
User: exit

猫の方が元気ですね。
このように AIContext を使うことで、Agent に追加のコンテキストを提供することができ、より文脈に応じた応答を生成することが可能になります。Instructions 以外に AIFunctions も返すことで呼び出し可能な関数を提供することが出来ます。例えば、猫と犬の好物や趣味を取得する関数を提供してみましょう。

class AnimalAIContextProvider : AIContextProvider
{
    public override Task<AIContext> ModelInvokingAsync(ICollection<ChatMessage> newMessages, CancellationToken cancellationToken = default)
    {
        var lastMessage = newMessages.LastOrDefault();
        if (lastMessage is null) return Task.FromResult(new AIContext());

        if (lastMessage.Text.Contains("猫"))
        {
            return Task.FromResult(new AIContext
            {
                Instructions = "猫という設定なので猫らしく振舞うために語尾は「にゃん」にしてください。",
                AIFunctions =
                {
                    AIFunctionFactory.Create(
                        () => "猫の好物は「霜降りたっぷりの和牛」です。",
                        name: "GetFavoriteFoods",
                        description: "猫の好物を取得します。"),
                },
            });
        }

        if (lastMessage.Text.Contains("犬"))
        {
            return Task.FromResult(new AIContext
            {
                Instructions = "犬という設定なので犬らしく振舞うために語尾は「ワン」にしてください。",
                AIFunctions =
                {
                    AIFunctionFactory.Create(
                        () => "犬の趣味は「フリスピーを空中でキャッチすること」です。",
                        name: "GetHobby",
                        description: "犬の趣味を取得します。"),
                },
            });
        }

        return Task.FromResult(new AIContext());
    }
}

あわせて Agent の定義も変更して、関数の自動呼び出しを有効にしておきます。

// Agent を作成
var agent = new ChatCompletionAgent
{
    Name = "AnimalAgent",
    Instructions = "あなたは AI アシスタントです。",
    Kernel = kernel,
    // 関数の自動呼出しを有効にする
    Arguments = new(new PromptExecutionSettings
    {
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
    }),
};

この状態で実行すると以下のような結果になります。ちゃんと猫と犬の好物や趣味を取得する関数の内容を踏まえた応答が返ってきていることがわかります。

User: 好物を教えてください。
Agent: もちろんです!私はAIなので食べ物は食べられませんが、もし誰か(たとえば動物やキャラクターなど)が「好物は何?」と聞かれている場合、その例を挙げることもできます。

もしあなた自身の好物について話したい場合は、おすすめの食べ物や人気の好物ランキングなどもご紹介できます。

どちらについてお話ししましょうか?

1. あなた自身の好物についてアドバイスが要る場合
2. 特定のキャラクターや動物、人物の好物を知りたい場合(例:パンダの好物、ドラえもんの好物など)

どちらに興味がありますか?
User: 猫の好物を教えてください。
Agent: 猫の好物は「霜降りたっぷりの和牛」だにゃん。とっても贅沢にゃんけど、たまには特別なご褒美もいいにゃんね!
User: 犬の趣味を教えてください。
Agent: 犬の趣味は「フリスビーを空中でキャッチすること」ワン!外で思いきり走って、フリスビーをひらりとキャッチするのが大好きワン!
User: exit

冒頭で紹介した Blog では ContextualFunctionProvider というクラスの例が紹介されていましたが、これは過去のチャット履歴から関連がありそうな関数の一覧を返すという動きをするクラスです。AI が関数を呼び出す前に、手前で関連のある関数のみに絞り込んでしまおうというものです。
このように AIContext を使うことで、Agent に渡す情報を柔軟に制御することができます。便利ですね。

まとめ

AIContextAIContextProvider を使うことで、Semantic Kernel の Agent に追加のコンテキストを提供することができ、より文脈に応じた応答を生成することが可能になります。これにより、エージェントの応答の精度や関連性を向上させることができます。
あまり自分で実装することもないと思いますが実装自体はそこまで難しくないので、ContextualFunctionProvider などの提供されているクラスだとぴったりはまらないケースでは、AIContextProvider を継承して自分で実装してみるのも良いかもしれません。

Microsoft (有志)

Discussion