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.