Memória, armazenamento e provedores de contexto no Microsoft Agent Framework

A memória no Agent Framework é o que permite a um agente de inteligência artificial lembrar o que aconteceu antes e manter coerência entre várias interações. Para isso, o Microsoft Agent Framework oferece um conjunto robusto de mecanismos que gerenciam o estado conversacional dos agentes.

Entender como sessões, armazenamento e provedores de contexto funcionam em conjunto é essencial para construir agentes confiáveis. Neste artigo, você vai ver cada peça em detalhes, com exemplos em C#.

Diagrama da memória no Agent Framework mostrando a sessão do agente, o provedor de histórico e os provedores de contexto em volta da chamada ao modelo
Visão geral das camadas que guardam e enriquecem o estado de um agente.

Se você quer se aprofundar no framework com acompanhamento, conheça a imersão em Microsoft Agent Framework do balta.io.

O papel da memória no Agent Framework

Por padrão, quando você cria um agente com um cliente que não exige persistência no servidor, o framework provisiona automaticamente um InMemoryChatHistoryProvider. Esse é o caso, por exemplo, do OpenAI Chat Completion, que não guarda o histórico do lado do serviço.

Esse provedor mantém o histórico de mensagens na memória do processo durante a sessão ativa. Portanto, ele é suficiente para cenários simples, em que não há necessidade de durabilidade.

Para personalizar esse comportamento, basta fornecer uma implementação própria de ChatHistoryProvider nas opções do agente.

AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
    .GetChatClient(deploymentName)
    .AsAIAgent(new ChatClientAgentOptions()
    {
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        // Substitui o provedor padrão por uma implementação customizada
        ChatHistoryProvider = new CustomChatHistoryProvider()
    });

Com essa troca, a memória no Agent Framework pode morar em qualquer lugar, de um banco relacional a um cache distribuído.

Repare que o exemplo usa AzureCliCredential apenas para desenvolvimento. Em produção, a documentação recomenda uma credencial específica, como ManagedIdentityCredential, para evitar latência e riscos de segurança.

Sessões e memória no Agent Framework

A sessão, representada pelo tipo AgentSession, é o veículo que compartilha o contexto entre execuções consecutivas do agente. Sem ela, cada chamada a RunAsync começaria do zero.

AgentSession session = await agent.CreateSessionAsync();

Console.WriteLine(await agent.RunAsync("Hello! What's the square root of 9?", session));
Console.WriteLine(await agent.RunAsync("My name is Alice", session));
Console.WriteLine(await agent.RunAsync("What is my name?", session));

No exemplo acima, a terceira chamada retorna corretamente o nome informado na segunda. Ou seja, a sessão preserva o contexto entre as execuções e sustenta a memória no Agent Framework.

Esse comportamento é a base das conversas com várias rodadas. Para ver o tema em um cenário mais completo, leia o artigo sobre conversas multi-turnos no Microsoft Agent Framework.

Modos de armazenamento da memória no Agent Framework

O framework distingue dois modos principais para guardar o histórico conversacional. A escolha depende do serviço de IA que está por trás do agente.

Modo O que fica guardado Uso típico
Estado local da sessão O histórico completo dentro da sessão Serviços que não persistem conversas no servidor
Armazenamento gerenciado pelo serviço O histórico fica no serviço, e a sessão guarda só um identificador Serviços com suporte nativo a conversas persistentes

No primeiro modo, o histórico completo fica no estado da própria sessão, por meio do InMemoryChatHistoryProvider ou de uma implementação equivalente. Assim, a cada execução, o framework envia ao modelo as mensagens relevantes.

No segundo modo, o estado conversacional reside no provedor de IA. Nesse caso, a sessão local mantém apenas um identificador remoto, acessível em C# pela propriedade ConversationId de ChatClientAgentSession.

// Em sessões gerenciadas pelo serviço, o histórico fica no provedor de IA
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
Console.WriteLine(typedSession.ConversationId);

Na documentação em Python, os nomes equivalentes são AgentSession.state e AgentSession.service_session_id. Contudo, o conceito é o mesmo nas duas linguagens.

Um alerta importante vale para aplicações com vários usuários. Os identificadores do serviço, como os prefixados com resp_ e conv_ na OpenAI, ficam vinculados à chave ou ao projeto, então guarde esses IDs no servidor e verifique o dono antes de retomar uma conversa.

Controle do tamanho do histórico

Quando o histórico cresce além do limite de contexto do modelo, você precisa aplicar uma estratégia de redução. Caso contrário, as chamadas ficam caras e podem até falhar.

Para isso, o framework disponibiliza a classe MessageCountingChatReducer, que limita o histórico ao número máximo de mensagens configurado.

ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
    // Mantém apenas as 20 mensagens mais recentes
    ChatReducer = new MessageCountingChatReducer(20)
})

Essa configuração vale para provedores de histórico em memória. Já no armazenamento gerenciado pelo serviço, a redução depende de cada provedor de IA.

O número ideal de mensagens varia conforme o modelo e o tamanho médio das respostas. Por isso, comece com um valor conservador e ajuste depois de medir o consumo de tokens em conversas reais.

Lembre também que reduzir o histórico significa esquecer detalhes antigos. Nesse caso, combinar o redutor com um provedor de contexto que guarda fatos importantes mantém a memória no Agent Framework útil mesmo em conversas longas.

Histórico customizado para memória no Agent Framework

Para guardar a memória no Agent Framework em um banco de dados, no Redis ou em um blob, você implementa um provedor de histórico próprio. A classe base ChatHistoryProvider expõe métodos virtuais que controlam tanto o carregamento quanto o armazenamento de mensagens.

Implementação simples

A implementação mais simples sobrescreve dois métodos. O ProvideChatHistoryAsync carrega o histórico relevante, enquanto o StoreChatHistoryAsync persiste as novas mensagens após cada execução.

protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(
    InvokingContext context, CancellationToken cancellationToken = default) =>
    new(this._sessionState.GetOrInitializeState(context.Session).Messages);

protected override ValueTask StoreChatHistoryAsync(
    InvokedContext context, CancellationToken cancellationToken = default)
{
    // Junta as mensagens de entrada e de resposta desta execução
    var state = this._sessionState.GetOrInitializeState(context.Session);
    var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
    state.Messages.AddRange(allNewMessages);
    this._sessionState.SaveState(context.Session, state);
    return default;
}

A documentação também lista algumas diretrizes para esse padrão. Por exemplo, guarde as mensagens sob uma chave ligada à sessão, mantenha o histórico retornado dentro do limite do modelo e salve os identificadores do provedor no estado da sessão.

Implementação avançada

Uma implementação avançada pode sobrescrever InvokingCoreAsync e InvokedCoreAsync diretamente. Dessa forma, você assume controle total sobre as mensagens enviadas ao modelo e sobre o que fica persistido depois da resposta.

A diferença entre os dois métodos é importante. O InvokingCoreAsync roda antes da chamada ao LLM, enquanto o InvokedCoreAsync roda depois, com acesso à resposta gerada.

Estado por sessão na memória no Agent Framework

Uma instância de ChatHistoryProvider é compartilhada por todas as sessões de um agente. Por isso, ela não deve guardar estado específico de sessão em campos de instância.

O provedor pode, sim, manter em um campo o cliente de um serviço de memória. No entanto, o identificador do conjunto de memórias de um usuário deve morar na própria sessão.

Para facilitar essa tarefa, o framework oferece a classe utilitária ProviderSessionState<T>. Com ela, você grava e recupera estado tipado diretamente no objeto AgentSession.

var sessionStateHelper = new ProviderSessionState<MyCustomState>(
    // Cria o estado inicial na primeira vez que a sessão é usada
    stateInitializer: currentSession => new MyCustomState() { DbKey = Guid.NewGuid().ToString() },
    stateKey: this.GetType().Name);

MyCustomState state = sessionStateHelper.GetOrInitializeState(session);
sessionStateHelper.SaveState(session, state);

Essa abordagem garante isolamento entre sessões concorrentes. Além disso, ela dispensa qualquer sincronização explícita dentro do provedor.

Em outras palavras, o ProviderSessionState<T> mantém a memória no Agent Framework separada por conversa, mesmo quando milhares de usuários usam o mesmo agente ao mesmo tempo.

Serialização e restauração de sessões

Para manter a continuidade das conversas depois de uma reinicialização, o framework permite serializar e desserializar o estado completo de AgentSession.

// Converte a sessão em JSON para gravar em um armazenamento durável
JsonElement serialized = agent.SerializeSession(session);

// Recria a sessão a partir do JSON salvo
AgentSession resumed = await agent.DeserializeSessionAsync(serialized);

Trate AgentSession como um objeto de estado opaco. Então, sempre restaure a sessão com a mesma configuração de agente e de provedores usada na criação.

Isso acontece porque cada provedor grava a própria parte do estado dentro da sessão, identificada por uma chave. Se a configuração mudar, um provedor novo pode não encontrar o que espera, e um provedor removido deixa dados órfãos.

Na prática, o JSON gerado pode ir para qualquer armazenamento durável, como um banco relacional, o Cosmos DB ou um blob. O importante é associar esse conteúdo a um identificador de conversa que a sua aplicação controla.

Em aplicações hospedadas ou multi-tenant, vincule cada sessão salva ao usuário autenticado antes de permitir a retomada. Afinal, a memória no Agent Framework pode conter dados pessoais e não deve vazar entre clientes.

Provedores de contexto

Os provedores de contexto, representados pela classe base AIContextProvider, participam do pipeline de execução do agente em dois momentos distintos. Eles são o ponto de extensão oficial para enriquecer a memória no Agent Framework.

Antes da chamada ao LLM, o método ProvideAIContextAsync enriquece o contexto com instruções adicionais, mensagens ou ferramentas. Depois da chamada, o método StoreAIContextAsync extrai informações das novas mensagens e as guarda para uso futuro.

AIAgent agent = new OpenAIClient("<your_api_key>")
    .GetChatClient(modelName)
    .AsAIAgent(new ChatClientAgentOptions()
    {
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        // Lista de provedores que participam de cada execução
        AIContextProviders = [new MyCustomMemoryProvider()],
    });

Assim como no histórico, a mesma instância de AIContextProvider atende todas as sessões. Por isso, a regra de guardar dados específicos no AgentSession também vale aqui.

Como os provedores podem injetar ferramentas, eles conversam bem com o tema do artigo sobre tools no Microsoft Agent Framework.

Provedor de contexto simples

A implementação simples de AIContextProvider sobrescreve ProvideAIContextAsync para injetar contexto e StoreAIContextAsync para persistir as informações relevantes.

No exemplo abaixo, o provedor consulta um serviço externo de memória. Em seguida, ele injeta as memórias encontradas como uma mensagem adicional no contexto.

protected override ValueTask<AIContext> ProvideAIContextAsync(
    InvokingContext context, CancellationToken cancellationToken = default)
{
    var state = this._sessionState.GetOrInitializeState(context.Session);

    // Sem memórias associadas, não há nada a acrescentar
    if (state.MemoriesId == null) return new ValueTask<AIContext>(new AIContext());

    var memories = this._client.LoadMemories(
        state.MemoriesId,
        string.Join("\n", context.AIContext.Messages?.Select(x => x.Text) ?? []));

    return new ValueTask<AIContext>(new AIContext
    {
        Messages = [new ChatMessage(ChatRole.User,
            "Here are some memories to help answer the user question: " +
            string.Join("\n", memories.Select(x => x.Text)))]
    });
}

Repare que o provedor busca o MemoriesId no estado da sessão, e não em um campo da classe. Com isso, cada usuário enxerga apenas as próprias memórias.

Esse padrão de buscar antes e gravar depois é o mesmo usado pelos provedores prontos. Portanto, entender esse exemplo ajuda a ler qualquer implementação de memória no Agent Framework.

Provedor de contexto avançado

A implementação avançada sobrescreve InvokingCoreAsync e InvokedCoreAsync. Assim, você assume controle completo sobre a composição do contexto e sobre o processo de armazenamento.

Uma prática essencial nessa abordagem é filtrar as mensagens que vieram de provedores de histórico ou de contexto. Do contrário, o provedor salva o que ele mesmo injetou e cria um loop de retroalimentação.

// Mantém apenas as mensagens que vieram de fora do pipeline de provedores
var filteredRequestMessages = context.RequestMessages.Where(
    m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);

Esse filtro garante que somente as mensagens externas sejam gravadas como novas memórias. Na verdade, a implementação base de InvokedCoreAsync já aplica esse filtro por padrão e ignora execuções que falharam.

Portanto, para a maioria dos cenários, sobrescrever StoreAIContextAsync é suficiente. Só desça para os métodos Core quando precisar controlar o tratamento de erros ou a filtragem de mensagens.

Provedores prontos de memória no Agent Framework

Além das implementações customizadas, o framework traz provedores prontos. O ChatHistoryMemoryProvider, por exemplo, guarda todo o histórico em um vector store e recupera mensagens semanticamente parecidas com a pergunta atual.

Ele trabalha em duas fases. Depois de cada execução, armazena as mensagens com embeddings e, antes da próxima, busca as mais relevantes para injetar no contexto.

Com escopos configuráveis por aplicação, agente, usuário e sessão, ele leva a memória no Agent Framework para além de uma única conversa.

Outro exemplo é o FileMemoryProvider, que expõe ao agente ferramentas como file_memory_write e file_memory_read. Nesse caso, o próprio modelo decide o que guardar e o que consultar.

Esses provedores também trazem cuidados de segurança. Segundo a documentação do ChatHistoryMemoryProvider, conteúdo recuperado do vector store pode carregar prompt injection indireto e dados pessoais, então proteja o armazenamento com controle de acesso e criptografia.

Como escolher a estratégia de memória no Agent Framework

Com tantas peças disponíveis, vale organizar a decisão em perguntas simples. Primeiro, defina se a conversa precisa sobreviver a uma reinicialização da aplicação.

Se a resposta for não, o InMemoryChatHistoryProvider padrão resolve o problema. Esse é o cenário de protótipos, testes e ferramentas internas de uso rápido.

Se a resposta for sim, verifique se o serviço de IA já guarda conversas. Quando guarda, o armazenamento gerenciado pelo serviço é o caminho mais simples, porque a sessão só precisa do identificador.

Quando o serviço não guarda, ou quando você precisa dos dados no seu próprio banco, implemente um ChatHistoryProvider customizado. Além disso, serialize as sessões para retomar conversas depois.

Em seguida, pense na memória de longo prazo, aquela que atravessa várias conversas. Nesse ponto, entram os provedores de contexto, como o ChatHistoryMemoryProvider com busca semântica ou um provedor próprio ligado a um serviço de memória.

Por fim, decida o que precisa ser auditado. Assim, a memória no Agent Framework fica dividida em camadas claras: histórico da sessão, persistência, memória de longo prazo e rastreabilidade.

Rastreabilidade com audit providers

Você pode encadear vários provedores para fins de auditoria e diagnóstico. Na versão Python, isso acontece com um provedor de histórico extra configurado com load_messages=False e store_context_messages=True.

Essa configuração captura o contexto enriquecido pelos outros provedores, além da entrada e da saída. Ao mesmo tempo, ela não interfere no carregamento principal do histórico.

Para completar a visibilidade do agente, vale combinar essa técnica com a observabilidade de agentes e workflows do framework.

Boas práticas e considerações finais

O framework foi projetado para que provedores de histórico e de contexto convivam de forma composta e extensível. Assim, cada um cuida de uma camada distinta da memória no Agent Framework.

A separação entre armazenamento local e gerenciado pelo serviço permite usar a mesma abstração de sessão em qualquer cenário. Ou seja, o código do agente não muda conforme o lugar onde o estado fica guardado.

Use ProviderSessionState<T> em qualquer provedor customizado, pois ele garante o isolamento entre sessões sem gerenciamento manual de concorrência. Além disso, filtre as mensagens injetadas para evitar memórias duplicadas.

Por fim, a capacidade de serializar e restaurar sessões completas é a base para construir agentes resilientes a falhas e reinicializações. Com essas peças, a memória no Agent Framework deixa de ser um detalhe e passa a ser parte central da arquitetura.

FAQ

O agente guarda o histórico automaticamente?

Sim. Quando o serviço não persiste conversas, o framework cria um InMemoryChatHistoryProvider por padrão, mas o histórico some quando o processo termina.

Qual a diferença entre ChatHistoryProvider e AIContextProvider?

O ChatHistoryProvider cuida do histórico da conversa. Já o AIContextProvider enriquece cada execução com instruções, mensagens ou ferramentas e extrai informações para uso futuro.

Posso guardar o ID do usuário em um campo do provedor?

Não. A mesma instância atende todas as sessões, então dados específicos devem ficar no AgentSession, de preferência com ProviderSessionState<T>.

Como limitar o tamanho do histórico?

Configure um ChatReducer, como o MessageCountingChatReducer, nas opções do InMemoryChatHistoryProvider.

Como retomar uma conversa depois de reiniciar a aplicação?

Serialize a sessão com SerializeSession, grave o JSON em um armazenamento durável e restaure com DeserializeSessionAsync, usando a mesma configuração do agente.