Padrões de aplicativos com agentes

Há duas abordagens gerais para criar aplicativos agente com IA:

  • Fluxos de trabalho determinísticos — seu código define o fluxo de controle. Você escreve a sequência de etapas, ramificação, paralelismo e tratamento de erros usando constructos de programação padrão. O LLM executa o trabalho dentro de cada etapa, mas não controla o fluxo geral.
  • Fluxos de trabalho direcionados por agentes (loops de agente) – o LLM conduz o fluxo de controle. O agente decide quais ferramentas chamar, em que ordem e quando a tarefa é concluída. Você fornece ferramentas e instruções, mas o agente determina o caminho de execução em runtime.

Ambas as abordagens se beneficiam da execução durável e podem ser implementadas usando o modelo de programação Da Tarefa Durável. Este artigo mostra como criar cada padrão usando exemplos de código.

Dica

Esses padrões se alinham com os designs de fluxo de trabalho orientado a agentes descritos no Building Effective Agents da Anthropic. O modelo de programação Tarefa Durável é mapeado naturalmente para esses padrões: as orquestrações definem o fluxo de controle do fluxo de trabalho e são automaticamente marcadas, enquanto as atividades encapsulam operações não determinísticas, como chamadas LLM, invocações de ferramentas e solicitações de API.

Escolher uma abordagem

A tabela a seguir ajuda você a decidir quando usar cada abordagem.

Use fluxos de trabalho determinísticos quando... Use loops de agentes quando...
A sequência de etapas é conhecida com antecedência. A tarefa é aberta e as etapas não podem ser previstas.
Você precisa de guardrails explícitos para o comportamento do agente. Você deseja que a LLM decida quais ferramentas usar e quando.
A conformidade ou a auditoria exigem um fluxo de controle revisível. O agente precisa adaptar sua abordagem com base em resultados intermediários.
Você deseja combinar várias estruturas de IA em um único fluxo de trabalho. Você está criando um agente conversacional com recursos de chamada de ferramentas.

Ambas as abordagens fornecerem ponto de verificação automático, políticas de repetição, escala distribuída e suporte human-in-the-loop usando a execução durável.

Padrões de fluxo de trabalho determinísticos

Em um fluxo de trabalho determinístico, seu código controla o caminho de execução. O LLM é chamado como uma etapa dentro do fluxo de trabalho, mas não decide o que acontece a seguir. O modelo de programação de Tarefa Durável é naturalmente mapeado para essa abordagem:

  • As orquestrações definem o fluxo de controle do fluxo de trabalho (sequência, ramificação, paralelismo, tratamento de erros) e são automaticamente marcadas.
  • As atividades encapsulam operações não determinísticas, como chamadas LLM, invocações de ferramentas e solicitações de API. As atividades podem ser executadas em qualquer instância de computação disponível.

Os exemplos a seguir usam Durable Functions, que é executado em Azure Functions com hospedagem sem servidor.

Os exemplos a seguir usam os SDKs de Tarefa Durável portáteis, que são executados em qualquer host de computação, incluindo Aplicativos de Contêiner do Azure, Kubernetes, máquinas virtuais ou localmente.

Encadeamento de prompts

O encadeamento de prompts é o padrão de agentes mais simples. Você divide uma tarefa complexa em uma série de interações sequenciais de LLM, em que a saída de cada etapa se alimenta da entrada da próxima etapa. Como cada chamada de atividade é automaticamente verificada, uma falha no meio do pipeline não força você a reiniciar desde o início e consumir novamente tokens LLM caros — a execução é retomada da última etapa concluída.

Você também pode inserir portões de validação programática entre as etapas. Por exemplo, depois de gerar uma estrutura de tópicos, você pode verificar se ela atende a uma restrição de comprimento ou tópico antes de passá-la para a etapa de redação.

Esse padrão mapeia diretamente para o padrão de encadeamento de funções no modelo de programação de Tarefas Duráveis.

Quando usar: Pipelines de geração de conteúdo, processamento de documentos em várias etapas, enriquecimento sequencial de dados, fluxos de trabalho que exigem portões de validação intermediários.

[Function(nameof(PromptChainingOrchestration))]
public async Task<string> PromptChainingOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var topic = context.GetInput<string>();

    // Step 1: Generate research outline
    string outline = await context.CallActivityAsync<string>(
        nameof(GenerateOutlineAgent), topic);

    // Step 2: Write first draft from outline
    string draft = await context.CallActivityAsync<string>(
        nameof(WriteDraftAgent), outline);

    // Step 3: Refine and polish the draft
    string finalContent = await context.CallActivityAsync<string>(
        nameof(RefineDraftAgent), draft);

    return finalContent;
}

Observação

O estado da orquestração é verificado automaticamente em cada instrução await. Se o processo de host falhar ou a VM for reciclada, a orquestração será retomada automaticamente da última etapa concluída em vez de precisar partir do zero.

[DurableTask]
public class PromptChainingOrchestration : TaskOrchestrator<string, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, string topic)
    {
        // Step 1: Generate research outline
        string outline = await context.CallActivityAsync<string>(
            nameof(GenerateOutlineAgent), topic);

        // Step 2: Write first draft from outline
        string draft = await context.CallActivityAsync<string>(
            nameof(WriteDraftAgent), outline);

        // Step 3: Refine and polish the draft
        string finalContent = await context.CallActivityAsync<string>(
            nameof(RefineDraftAgent), draft);

        return finalContent;
    }
}

Observação

O estado da orquestração é verificado automaticamente em cada instrução await. Se o processo de host falhar ou a VM for reciclada, a orquestração será retomada automaticamente da última etapa concluída em vez de precisar partir do zero.

Routing

O roteamento usa uma etapa de classificação para determinar qual agente ou modelo downstream deve lidar com uma solicitação. A orquestração primeiro chama uma atividade de classificador e, depois, ramifica para o manipulador apropriado com base no resultado. Essa abordagem permite que você adapte o prompt, o modelo e o conjunto de ferramentas de cada manipulador de forma independente, por exemplo, direcionando perguntas de cobrança para um agente especializado com acesso a APIs de pagamento ao enviar perguntas gerais para um modelo mais leve.

Quando usar: Triagem de suporte ao cliente, classificação de intenção para agentes especializados, seleção de modelo dinâmico com base na complexidade da tarefa.

[Function(nameof(RoutingOrchestration))]
public async Task<string> RoutingOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<SupportRequest>();

    // Classify the request type
    string category = await context.CallActivityAsync<string>(
        nameof(ClassifyRequestAgent), request.Message);

    // Route to the appropriate specialized agent
    return category switch
    {
        "billing" => await context.CallActivityAsync<string>(
            nameof(BillingAgent), request),
        "technical" => await context.CallActivityAsync<string>(
            nameof(TechnicalSupportAgent), request),
        "general" => await context.CallActivityAsync<string>(
            nameof(GeneralInquiryAgent), request),
        _ => await context.CallActivityAsync<string>(
            nameof(GeneralInquiryAgent), request),
    };
}
[DurableTask]
public class RoutingOrchestration : TaskOrchestrator<SupportRequest, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, SupportRequest request)
    {
        // Classify the request type
        string category = await context.CallActivityAsync<string>(
            nameof(ClassifyRequestAgent), request.Message);

        // Route to the appropriate specialized agent
        return category switch
        {
            "billing" => await context.CallActivityAsync<string>(
                nameof(BillingAgent), request),
            "technical" => await context.CallActivityAsync<string>(
                nameof(TechnicalSupportAgent), request),
            _ => await context.CallActivityAsync<string>(
                nameof(GeneralInquiryAgent), request),
        };
    }
}

Paralelização

Quando você tiver várias subtarefas independentes, poderá expedi-las como chamadas de atividade paralela e aguardar todos os resultados antes de prosseguir. O Agendador de Tarefas Duráveis distribui essas atividades em todas as instâncias de computação disponíveis automaticamente, o que significa que adicionar mais trabalhadores reduz diretamente o tempo total do relógio de parede.

Uma variante comum é a votação de vários modelos: você envia o mesmo prompt para vários modelos (ou o mesmo modelo com temperaturas diferentes) em paralelo e, em seguida, agrega ou seleciona entre as respostas. Como cada ramo paralelo é verificado de forma independente, uma falha transitória em um ramo não afeta os outros.

Esse padrão se alinha diretamente com o padrão fan-out/fan-in no Durable Task.

Quando usar: Análise em lote de documentos, chamadas de ferramenta paralela, avaliação de vários modelos, moderação de conteúdo com vários revisores.

[Function(nameof(ParallelResearchOrchestration))]
public async Task<string> ParallelResearchOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<ResearchRequest>();

    // Fan-out: research multiple subtopics in parallel
    var researchTasks = request.Subtopics
        .Select(subtopic => context.CallActivityAsync<string>(
            nameof(ResearchSubtopicAgent), subtopic))
        .ToList();
    string[] researchResults = await Task.WhenAll(researchTasks);

    // Aggregate: synthesize all research into a single summary
    string summary = await context.CallActivityAsync<string>(
        nameof(SynthesizeAgent),
        new { request.Topic, Research = researchResults });

    return summary;
}
[DurableTask]
public class ParallelResearchOrchestration : TaskOrchestrator<ResearchRequest, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, ResearchRequest request)
    {
        // Fan-out: research multiple subtopics in parallel
        var researchTasks = request.Subtopics
            .Select(subtopic => context.CallActivityAsync<string>(
                nameof(ResearchSubtopicAgent), subtopic))
            .ToList();
        string[] researchResults = await Task.WhenAll(researchTasks);

        // Aggregate: synthesize all research into a single summary
        string summary = await context.CallActivityAsync<string>(
            nameof(SynthesizeAgent),
            new { request.Topic, Research = researchResults });

        return summary;
    }
}

Orquestrador-trabalhadores

Nesse padrão, um orquestrador central primeiro chama um LLM (via atividade) para planejar o trabalho. Com base na saída do LLM, o orquestrador determina as subtarefas necessárias. O orquestrador então envia essas subtarefas para orquestrações de trabalho especializadas. A principal diferença da paralelização é que o conjunto de subtarefas não é corrigido em tempo de design; o orquestrador determina-os dinamicamente em runtime.

Esse padrão usa sub-orquestrações, que são fluxos de trabalho secundários marcados de forma independente. Cada orquestração de trabalho pode conter várias etapas, repetições e paralelismo aninhado.

Quando usar: Pipelines de pesquisa aprofundada, fluxos de trabalho de agente de codificação que alteram vários arquivos, colaboração entre múltiplos agentes em que cada agente tem uma função distinta.

[Function(nameof(OrchestratorWorkersOrchestration))]
public async Task<string> OrchestratorWorkersOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<ResearchRequest>();

    // Central orchestrator: determine what research is needed
    string[] subtasks = await context.CallActivityAsync<string[]>(
        nameof(PlanResearchAgent), request.Topic);

    // Delegate to worker orchestrations in parallel
    var workerTasks = subtasks
        .Select(subtask => context.CallSubOrchestratorAsync<string>(
            nameof(ResearchWorkerOrchestration), subtask))
        .ToList();
    string[] results = await Task.WhenAll(workerTasks);

    // Synthesize results
    string finalReport = await context.CallActivityAsync<string>(
        nameof(SynthesizeAgent),
        new { request.Topic, Research = results });

    return finalReport;
}
[DurableTask]
public class OrchestratorWorkersOrchestration : TaskOrchestrator<ResearchRequest, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, ResearchRequest request)
    {
        // Central orchestrator: determine what research is needed
        string[] subtasks = await context.CallActivityAsync<string[]>(
            nameof(PlanResearchAgent), request.Topic);

        // Delegate to worker orchestrations in parallel
        var workerTasks = subtasks
            .Select(subtask => context.CallSubOrchestratorAsync<string>(
                nameof(ResearchWorkerOrchestration), subtask))
            .ToList();
        string[] results = await Task.WhenAll(workerTasks);

        // Synthesize results
        string finalReport = await context.CallActivityAsync<string>(
            nameof(SynthesizeAgent),
            new { request.Topic, Research = results });

        return finalReport;
    }
}

Avaliador-otimizador

O padrão avaliador-otimizador emparelha um agente gerador com um agente avaliador em um loop de refinamento. O gerador produz a saída, o avaliador pontua-a em relação aos critérios de qualidade e fornece comentários e o loop se repete até que a saída passe ou uma contagem máxima de iteração seja atingida. Como cada iteração do loop é marcada, uma falha após três rodadas com êxito de refinamento não resultará em perda desse progresso.

Esse padrão é especialmente útil quando a qualidade pode ser medida programaticamente — por exemplo, validar que o código gerado compila ou que uma tradução preserva entidades nomeadas.

Quando usar: Geração de código com revisão automatizada, tradução literária, refinamento de conteúdo iterativo, tarefas de pesquisa complexas que exigem várias rodadas de análise.

[Function(nameof(EvaluatorOptimizerOrchestration))]
public async Task<string> EvaluatorOptimizerOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var request = context.GetInput<ContentRequest>();
    int maxIterations = 5;
    string content = "";
    string feedback = "";

    for (int i = 0; i < maxIterations; i++)
    {
        // Generate or refine content
        content = await context.CallActivityAsync<string>(
            nameof(GenerateContentAgent),
            new { request.Prompt, PreviousContent = content, Feedback = feedback });

        // Evaluate quality
        var evaluation = await context.CallActivityAsync<EvaluationResult>(
            nameof(EvaluateContentAgent), content);

        if (evaluation.MeetsQualityBar)
            return content;

        feedback = evaluation.Feedback;
    }

    return content; // Return best effort after max iterations
}
[DurableTask]
public class EvaluatorOptimizerOrchestration : TaskOrchestrator<ContentRequest, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, ContentRequest request)
    {
        int maxIterations = 5;
        string content = "";
        string feedback = "";

        for (int i = 0; i < maxIterations; i++)
        {
            // Generate or refine content
            content = await context.CallActivityAsync<string>(
                nameof(GenerateContentAgent),
                new { request.Prompt, PreviousContent = content, Feedback = feedback });

            // Evaluate quality
            var evaluation = await context.CallActivityAsync<EvaluationResult>(
                nameof(EvaluateContentAgent), content);

            if (evaluation.MeetsQualityBar)
                return content;

            feedback = evaluation.Feedback;
        }

        return content; // Return best effort after max iterations
    }
}

Loops do agente

Em uma implementação típica de agente de IA, uma LLM é invocada em um loop, chamando ferramentas e tomando decisões até concluir a tarefa ou atingir uma condição de parada. Ao contrário dos fluxos de trabalho determinísticos, o caminho de execução não é predefinido. O agente determina o que fazer em cada etapa com base nos resultados das etapas anteriores.

Os loops do agente são adequados a tarefas em que não é possível prever o número ou a ordem de etapas. Exemplos comuns incluem agentes de codificação abertos, pesquisas autônomas e bots de conversa com recursos de chamada de ferramentas.

Há duas abordagens recomendadas para implementar loops de agente com o modelo de programação da Tarefa Durável:

Abordagem Descrição Quando usar
Baseado em orquestração Escreva o loop do agente como uma orquestração durável. As chamadas de ferramenta são implementadas como atividades; a entrada humana usa eventos externos. A orquestração controla a estrutura do loop enquanto o LLM controla as decisões dentro dele. Você precisa de controle refinado sobre o loop, políticas de repetição por ferramenta, balanceamento de carga distribuído de chamadas de ferramenta, ou a capacidade de depurar o loop em seu IDE com pontos de interrupção.
Baseado em entidade Cada instância de agente é uma entidade durável. A estrutura do agente gerencia o loop internamente, e a entidade fornece estado durável e persistência de sessão. Você está usando uma estrutura de agente (como Microsoft Agent Framework) que já implementa o loop do agente e deseja adicionar durabilidade com alterações mínimas de código.

Loops de agente baseados em orquestração

Um loop de agente baseado em orquestração associa vários recursos de Tarefa Durável: orquestrações eternas (continue-as-new) para manter a memória controlada, fan-out/fan-in para a execução paralela de ferramentas e eventos externos para interações human-in-the-loop. Cada iteração do loop:

  1. Envia o contexto de conversa atual para o LLM via uma atividade ou entidade com estado.
  2. Recebe a resposta da LLM, que pode incluir chamadas de ferramentas.
  3. Executa quaisquer chamadas de ferramenta como atividades (distribuídas entre os recursos computacionais disponíveis).
  4. Opcionalmente, aguarda a entrada humana usando eventos externos.
  5. Continua o loop com o estado atualizado, ou se conclui quando o agente sinaliza que terminou.
[Function(nameof(AgentLoopOrchestration))]
public async Task<string> AgentLoopOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    // Get state from input (supports continue-as-new)
    var state = context.GetInput<AgentState>() ?? new AgentState();

    int maxIterations = 100;
    while (state.Iteration < maxIterations)
    {
        // Send conversation history to the LLM
        var llmResponse = await context.CallActivityAsync<LlmResponse>(
            nameof(CallLlmAgent), state.Messages);

        state.Messages.Add(llmResponse.Message);

        // If the LLM returned tool calls, execute them in parallel
        if (llmResponse.ToolCalls is { Count: > 0 })
        {
            var toolTasks = llmResponse.ToolCalls
                .Select(tc => context.CallActivityAsync<ToolResult>(
                    nameof(ExecuteTool), tc))
                .ToList();
            ToolResult[] toolResults = await Task.WhenAll(toolTasks);

            foreach (var result in toolResults)
                state.Messages.Add(result.ToMessage());
        }
        // If the LLM needs human input, wait for it
        else if (llmResponse.NeedsHumanInput)
        {
            string humanInput = await context.WaitForExternalEvent<string>("HumanInput");
            state.Messages.Add(new Message("user", humanInput));
        }
        // LLM is done
        else
        {
            return llmResponse.FinalAnswer;
        }

        state.Iteration++;

        // Periodically continue-as-new to keep the history bounded
        if (state.Iteration % 10 == 0)
        {
            context.ContinueAsNew(state);
            return null!; // Orchestration will restart with updated state
        }
    }

    return "Max iterations reached.";
}
[DurableTask]
public class AgentLoopOrchestration : TaskOrchestrator<AgentState, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, AgentState? state)
    {
        state ??= new AgentState();

        int maxIterations = 100;
        while (state.Iteration < maxIterations)
        {
            // Send conversation history to the LLM
            var llmResponse = await context.CallActivityAsync<LlmResponse>(
                nameof(CallLlmAgent), state.Messages);

            state.Messages.Add(llmResponse.Message);

            // If the LLM returned tool calls, execute them
            if (llmResponse.ToolCalls is { Count: > 0 })
            {
                var toolTasks = llmResponse.ToolCalls
                    .Select(tc => context.CallActivityAsync<ToolResult>(
                        nameof(ExecuteTool), tc))
                    .ToList();
                ToolResult[] toolResults = await Task.WhenAll(toolTasks);

                foreach (var result in toolResults)
                    state.Messages.Add(result.ToMessage());
            }
            // If the LLM needs human input, wait for it
            else if (llmResponse.NeedsHumanInput)
            {
                string humanInput = await context.WaitForExternalEvent<string>("HumanInput");
                state.Messages.Add(new Message("user", humanInput));
            }
            // LLM is done
            else
            {
                return llmResponse.FinalAnswer;
            }

            state.Iteration++;

            // Periodically continue-as-new to keep the history bounded
            if (state.Iteration % 10 == 0)
            {
                context.ContinueAsNew(state);
                return null!;
            }
        }

        return "Max iterations reached.";
    }
}

Loops de agente baseados em entidade

Se você estiver usando uma estrutura de agente que já implemente seu próprio loop de agente, poderá encapsulá-lo em uma entidade durável para adicionar durabilidade sem reescrever a lógica de loop. Cada instância de entidade representa uma única sessão de agente. A entidade recebe mensagens, delega internamente à estrutura do agente e mantém o estado da conversa em interações.

A principal vantagem dessa abordagem é a simplicidade: você escreve seu agente usando sua ferramenta preferida e integra a durabilidade como uma questão de hospedagem em vez de redesenhar o fluxo de controle do agente. A entidade atua como um wrapper durável, tratando a persistência e a recuperação da sessão automaticamente.

Os exemplos a seguir mostram como encapsular um SDK de agente existente como uma entidade durável. A entidade expõe uma operação message chamada por clientes para enviar a entrada de usuário. Internamente, a entidade delega à estrutura do agente, que gerencia seu próprio loop de chamada de ferramentas.

// Define the entity that wraps an existing agent SDK
public class ChatAgentEntity : TaskEntity<ChatAgentState>
{
    private readonly IChatClient _chatClient;

    public ChatAgentEntity(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }

    // Called by clients to send a message to the agent
    public async Task<string> Message(string userMessage)
    {
        // Add the user message to the conversation history
        State.Messages.Add(new ChatMessage(ChatRole.User, userMessage));

        // Delegate to the agent SDK for the LLM call (with tool loop)
        ChatResponse response = await _chatClient.GetResponseAsync(
            State.Messages, State.Options);

        // Persist the response in the entity state
        State.Messages.AddRange(response.Messages);

        return response.Text;
    }

    // Azure Functions entry point for the entity
    [Function(nameof(ChatAgentEntity))]
    public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
    {
        return dispatcher.DispatchAsync<ChatAgentEntity>();
    }
}
// Define the entity that wraps an existing agent SDK
[DurableTask(Name = "ChatAgent")]
public class ChatAgentEntity : TaskEntity<ChatAgentState>
{
    private readonly IChatClient _chatClient;

    public ChatAgentEntity(IChatClient chatClient)
    {
        _chatClient = chatClient;
    }

    // Called by clients to send a message to the agent
    public async Task<string> Message(string userMessage)
    {
        // Add the user message to the conversation history
        State.Messages.Add(new ChatMessage(ChatRole.User, userMessage));

        // Delegate to the agent SDK for the LLM call (with tool loop)
        ChatResponse response = await _chatClient.GetResponseAsync(
            State.Messages, State.Options);

        // Persist the response in the entity state
        State.Messages.AddRange(response.Messages);

        return response.Text;
    }
}

A extensão Durable Task para Microsoft Agent Framework usa essa abordagem. Ela encapsula agentes do Microsoft Agent Framework como entidades duráveis, fornecendo sessões persistentes, marcação automática e pontos de extremidade de API internos com uma única linha de configuração.

Próximas Etapas