ストレージは、会話履歴が存在する場所、読み込まれる履歴の量、およびセッションを再開できる信頼性を制御します。
組み込みのストレージ モード
Agent Framework では、次の 2 つの通常のストレージ モードがサポートされています。
| モード | 格納される内容 | 一般的な使用 |
|---|---|---|
| ローカル セッションの状態 |
AgentSession.stateでの完全なチャット履歴 (InMemoryHistoryProvider など) |
サーバー側の会話の永続化を必要としないサービス |
| サービスで管理されるストレージ | サービスの会話状態。 AgentSession.service_session_id がポイントする |
ネイティブの永続的な会話をサポートするサービス |
メモリ内チャット履歴ストレージ
プロバイダーがサーバー側のチャット履歴を必要としない場合、Agent Framework はセッション内の履歴をローカルに保持し、実行ごとに関連するメッセージを送信します。
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetChatClient(modelName)
.AsAIAgent(instructions: "You are a helpful assistant.", name: "Assistant");
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// When in-memory chat history storage is used, it's possible to access the chat history
// that is stored in the session via the provider attached to the agent.
var provider = agent.GetService<InMemoryChatHistoryProvider>();
List<ChatMessage>? messages = provider?.GetMessages(session);
from agent_framework import InMemoryHistoryProvider
from agent_framework.openai import OpenAIChatClient
agent = OpenAIChatClient().as_agent(
name="StorageAgent",
instructions="You are a helpful assistant.",
context_providers=[InMemoryHistoryProvider("memory", load_messages=True)],
)
session = agent.create_session()
await agent.run("Remember that I like Italian food.", session=session)
Go は、agent.Sessionを介してローカル チャット履歴をagent.HistoryProviderに格納します。 履歴プロバイダーを構成しない場合、Agent Framework は、明示的なローカル セッションを渡すときに使用される既定のメモリ内プロバイダーを作成します。 安定したソース ID またはカスタム フィルターが必要な場合は、明示的に構成します。
history := agent.NewInMemoryHistoryProvider(agent.InMemoryHistoryProviderConfig{
SourceID: "chat_history",
})
a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
Instructions: "You are a helpful assistant.",
Config: agent.Config{
Name: "StorageAgent",
HistoryProvider: history,
},
})
session, err := a.CreateSession(ctx)
if err != nil {
panic(err)
}
_, err = a.RunText(ctx, "Remember that I like Italian food.", agent.WithSession(session)).Collect()
_, err = a.RunText(ctx, "What kind of food do I like?", agent.WithSession(session)).Collect()
メモリ内履歴サイズの縮小
履歴がモデルの制限を超えるほど大きくなった場合は、縮小ツールを適用してください。
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetChatClient(modelName)
.AsAIAgent(new ChatClientAgentOptions
{
Name = "Assistant",
ChatOptions = new() { Instructions = "You are a helpful assistant." },
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
ChatReducer = new MessageCountingChatReducer(20)
})
});
HistoryProvider フィルターを使用して、次の要求に読み込まれる履歴メッセージを制限します。 たとえば、最新の 20 件の履歴メッセージのみを保持します。
history := agent.NewInMemoryHistoryProvider(agent.InMemoryHistoryProviderConfig{
SourceID: "chat_history",
ProvideOutputMessageFilter: func(_ context.Context, messages []*message.Message) ([]*message.Message, error) {
if len(messages) <= 20 {
return messages, nil
}
return messages[len(messages)-20:], nil
},
})
セマンティックまたはトークン対応の削減の場合は、メッセージ数のみに依存するのではなく、実行前に圧縮戦略を使用します。
注
Reducer の構成は、メモリ内履歴プロバイダーに適用されます。 サービス管理履歴の場合、削減動作はプロバイダー/サービス固有です。
サービスで管理されるストレージ
サービスが会話履歴を管理すると、セッションにはリモート会話識別子が格納されます。
OpenAI 応答と会話の場合、 resp_* や conv_* などのサービス側 ID は、既定で不透明であり、バッキング API キーまたはプロジェクトにスコープが設定されます。 通常、これは、そのキーまたはプロジェクトのスコープが 1 つのアプリケーション、ユーザー、またはテナントである場合に十分です。 同じバッキング キーまたはプロジェクトを持つ複数のエンド ユーザーのエージェントをホストする場合は、それらの ID を信頼されたサーバー側ストレージに保持し、独自のセッション ID からマップし、会話を再開する前に所有権を確認します。
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetOpenAIResponseClient(modelName)
.AsAIAgent(instructions: "You are a helpful assistant.", name: "Assistant");
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", session));
// In this case, since we know we are working with a ChatClientAgent, we can cast
// the AgentSession to a ChatClientAgentSession to retrieve the remote conversation
// identifier.
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
Console.WriteLine(typedSession.ConversationId);
# Rehydrate when the service already has the conversation state.
session = agent.get_session(service_session_id="<service-conversation-id>")
response = await agent.run("Continue this conversation.", session=session)
Go では、プロバイダー固有の会話識別子が session.ServiceID()に格納されます。 サービス管理履歴を再開する必要がある場合は、既存のサービス会話 ID を使用してセッションを作成します。
session, err := a.CreateSession(ctx, agent.WithServiceID("<service-conversation-id>"))
if err != nil {
panic(err)
}
_, err = a.RunText(ctx, "Continue this conversation.", agent.WithSession(session)).Collect()
プロバイダーが実行中にリモート会話識別子を作成または更新すると、セッションが更新され、呼び出し後に検査できます。
fmt.Println(session.ServiceID())
構成されたローカル履歴プロバイダーは、サービスで管理されるセッションではスキップされるため、サービスは会話履歴のソースのままです。
サービスごとの呼び出しのローカル履歴の永続化
ツール呼び出し実行では、1 つの agent.run() が完了する前に複数のモデル呼び出しを行うことができます。 既定では、ローカルヒストリープロバイダーは、完全実行後に一度保持されます。 ローカル履歴でサービスで管理される会話をより厳密にミラーリングする場合は、代わりに履歴プロバイダーが各モデル呼び出しを実行するように require_per_service_call_history_persistence=True 設定します。
from agent_framework import Agent, InMemoryHistoryProvider
from agent_framework.openai import OpenAIChatClient
agent = Agent(
client=OpenAIChatClient(),
name="StorageAgent",
instructions="You are a helpful assistant.",
context_providers=[InMemoryHistoryProvider("memory", load_messages=True)],
require_per_service_call_history_persistence=True,
)
Important
このモードは、フレームワークで管理されるローカル履歴にのみ使用します。 実行が既にサービス管理型の会話にバインドされている場合 (たとえば、 session.service_session_id または options={"conversation_id": ...}経由)、Agent Framework は 2 つの永続化モデルを混在させる代わりにエラーを発生させます。
このモードは、ミドルウェアがツール呼び出しの直後に終了できる場合に特に便利です。モデルごとの呼び出しを永続化すると、ローカル履歴がサービスで管理される会話に合わせて調整されます。
Go 履歴プロバイダーは、エージェント呼び出しを中心に実行されます。 個別のサービス呼び出しごとの永続化スイッチはありません。ツール ループが 1 回の実行内で複数のプロバイダー呼び出しを行う場合は、完全実行後にローカル履歴を保持するか、アプリケーションのストレージ ニーズに合わせてカスタム プロバイダー/ミドルウェアを実装します。
サード パーティ/カスタム ストレージ パターン
データベース/Redis/BLOB ベースの履歴の場合は、カスタム履歴プロバイダーを実装します。
主要なガイダンス:
- セッション スコープ キーの下にメッセージを格納します。
- 返された履歴をモデル コンテキストの制限内に保持します。
- プロバイダー固有の識別子をセッション状態で保持します。
履歴プロバイダーの基本クラスは Microsoft.Agents.AI.ChatHistoryProvider。
履歴プロバイダーは、エージェント パイプラインに参加し、エージェント入力メッセージに貢献したりオーバーライドしたりする機能を持ち、新しいメッセージを格納できます。
ChatHistoryProvider には、独自のカスタム履歴プロバイダーを実装するためにオーバーライドできるさまざまな仮想メソッドがあります。
オーバーライドする内容の詳細については、以下のさまざまな実装オプションを参照してください。
ChatHistoryProvider 状態
ChatHistoryProvider インスタンスはエージェントにアタッチされ、すべてのセッションで同じインスタンスが使用されます。
つまり、 ChatHistoryProvider は、プロバイダー インスタンスにセッション固有の状態を格納しないでください。
ChatHistoryProviderはフィールド内のデータベース クライアントへの参照を持つことができますが、フィールド内のチャット履歴のデータベース キーを持つべきではありません。
代わりに、 ChatHistoryProvider は、データベース キー、メッセージ、または AgentSession 自体に関連するその他のセッション固有の値を格納できます。
ChatHistoryProviderの仮想メソッドはすべて、現在のAIAgentとAgentSessionへの参照を渡されます。
型指定された状態を AgentSessionに簡単に格納できるようにするために、ユーティリティ クラスが用意されています。
// First define a type containing the properties to store in state
internal class MyCustomState
{
public string? DbKey { get; set; }
}
// Create the helper
var sessionStateHelper = new ProviderSessionState<MyCustomState>(
// stateInitializer is called when there is no state in the session for this ChatHistoryProvider yet
stateInitializer: currentSession => new MyCustomState() { DbKey = Guid.NewGuid().ToString() },
// The key under which to store state in the session for this provider. Make sure it does not clash with the keys of other providers.
stateKey: this.GetType().Name,
// An optional jsonSerializerOptions to control the serialization/deserialization of the custom state object
jsonSerializerOptions: myJsonSerializerOptions);
// Using the helper you can read state:
MyCustomState state = sessionStateHelper.GetOrInitializeState(session);
Console.WriteLine(state.DbKey);
// And write state:
sessionStateHelper.SaveState(session, state);
単純な ChatHistoryProvider 実装
最も単純な ChatHistoryProvider 実装では、通常、次の 2 つのメソッドをオーバーライドします。
- ChatHistoryProvider.ProvideChatHistoryAsync - 関連するチャット履歴を読み込み、読み込まれたメッセージを返します。
- ChatHistoryProvider.StoreChatHistoryAsync - 要求メッセージと応答メッセージを格納します。これらはすべて新規である必要があります。
チャット履歴をセッション状態に直接格納する単純な ChatHistoryProvider の例を次に示します。
public sealed class SimpleInMemoryChatHistoryProvider : ChatHistoryProvider
{
private readonly ProviderSessionState<State> _sessionState;
public SimpleInMemoryChatHistoryProvider(
Func<AgentSession?, State>? stateInitializer = null,
string? stateKey = null)
{
this._sessionState = new ProviderSessionState<State>(
stateInitializer ?? (_ => new State()),
stateKey ?? this.GetType().Name);
}
public override string StateKey => this._sessionState.StateKey;
protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default) =>
// return all messages in the session state
new(this._sessionState.GetOrInitializeState(context.Session).Messages);
protected override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
var state = this._sessionState.GetOrInitializeState(context.Session);
// Add both request and response messages to the session state.
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
state.Messages.AddRange(allNewMessages);
this._sessionState.SaveState(context.Session, state);
return default;
}
public sealed class State
{
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = [];
}
}
ChatHistoryProvider の高度な実装
より高度な実装では、次のメソッドをオーバーライドすることを選択できます。
- ChatHistoryProvider.InvokeingCoreAsync - エージェントが LLM を呼び出す前に呼び出され、要求メッセージ リストの変更が許可されます。
- ChatHistoryProvider.InvokedCoreAsync - エージェントが LLM を呼び出した後に呼び出され、すべての要求メッセージと応答メッセージへのアクセスが許可されます。
ChatHistoryProvider は、 InvokingCoreAsync と InvokedCoreAsyncの基本実装を提供します。
InvokingCoreAsync基本実装では、次の処理が行われます。
-
ProvideChatHistoryAsync呼び出して、実行のチャット履歴として使用する必要があるメッセージを取得します - は、
Funcによって返されるメッセージに対してオプションのフィルターprovideOutputMessageFilterProvideChatHistoryAsyncを実行します。 このフィルターFuncは、ChatHistoryProviderコンストラクターを介して指定できます。 - は、
ProvideChatHistoryAsyncによって返されたフィルター処理されたメッセージを、呼び出し元によってエージェントに渡されたメッセージとマージして、エージェント要求メッセージを生成します。 チャット履歴は、エージェント入力メッセージの前に付加されます。 - では、
ProvideChatHistoryAsyncによって返されたすべてのフィルター処理されたメッセージにソース情報がスタンプされ、これらのメッセージがチャット履歴から送信されていることを示します。
InvokedCoreAsync ベースでは、次の処理が行われます。
- は、実行が失敗したかどうかを確認し、失敗した場合は、それ以上の処理を行わずに戻ります。
- はエージェント要求メッセージをフィルター処理して、
ChatHistoryProviderによって生成されたメッセージを除外します。これは、最初にChatHistoryProviderによって生成されたメッセージではなく、新しいメッセージのみを格納するためです。 このフィルターは、storeInputMessageFilterコンストラクターのChatHistoryProviderパラメーターを使用してオーバーライドできることに注意してください。 - は、フィルター処理された要求メッセージとすべての応答メッセージをストレージの
StoreChatHistoryAsyncに渡します。
これらのメソッドをオーバーライドして ChatHistoryProviderを実装することはできますが、実装者は必要に応じて基本機能自体を実装する必要があります。
このような実装の例を次に示します。
public sealed class AdvancedInMemoryChatHistoryProvider : ChatHistoryProvider
{
private readonly ProviderSessionState<State> _sessionState;
public AdvancedInMemoryChatHistoryProvider(
Func<AgentSession?, State>? stateInitializer = null,
string? stateKey = null)
{
this._sessionState = new ProviderSessionState<State>(
stateInitializer ?? (_ => new State()),
stateKey ?? this.GetType().Name);
}
public override string StateKey => this._sessionState.StateKey;
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
// Retrieve the chat history from the session state.
var chatHistory = this._sessionState.GetOrInitializeState(context.Session).Messages;
// Stamp the messages with this class as the source, so that they can be filtered out later if needed when storing the agent input/output.
var stampedChatHistory = chatHistory.Select(message => message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, this.GetType().FullName!));
// Merge the original input with the chat history to produce a combined agent input.
return new(stampedChatHistory.Concat(context.RequestMessages));
}
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
if (context.InvokeException is not null)
{
return default;
}
// Since we are receiving all messages that were contributed earlier, including those from chat history, we need to filter out the messages that came from chat history
// so that we don't store message we already have in storage.
var filteredRequestMessages = context.RequestMessages.Where(m => m.GetAgentRequestMessageSourceType() != AgentRequestMessageSourceType.ChatHistory);
var state = this._sessionState.GetOrInitializeState(context.Session);
// Add both request and response messages to the state.
var allNewMessages = filteredRequestMessages.Concat(context.ResponseMessages ?? []);
state.Messages.AddRange(allNewMessages);
this._sessionState.SaveState(context.Session, state);
return default;
}
public sealed class State
{
[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } = [];
}
}
- Python では、
load_messages=Trueを使用する履歴プロバイダーは 1 つだけです。
from agent_framework.openai import OpenAIChatClient
history = DatabaseHistoryProvider(db_client)
agent = OpenAIChatClient().as_agent(
name="StorageAgent",
instructions="You are a helpful assistant.",
context_providers=[history],
)
session = agent.create_session()
await agent.run("Store this conversation.", session=session)
Go で、データベース、Redis、BLOB、またはファイルに基づく履歴が必要な場合に、 agent.HistoryProvider を実装します。
agent.NewHistoryProviderによって作成された既定のヘルパーは、以前のメッセージをProvideに読み込み、新しい要求/応答メッセージをStoreに保持します。 プロバイダー インスタンスをセッション間で再利用できるように、セッション内のすべてのストレージ キーを保持します。
import (
"context"
"fmt"
"time"
"github.com/microsoft/agent-framework-go/agent"
"github.com/microsoft/agent-framework-go/message"
)
type MessageStore interface {
LoadMessages(context.Context, string) ([]*message.Message, error)
AppendMessages(context.Context, string, []*message.Message) error
}
func NewDatabaseHistoryProvider(store MessageStore) agent.HistoryProvider {
const stateKey = "database_history.key"
historyKey := func(session *agent.Session) string {
var key string
if ok, _ := session.Get(stateKey, &key); ok && key != "" {
return key
}
key = fmt.Sprintf("history-%d", time.Now().UnixNano())
session.Set(stateKey, key)
return key
}
return agent.NewHistoryProvider(agent.HistoryProviderConfig{
SourceID: "database_history",
Provide: func(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, error) {
session, _ := agent.GetOption(invoking.Options, agent.WithSession)
if session == nil {
return nil, nil
}
return store.LoadMessages(ctx, historyKey(session))
},
Store: func(ctx context.Context, invoked agent.InvokedContext) error {
session, _ := agent.GetOption(invoked.Options, agent.WithSession)
if session == nil {
return nil
}
allMessages := make([]*message.Message, 0, len(invoked.RequestMessages)+len(invoked.ResponseMessages))
allMessages = append(allMessages, invoked.RequestMessages...)
allMessages = append(allMessages, invoked.ResponseMessages...)
return store.AppendMessages(ctx, historyKey(session), allMessages)
},
})
}
カスタム プロバイダーをエージェントにアタッチします。
a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
Instructions: "You are a helpful assistant.",
Config: agent.Config{
Name: "StorageAgent",
HistoryProvider: NewDatabaseHistoryProvider(store),
},
})
構成されたローカル HistoryProvider をサービス管理セッションと組み合わせないでください。 特定のセッションに対して、ローカル履歴ストレージまたはプロバイダーのリモート会話状態を使用します。
再起動後のセッションの永続化
メッセージ テキストだけでなく、セッション オブジェクト全体を保持します。
JsonElement serialized = agent.SerializeSession(session);
// Store serialized payload in durable storage.
AgentSession resumed = await agent.DeserializeSessionAsync(serialized);
serialized = session.to_dict()
# Store serialized payload in durable storage.
resumed = AgentSession.from_dict(serialized)
セッションは、JSON シリアル化を使用して保持できます。 メッセージ テキストや履歴キーだけでなく、 agent.Session全体を格納します。
data, err := json.Marshal(session)
if err != nil {
panic(err)
}
if err := os.WriteFile("session.json", data, 0o644); err != nil {
panic(err)
}
loaded, err := os.ReadFile("session.json")
if err != nil {
panic(err)
}
var resumed agent.Session
if err := json.Unmarshal(loaded, &resumed); err != nil {
panic(err)
}
_, err = a.RunText(ctx, "Continue this conversation.", agent.WithSession(&resumed)).Collect()
データベースベースのストレージの場合は、セッションをシリアル化して []byte し、任意のバックエンドに格納します。
data, _ := json.Marshal(session)
db.Set(sessionID, data)
data, _ := db.Get(sessionID)
var resumed agent.Session
_ = json.Unmarshal(data, &resumed)
ヒント
完全な例については、 サード パーティのセッション ストレージのサンプル を参照してください。
Important
AgentSessionを不透明な状態オブジェクトとして扱い、それを作成したのと同じエージェント/プロバイダー構成で復元します。 シリアル化されたセッションとサービス側のセッション ID を信頼されたアプリケーションの状態として格納します。 ホストまたはマルチテナント アプリでは、再開を許可する前に、格納されている各セッションを認証済みのユーザーまたはテナントにバインドします。
ヒント
追加の監査/評価履歴プロバイダー (load_messages=False、 store_context_messages=True) を使用して、プライマリ履歴の読み込みに影響を与えずに、エンリッチされたコンテキストと入力/出力をキャプチャします。