Storage

ストレージは、会話履歴が存在する場所、読み込まれる履歴の量、およびセッションを再開できる信頼性を制御します。

組み込みのストレージ モード

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の仮想メソッドはすべて、現在のAIAgentAgentSessionへの参照を渡されます。

型指定された状態を 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 は、 InvokingCoreAsyncInvokedCoreAsyncの基本実装を提供します。

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=Falsestore_context_messages=True) を使用して、プライマリ履歴の読み込みに影響を与えずに、エンリッチされたコンテキストと入力/出力をキャプチャします。

次のステップ