Storage

Хранилище определяет, где живет журнал бесед, сколько загрузится журнал и как надежно можно возобновить сеансы.

Встроенные режимы хранения

Agent Framework поддерживает два регулярных режима хранения:

Mode Хранимые данные Типичное использование
Состояние локального сеанса Полная история чата в AgentSession.state (например, через InMemoryHistoryProvider) Службы, для которых не требуется сохраняемость бесед на стороне сервера
Управляемое службой хранилище Состояние беседы в службе; AgentSession.service_session_id указывает на него Службы с поддержкой собственной постоянной беседы

Хранилище журнала чата в памяти

Если поставщику не требуется журнал чата на стороне сервера, агент 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 создаёт поставщик в памяти, используемый по умолчанию, который применяется, если вы передаёте явный локальный сеанс. Настройте один явным образом, если требуется стабильный исходный идентификатор или пользовательские фильтры.

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

Для сокращения с учётом семантики или токенов используйте стратегию сжатия перед запуском вместо того, чтобы полагаться только на количество сообщений.

Замечание

Конфигурация редьюсеров применяется к поставщикам истории в памяти. Для истории, управляемой службой, поведение сокращения зависит от конкретного поставщика или службы.

Управляемое службой хранилище

Когда служба управляет журналом бесед, сеанс сохраняет идентификатор удаленной беседы.

Для OpenAI Responses и Conversations идентификаторы, создаваемые на стороне сервиса, такие как resp_* и conv_*, по умолчанию являются непрозрачными и привязаны к соответствующему API-ключу или проекту. Обычно это достаточно, если этот ключ или проект уже ограничен одним приложением, пользователем или клиентом. Если вы размещаете агент для нескольких конечных пользователей с одним и тем же ключом или проектом, сохраните эти идентификаторы в доверенном серверном хранилище, сопоставите их с идентификаторами сеансов и проверьте владение перед возобновлением беседы.

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(). Создайте сеанс, используя идентификатор существующего диалога службы, если необходимо возобновить историю, управляемую службой:

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

Настроенные поставщики локальной истории бесед не используются для сеансов, управляемых службой, поэтому служба остается источником истории бесед.

Сохраняемость локального журнала вызовов по службе

Запуски инструментов могут выполнять несколько вызовов модели до завершения одного 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,
)

Это важно

Используйте этот режим только для локальной истории, управляемой фреймворком. Если выполнение уже привязано к беседе, управляемой службой (например, через session.service_session_id или options={"conversation_id": ...}), Agent Framework вызывает ошибку, чтобы избежать смешивания двух моделей сохраняемости.

Этот режим особенно полезен, если посредническое ПО может завершиться немедленно после вызова средства: сохранение данных каждого вызова модели сохраняет локальную историю в соответствии с тем, как это сделал бы управляемый службой разговор.

Поставщики истории Go оборачивают вызов агента. Отдельного переключателя сохранения для каждого сервисного вызова нет; если в цикле работы инструментов выполняется несколько обращений к провайдеру в рамках одного запуска, сохраняйте локальную историю после завершения всего запуска или реализуйте собственного провайдера либо промежуточный слой под потребности хранения данных вашего приложения.

Шаблон стороннего или пользовательского хранилища

Для хранилища истории на базе базы данных/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 реализация обычно переопределяет два метода:

  • 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.InvokingCoreAsync — вызывается перед вызовом агента LLM и позволяет изменять список сообщений запроса.
  • ChatHistoryProvider.InvokedCoreAsync — вызывается после вызова LLM агентом и разрешает доступ ко всем сообщениям запроса и ответа.

ChatHistoryProvider предоставляет базовые реализации InvokingCoreAsync и InvokedCoreAsync.

Базовая InvokingCoreAsync реализация выполняет следующие действия:

  • вызывает ProvideChatHistoryAsync для получения сообщений, которые должны использоваться в качестве истории чатов для данного запуска
  • запускает необязательный фильтр FuncprovideOutputMessageFilter для сообщений, возвращаемых ProvideChatHistoryAsync. Этот фильтр 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 должен использовать только один поставщик истории.
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 используйте agent.HistoryProvider, если вам нужна история, хранящаяся в базе данных, Redis, BLOB-объектном хранилище или файловом хранилище. Вспомогательный компонент, создаваемый по умолчанию с помощью 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)

Это важно

Относитесь к AgentSession как к непрозрачному объекту состояния и восстанавливайте его с той же конфигурацией агента или поставщика, которая его создала. Храните сериализованные сеансы и все идентификаторы сеансов на стороне службы в качестве доверенного состояния приложения. В хостируемых или мультитенантных приложениях привязывайте каждый сохраненный сеанс к аутентифицированному пользователю или арендатору, прежде чем разрешить его возобновление.

Подсказка

Используйте дополнительного поставщика журнала аудита/оценки (load_messages=False, store_context_messages=True), чтобы фиксировать обогащенный контекст и входные/выходные данные, не влияя на загрузку основного журнала.

Дальнейшие шаги