Compartir a través de


Patrones de aplicación agentiva

Hay dos enfoques generales para desarrollar aplicaciones de agentes con IA.

  • Flujos de trabajo deterministas : el código define el flujo de control. Puede escribir la secuencia de pasos, bifurcación, paralelismo y control de errores mediante construcciones de programación estándar. El LLM realiza trabajo dentro de cada paso, pero no controla el flujo general.
  • Flujos de trabajo dirigidos por agente (bucles de agente): LLM controla el flujo de control. El agente decide qué herramientas llamar, en qué orden y cuándo se completa la tarea. Proporciona herramientas e instrucciones, pero el agente determina la ruta de ejecución en tiempo de ejecución.

Ambos enfoques se benefician de la ejecución duradera y se pueden implementar mediante el modelo de programación durable Task. En este artículo se muestra cómo compilar cada patrón mediante ejemplos de código.

Sugerencia

Estos patrones se alinean con los diseños de flujo de trabajo agente descritos en Building Effective Agents de Anthropic. El modelo de programación Durable Task se asigna naturalmente a estos patrones: las orquestaciones definen el control del flujo de trabajo y se guardan automáticamente, mientras que las actividades encapsulan operaciones no determinísticas como las llamadas LLM, invocaciones de herramientas y solicitudes de API.

Elección de un enfoque

La tabla siguiente le ayuda a decidir cuándo usar cada enfoque.

Use flujos de trabajo deterministas cuando... Use los bucles de agente cuando...
La secuencia de pasos se conoce con antelación. La tarea está abierta y no se pueden predecir los pasos.
Necesita límites de protección explícitos sobre el comportamiento del agente. Quiere que el LLM decida qué herramientas usar y cuándo.
El cumplimiento o la auditoría requieren un flujo de control revisable. El agente debe adaptar su enfoque en función de los resultados intermedios.
Quiere combinar varios marcos de inteligencia artificial en un único flujo de trabajo. Va a crear un agente conversacional con funcionalidades de llamada a herramientas.

Ambos enfoques proporcionan puntos de comprobación automáticos, políticas de reintento, escalado distribuido y soporte humano en el bucle a través de una ejecución duradera.

Patrones de flujo de trabajo deterministas

En un flujo de trabajo determinista, el código controla la ruta de ejecución. El LLM es llamado como un paso dentro del flujo de trabajo, pero no decide lo que sucede a continuación. El modelo de programación Durable Task se asigna de forma natural a este enfoque.

  • Las orquestaciones definen el control del flujo de trabajo (secuencia, bifurcación, paralelismo, manejo de errores) y se registran automáticamente.
  • Las actividades encapsulan operaciones no deterministas, como llamadas LLM, invocaciones de herramientas y solicitudes de API. Las actividades se pueden ejecutar en cualquier instancia de proceso disponible.

En los ejemplos siguientes se usa Durable Functions, que se ejecuta en Azure Functions con hospedaje sin servidor.

En los ejemplos siguientes se usan los SDK portátiles de Durable Task, que se ejecutan en cualquier proceso host, incluidos Azure Container Apps, Kubernetes, máquinas virtuales o localmente.

Encadenamiento de mensajes

El encadenamiento de mensajes es el patrón agencial más sencillo. Se divide una tarea compleja en una serie de interacciones LLM secuenciales, donde la salida de cada paso se convierte en la entrada del siguiente paso. Dado que cada llamada de actividad se controla automáticamente, un bloqueo a mitad de la canalización no le obliga a reiniciar desde cero y volver a consumir tokens LLM costosos: la ejecución se reanuda desde el último paso completado.

También puede insertar puertas de validación mediante programación entre pasos. Por ejemplo, después de generar un esquema, puede comprobar que cumple una restricción de longitud o tema antes de pasarla al paso de borrador.

Este patrón se asigna directamente al patrón de encadenamiento de funciones en el modelo de programación Durable Task.

Cuándo usar: Canalizaciones de generación de contenido, procesamiento de documentos de varios pasos, enriquecimiento secuencial de datos, flujos de trabajo que requieren puertas de validación intermedias.

[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;
}

Nota:

El estado de la orquestación se controla automáticamente en cada await instrucción. Si el proceso de host se bloquea o la máquina virtual se reinicia, la orquestación se reanudará automáticamente desde el último paso completado en lugar de empezar de nuevo.

[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;
    }
}

Nota:

El estado de la orquestación se controla automáticamente en cada await instrucción. Si el proceso anfitrión se bloquea o la máquina virtual se recicla, la orquestación se reanudará automáticamente desde el último paso completado en lugar de comenzar de nuevo.

Enrutamiento

El enrutamiento utiliza un paso de clasificación para determinar qué agente o modelo posterior debe gestionar una solicitud. La orquestación llama primero a una actividad clasificadora y, a continuación, se bifurca en el controlador adecuado en función del resultado. Esta enfoque le permite adaptar el mensaje, modelo, y conjunto de herramientas de cada controlador de forma independiente, por ejemplo, dirigiendo las preguntas de facturación a un agente especializado con acceso a las API de pago mientras se envían preguntas generales a un modelo más ligero.

Cuándo usar: Evaluación de prioridades de atención al cliente, clasificación de intenciones a agentes especializados, selección dinámica de modelos en función de la complejidad de la tarea.

[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),
        };
    }
}

Paralelización

Cuando tenga varias subtareas independientes, puede enviarlas como llamadas de actividad paralelas y esperar todos los resultados antes de continuar. Durable Task Scheduler distribuye estas actividades automáticamente en todas las instancias de proceso disponibles, lo que significa que agregar más trabajos reduce directamente el tiempo total del reloj.

Una variante común es la votación multimodelo: se envía el mismo mensaje a varios modelos (o el mismo modelo con temperaturas diferentes) en paralelo y, a continuación, agrega o selecciona entre las respuestas. Dado que cada rama paralela está en puntos de control de forma independiente, un error transitorio en una rama no afecta a los demás.

Este patrón se asigna directamente al patrón fan-out/fan-in en Durable Task.

Cuándo usar: Análisis por lotes de documentos, llamadas a herramientas paralelas, evaluación multimodelo, moderación de contenido con varios 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;
    }
}

Trabajadores del Orquestador

En este patrón, un orquestador central llama primero a un LLM (a través de una actividad) para planificar el trabajo. En función de la salida de LLM, el orquestador determina a continuación qué subtareas se necesitan. A continuación, el orquestador envía esas subtareas a orquestaciones de trabajo especializadas. La diferencia clave de paralelización es que el conjunto de subtareas no se fija en tiempo de diseño; el orquestador los determina dinámicamente en tiempo de ejecución.

Este patrón usa suborquestaciones, que son subflujos de trabajo con puntos de control independientes. Cada orquestación de trabajo puede contener varios pasos, reintentos y paralelismo anidado.

Cuándo usar: Procesos de investigación profunda, flujos de trabajo de agentes de codificación que modifican varios archivos, colaboración entre múltiples agentes donde cada uno tiene un rol 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;
    }
}

Evaluador-optimizador

El patrón de optimizador-optimizador empareja un agente de generador con un agente de evaluador en un bucle de refinamiento. El generador produce la salida, el evaluador la puntúa según los criterios de calidad y proporciona comentarios, y el bucle se repite hasta que la salida cumpla los criterios o se alcance un número máximo de iteraciones. Dado que cada iteración del bucle se guarda en un punto de control, un bloqueo después de tres rondas exitosas de refinamiento no perderá ese progreso.

Este patrón es especialmente útil cuando la calidad se puede medir mediante programación, por ejemplo, validar que el código generado se compila o que una traducción conserva las entidades con nombre.

Cuándo usar: Generación de código con revisión automatizada, traducción literaria, refinamiento de contenido iterativo, tareas de búsqueda complejas que requieren varias rondas de análisis.

[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
    }
}

Bucles de agentes

En una implementación típica de un agente de IA, se invoca un LLM en un bucle, llamando a herramientas y tomando decisiones hasta que la tarea se completa o se alcanza una condición de parada. A diferencia de los flujos de trabajo deterministas, la trayectoria de ejecución no está predefinida. El agente determina qué hacer en cada paso en función de los resultados de los pasos anteriores.

Los bucles de agente son adecuados para las tareas en las que no se puede predecir el número o el orden de los pasos. Entre los ejemplos comunes se incluyen agentes de codificación abiertos, investigaciones autónomas y bots conversacionales con funcionalidades de llamada a herramientas.

Hay dos enfoques recomendados para implementar bucles de agentes con el modelo de programación de Durable Task:

Enfoque Descripción Cuándo se deben usar
Basado en orquestación Plantee el ciclo del agente como una orquestación duradera. Las llamadas a herramientas se implementan como actividades y la entrada del usuario se gestiona mediante eventos externos. La orquestación controla la estructura del bucle mientras LLM controla las decisiones dentro de este. Necesita un control detallado sobre el bucle, las directivas de reintento por herramienta, el balanceo de carga distribuido de las llamadas a herramientas o la capacidad de depurar el bucle en su IDE con puntos de interrupción.
Basado en entidades Cada instancia del agente es una entidad duradera. El marco del agente gestiona el bucle internamente, y la entidad ofrece persistencia tanto del estado como de la sesión de manera duradera. Usa un marco de agente (como Microsoft Agent Framework) que ya implementa el bucle del agente y quiere agregar durabilidad con cambios mínimos de código.

Bucles de agente basados en la orquestación

Un bucle de agente basado en orquestación combina varias capacidades de Durable Task: orquestaciones eternas (continue-as-new) para mantener la memoria limitada, fan-out/fan-in para la ejecución paralela de herramientas y eventos externos para interacciones humanas en el bucle. Cada iteración del bucle:

  1. Envía el contexto actual de la conversación al LLM a través de una actividad o entidad con estado.
  2. Recibe la respuesta del LLM, que puede incluir llamadas a herramientas.
  3. Ejecuta cualquier llamada de herramienta como actividades (distribuidas entre los recursos informáticos disponibles).
  4. Opcionalmente, espera la entrada humana mediante eventos externos.
  5. Continúa el bucle con el estado actualizado o se completa cuando el agente señala que ha terminado.
[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.";
    }
}

Bucles de agentes basados en entidades

Si usa un marco de agente que ya implementa su propio bucle de agente, puede encapsularlo en una entidad duradera para agregar durabilidad sin volver a escribir la lógica del bucle. Cada instancia de entidad representa una sesión de agente única. La entidad recibe mensajes, los delega internamente al marco de agentes y conserva el estado de la conversación a lo largo de las interacciones.

La principal ventaja de este enfoque es la simplicidad: escribes tu agente usando tu entorno preferido y agregas durabilidad como un asunto de hospedaje en lugar de rediseñar el flujo de control del agente. La entidad actúa como un contenedor duradero, gestionando automáticamente la persistencia y recuperación de la sesión.

En los ejemplos siguientes se muestra cómo encapsular un SDK de agente existente como una entidad duradera. La entidad expone una operación message que los clientes invocan para enviar la entrada del usuario. Internamente, la entidad se delega en el marco del agente, que administra su propio bucle de llamada a herramientas.

// 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;
    }
}

La extensión Durable Task para Microsoft Agent Framework usa este enfoque. Encapsula agentes del Microsoft Agent Framework como entidades duraderas, proporcionando sesiones persistentes, puntos de control automáticos y puntos de conexión API integrados con una sola línea de configuración.

Pasos siguientes