Compartilhar via


Padrões de aplicação agentic

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 controle do fluxo de trabalho e são automaticamente registradas, 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 agente 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 fornecem ponto de verificação automático, políticas de repetição, dimensionamento distribuído e suporte humano no loop por meio da 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 de fluxo de trabalho (sequência, ramificação, paralelismo, tratamento de erros) e são automaticamente colocadas em ponto de verificação.
  • 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 portáteis SDKs de Tarefa Durable, que são executados em qualquer host de computação, incluindo Aplicativos de Contêiner do Azure, Kubernetes, máquinas virtuais ou até mesmo localmente.

Encadeamento de prompts

O encadeamento de prompt é o padrão agente 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 atividade chamada é automaticamente checada, uma falha no meio do pipeline não força você a reiniciar desde o início e consumir de novo 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 é automaticamente verificado em cada await instrução. 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 recomeçar.

[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 é automaticamente verificado em cada await instrução. Se o processo hospedeiro falhar ou a VM for reciclada, a orquestração será retomada automaticamente da última etapa concluída, em vez de começar 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 chama uma atividade de classificador primeiro e, em seguida, 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;
    }
}

Orquestradores-trabalhadores

Nesse padrão, um orquestrador central inicialmente chama uma LLM (por meio de uma atividade) para planejar o trabalho. Com base na saída do LLM, o orquestrador determina quais subtarefas são 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 subfluxos de trabalho com pontos de verificação realizados de forma independente. Cada orquestração de trabalho pode conter várias etapas, novas tentativas 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 é registrada em um ponto de verificação de progresso, uma falha após três rodadas bem-sucedidas 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é que a tarefa seja concluída ou uma condição de parada seja atingida. 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 para tarefas em que o número ou a ordem das etapas não podem ser previstas. 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 e 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 persistência de estado duradouro e 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 combina várias funcionalidades de Tarefa Durável: orquestrações eternas (continue-as-new) para manter a memória controlada, fan-out/fan-in para execução simultânea de ferramenta e eventos externos para interações com humanos no circuito. Cada iteração do loop:

  1. Envia o contexto de conversa atual para o LLM por meio de uma atividade ou entidade com estado.
  2. Recebe a resposta da LLM, que pode incluir chamadas de ferramenta.
  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á implementa seu próprio loop de agente, poderá encapsule-o 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 ao framework do agente e mantém o estado da conversa ao longo das 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 encapsulador robusto, 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 message operação que clientes invocam para enviar 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. Ele encapsula agentes do Microsoft Agent Framework como entidades duráveis, fornecendo sessões persistentes, checkpointing automático e endpoints de API incorporados com uma única linha de configuração.

Próximas Etapas