Modèles d’application agentiques

Il existe deux approches générales pour créer des applications agentiques avec l’IA :

  • Flux de travail déterministes : votre code définit le flux de contrôle. Vous écrivez la séquence d’étapes, de branchement, de parallélisme et de gestion des erreurs à l’aide de constructions de programmation standard. Le LLM effectue un travail à l’intérieur de chaque étape, mais ne contrôle pas le flux global.
  • Flux de travail dirigés par l’agent (boucles d’agent) : le LLM pilote le flux de contrôle. L’agent décide quels outils appeler, dans quel ordre et quand la tâche est terminée. Vous fournissez des outils et des instructions, mais l’agent détermine le chemin d’exécution au moment de l’exécution.

Les deux approches bénéficient d’une exécution durable et peuvent être implémentées à l’aide du modèle de programmation Durable Task. Cet article explique comment générer chaque modèle à l’aide d’exemples de code.

Conseil / Astuce

Ces modèles s’alignent sur les conceptions de flux de travail agentiques décrites dans les agents de construction efficaces d’Anthropic. La correspondance avec le modèle de programmation Durable Task s’aligne naturellement sur ces schémas : les orchestrations définissent le flux de contrôle du workflow et bénéficient de points de contrôle automatiques, tandis que les activités encapsulent les opérations non déterministes telles que les appels à des GML, les invocations d’outils et les requêtes API.

Choisir une approche

Le tableau suivant vous aide à décider quand utiliser chaque approche.

Utilisez des flux de travail déterministes quand... Utiliser des boucles d’agent quand...
La séquence d’étapes est connue à l’avance. La tâche est ouverte et les étapes ne peuvent pas être prédites.
Vous avez besoin de garde-fous explicites sur le comportement de l’agent. Vous souhaitez que le LLM décide quels outils utiliser et quand.
La conformité ou l’auditabilité nécessite un flux de contrôle révisable. L’agent doit adapter son approche en fonction des résultats intermédiaires.
Vous souhaitez combiner plusieurs frameworks IA dans un seul flux de travail. Vous créez un agent conversationnel avec des fonctionnalités d’appel d’outils.

Ces deux approches proposent des points de contrôle automatiques, des stratégies de nouvelle tentative, une scalabilité distribuée ainsi qu’une prise en charge de l’intervention humaine via une exécution durable.

Modèles de flux de travail déterministes

Dans un flux de travail déterministe, votre code contrôle le chemin d’exécution. Le LLM est appelé en tant qu’étape dans le flux de travail, mais ne détermine pas ce qui se passe ensuite. Le modèle de programmation Durable Task correspond naturellement à cette approche.

  • Les orchestrations définissent le flux de contrôle du flux de travail (séquence, branchement, parallélisme, gestion des erreurs) et sont automatiquement enregistrées.
  • Les activités encapsulent des opérations non déterministes telles que les appels LLM, les appels d’outils et les demandes d’API. Les activités peuvent s’exécuter sur n’importe quelle instance de calcul disponible.

Les exemples suivants utilisent Durable Functions, qui s’exécute sur Azure Functions avec un hébergement serverless.

Les exemples suivants utilisent les kits SDK de tâche portables Durable Task, qui s’exécutent sur n’importe quel calcul hôte, y compris Azure Container Apps, Kubernetes, machines virtuelles ou localement.

Chaînage de requêtes

Le chaînage d’invites est le modèle agentique le plus simple. Vous décomposez une tâche complexe en une série d'interactions LLM séquentielles, où le résultat de chaque étape alimente l'entrée de l'étape suivante. Étant donné que chaque appel d'activité crée automatiquement un point de contrôle, une panne au milieu du pipeline ne vous oblige pas à redémarrer à partir de zéro ni à consommer à nouveau les jetons LLM coûteux, mais l'exécution reprend à partir de la dernière étape terminée.

Vous pouvez également insérer des portes de validation par programmation entre les étapes. Par exemple, après avoir généré un plan, vous pouvez vérifier qu’il répond à une contrainte de longueur ou de rubrique avant de le passer à l’étape de rédaction.

Ce modèle est mappé directement au modèle de chaînage de fonctions dans le modèle de programmation Durable Task.

Quand utiliser : Pipelines de génération de contenu, traitement de documents en plusieurs étapes, enrichissement séquentiel des données, flux de travail nécessitant des portes de validation intermédiaires.

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

Note

L’état de l’orchestration est automatiquement enregistré à chaque instruction await. Si le processus hôte se bloque ou que la machine virtuelle recycle, l’orchestration reprend automatiquement à partir de la dernière étape terminée plutôt que de recommencer.

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

Note

L’état de l’orchestration est automatiquement enregistré à chaque instruction await. Si le processus hôte se bloque ou que la machine virtuelle recycle, l’orchestration reprend automatiquement à partir de la dernière étape terminée plutôt que de recommencer.

Routage

Le routage utilise une étape de classification pour déterminer quel agent ou modèle en aval doit gérer une demande. L’orchestration appelle d’abord une activité de classification, puis se dirige vers le manipulateur approprié en fonction du résultat. Cette approche vous permet de personnaliser indépendamment l’invite, le modèle et l’ensemble d’outils de chaque gestionnaire , par exemple, de diriger les questions de facturation vers un agent spécialisé avec accès aux API de paiement tout en envoyant des questions générales à un modèle plus léger.

Quand utiliser : Triage du support client, classification des intentions pour les agents spécialisés, sélection de modèles dynamiques en fonction de la complexité des tâches.

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

Parallélisation

Lorsque vous avez plusieurs tâches subordonnées indépendantes, vous pouvez les distribuer en tant qu’appels d’activité parallèles et attendre tous les résultats avant de continuer. Le Durable Task Scheduler distribue ces activités automatiquement sur toutes les instances de calcul disponibles, ce qui signifie que l’ajout de plus d'agents réduit directement le temps total d'exécution.

Une variante commune est un vote multimodèle : vous envoyez la même invite à plusieurs modèles (ou le même modèle avec des températures différentes) en parallèle, puis agréger ou sélectionner parmi les réponses. Étant donné que chaque branche parallèle est indépendamment point de contrôle, une défaillance temporaire dans une branche n’affecte pas les autres.

Cette correspondance reflète directement le modèle fan-out/fan-in de Durable Task.

Quand utiliser : Analyse par lots de documents, appels d’outils parallèles, évaluation multimodèle, modération du contenu avec plusieurs réviseurs.

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

Orchestrateurs-travailleurs

Dans ce modèle, un orchestrateur central commence par invoquer un GML (via une activité) afin de planifier le travail. En fonction de la sortie de LLM, l’orchestrateur détermine ensuite les tâches subordonnées nécessaires. L’orchestrateur répartit ensuite ces tâches secondaires vers des orchestrations de travail spécialisées. La principale différence par rapport à la parallélisation est que l’ensemble de tâches subordonnées n’est pas résolu au moment du design ; l’orchestrateur les détermine dynamiquement au moment de l’exécution.

Ce modèle repose sur des sous-orchestrations, qui sont des workflows enfants enregistrés de manière indépendante. Chaque orchestration de travail peut à son tour inclure plusieurs étapes, des mécanismes de reprise et un parallélisme imbriqué.

Quand utiliser : Pipelines de recherche approfondie, flux de travail d’agent de codage qui modifient plusieurs fichiers, collaboration multi-agent où chaque agent a un rôle distinct.

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

Évaluateur-optimiseur

Le modèle évaluateur-optimiseur associe un agent générateur à un agent évaluateur dans une boucle d’affinement. Le générateur produit une sortie, l’évaluateur la note par rapport aux critères de qualité et fournit des commentaires, et la boucle se répète jusqu’à ce que la sortie passe ou qu’un nombre maximal d’itérations soit atteint. Chaque itération de boucle étant enregistrée, une défaillance survenant après trois cycles de raffinement réussis n’entraînera pas la perte des progrès réalisés.

Ce modèle est particulièrement utile lorsque la qualité peut être mesurée de manière programmatique — par exemple, en vérifiant que le code généré compile, ou qu'une traduction préserve les entités nommées.

Quand utiliser : Génération de code avec révision automatisée, traduction littéraire, affinement de contenu itératif, tâches de recherche complexes nécessitant plusieurs rondes d’analyse.

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

Boucles d’agent

Dans une implémentation d’agent IA classique, un LLM est appelé dans une boucle, appelant des outils et prenant des décisions jusqu’à ce que la tâche soit terminée ou qu’une condition d’arrêt soit atteinte. Contrairement aux flux de travail déterministes, le chemin d’exécution n’est pas prédéfini. L’agent détermine ce qu’il faut faire à chaque étape en fonction des résultats des étapes précédentes.

Les boucles d’agent conviennent parfaitement aux tâches où le nombre ou l’ordre des étapes ne peuvent pas être prédits. Les exemples courants incluent des agents de codage ouverts, des recherches autonomes et des bots conversationnels avec des fonctionnalités d’appel d’outils.

Il existe deux approches recommandées pour implémenter des boucles d’agent avec le modèle de programmation Durable Task :

Approche Description Quand utiliser
Basé sur l’orchestration Écrivez la boucle de l’agent en tant qu’orchestration durable. Les appels d’outils sont implémentés sous forme d’activités, tandis que les entrées humaines reposent sur des événements externes. L’orchestration pilote la structure de la boucle, tandis que le GML en gouverne les décisions internes. Vous avez besoin d’un contrôle précis sur la boucle, les stratégies de nouvelle tentative par outil, l’équilibrage de charge distribué des appels d’outils ou la possibilité de déboguer la boucle dans votre IDE avec des points d’arrêt.
Basé sur une entité Chaque instance de l’agent est une entité durable. Le framework d’agent administre la boucle en interne, et l’entité garantit la persistance de l’état ainsi que de la session. Vous utilisez une infrastructure d'agent (comme Microsoft Agent Framework) qui implémente déjà la boucle de l'agent et que vous souhaitez ajouter la durabilité avec des modifications de code minimales.

Boucles d’agent basées sur l’orchestration

Une boucle d’agent fondée sur l’orchestration combine plusieurs capacités de Durable Task : des orchestrations éternelles (continue-as-new) afin de limiter l’usage mémoire, le fan-out/fan-in pour l’exécution parallèle des outils, ainsi que des événements externes pour les interactions impliquant une intervention humaine. Chaque itération de la boucle :

  1. Transmet le contexte de conversation courant au GML via une activité ou une entité avec état.
  2. Reçoit la réponse du LLM, qui peut inclure des appels d’outil.
  3. Exécute tous les appels d’outils en tant qu’activités (réparties entre les calculs disponibles).
  4. Attend éventuellement une entrée humaine à l’aide d’événements externes.
  5. Poursuit la boucle avec l’état actualisé, ou s’interrompt lorsque l’agent indique avoir terminé.
[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.";
    }
}

Boucles d’agent basées sur des entités

Si vous utilisez une infrastructure d’agent qui implémente déjà sa propre boucle d’agent, vous pouvez l’encapsuler dans une entité durable pour ajouter la durabilité sans réécrire la logique de boucle. Chaque instance d’entité représente une session d’agent unique. L’entité reçoit des messages, délègue en interne au framework d’agent et assure la persistance de l’état de conversation d’une interaction à l’autre.

L’avantage clé de cette approche est la simplicité : vous écrivez votre agent à l’aide de votre infrastructure préférée et ajoutez la durabilité en tant que préoccupation d’hébergement plutôt que de redéfinir le flux de contrôle de l’agent. L’entité agit en tant qu'enveloppe persistante, gérant automatiquement la persistance et la récupération des sessions.

Les exemples suivants montrent comment encapsuler un SDK d’agent existant en tant qu’entité durable. L’entité expose une message opération que les clients appellent pour envoyer une entrée utilisateur. En interne, l’entité délègue au framework d’agent, lequel gère sa propre boucle d’appel d’outils.

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

L’extension Durable Task pour Microsoft Agent Framework utilise cette approche. Il encapsule Microsoft agents Agent Framework en tant qu’entités durables, fournissant des sessions persistantes, des points de contrôle automatiques et des points de terminaison d’API intégrés avec une seule ligne de configuration.

Étapes suivantes