Magazyn

Pamięć kontroluje, gdzie przechowywana jest historia konwersacji, ile historii jest ładowane oraz jak niezawodnie można wznawiać sesje.

Wbudowane tryby przechowywania

Struktura agenta obsługuje dwa zwykłe tryby przechowywania:

Tryb Co jest przechowywane Typowe użycie
Stan sesji lokalnej Pełna historia czatu w AgentSession.state (na przykład za pośrednictwem InMemoryHistoryProvider) Usługi, które nie wymagają trwałości konwersacji po stronie serwera
Magazyn zarządzany przez usługę Stan konwersacji w usłudze; AgentSession.service_session_id wskazuje na to Usługi z natywną trwałą obsługą konwersacji

Magazyn historii czatów w pamięci

Jeśli dostawca nie wymaga historii czatów po stronie serwera, program Agent Framework przechowuje historię lokalnie w sesji i wysyła odpowiednie komunikaty w każdym uruchomieniu.

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 przechowuje lokalną historię czatów w agent.Session za pośrednictwem agent.HistoryProvider. Jeśli nie skonfigurujesz dostawcy historii, program Agent Framework utworzy domyślnego dostawcę w pamięci, który jest używany podczas przekazywania jawnej sesji lokalnej. Skonfiguruj je jawnie, jeśli chcesz uzyskać stabilny identyfikator źródła lub filtry niestandardowe.

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()

Zmniejszenie rozmiaru historii w pamięci

Jeśli historia będzie zbyt duża dla limitów modelu, zastosuj narzędzie redukujące.

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)
        })
    });

Użyj filtru HistoryProvider, aby ograniczyć liczbę wiadomości z historii wczytywanych w następnym żądaniu. Na przykład zachowaj tylko 20 najnowszych wiadomości w historii:

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
    },
})

W przypadku redukcji semantycznej lub opartej na tokenach należy użyć strategii kompaktowania przed uruchomieniem zamiast polegać tylko na licznikach komunikatów.

Uwaga / Notatka

Konfiguracja reduktora ma zastosowanie do dostawców historii w pamięci. W przypadku historii zarządzanej przez usługę zachowanie redukcji jest specyficzne dla dostawcy/usługi.

Magazyn zarządzany przez usługę

Gdy usługa zarządza historią konwersacji, sesja przechowuje identyfikator konwersacji zdalnej.

W przypadku OpenAI Responses i Conversations identyfikatory po stronie usługi, takie jak resp_* i conv_*, są niejawne i domyślnie ograniczone do danego klucza API lub projektu. Zwykle jest to wystarczające, gdy ten klucz lub projekt jest już przypisany do jednej aplikacji, użytkownika lub dzierżawcy. Jeśli udostępniasz agenta wielu użytkownikom końcowym przy użyciu tego samego bazowego klucza lub projektu, przechowuj te identyfikatory w zaufanym magazynie po stronie serwera, powiąż je z własnymi identyfikatorami sesji i zweryfikuj, kto jest ich właścicielem, przed wznowieniem rozmowy.

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 przechowuje identyfikatory konwersacji specyficzne dla danego dostawcy w session.ServiceID(). Utwórz sesję z istniejącym identyfikatorem konwersacji usługi, gdy musisz wznowić historię zarządzaną przez usługę:

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()

Gdy dostawca tworzy lub aktualizuje zdalny identyfikator konwersacji podczas uruchomienia, sesja jest aktualizowana i można ją sprawdzić po zakończeniu wywołania:

fmt.Println(session.ServiceID())

Skonfigurowani lokalni dostawcy historii są pomijani dla sesji zarządzanych przez usługę, dzięki czemu usługa pozostaje źródłem historii konwersacji.

Trwałość historii lokalnej dla wywołania usługi

Wywołania narzędzi mogą wykonać wiele wywołań modelu, zanim jedno agent.run() zakończy działanie. Domyślnie lokalni dostawcy historii są utrwalani raz po pełnym uruchomieniu. Aby lokalna historia ściślej odzwierciedlała konwersacje zarządzane przez usługę, ustaw require_per_service_call_history_persistence=True, aby dostawcy historii byli uruchamiani dla każdego wywołania modelu.

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,
)

Ważne

Ten tryb jest używany tylko w przypadku historii lokalnej zarządzanej przez platformę. Jeśli przebieg jest już powiązany z konwersacją zarządzaną przez usługę (na przykład za pośrednictwem session.service_session_id lub options={"conversation_id": ...}), program Agent Framework zgłasza błąd, aby zapobiec mieszaniu dwóch modeli trwałości.

Ten tryb jest szczególnie przydatny, gdy oprogramowanie pośredniczące może zakończyć się natychmiast po wywołaniu narzędzia: utrwalanie poszczególnych wywołań modelu utrzymuje historię lokalną zgodną z zawartością konwersacji zarządzanej przez usługę.

Dostawcy historii dla Go działają przy wywołaniu agenta. Nie ma oddzielnego przełącznika trwałości dla każdego wywołania usługi; jeśli pętla narzędzia wykonuje wiele wywołań dostawcy w ramach jednego uruchomienia, utrwal historię lokalną po zakończeniu całego uruchomienia lub zaimplementuj niestandardowego dostawcę albo warstwę pośrednią na potrzeby przechowywania danych w aplikacji.

Wzorzec magazynu innego dostawcy/magazynu niestandardowego

W przypadku historii opartej na bazie danych, Redisie lub blobie, zaimplementuj niestandardowego dostawcę historii.

Najważniejsze wskazówki:

  • Przechowuj komunikaty pod kluczem zasięgu sesji.
  • Zachowaj zwracaną historię w granicach kontekstu modelu.
  • Utrwalanie identyfikatorów specyficznych dla dostawcy w stanie sesji.

Klasa bazowa dla dostawców historii to Microsoft.Agents.AI.ChatHistoryProvider. Dostawcy historii uczestniczą w pipeline'u agenta, mają możliwość wpływania na lub zastępowania wejściowych komunikatów agenta i mogą przechowywać nowe komunikaty. ChatHistoryProvider program ma różne metody wirtualne, które można zastąpić w celu zaimplementowania własnego niestandardowego dostawcy historii. Zobacz poniższe różne opcje implementacji, aby uzyskać więcej informacji o tym, co można nadpisać.

ChatHistoryProvider stan

Wystąpienie ChatHistoryProvider jest dołączone do agenta, a to samo wystąpienie będzie używane dla wszystkich sesji. Oznacza to, że element ChatHistoryProvider nie powinien przechowywać żadnego stanu określonej sesji w wystąpieniu dostawcy. Element ChatHistoryProvider może mieć odwołanie do klienta bazy danych w polu, ale nie powinien mieć klucza bazy danych dla historii czatów w polu.

ChatHistoryProvider Zamiast tego program może przechowywać dowolne wartości specyficzne dla sesji, takie jak klucze bazy danych, komunikaty lub inne elementy, które są istotne w AgentSession samej sobie. Wszystkim metodom wirtualnym na ChatHistoryProvider jest przekazywane odwołanie do bieżącego AIAgent i AgentSession.

Aby łatwo przechowywać określony typ stanu w AgentSession, udostępniana jest klasa narzędziowa:

// 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);

Prosta implementacja ChatHistoryProvider

Najprostsza ChatHistoryProvider implementacja zwykle zastępuje dwie metody:

  • ChatHistoryProvider.ProvideChatHistoryAsync — załaduj odpowiednią historię czatu i zwróć załadowane wiadomości.
  • ChatHistoryProvider.StoreChatHistoryAsync — przechowuj komunikaty dotyczące żądań i odpowiedzi, z których wszystkie powinny być nowe.

Oto przykład prosty ChatHistoryProvider , który przechowuje historię czatu bezpośrednio w stanie sesji.

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; } = [];
    }
}

Implementacja zaawansowana ChatHistoryProvider

Bardziej zaawansowana implementacja może zastąpić następujące metody:

  • ChatHistoryProvider.InvokingCoreAsync — wywoływana przed wywołaniem przez agenta modułu LLM i umożliwia zmodyfikowanie listy komunikatów żądania.
  • ChatHistoryProvider.InvokedCoreAsync — wywoływana po wywołaniu agenta LLM i umożliwia dostęp do wszystkich komunikatów żądania i odpowiedzi.

ChatHistoryProvider Udostępnia podstawowe implementacje elementów InvokingCoreAsync i InvokedCoreAsync.

Implementacja podstawowa InvokingCoreAsync wykonuje następujące czynności:

  • wywołuje ProvideChatHistoryAsync w celu pobrania wiadomości, które powinny być używane jako historia czatu podczas sesji
  • uruchamia opcjonalny filtr FuncprovideOutputMessageFilter na komunikatach zwracanych przez ProvideChatHistoryAsync. Ten filtr Func można dostarczyć za pomocą konstruktora ChatHistoryProvider .
  • łączy przefiltrowane komunikaty zwracane przez ProvideChatHistoryAsync z komunikatami przekazanymi agentowi przez obiekt wywołujący, w celu wygenerowania wiadomości żądania agenta. Historia czatu jest dołączana przed wiadomościami wprowadzanymi przez agenta.
  • oznacza wszystkie przefiltrowane wiadomości zwracane przez ProvideChatHistoryAsync z informacją o źródle, co wskazuje, że te wiadomości pochodzą z historii czatu.

Baza InvokedCoreAsync wykonuje następujące czynności:

  • sprawdza, czy przebieg zakończył się niepowodzeniem, a jeśli tak, zwraca bez dalszego przetwarzania.
  • filtruje komunikaty żądania agenta, aby wykluczyć komunikaty, które zostały wygenerowane przez ChatHistoryProviderelement , ponieważ chcemy przechowywać tylko nowe komunikaty, a nie te, które zostały wygenerowane przez ChatHistoryProvider element w pierwszej kolejności. Należy pamiętać, że ten filtr można zastąpić za pomocą parametru storeInputMessageFilter w konstruktorze ChatHistoryProvider .
  • przekazuje przefiltrowane komunikaty żądań i wszystkie komunikaty odpowiedzi do StoreChatHistoryAsync w celu przechowywania.

Można zastąpić te metody, aby zaimplementować element ChatHistoryProvider, jednak wymaga to, aby implementator samodzielnie zaimplementował podstawową funkcjonalność. Oto przykład takiej implementacji.

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; } = [];
    }
}
  • W języku Python tylko jeden dostawca historii powinien używać load_messages=True.
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)

W Go zaimplementuj agent.HistoryProvider, gdy chcesz mieć historię opartą na bazie danych, Redisie, obiektach blob lub plikach. Domyślny moduł pomocniczy utworzony przez agent.NewHistoryProvider ładuje wcześniejsze komunikaty w Provide i zapisuje nowe komunikaty żądań i odpowiedzi w Store. Zachowaj klucze pamięci w ramach sesji, aby można było ponownie wykorzystać instancję dostawcy między sesjami.

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)
        },
    })
}

Dołącz dostawcę niestandardowego do agenta:

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant.",
    Config: agent.Config{
        Name:            "StorageAgent",
        HistoryProvider: NewDatabaseHistoryProvider(store),
    },
})

Nie należy łączyć skonfigurowanego lokalnego HistoryProvider z sesją zarządzaną przez usługę. Użyj lokalnego magazynu historii albo zdalnego stanu konwersacji dostawcy dla danej sesji.

Utrwalanie sesji przy ponownych uruchomieniach

Zapisz cały obiekt sesji, nie tylko tekst wiadomości.

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)

Sesje można utrwalać za pomocą serializacji JSON. Przechowuj cały agent.Session, a nie tylko tekst wiadomości lub klucz historii.

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()

W przypadku przechowywania opartego na bazie danych należy zserializować sesję do []byte i przechować ją przy użyciu wybranego backendu:

data, _ := json.Marshal(session)
db.Set(sessionID, data)

data, _ := db.Get(sessionID)
var resumed agent.Session
_ = json.Unmarshal(data, &resumed)

Tip

Zobacz przykład magazynowania sesji innej firmy, aby zobaczyć pełny przykład.

Ważne

Traktuj AgentSession jako nieprzezroczystym obiekt stanu i przywróć go przy użyciu tej samej konfiguracji agenta/dostawcy, która go utworzyła. Przechowuj sesje serializowane i wszystkie identyfikatory sesji po stronie usługi jako stan zaufanej aplikacji. W aplikacjach hostowanych lub wielodzierżawnych należy powiązać każdą przechowywaną sesję z uwierzytelnionym użytkownikiem lub dzierżawcą, zanim zezwoli się na jej wznowienie.

Tip

Użyj dodatkowego dostawcy historii audytu/ewaluacji (load_messages=False, store_context_messages=True), aby przechwycić wzbogacony kontekst oraz dane wejściowe/wyjściowe, nie wpływając na ładowanie historii głównej.

Dalsze kroki