🕌

Microsoft Agent Framework をローカルLLMで試してみる その6(ChatHistoryProvider)

に公開

シリーズ一覧

一覧

はじめに

Microsoft Agent FrameworkをローカルLLMで試してみる その5ではAIFunctionを自作してみました。
その6ではチャット履歴がどのように管理されているかを見ていきます。

チャット履歴の管理

ここまでの例ではエージェントのチャット履歴の管理にAgentSessionを利用していました。

Microsoft Agent FrameworkではChatHistoryProviderという抽象クラスを用いて、チャット履歴の管理方法を柔軟にカスタマイズできるようになっています。
今回は既定のInMemoryChatHistoryProviderについて確認していきます。

InMemoryChatHistoryProviderの役割

  • InMemoryChatHistoryProviderは、チャット履歴をメモリ上に保持する履歴プロバイダーです。
  • 履歴はセッションごとに分離され、同じセッションIDの会話だけが同じ履歴として扱われます。
  • そのため、複数ユーザーや複数会話を同時に扱っても、会話コンテキストが混ざることなく管理できます。
  • ただし、プロセス内メモリに保持するため、アプリ再起動で履歴は消える点に注意が必要です。

実際の動作を確認してみます

Toolの定義

//Toolとして使用する関数の定義
[DisplayName("GetDateTime")]
[Description("現在の日時を取得します。")]
static string GetDateTime() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

確認用のヘルパー関数

//ChatMessageのContentsを表示するためのヘルパー関数
static string FormatContent(object content) => content switch
{
    TextContent textContent => $"{nameof(TextContent)}: {textContent.Text}",
    _ => content.GetType().Name
};

static void PrintMessages(IEnumerable<ChatMessage> messages)
{
    foreach (var message in messages)
    {
        Console.WriteLine($"Role: {message.Role}, Contents: {string.Join(", ", message.Contents.Select(FormatContent))}");
    }
}

//StateBagの内容を表示するためのヘルパー関数
static void PrintStateBag(AgentSession session)
{
    Console.WriteLine("stateBagCount:" + session.StateBag.Count);

    JsonElement stateBagJson = session.StateBag.Serialize();

    foreach (JsonProperty entry in stateBagJson.EnumerateObject())
    {
        Console.WriteLine($"stateBag Key: {entry.Name}, ValueKind: {entry.Value.ValueKind}, Value: {entry.Value}");
    }
}

Agent作成

//インスタンスをこちらで管理する
InMemoryChatHistoryProvider chatHistoryProvider = new InMemoryChatHistoryProvider();

//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,
});
  • ChatHistoryProvider = chatHistoryProvider を設定することで、エージェントは履歴保存先として こちらが作成したInMemoryChatHistoryProvider を使用します。
  • 以後は CreateSessionAsync で作ったセッションIDをキーに、会話履歴がメモリ上で管理されます。

エージェントの実行

セッションの初期化

//-----セッションの初期化  -----
//sessionの作成
AgentSession session1 = await agent.CreateSessionAsync();
AgentSession session2 = await agent.CreateSessionAsync();

//セッション開始前の状態を確認
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session1).Count);
PrintMessages(chatHistoryProvider.GetMessages(session1));
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session2).Count);
PrintMessages(chatHistoryProvider.GetMessages(session2));
  • 2つセッションを作成して状態を確認しています。

出力

messagesCount:0
messagesCount:0
  • どちらのセッションも履歴は空の状態で開始されていることがわかります。

ToolCallingを伴うプロンプトの送信

// -----プロンプトの送信と応答の確認  -----
//プロンプトの送信
AgentResponse response = await agent.RunAsync("現在時刻を教えてください", session1);

//応答の表示
Console.WriteLine(response);

//セッション開始後の状態を確認
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session1).Count);
PrintMessages(chatHistoryProvider.GetMessages(session1));

//10秒待機してから再度プロンプトを送信
await Task.Delay(10000);

//再度プロンプトの送信
response = await agent.RunAsync("もう一度現在時刻を取得してください", session1);

//応答の表示
Console.WriteLine(response);

//セッション開始後の状態を確認
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session1).Count);
PrintMessages(chatHistoryProvider.GetMessages(session1));

//最初のプロンプトからどれくらい経過しているかをプロンプトしてみる
response = await agent.RunAsync("最初の時刻からどれくらい経過していますか?", session1);

//応答の表示
Console.WriteLine(response);

//セッション開始後の状態を確認
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session1).Count);
PrintMessages(chatHistoryProvider.GetMessages(session1));
  • 下記のシナリオでプロンプトを送信しています。
    1. 「現在時刻を教えてください」とプロンプトを送信し、エージェントがGetDateTime関数を呼び出して応答する。
    2. 10秒待機してから「もう一度現在時刻を取得してください」とプロンプトを送信し、再度GetDateTime関数を呼び出して応答する。
    3. 最初のプロンプトからどれくらい経過しているかを「最初の時刻からどれくらい経過していますか?」とプロンプトして、エージェントが両方の時刻を比較して経過時間を計算して応答する。
  • それぞれのプロンプト送信後に、セッションの履歴がどのように積み上がっているかを確認しています。

結果

現在の日時は **2026年4月9日 21時12分36秒** です。
messagesCount:4
Role: user, Contents: TextContent: 現在時刻を教えてください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 現在の日時は **2026年4月9日 21時12分36秒** です。

再確認した日時は **2026年4月9日 21時12分49秒** です。
messagesCount:8
Role: user, Contents: TextContent: 現在時刻を教えてください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 現在の日時は **2026年4月9日 21時12分36秒** です。
Role: user, Contents: TextContent: もう一度現在時刻を取得してください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 再確認した日時は **2026年4月9日 21時12分49秒** です。

最初の時刻(21?:?12?:?36)から再取得した時刻(21?:?12?:?49)までに **13 秒** が経過しています。
messagesCount:10
Role: user, Contents: TextContent: 現在時刻を教えてください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 現在の日時は **2026年4月9日 21時12分36秒** です。
Role: user, Contents: TextContent: もう一度現在時刻を取得してください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 再確認した日時は **2026年4月9日 21時12分49秒** です。
Role: user, Contents: TextContent: 最初の時刻からどれくらい経過していますか?
Role: assistant, Contents: TextContent: 最初の時刻(21?:?12?:?36)から再取得した時刻(21?:?12?:?49)までに **13 秒** が経過しています。
  • 最初のプロンプトからの応答では、GetDateTime関数が呼び出されて現在の日時が返っています。
  • 2回目のプロンプトでも同様にGetDateTime関数が呼び出されて再度現在の日時が返っています。
  • 3回目のプロンプトでは、最初の時刻と再取得した時刻を比較して経過時間を計算した応答が返っています。
  • それぞれのプロンプト送信後に、セッションの履歴が積み上がっていることがわかります。
  • これらの履歴はすべて同じセッションIDのもとで管理されているため、エージェントは前のやり取りを参照しながら応答を生成できています。
  • Roleがsystemのデータがないため、Instructionsは別に管理していることが分かります。
    • LM Studio上のログにはInstructions は反映されていました。

別セッションの確認

//----- 別のセッションの確認 -----
//session2でプロンプトの送信
AgentResponse response2 = await agent.RunAsync("現在時刻を教えてください", session2);

//応答の表示
Console.WriteLine(response2);

//別のセッションの状態を確認
Console.WriteLine("messagesCount:" + chatHistoryProvider.GetMessages(session2).Count);
PrintMessages(chatHistoryProvider.GetMessages(session2));
  • 別のセッションで同じプロンプトを送信して、履歴が分離されていることを確認しています。

結果

現在の時刻は **2026年4月9日 21:12:50** です。
messagesCount:4
Role: user, Contents: TextContent: 現在時刻を教えてください
Role: assistant, Contents: TextContent: , FunctionCallContent
Role: tool, Contents: FunctionResultContent
Role: assistant, Contents: TextContent: 現在の時刻は **2026年4月9日 21:12:50** です。
  • 別のセッションでプロンプトを送信したところ、同様にGetDateTime関数が呼び出されて現在の日時が返っています。
  • ただし、セッション1の履歴とは完全に分離されているため、セッション2の履歴はセッション1のやり取りを一切参照していないことがわかります。
  • これにより、複数のセッションを同時に扱っても会話コンテキストが混ざることなく管理できることが確認できました。

StateBagの確認とChatHistoryのクリア

//----- StateBagの確認とChatHistoryのクリア -----
//sessionクリア前のStateBagを見てみる
PrintStateBag(session1);

//メッセージをクリアしてみる
chatHistoryProvider.SetMessages(session1, new List<ChatMessage>());
PrintMessages(chatHistoryProvider.GetMessages(session1));

//sessionクリア後のStateBagを見てみる
PrintStateBag(session1);
  • sessionのStateBagにどのような情報が入っているかを確認しています。
  • SetMessages を使ってチャット履歴をクリアしてみて、その前後でStateBagの内容がどう変わるかを見ています。

結果

stateBagCount:1
stateBag Key: InMemoryChatHistoryProvider, ValueKind: Object, Value: {"messages":[{"role":"user","contents":[{"$type":"text","text":"現在時刻を教えてください"}]},

~

,"role":"assistant","contents":[{"$type":"text","text":"最初の時刻(21\u202F:\u202F12\u202F:\u202F36)から再取得した時刻(21\u202F:\u202F12\u202F:\u202F49)までに **13 秒** が経過しています。"}],"messageId":"chatcmpl-7euw6agpl9febpa04gf14"}]}
stateBagCount:1
stateBag Key: InMemoryChatHistoryProvider, ValueKind: Object, Value: {"messages":[]}
  • StateBagにはInMemoryChatHistoryProviderが管理する履歴情報が入っていることがわかります。
  • SetMessages で履歴をクリアした後、StateBagの中のmessagesが空になっていることがわかります。
  • StateBag自体はクリアされず、InMemoryChatHistoryProviderのエントリーは残ったまま、messagesの内容だけが変わっていることがわかります。

まとめ

  • InMemoryChatHistoryProviderは、チャット履歴をメモリ上に保持する履歴プロバイダーで、セッションごとに履歴が分離される。
  • 実際にプロンプトを送信してみると、セッションIDごとに履歴が積み上がっていくことが確認できる。
  • 別のセッションで同じプロンプトを送信しても、履歴が分離されているため、会話コンテキストが混ざることなく管理できる。
  • StateBagにはInMemoryChatHistoryProviderが管理する履歴情報が入っており、SetMessages を使って履歴をクリアするとStateBagの中のmessagesも空になることがわかる。
  • これらの機能を活用することで、複数ユーザーや複数会話を同時に扱うシナリオでも、会話コンテキストを適切に管理しながらエージェントを運用できることが分かりました。
  • 次回は、ChatHistoryProviderのカスタム実装例を見ていきたいと思います。

Discussion