Microsoft Agent Framework をローカルLLMで試してみる その9(AIContextProvider)
シリーズ一覧
一覧
Microsoft Agent Framework をローカルLLMで試してみる その1
Microsoft Agent Framework をローカルLLMで試してみる その2
Microsoft Agent Framework をローカルLLMで試してみる その3(LoggingFactory)
Microsoft Agent Framework をローカルLLMで試してみる その4(Tool)
Microsoft Agent Framework をローカルLLMで試してみる その5(AIFunction)
Microsoft Agent Framework をローカルLLMで試してみる その6(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その7(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その8(ChatHistoryProvider)
Microsoft Agent Framework をローカルLLMで試してみる その9(AIContextProvider)
Microsoft Agent Framework をローカルLLMで試してみる その10(AIContextProvider)
Microsoft Agent Framework をローカルLLMで試してみる その11(AIContextProviderで簡易RAG)
Microsoft Agent Framework をローカルLLMで試してみる その12(Structured Output)
Microsoft Agent Framework をローカルLLMで試してみる その13(ミドルウェア)
はじめに
Microsoft Agent FrameworkをローカルLLMで試してみる その8では、RemoteChatHistoryProvider を使ってチャット履歴の保存先を別プロセスのサーバーに切り出しました。
その9からは、LLM を呼び出す直前と直後のコンテキストに介入できるAIContextProviderを見ていきます。
AIContextProviderの目的
AIContextProviderはLLMへのリクエスト前とレスポンス後でコンテキストに介入する仕組みを提供しています。
これを利用するとステートに応じたシステムプロンプトやToolCalling定義の追加、チャット履歴に対するフィルター関数等が実現できます。
パイプラインを通じて複数のAIContextProviderを登録できるため、関心ごとに分割して実装することも可能です。
- LLMリクエスト前
- システムプロンプトを追加する
- チャット履歴を追加する
- ToolCalling定義を追加する
- LLMレスポンス後
- 今回の入力と出力からメモリや要約を抽出する
- 次回以降に使うための状態を保存する
確認内容
今回は最小構成のAIContextProviderを作成して、次の内容をトレースします。
- InvokingCoreAsync と ProvideAIContextAsync がどの順序で呼ばれるのか
- InvokedCoreAsync と StoreAIContextAsync がどのタイミングで呼ばれるのか
AIContextProviderのメソッド
| 名前 | 役割 |
|---|---|
| StateKeys | 管理する状態のキー一覧 |
| InvokingCoreAsync | 呼び出し前の処理 |
| ProvideAIContextAsync | 呼び出し前に追加するコンテキスト |
| InvokedCoreAsync | 呼び出し後の処理 |
| StoreAIContextAsync | 呼び出し後のコンテキスト |
実装
MyAIContextProvider
MyAIContextProvider.cs
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace AIContextProviderSample
{
public class MyAIContextProvider : AIContextProvider
{
//確認用にチャット履歴プロバイダーを外部からセットできるようにする
public InMemoryChatHistoryProvider? ChatHistoryProvider { get; set; }
// InvokingContextのチャット履歴確認
private void DumpStateBag(InvokingContext? context)
{
if( ChatHistoryProvider is not null ) {
ChatHistoryProvider.GetMessages(context.Session).ForEach(message => {
Console.WriteLine($"[ChatHistory] {message.Role}: {message.Text}");
});
}
}
// InvokedXontextのチャット履歴確認
private void DumpStateBag(InvokedContext? context)
{
if (ChatHistoryProvider is not null)
{
ChatHistoryProvider.GetMessages(context.Session).ForEach(message =>
{
Console.WriteLine($"[ChatHistory] {message.Role}: {message.Text}");
});
}
}
public override IReadOnlyList<string> StateKeys
{
get
{
Console.WriteLine("[StateKeys]");
return base.StateKeys;
}
}
protected override async ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine("[1:InvokingCoreAsync]");
DumpStateBag(context);
var returnValue = await base.InvokingCoreAsync(context, cancellationToken);//内部でProvideAIContextAsyncが呼ばれる
Console.WriteLine("[1:InvokingCoreAsync] return value");
Console.WriteLine(returnValue.Instructions);
Console.WriteLine("[1:InvokingCoreAsync] return value messages");
returnValue.Messages.ToList().ForEach(message =>
{
Console.WriteLine($"{message.Role} :{message.Text}");
});
return returnValue;
}
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine("[2:ProvideAIContextAsync]");
DumpStateBag(context);
return new ValueTask<AIContext>(new AIContext());
}
protected override async ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine("[3:InvokedCoreAsync]");
DumpStateBag(context);
var returnValue = base.InvokedCoreAsync(context, cancellationToken);//内部でStoreAIContextAsyncが呼ばれる
Console.WriteLine("[3:InvokedCoreAsync] base invoked");
await returnValue;
Console.WriteLine("[3:InvokedCoreAsync] base completed");
}
protected override ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine("[4:StoreAIContextAsync]");
DumpStateBag(context);
return ValueTask.CompletedTask;
}
}
}
-
Console.WriteLineでログを出力している部分を除き、既定の動作をそのまま使用しています。
実行シナリオ
下記のシナリオで実行してみます。
なお、実行ログには AIContextProvider のメソッド呼び出し以外に、次のログも含まれます。
| ログ | 出力箇所 |
|---|---|
[Session Created] |
セッション作成後に Program.cs で出力 |
[HTTP Request] |
LLM への HTTP リクエスト時に LoggingHttpMessageHandler で出力 |
[GetDateTime] called |
Tool 実行時に GetDateTime 関数内で出力 |
[Response] |
Agent の応答受信後に Program.cs で出力 |
実行結果
[StateKeys]
[Session Created]
[1:InvokingCoreAsync]
[2:ProvideAIContextAsync]
[1:InvokingCoreAsync] return value
ユーザーの質問に対して適切なツールを使用して回答してください。日時は必ず{{GetDateTime}}で最新の情報を取得してください
[1:InvokingCoreAsync] return value messages
user :現在時刻を教えてください
[HTTP Request] POST http://localhost:1234/v1/chat/completions
[GetDateTime] called
[HTTP Request] POST http://localhost:1234/v1/chat/completions
[3:InvokedCoreAsync]
[ChatHistory] user: 現在時刻を教えてください
[ChatHistory] assistant:
[ChatHistory] tool:
[ChatHistory] assistant: 現在の時刻は **2026年4月18日 20:18:54**(日本標準時間)です。
[4:StoreAIContextAsync]
[ChatHistory] user: 現在時刻を教えてください
[ChatHistory] assistant:
[ChatHistory] tool:
[ChatHistory] assistant: 現在の時刻は **2026年4月18日 20:18:54**(日本標準時間)です。
[3:InvokedCoreAsync] base invoked
[3:InvokedCoreAsync] base completed
[Response]
現在の時刻は **2026年4月18日 20:18:54**(日本標準時間)です。
解説
ログから、次の順序でメソッドが呼ばれることが確認できます。
-
セッション作成時に
StateKeysが呼ばれる
[StateKeys]がセッション作成直後に出力されています。 -
LLMへのリクエストが送信される前に
InvokingCoreAsync→ProvideAIContextAsyncの順で呼ばれる
[1:InvokingCoreAsync]の後に[2:ProvideAIContextAsync]が出力されています。
コードでbase.InvokingCoreAsyncを呼び出さないように変更すると[2:ProvideAIContextAsync]がログに出なくなったため、baseの内部でProvideAIContextAsyncが呼ばれていることが確認できました。 -
InvokingCoreAsyncの戻り値には元のコンテキストの内容が含まれる
ProvideAIContextAsyncは空のAIContextを返したため、戻り値に含まれているChatOptions.Instructionsの文字列とユーザーメッセージは元のinputContextの内容です。 -
InvokedCoreAsyncは LLM の応答がすべて完了した後に呼ばれる
2 回目の HTTP リクエストの後に[3:InvokedCoreAsync]が出力されています。ToolCalling を含む一連の推論がすべて終わってから呼ばれます。 -
StoreAIContextAsyncはInvokedCoreAsyncのbase呼び出しの前に呼ばれる
[4:StoreAIContextAsync]は[3:InvokedCoreAsync] base invokedより前に出力されています。
コードでbase.InvokedCoreAsyncを呼び出さないように変更すると[4:StoreAIContextAsync]がログに出なくなったため、baseの内部でStoreAIContextAsyncが呼ばれていることが確認できました。
観測したフロー
エージェントのソース
Program.cs
using AIContextProviderSample;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenAI;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
//Toolとして使用する関数の定義
[DisplayName("GetDateTime")]
[Description("現在の日時を取得します。")]
static string GetDateTime()
{
Console.WriteLine("[GetDateTime] called");
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
//LM Studioの接続設定
const string lmStudioUrl = "http://localhost:1234/v1";
const string modelName = "openai/gpt-oss-20b";
const string dummyApiKey = "sk-dummy";
var httpClient = new HttpClient(new LoggingHttpMessageHandler(new HttpClientHandler()));
var clientOptions = new OpenAIClientOptions
{
Endpoint = new Uri(lmStudioUrl),
Transport = new HttpClientPipelineTransport(httpClient)
};
//OpenAI互換クライアントの作成
var openAIClient = new OpenAIClient(
new ApiKeyCredential(dummyApiKey),
clientOptions
);
var openAIChatClient = openAIClient.GetChatClient(modelName);
IChatClient chatClient = openAIChatClient.AsIChatClient();
//チャット履歴プロバイダーの作成
var chatHistoryProvider = new InMemoryChatHistoryProvider();
//カスタムしたAIコンテキストプロバイダーの作成
var aIContextProvider = new MyAIContextProvider();
aIContextProvider.ChatHistoryProvider = chatHistoryProvider;
//ChatClientAgentOptionsでAgentの設定を行う
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "SampleAgent",
ChatOptions = new ChatOptions
{
Instructions = "ユーザーの質問に対して適切なツールを使用して回答してください。日時は必ず{{GetDateTime}}で最新の情報を取得してください",
Tools = [AIFunctionFactory.Create(GetDateTime)],
MaxOutputTokens = 5000
},
Description = "このAI Agentは日時に関する情報を提供します。",
ChatHistoryProvider = chatHistoryProvider,
AIContextProviders = [aIContextProvider],
});
//-----セッションの初期化 -----
//sessionの作成
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine("[Session Created]");
//ユーザーからの質問
// -----プロンプトの送信と応答の確認 -----
//プロンプトの送信
AgentResponse response = await agent.RunAsync("現在時刻を教えてください", session);
//応答の表示
Console.WriteLine("[Response]");
Console.WriteLine(response);
sealed class LoggingHttpMessageHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler)
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"[HTTP Request] {request.Method} {request.RequestUri}");
return base.SendAsync(request, cancellationToken);
}
}
まとめ
- 最小構成の AIContextProvider を実装して、各メソッドが呼ばれる順序を確認しました。
- セッション作成時に
StateKeysが呼ばれる - LLMへのリクエスト送信前に
InvokingCoreAsync→ProvideAIContextAsyncの順で呼ばれる - ToolCalling を含む推論がすべて完了した後に
InvokedCoreAsync→StoreAIContextAsyncの順で呼ばれる
- セッション作成時に
- 次回は実際にコンテキストを追加・変更して、Instructions や Messages の差分マージがどのように動作するかを確認します。
Discussion