Partilhar via


Padrões de aplicação agêntica

Existem duas abordagens gerais para construir aplicações agenticas com IA:

  • Fluxos de trabalho determinísticos — O seu código define o fluxo de controlo. Escreves a sequência de passos, ramificações, paralelismo e tratamento de erros usando construtos de programação padrão. O LLM realiza o trabalho em cada etapa, mas não controla o fluxo geral.
  • Fluxos de trabalho dirigidos por agentes (loops de agentes) — O LLM gere o fluxo de controlo. O agente decide que ferramentas chamar, em que ordem e quando a tarefa está concluída. Forneces ferramentas e instruções, mas o agente determina o caminho de execução em tempo de execução.

Ambas as abordagens beneficiam de uma execução durável e podem ser implementadas usando o modelo de programação Durable Task. Este artigo mostra como construir cada padrão usando exemplos de código.

Sugestão

Estes padrões alinham-se com os desenhos de fluxos de trabalho agentivos descritos em Building Effective Agents da Anthropic. O modelo de programação Durable Task corresponde naturalmente a estes padrões: as orquestrações definem o controle do fluxo de trabalho e são automaticamente guardadas, enquanto as atividades envolvem operações não determinísticas, como chamadas LLM, invocações de ferramentas e pedidos de API.

Escolha uma abordagem

A tabela seguinte ajuda-o a decidir quando usar cada abordagem.

Use fluxos de trabalho determinísticos quando... Usar ciclos de agentes quando...
A sequência de passos é conhecida antecipadamente. A tarefa é indefinida e os passos não podem ser previstos.
Precisas de barreiras explícitas sobre o comportamento dos agentes. Queres que o LLM decida que ferramentas usar e quando.
A conformidade ou auditabilidade requer um fluxo de controlo revisável. O agente precisa de adaptar a sua abordagem com base em resultados intermédios.
Queres combinar vários frameworks de IA num único fluxo de trabalho. Está a construir um agente conversacional com capacidades de chamada de ferramentas.

Ambas as abordagens fornecem checkpoints automáticos, políticas de novas tentativas, escalonamento distribuído e suporte humano integrado através de execução contínua.

Padrões determinísticos de fluxo de trabalho

Num fluxo de trabalho determinístico, o seu código controla o caminho de execução. O LLM é chamado como um passo dentro do fluxo de trabalho, mas não decide o que acontece a seguir. O modelo de programação Durable Task corresponde naturalmente a esta abordagem:

  • As orquestrações definem o controlo do fluxo do trabalho (sequência, ramificação, paralelismo, tratamento de erros) e são automaticamente guardadas em pontos de verificação.
  • As atividades envolvem operações não determinísticas como chamadas de LLM, invocações de ferramentas e pedidos de API. As atividades podem correr em qualquer instância de computação disponível.

Os exemplos seguintes usam Durable Functions, que corre em Funções do Azure com alojamento serverless.

Os exemplos seguintes utilizam os SDKs portáteis Durable Task, que podem correr em qualquer ambiente hospedeiro, incluindo os Azure Container Apps, Kubernetes, máquinas virtuais ou localmente.

Encadeamento imediato

O encadeamento de prompts é o padrão agente mais simples. Divides uma tarefa complexa numa série de interações sequenciais com o LLM, onde a saída de cada passo serve como entrada para o passo seguinte. Como cada chamada de atividade é automaticamente sujeita a um checkpoint, um falha a meio do pipeline não o obriga a reiniciar do zero e a voltar a consumir tokens LLM caros — a execução recomeça a partir do último passo concluído.

Também podes inserir portas de validação programática entre etapas. Por exemplo, depois de gerar um esboço, pode verificar se cumpre uma restrição de comprimento ou tema antes de o passar para a etapa de rascunho.

Este padrão corresponde diretamente ao padrão de encadeamento de funções no modelo de programação Durable Task.

Quando usar: Pipelines de geração de conteúdo, processamento de documentos em vários passos, enriquecimento sequencial de dados, fluxos de trabalho que requerem portas de validação intermédias.

[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 afirmação. Se o processo anfitrião falhar ou a VM reiniciar, a orquestração será retomada automaticamente a partir do último passo concluído 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 afirmação. Se o processo anfitrião falhar ou a VM reiniciar, a orquestração retomará automaticamente a partir da última etapa concluída em vez de recomeçar.

Roteamento

O encaminhamento utiliza uma etapa de classificação para determinar qual agente ou modelo subsequente deve gerir um pedido. A orquestração chama primeiro uma atividade de classificador, depois ramifica para o handler apropriado com base no resultado. Esta abordagem permite-lhe adaptar o prompt, modelo e conjunto de ferramentas de cada handler de forma independente — por exemplo, encaminhando perguntas de faturação para um agente especializado com acesso a APIs de pagamento, enquanto envia perguntas gerais para um modelo mais leve.

Quando usar: Triagem de apoio ao cliente, classificação de intenções para agentes especializados, seleção dinâmica de modelos 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 tem várias subtarefas independentes, pode despachá-las como chamadas de atividade paralelas e esperar por todos os resultados antes de avançar. O Durable Task Scheduler distribui estas atividades automaticamente por todas as instâncias de computação disponíveis, o que significa que adicionar mais trabalhadores reduz diretamente o tempo total de relógio de parede.

Uma variante comum é a votação multi-modelo: envia-se o mesmo prompt para vários modelos (ou para o mesmo modelo com temperaturas diferentes) em paralelo, depois agrega-se ou seleciona-se entre as respostas. Como cada ramificação paralela é verificada independentemente, uma falha transitória numa ramificação não afeta as outras.

Este padrão corresponde diretamente ao padrão fan-out/fan-in em Durable Task.

Quando usar: Análise em lote de documentos, chamadas paralelas de ferramentas, avaliação multimodelo, moderação de conteúdos com múltiplos 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

Neste padrão, um orquestrador central chama primeiro um LLM (através de uma atividade) para planear o trabalho. Com base no resultado do LLM, o orquestrador determina então quais subtarefas são necessárias. O orquestrador despacha então essas subtarefas para orquestrações especializadas de trabalhadores. A principal diferença em relação à paralelização é que o conjunto de subtarefas não é fixo no momento do design; O orquestrador determina-as dinamicamente em tempo de execução.

Este padrão utiliza suborquestrações, que são fluxos de trabalho filhos que têm pontos de verificação independentes. Cada orquestração de trabalhadores pode conter múltiplos passos, tentativas e paralelismo aninhado.

Quando usar: Pipelines de investigação profundos, fluxos de trabalho de agentes de codificação que modificam múltiplos ficheiros, colaboração entre múltiplos agentes onde cada agente tem um papel distinto.

[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 num ciclo de refinamento. O gerador produz a saída, o avaliador avalia-a com base nos critérios de qualidade e fornece feedback, e o ciclo repete-se até que a saída passe ou seja atingida uma contagem máxima de iterações. Como cada iteração do ciclo é registada, uma falha após três iterações bem-sucedidas de refinamento não perderá esse progresso.

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

Quando usar: Geração de código com revisão automática, tradução literária, refinamento iterativo de conteúdos, tarefas complexas de pesquisa que requerem múltiplas rondas 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
    }
}

Ciclos de agente

Numa implementação típica de um agente de IA, um LLM é invocado em um loop, chamando ferramentas e tomando decisões até que a tarefa esteja concluída ou uma condição de paragem seja atingida. Ao contrário dos fluxos de trabalho determinísticos, o caminho de execução não é pré-definido. O agente determina o que fazer em cada passo com base nos resultados dos passos anteriores.

Os ciclos de agentes são bem adequados para tarefas onde o número ou a ordem dos passos não podem ser previstos. Exemplos comuns incluem agentes de codificação abertos, investigação autónoma e bots conversacionais com capacidades de chamada de ferramentas.

Existem duas abordagens recomendadas para implementar ciclos de agentes com o modelo de programação Durable Task:

Abordagem Descrição Quando utilizar
Baseado em orquestração Escreve o loop agente como uma orquestração duradoura. As chamadas de ferramenta são implementadas como atividades, e a entrada humana utiliza eventos externos. A orquestração controla a estrutura do loop enquanto o LLM controla as decisões dentro dessa estrutura. Necessitas de controlo minucioso sobre o loop, políticas de retentativa por ferramenta, balanceamento distribuído de carga das chamadas de ferramenta, ou a capacidade de depurar o loop no teu ambiente de desenvolvimento integrado (IDE) com pontos de interrupção.
Baseado em entidades Cada instância de agente é uma entidade duradoura. A framework do agente gere o ciclo internamente, e a entidade fornece persistência duradoura no estado e na sessão. Estás a usar um agent framework (como Microsoft Agent Framework) que já implementa o agente loop e queres adicionar durabilidade com alterações mínimas ao código.

Loops agentes baseados em orquestração

Um ciclo de agentes baseado em orquestração combina várias capacidades de Tarefas Duráveis: orquestrações eternas (continue-as-new) para manter a memória limitada, fan-out/fan-in para execução paralela de ferramentas, e eventos externos para interações com humanos no loop. Cada iteração do ciclo:

  1. Envia o contexto atual da conversa para o LLM através de uma atividade ou entidade com estado.
  2. Recebe a resposta do LLM, que pode incluir chamadas de ferramenta.
  3. Executa quaisquer chamadas de ferramenta como atividades (distribuídas entre os cálculos disponíveis).
  4. Opcionalmente, espera por input humano usando eventos externos.
  5. Continua o ciclo com o estado atualizado, ou 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.";
    }
}

Ciclos de agentes baseados em entidades

Se estiveres a usar um framework de agentes que já implementa o seu próprio loop de agentes, podes envolvê-lo numa entidade durável para adicionar durabilidade sem reescrever a lógica do ciclo. Cada instância de entidade representa uma única sessão de agente. A entidade recebe mensagens, delega internamente para a framework do agente e mantém o estado da conversa através das interações.

A principal vantagem desta abordagem é a simplicidade: escreve o seu agente usando o seu framework preferido e acrescenta durabilidade como uma questão de alojamento, em vez de redesenhar o fluxo de controlo do agente. A entidade atua como um wrapper durável, tratando automaticamente a persistência e recuperação da sessão.

Os exemplos seguintes mostram como envolver um SDK de agente existente como uma entidade duradoura. A entidade expõe uma message operação que os clientes chamam para enviar dados do utilizador. Internamente, a entidade delega ao framework do agente, que gere o seu próprio ciclo de invocação 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 utiliza esta abordagem. Envolve agentes do Microsoft Agent Framework como entidades duradouras, fornecendo sessões persistentes, checkpointing automático e endpoints de API integrados com uma única linha de configuração.

Passos seguintes