Memória, armazenamento e provedores de contexto no Microsoft Agent Framework
Os provedores de contexto são a peça do Microsoft Agent Framework que dá memória aos agentes de inteligência artificial, enriquecendo cada chamada ao modelo e extraindo informações depois da resposta. Junto com o histórico de conversa e o armazenamento da sessão, eles formam a base para agentes que se comportam de forma coerente entre várias interações.
Neste artigo, você vai entender como essas três camadas trabalham juntas. Além disso, vai ver exemplos em C# de provedores simples e avançados, além das boas práticas recomendadas pela documentação oficial.
O papel da memória em agentes conversacionais
Um modelo de linguagem não lembra nada entre duas chamadas. Por isso, toda a sensação de continuidade de uma conversa depende do que a aplicação envia ao modelo a cada execução.
No Agent Framework, essa responsabilidade fica dividida entre o histórico de mensagens e os provedores de contexto. O histórico guarda o que foi dito, enquanto os provedores injetam conhecimento adicional, como memórias de longo prazo ou documentos recuperados.
Por padrão, quando você cria um agente com um cliente que não guarda a conversa no servidor, o framework provisiona automaticamente um InMemoryChatHistoryProvider. Esse provedor mantém as mensagens na memória do processo durante a sessão ativa.
Para cenários simples, isso basta. No entanto, quando você precisa de durabilidade ou de regras próprias, pode fornecer uma implementação customizada 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 histórico em memória padrão por uma implementação própria
ChatHistoryProvider = new CustomChatHistoryProvider()
});
A sessão como veículo do contexto
A sessão, representada pelo tipo AgentSession, é o objeto que carrega o estado entre execuções consecutivas do mesmo agente. Assim, tanto o histórico quanto os provedores de contexto gravam nela tudo o que precisa sobreviver de uma chamada para outra.
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, a terceira chamada responde corretamente o nome informado na segunda. Isso acontece porque a sessão preserva o contexto entre as execuções.
Se você quiser se aprofundar nesse fluxo, vale ler também o artigo sobre conversas multi-turnos no Agent Framework.
Como o pipeline organiza histórico e provedores de contexto
Antes de escrever código, ajuda entender onde cada peça entra. A documentação descreve o ChatClientAgent como um pipeline em três camadas: middleware do agente, camada de contexto e cliente de chat.
A camada de contexto é a que nos interessa aqui. Ela aceita um único ChatHistoryProvider e uma lista de provedores de contexto, configurada na propriedade AIContextProviders.
var agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
// Um único provedor de histórico
ChatHistoryProvider = new InMemoryChatHistoryProvider(),
// Vários provedores de contexto, executados em sequência
AIContextProviders = [new MyMemoryProvider(), new MyRagProvider()],
});
Quando você chama RunAsync, o fluxo segue uma ordem previsível. Primeiro, o provedor de histórico carrega as mensagens anteriores; em seguida, os provedores de contexto acrescentam mensagens, ferramentas ou instruções.
Depois disso, o cliente envia a requisição ao modelo. Por fim, com a resposta pronta, o histórico e os provedores de contexto recebem as novas mensagens para armazenar o que for relevante.
Um detalhe importante: a saída de cada provedor vira a entrada do próximo. Portanto, a ordem da lista em AIContextProviders influencia o resultado final.
Modos de armazenamento suportados
O Agent Framework distingue dois modos principais de armazenamento do histórico conversacional. A escolha depende do serviço de IA que você usa.
| Modo | O que fica armazenado | Uso típico |
|---|---|---|
| Estado local da sessão | Histórico completo dentro da AgentSession |
Serviços sem persistência de conversa no servidor |
| Armazenamento gerenciado pelo serviço | Estado da conversa no provedor de IA, com um identificador na sessão | Serviços com conversa persistente nativa |
No primeiro modo, o InMemoryChatHistoryProvider ou uma implementação equivalente guarda tudo na própria sessão. Dessa forma, a aplicação envia ao modelo as mensagens relevantes em cada execução.
No segundo modo, o serviço mantém a conversa e a sessão local guarda apenas um identificador remoto. Em C#, você acessa esse valor pela propriedade ConversationId de ChatClientAgentSession.
// Converte a sessão para o tipo concreto para ler o identificador remoto
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
Console.WriteLine(typedSession.ConversationId);
A documentação faz um alerta de segurança sobre esse identificador. Em aplicações com vários usuários sobre a mesma chave de API, guarde esses IDs em armazenamento confiável no servidor e verifique o dono antes de retomar uma conversa.
Vale notar que os provedores de contexto funcionam nos dois modos. Afinal, eles não dependem de onde o histórico mora, e sim do momento da execução.
Controle do tamanho do histórico em memória
Todo modelo tem um limite de janela de contexto. Assim, quando o histórico cresce demais, você precisa aplicar uma estratégia de redução para não estourar esse limite nem o orçamento de tokens.
O framework oferece a classe MessageCountingChatReducer, que limita o histórico ao número máximo de mensagens configurado. Você a conecta ao provedor em memória por meio das opções.
ChatHistoryProvider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
// Mantém apenas as 20 mensagens mais recentes
ChatReducer = new MessageCountingChatReducer(20)
})
Essa configuração vale apenas para provedores de histórico em memória. Já no armazenamento gerenciado pelo serviço, a redução depende de cada provedor de IA.
Implementando um provedor de histórico customizado
Para guardar o histórico em um banco de dados, no Redis ou em um blob, você implementa um provedor próprio. A classe base ChatHistoryProvider expõe métodos virtuais para controlar tanto o carregamento quanto a gravação das mensagens.
A documentação resume três orientações para esse cenário. Ou seja, guarde as mensagens sob uma chave da sessão, devolva um histórico que caiba no limite do modelo e persista identificadores próprios no estado da sessão.
Implementação simples
A forma mais simples sobrescreve dois métodos. O ProvideChatHistoryAsync carrega o histórico relevante, e o StoreChatHistoryAsync grava as novas mensagens depois de cada execução.
protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(
InvokingContext context, CancellationToken cancellationToken = default) =>
// Devolve todas as mensagens guardadas na sessão
new(this._sessionState.GetOrInitializeState(context.Session).Messages);
protected override ValueTask StoreChatHistoryAsync(
InvokedContext context, CancellationToken cancellationToken = default)
{
var state = this._sessionState.GetOrInitializeState(context.Session);
// Junta as mensagens de entrada e de saída desta execução
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
state.Messages.AddRange(allNewMessages);
this._sessionState.SaveState(context.Session, state);
return default;
}
Nesse modelo, a classe base faz o trabalho pesado. Por exemplo, ela mescla o histórico com as mensagens do chamador e marca a origem de cada mensagem carregada.
Implementação avançada
Uma implementação avançada sobrescreve InvokingCoreAsync e InvokedCoreAsync diretamente. Com isso, você assume controle total sobre a composição das mensagens enviadas ao modelo e sobre o que vai para o armazenamento.
A diferença entre os dois é o momento. O framework chama InvokingCoreAsync antes da invocação do LLM, enquanto InvokedCoreAsync roda depois, já com acesso à resposta gerada.
Porém, ao sobrescrever esses métodos, você precisa reproduzir o comportamento da base. Isso inclui ignorar execuções que falharam e filtrar as mensagens que vieram do próprio histórico, para não gravá-las em duplicidade.
Estado por sessão em provedores de contexto
Um ponto costuma pegar muita gente de surpresa. O agente compartilha uma única instância de ChatHistoryProvider entre todas as sessões, e o mesmo vale para os provedores de contexto.
Por isso, nenhum deles deve guardar estado específico de sessão em campos de instância. O provedor pode ter um cliente de banco de dados em um campo, mas nunca a chave do histórico de um usuário.
Para resolver isso, o framework oferece a classe utilitária ProviderSessionState<T>. Ela grava e lê estado tipado diretamente no StateBag da AgentSession, usando uma chave configurável e serialização JSON.
var sessionStateHelper = new ProviderSessionState<MyCustomState>(
// Chamado quando ainda não existe estado deste provedor na sessão
stateInitializer: currentSession => new MyCustomState() { DbKey = Guid.NewGuid().ToString() },
// Chave única, que não pode colidir com a de outros provedores
stateKey: this.GetType().Name);
MyCustomState state = sessionStateHelper.GetOrInitializeState(session);
sessionStateHelper.SaveState(session, state);
Essa abordagem garante isolamento entre sessões concorrentes sem exigir sincronização explícita. Além disso, permite que vários provedores de contexto mantenham estados independentes dentro da mesma sessão.
Serialização e restauração de sessões
Para que uma conversa sobreviva a uma reinicialização do processo, você precisa persistir a sessão inteira. A documentação é clara: salve o objeto de sessão completo, e não apenas o texto das mensagens.
// Converte a sessão em JSON para gravar em armazenamento durável
JsonElement serialized = agent.SerializeSession(session);
// Mais tarde, reconstrói a sessão a partir do JSON salvo
AgentSession resumed = await agent.DeserializeSessionAsync(serialized);
Como o estado dos provedores de contexto também mora na sessão, ele volta junto na restauração. Dessa forma, memórias de longo prazo e identificadores externos continuam disponíveis.
Trate a AgentSession como um objeto de estado opaco. Além disso, restaure sempre com a mesma configuração de agente e de provedores usada na criação.
Em aplicações hospedadas ou multi-tenant, vincule cada sessão salva ao usuário autenticado antes de permitir a retomada. Afinal, a sessão serializada é estado confiável da aplicação.
Provedores de contexto na prática
Os provedores de contexto, representados pela classe base AIContextProvider, participam do pipeline do agente em dois momentos distintos. Eles são o ponto de extensão nativo para memória e enriquecimento de contexto.
Antes da invocação do LLM, o método ProvideAIContextAsync enriquece a requisição com instruções, mensagens ou ferramentas. Depois da resposta, o método StoreAIContextAsync extrai informações das novas mensagens e as guarda para uso futuro.
Você registra os provedores de contexto na criação do agente, pela propriedade AIContextProviders das opções.
AIAgent agent = new OpenAIClient("<your_api_key>")
.GetChatClient(modelName)
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a helpful assistant." },
// Lista de provedores que enriquecem cada execução
AIContextProviders = [new MyCustomMemoryProvider()],
});
Na prática, os provedores de contexto servem para casos variados. Por exemplo, recuperar memórias de um serviço externo, buscar documentos em um índice vetorial ou adicionar instruções dinâmicas conforme o usuário.
Eles também podem acrescentar ferramentas para a execução atual. Se esse assunto for novo para você, o artigo sobre como usar tools no Agent Framework mostra como funções C# viram ferramentas.
Implementando provedores de contexto simples
A implementação simples de AIContextProvider sobrescreve dois métodos. O ProvideAIContextAsync injeta contexto e o StoreAIContextAsync persiste o que for relevante.
No exemplo a seguir, o provedor consulta um serviço externo de memória. Então, ele injeta as memórias encontradas como uma mensagem adicional.
protected override ValueTask<AIContext> ProvideAIContextAsync(
InvokingContext context, CancellationToken cancellationToken = default)
{
var state = this._sessionState.GetOrInitializeState(context.Session);
// Ainda não existem memórias guardadas para esta sessão
if (state.MemoriesId == null) return new ValueTask<AIContext>(new AIContext());
// Busca memórias relacionadas à entrada atual do usuário
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)))]
});
}
Do outro lado, o StoreAIContextAsync cria um contêiner de memória no serviço, se ainda não existir. Em seguida, grava o identificador na sessão e envia as mensagens da execução para o serviço extrair novas memórias.
protected override async ValueTask StoreAIContextAsync(
InvokedContext context, CancellationToken cancellationToken = default)
{
var state = this._sessionState.GetOrInitializeState(context.Session);
// Cria o contêiner de memórias na primeira execução e salva o id na sessão
state.MemoriesId ??= this._client.CreateMemoryContainer();
this._sessionState.SaveState(context.Session, state);
// Envia entrada e saída para o serviço extrair memórias
await this._client.StoreMemoriesAsync(
state.MemoriesId,
context.RequestMessages.Concat(context.ResponseMessages ?? []),
cancellationToken);
}
Repare que o identificador das memórias fica na sessão, e não no provedor. Assim, o mesmo provedor atende muitas sessões sem misturar dados entre usuários.
Implementando provedores de contexto avançados
Na versão avançada, os provedores de contexto sobrescrevem InvokingCoreAsync e InvokedCoreAsync. Dessa forma, assumem controle completo sobre a composição do contexto e sobre o processo de armazenamento.
A implementação base desses métodos faz várias tarefas que você passa a assumir. Por exemplo, ela filtra a entrada para considerar só as mensagens do chamador, marca a origem das mensagens injetadas e mescla tudo com o contexto existente.
Uma prática essencial nessa abordagem é filtrar as mensagens que vieram do histórico ou de outros provedores. Sem esse filtro, o provedor grava como memória nova algo que ele mesmo injetou, criando um loop de retroalimentação.
// Mantém apenas as mensagens que vieram de fora do pipeline
var filteredRequestMessages = context.RequestMessages.Where(
m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External);
Esse filtro garante que somente mensagens originadas externamente virem novas memórias. Para marcar a origem das mensagens que o provedor injeta, use WithAgentRequestMessageSource com o tipo AIContextProvider.
Além disso, no InvokedCoreAsync, verifique a propriedade InvokeException logo no início. Se a execução falhou, o provedor não deve gravar nada.
Rastreabilidade com provedores de auditoria
Às vezes você quer registrar exatamente o que chegou ao modelo, incluindo o contexto injetado. Para isso, é possível encadear provedores adicionais com fins de auditoria e diagnóstico.
No SDK Python, a documentação recomenda um provedor de histórico extra configurado com load_messages=False e store_context_messages=True. Com isso, ele captura o contexto enriquecido por outros provedores sem interferir no carregamento principal.
Nesse SDK, apenas um provedor de histórico deve carregar mensagens. Os demais servem para diagnóstico e avaliação, e funcionam como uma trilha de auditoria da conversa.
Segurança em provedores de contexto e histórico
A documentação da classe ChatHistoryProvider faz um alerta importante. O framework não valida nem filtra as mensagens devolvidas pelo provedor, e as trata como se o próprio usuário as tivesse enviado.
Na prática, se o armazenamento for comprometido, um atacante pode influenciar o modelo por injeção indireta de prompt. O mesmo raciocínio vale para os provedores de contexto que buscam dados externos.
Por isso, devolva apenas dados confiáveis e proteja o armazenamento com controle de acesso. Além disso, considere criptografia em repouso, porque o histórico pode conter dados pessoais e conteúdo sensível.
Boas práticas com provedores de contexto
O framework foi desenhado para que histórico e provedores de contexto coexistam de forma composta e extensível. Cada um cuida de uma camada distinta da memória do agente.
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, seu código não muda conforme o lugar físico onde o estado fica.
Use ProviderSessionState<T> em qualquer provedor customizado, porque ele garante o isolamento entre sessões sem gerenciamento manual de concorrência. Também mantenha chaves de estado únicas, para que dois provedores de contexto nunca sobrescrevam os dados um do outro.
Por fim, a capacidade de serializar e restaurar sessões completas é a base para agentes resilientes a falhas e reinicializações. Para uma visão mais ampla do framework, leia também uma visão geral do Agent Framework.
A documentação oficial sobre Context Providers no Microsoft Learn traz ainda as integrações prontas, que poupam trabalho em cenários comuns. Com essas peças, os provedores de contexto deixam de ser um detalhe e passam a ser o centro da memória dos seus agentes.
FAQ
Qual a diferença entre ChatHistoryProvider e AIContextProvider?
O ChatHistoryProvider carrega e grava o histórico de mensagens da conversa. Já o AIContextProvider injeta contexto extra, como memórias, documentos ou instruções, e extrai informações depois da resposta.
Posso usar mais de um provedor de contexto no mesmo agente?
Sim. A propriedade AIContextProviders aceita uma lista, e o framework executa os provedores em sequência, passando a saída de um como entrada do próximo.
Por que não posso guardar estado em campos do provedor?
Porque o agente compartilha a mesma instância do provedor entre todas as sessões. Guarde o estado da sessão com ProviderSessionState<T> para evitar vazamento de dados entre usuários.
O histórico em memória sobrevive a uma reinicialização?
Não por conta própria. Você precisa serializar a sessão com SerializeSession e restaurá-la com DeserializeSessionAsync a partir de um armazenamento durável.
Como evito que o histórico estoure a janela do modelo?
Configure um redutor, como o MessageCountingChatReducer, nas opções do InMemoryChatHistoryProvider. Ele mantém apenas as mensagens mais recentes.