Conversas multi-turnos no Microsoft Agent Framework
As conversas multi-turnos permitem que um agente do Microsoft Agent Framework (MAF) mantenha o contexto entre interações sucessivas com o usuário. Em vez de tratar cada mensagem como um pedido isolado, o agente lembra o que foi dito antes e usa essa informação para responder melhor.
Esse recurso é fundamental quando uma única mensagem não basta para concluir uma tarefa. Assistentes de suporte técnico, fluxos de onboarding e agentes de diagnóstico, por exemplo, dependem de várias trocas até chegar a uma resposta útil.
Neste artigo, você vai entender como o MAF guarda o histórico, como montar um loop de conversa em C#, como persistir a sessão e como evitar que o contexto estoure o limite de tokens do modelo.
O que são conversas multi-turnos
Uma conversa multi-turno é aquela em que o agente mantém o histórico das mensagens anteriores. Com isso, ele formula respostas mais precisas e coerentes a cada nova pergunta.
Em interações isoladas, o modelo processa cada prompt de forma independente. Por outro lado, nas conversas multi-turnos o agente "lembra" o que foi dito, e o diálogo fica mais natural e eficiente.
Vale lembrar que os modelos de linguagem não têm memória própria entre chamadas. Na prática, a "memória" é o histórico que a aplicação reenvia ao modelo junto com cada nova mensagem.
Por isso, quem cuida do contexto é o framework, e não o modelo. É justamente aí que o Microsoft Agent Framework entra, oferecendo abstrações prontas para guardar, restaurar e reduzir esse histórico.
Se você ainda não conhece o framework, vale ler antes a visão geral do Microsoft Agent Framework aqui no blog. Ela explica a origem do projeto e os conceitos de agente e workflow.
Do ChatHistory ao AgentSession
Quem vem do Semantic Kernel conhece a classe ChatHistory. Nela, o desenvolvedor acumulava objetos ChatMessageContent com os papéis User, Assistant e System, e reenviava a lista inteira ao modelo a cada turno.
Nesse modelo antigo, a aplicação precisava chamar AddAssistantMessage depois de cada resposta. Sem esse passo, o histórico crescia apenas pelo lado do usuário, e o modelo perdia a coerência entre os turnos.
O Microsoft Agent Framework é o sucessor do Semantic Kernel e do AutoGen, e ele simplifica esse fluxo. Agora, o estado da conversa mora em um AgentSession, e o próprio agente registra as mensagens do usuário e as respostas a cada execução.
Segundo a documentação oficial, o AgentSession é o contêiner de estado da conversa usado entre as execuções do agente. Ele pode guardar o histórico local, uma referência a um histórico armazenado no serviço, memórias e qualquer outro estado que o agente precise manter.
Como o MAF gerencia conversas multi-turnos
No MAF, as conversas multi-turnos seguem um fluxo de quatro passos. Primeiro, você cria uma sessão com CreateSessionAsync().
Em seguida, você passa essa mesma sessão para cada chamada de RunAsync(...). Depois, quando precisar retomar a conversa, você restaura a sessão a partir de um estado serializado com DeserializeSessionAsync(...).
Por fim, alguns agentes aceitam continuar uma conversa a partir de um identificador do serviço. Um ChatClientAgent, por exemplo, aceita CreateSessionAsync(conversationId).
O exemplo mínimo mostra bem a ideia:
AgentSession session = await agent.CreateSessionAsync();
// Primeiro turno: o usuário se apresenta
var first = await agent.RunAsync("My name is Alice.", session);
// Segundo turno: o agente usa o contexto do turno anterior
var second = await agent.RunAsync("What is my name?", session);
Como as duas chamadas compartilham a mesma sessão, o agente responde corretamente que o nome é Alice. Se você omitir a sessão, cada chamada vira uma interação isolada e o contexto se perde.
Histórico local e histórico gerenciado pelo serviço
O MAF trabalha com dois modos de armazenamento. No primeiro, o próprio agente guarda as mensagens em memória por meio do InMemoryChatHistoryProvider, que é o padrão para agentes baseados em Chat Completions.
No segundo modo, o serviço do modelo guarda o histórico. É o caso da Responses API da OpenAI com armazenamento ativo, dos agentes do Microsoft Foundry e dos agentes do Copilot Studio.
Nesse caso, a sessão carrega apenas um identificador remoto, como um conversationId. Assim, a aplicação não precisa reenviar todas as mensagens, porque o serviço já conhece o contexto.
Implementando conversas multi-turnos em C#
O ponto de entrada é um AIAgent. A forma mais direta de criá-lo é a partir de um cliente de chat, usando o método de extensão AsAIAgent.
O trecho abaixo monta um loop interativo de conversas multi-turnos com Azure OpenAI:
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = builder.Configuration["AzureOpenAI:Endpoint"]!;
var apiKey = builder.Configuration["AzureOpenAI:ApiKey"]!;
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureKeyCredential(apiKey))
.GetChatClient("gpt-4o")
.AsAIAgent(
instructions: "You are a technical assistant specialized in C# and .NET.",
name: "DotNetAssistant");
// A sessão guarda o contexto de todos os turnos
AgentSession session = await agent.CreateSessionAsync();
while (true)
{
Console.Write("User: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) break;
var response = await agent.RunAsync(input, session);
Console.WriteLine($"Agent: {response}");
}
Repare que o código não adiciona mensagens manualmente a nenhuma lista. O agente registra a pergunta e a resposta na sessão a cada chamada de RunAsync.
Dessa forma, some o erro clássico de esquecer de gravar a resposta do assistente. Afinal, esse esquecimento quebrava a coerência das conversas multi-turnos no modelo baseado em ChatHistory.
Respostas em streaming
Em interfaces de chat, mostrar a resposta enquanto ela é gerada melhora muito a experiência. Para isso, o MAF oferece o método RunStreamingAsync, que também recebe a sessão.
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(input, session))
{
// Cada atualização traz um pedaço do texto gerado
Console.Write(update.Text);
}
Mesmo em streaming, o agente consolida a resposta e a grava na sessão ao final. Portanto, o próximo turno enxerga o contexto completo.
Lendo o histórico guardado na sessão
Às vezes você precisa inspecionar as mensagens acumuladas, por exemplo para exibir o histórico na tela. Quando o armazenamento é local, basta recuperar o provedor de histórico do agente:
var provider = agent.GetService<InMemoryChatHistoryProvider>();
// Lista de ChatMessage acumulada na sessão atual
List<ChatMessage>? messages = provider?.GetMessages(session);
Já com histórico gerenciado pelo serviço, a sessão guarda apenas o identificador remoto. Nesse caso, você pode convertê-la para ChatClientAgentSession e ler a propriedade ConversationId.
O histórico como contexto acumulado
O histórico funciona como uma janela de contexto enviada ao modelo a cada requisição. Isso significa que, à medida que a conversa cresce, o volume de tokens transmitidos também aumenta.
Em algum momento, esse volume pode atingir o limite de contexto do modelo. Além disso, cada token extra aumenta o custo da chamada e a latência da resposta.
Por isso, conversas multi-turnos longas pedem alguma estratégia de redução. A ideia é preservar apenas as mensagens mais relevantes para o turno atual, sem perder fatos importantes ditos no início.
Mais adiante, você vai ver como o MAF resolve isso com compactação. Antes, porém, vale entender como as ferramentas entram no diálogo.
Ferramentas dentro de conversas multi-turnos
No Semantic Kernel, as funções expostas ao modelo usavam o atributo KernelFunction. Já no MAF, qualquer método .NET pode virar uma ferramenta por meio do AIFunctionFactory.Create, e o atributo Description explica ao modelo para que ela serve.
using System.ComponentModel;
using Microsoft.Extensions.AI;
[Description("Searches technical documentation for a given term.")]
static async Task<string> SearchDocumentation(
[Description("The term to search for.")] string term)
{
// Implementação da busca (banco, índice vetorial, API etc.)
await Task.CompletedTask;
return $"Documentation found for: {term}";
}
AIAgent agent = chatClient.AsAIAgent(
instructions: "You are a technical assistant specialized in .NET.",
tools: [AIFunctionFactory.Create(SearchDocumentation)]);
Com a sessão compartilhada, o agente decide quando acionar a ferramenta com base no histórico acumulado. Por exemplo, se o usuário citou um tema três turnos atrás, o agente pode buscar a documentação desse tema sem que o usuário repita a informação.
As chamadas de ferramenta e os resultados também ficam no histórico. Assim, o agente sabe o que já consultou e evita buscas repetidas ao longo das conversas multi-turnos.
Para se aprofundar nesse tema, veja o artigo sobre como utilizar tools no Microsoft Agent Framework.
Conversas com vários agentes no group chat
No Semantic Kernel, a classe AgentGroupChat coordenava vários agentes em uma mesma conversa. Ela usava estratégias de seleção, como a sequencial, e estratégias de término, como um número máximo de iterações.
No MAF, esse papel cabe à orquestração de group chat, construída com workflows. O AgentWorkflowBuilder cria o grupo e um gerenciador define quem fala em cada rodada.
// Gerenciador round-robin com no máximo 5 rodadas
var workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents =>
new RoundRobinGroupChatManager(agents)
{
MaximumIterationCount = 5
})
.AddParticipants(writer, reviewer)
.Build();
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Review the code below and suggest improvements.")
};
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
if (evt is AgentResponseUpdateEvent update)
{
Console.Write(update.Update.Text);
}
else if (evt is WorkflowOutputEvent)
{
break;
}
}
Um detalhe importante é que os agentes do grupo não compartilham a mesma instância de sessão. Em vez disso, o orquestrador sincroniza a sessão de cada agente com o histórico antes de cada turno e transmite cada resposta para os demais participantes.
Segundo a documentação, isso acontece porque tipos diferentes de agente podem implementar o AgentSession de formas diferentes. Assim, compartilhar a mesma instância poderia gerar inconsistências no contexto.
O artigo sobre como orquestrar agentes com workflows mostra outras orquestrações, como sequencial, concorrente e handoff.
Persistência de conversas multi-turnos
Em aplicações reais, o histórico precisa sobreviver entre requisições HTTP e reinicializações do serviço. Portanto, as conversas multi-turnos exigem uma camada de persistência.
O MAF não impõe um banco específico. Você pode usar banco relacional, Redis, Cosmos DB ou qualquer mecanismo que guarde JSON, porque a sessão é serializável.
O padrão comum é serializar a sessão e associá-la a um identificador:
// Serialização: a sessão vira um JsonElement
JsonElement serialized = await agent.SerializeSessionAsync(session);
await cache.SetStringAsync(sessionId, serialized.GetRawText());
// Restauração: o mesmo agente reconstrói a sessão
var stored = await cache.GetStringAsync(sessionId);
var element = JsonDocument.Parse(stored!).RootElement;
AgentSession resumed = await agent.DeserializeSessionAsync(element);
Essa abordagem garante a continuidade da conversa mesmo após a reinicialização do serviço. Contudo, a documentação alerta que a sessão é específica do agente e do provedor, então restaurar em outro agente pode gerar contexto inválido.
Cuidados de segurança
Uma sessão serializada pode conter o conteúdo da conversa, identificadores e dados pessoais. Por isso, trate esse JSON como dado sensível, com controle de acesso e criptografia em repouso.
Além disso, restaurar uma sessão de uma fonte não confiável equivale a aceitar entrada não confiável. Um armazenamento comprometido poderia alterar papéis de mensagens ou injetar conteúdo para manipular o modelo.
Em aplicações com vários usuários, nunca use o identificador da conversa como prova de autorização. Em vez disso, guarde o mapeamento no servidor e confira o usuário ou o tenant antes de retomar a sessão.
Nos cenários hospedados, a classe AgentSessionStore carrega e salva sessões por identificador. O decorador IsolationKeyScopedAgentSessionStore, por sua vez, acrescenta uma chave de isolamento por usuário.
O tema de memória e armazenamento rende bastante, e o artigo sobre memória, armazenamento e provedores de contexto no MAF aprofunda essas opções.
Controle de tokens e limites de contexto
Cada modelo tem um limite máximo de tokens por requisição, e esse limite inclui o histórico completo mais a nova mensagem. No GPT-4o, por exemplo, a janela de contexto chega a 128 mil tokens, o que comporta históricos longos sem truncamento imediato.
Ainda assim, convém monitorar o tamanho do histórico para manter custo e latência previsíveis. Para isso, o MAF oferece um framework de compactação, ainda experimental no C#, que exige #pragma warning disable MAAI001.
A compactação só vale para agentes que guardam o histórico em memória. Quando o serviço gerencia o contexto, como no Foundry ou na Responses API com armazenamento, ele mesmo cuida dessa tarefa.
Estratégias de compactação
O framework traz estratégias prontas, da mais suave à mais agressiva. A ToolResultCompactionStrategy resume resultados antigos de ferramentas, enquanto a SummarizationCompactionStrategy usa um modelo para sumarizar blocos antigos da conversa.
Já a SlidingWindowCompactionStrategy mantém apenas os últimos turnos do usuário. Por fim, a TruncationCompactionStrategy descarta os grupos mais antigos como último recurso.
Você pode combinar todas em um pipeline e registrá-lo no agente:
#pragma warning disable MAAI001
PipelineCompactionStrategy pipeline = new(
new ToolResultCompactionStrategy(CompactionTriggers.TokensExceed(0x200)),
new SummarizationCompactionStrategy(summarizerChatClient, CompactionTriggers.TokensExceed(0x500)),
new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(4)),
new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(0x8000)));
AIAgent agent = agentChatClient
.AsBuilder()
.UseAIContextProviders(new CompactionProvider(pipeline))
.BuildAIAgent(new ChatClientAgentOptions
{
Name = "SupportAssistant",
ChatOptions = new() { Instructions = "You are a helpful support assistant." }
});
Nesse arranjo, cada estratégia avalia o próprio gatilho em ordem. Além disso, todas preservam as mensagens de sistema e mantêm juntos uma chamada de ferramenta e o seu resultado, já que separá-los causaria erro na API do modelo.
A documentação recomenda um modelo menor e mais barato, como o gpt-4o-mini, para o cliente de sumarização. Dessa forma, o custo extra da compactação fica baixo.
Boas práticas para conversas multi-turnos
Algumas práticas ajudam a manter as conversas multi-turnos estáveis em produção:
- Crie uma sessão por conversa e reutilize a mesma instância em todos os turnos.
- Persista a sessão serializada e restaure com o mesmo agente que a criou.
- Defina uma estratégia de compactação antes que o histórico vire problema.
- Trate sessões serializadas como dado sensível e isole por usuário.
- Prefira instruções curtas no sistema, porque elas acompanham todas as requisições.
Também vale medir o consumo real de tokens. Afinal, o artigo sobre observabilidade de agentes e workflows mostra como coletar essas métricas com OpenTelemetry.
Conclusão
O suporte a conversas multi-turnos no Microsoft Agent Framework é direto e composível. O AgentSession concentra o contexto, e o agente cuida sozinho de registrar perguntas e respostas a cada turno.
Somando ferramentas, orquestração em group chat, persistência externa e compactação, você constrói assistentes robustos e prontos para produção em aplicações .NET. Em resumo, basta criar a sessão, reutilizá-la e decidir com antecedência onde guardá-la e como reduzi-la.
Para os próximos passos, a documentação oficial sobre sessões no Agent Framework detalha todos os tipos de sessão disponíveis.
FAQ
O que é o AgentSession no Microsoft Agent Framework?
É o contêiner de estado da conversa usado entre as execuções do agente. Ele guarda o histórico ou uma referência a ele, além de outros estados que o agente precise manter.
Preciso adicionar a resposta do agente ao histórico manualmente?
Não. Ao passar a sessão para RunAsync ou RunStreamingAsync, o próprio agente registra a pergunta e a resposta.
Posso reutilizar uma sessão em outro agente?
A documentação desaconselha, porque cada agente pode anexar comportamentos próprios à sessão. Restaurar em outro agente ou provedor pode gerar contexto inválido.
A compactação funciona com agentes do Foundry?
Não. A compactação vale apenas para agentes que guardam o histórico em memória, já que o serviço gerencia o contexto nos agentes do Foundry.
Como persistir uma conversa entre reinicializações?
Serialize a sessão com SerializeSessionAsync, salve o JSON em um armazenamento e restaure com DeserializeSessionAsync no mesmo agente.