Condividi tramite


Modelli di applicazione agenziale

Esistono due approcci generali per la creazione di applicazioni agenti con l'intelligenza artificiale:

  • Flussi di lavoro deterministici : il codice definisce il flusso di controllo. Si scrive la sequenza di passaggi, diramazione, parallelismo e gestione degli errori usando costrutti di programmazione standard. L'LLM esegue operazioni all'interno di ogni passaggio, ma non controlla il processo complessivo.
  • Flussi di lavoro diretti dall'agente (cicli dell'agente) — LLM determina il flusso di controllo. L'agente decide quali strumenti chiamare, in quale ordine e quando l'attività è stata completata. Sono disponibili strumenti e istruzioni, ma l'agente determina il percorso di esecuzione in fase di esecuzione.

Entrambi gli approcci traggono vantaggio dall'esecuzione durevole e possono essere implementati usando il modello di programmazione Durable Task. Questo articolo illustra come compilare ogni modello usando esempi di codice.

Suggerimento

Questi modelli sono allineati alle progettazioni del flusso di lavoro agentico descritte in Building Effective Agents di Anthropic. Il modello di programmazione Durable Task esegue naturalmente il mapping a questi modelli: le orchestrazioni definiscono il flusso di controllo del flusso di lavoro e vengono automaticamente controllate, mentre le attività eseguono il wrapping di operazioni non deterministiche come chiamate LLM, chiamate degli strumenti e richieste API.

Scegliere un approccio

La tabella seguente consente di decidere quando usare ogni approccio.

Usare flussi di lavoro deterministici quando... Usa cicli dell'agente quando...
La sequenza di passaggi è nota in anticipo. L'attività è aperta e i passaggi non possono essere stimati.
Sono necessarie protezioni esplicite sul comportamento dell'agente. Si vuole che l'LLM decida quali strumenti usare e quando.
La conformità o la verificabilità richiede un flusso di controllo verificabile. L'agente deve adattare il proprio approccio in base ai risultati intermedi.
Si vogliono combinare più framework di intelligenza artificiale in un singolo flusso di lavoro. Si sta creando un agente di conversazione con funzionalità di chiamata agli strumenti.

Entrambi gli approcci offrono i checkpoint automatici, le politiche di ripetizione, il ridimensionamento distribuito e il supporto umano nel ciclo tramite esecuzione durevole.

Modelli di flusso di lavoro deterministici

In un flusso di lavoro deterministico, il codice controlla il percorso di esecuzione. LLM viene chiamato come passaggio all'interno del flusso di lavoro, ma non decide cosa accade successivamente. Il modello di programmazione Durable Task si adatta naturalmente a questo approccio.

  • Le orchestrazioni definiscono il flusso di controllo del flusso di lavoro (sequenza, diramazione, parallelismo, gestione degli errori) e vengono automaticamente salvate con un checkpoint.
  • Le attività avvolgono operazioni non deterministiche come chiamate LLM, invocazioni di strumenti e richieste API. Le attività possono essere eseguite in qualsiasi istanza di calcolo disponibile.

Gli esempi seguenti usano Durable Functions, che viene eseguito in Funzioni di Azure con hosting serverless.

Gli esempi seguenti usano gli SDK delle attività portabili Durable, che vengono eseguiti in qualsiasi ambiente di calcolo host, tra cui App contenitore di Azure, Kubernetes, macchine virtuali o localmente.

Concatenamento istruzioni

Il concatenamento dei prompt è il modello agentico più semplice. Un'attività complessa viene suddivisa in una serie di interazioni LLM sequenziali, in cui l'output di ogni passaggio viene inserito nell'input del passaggio successivo. Poiché ogni chiamata di attività viene automaticamente sottoposta a checkpoint, un arresto anomalo a metà della pipeline non forza il riavvio da zero e il riutilizzo di token LLM costosi, l'esecuzione riprende dall'ultimo passaggio completato.

È anche possibile inserire controlli di convalida a livello di codice tra i passaggi. Ad esempio, dopo aver generato una struttura, è possibile verificare che soddisfi un vincolo di lunghezza o argomento prima di passarlo al passaggio di stesura.

Questo modello corrisponde direttamente al modello di concatenamento delle funzioni nel modello di programmazione Durable Task.

Quando usare: Pipeline di generazione del contenuto, elaborazione di documenti in più passaggi, arricchimento sequenziale dei dati, flussi di lavoro che richiedono controlli di convalida intermedi.

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

Annotazioni

Lo stato dell'orchestrazione viene automaticamente sottoposto a checkpoint in ogni await istruzione. Se il processo host si arresta in modo anomalo o la macchina virtuale viene riciclata, l'orchestrazione riprenderà automaticamente dall'ultimo passaggio completato anziché ricominciare.

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

Annotazioni

Lo stato dell'orchestrazione viene automaticamente sottoposto a checkpoint a ciascuna istruzione await. Se il processo host si arresta in modo anomalo o la macchina virtuale viene riciclata, l'orchestrazione riprenderà automaticamente dall'ultimo passaggio completato anziché ricominciare.

Instradamento

Il routing usa un passaggio di classificazione per determinare quale agente downstream o modello deve gestire una richiesta. L'orchestrazione chiama prima un'attività di classificazione, quindi passa al gestore appropriato in base al risultato. Questo approccio consente di personalizzare in modo indipendente il prompt, il modello e il set di strumenti di ogni gestore, ad esempio indirizzando domande di fatturazione a un agente specializzato con accesso alle API di pagamento inviando domande generali a un modello più leggero.

Quando usare: Valutazione del supporto clienti, classificazione delle finalità per agenti specializzati, selezione dinamica del modello in base alla complessità delle attività.

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

Parallelizzazione

Quando si dispone di più sottoattività indipendenti, è possibile inviarle come chiamate di attività parallele e attendere tutti i risultati prima di procedere. L'utilità di pianificazione delle attività durevoli distribuisce automaticamente queste attività in tutte le istanze di calcolo disponibili, ciò implica che l'aggiunta di più worker riduce direttamente il tempo totale.

Una variante comune è il voto a più modelli: si invia la stessa richiesta a più modelli (o allo stesso modello con temperature diverse) in parallelo, quindi aggregare o selezionare le risposte. Poiché ogni ramo parallelo è sottoposto a checkpoint indipendentemente, un errore temporaneo in un ramo non influisce sugli altri.

Questo modello si mappa direttamente al modello fan-out/fan-in nel Durable Task.

Quando usare: Analisi batch di documenti, chiamate di strumenti paralleli, valutazione multimodelli, moderazione del contenuto con più revisori.

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

Agenti di orchestrazione

In questo modello, un orchestratore centrale chiama prima un LLM (tramite un'attività) per pianificare il lavoro. In base all'output dell'LLM, l'agente di orchestrazione determina quindi quali sottoattività sono necessarie. L'orchestratore invia quindi tali sottoattività alle orchestrazioni di lavoratori specializzati. La differenza principale rispetto alla parallelizzazione è che il set di sottoattività non è fisso in fase di progettazione; l'agente di orchestrazione li determina in modo dinamico in fase di esecuzione.

Questo modello usa sotto orchestrazioni, che sono flussi di lavoro figlio con checkpoint indipendente. Ogni orchestrazione del worker può contenere più passaggi, tentativi e parallelismo annidato.

Quando usare: Pipeline di ricerca approfondita, codifica dei flussi di lavoro dell'agente che modificano più file, collaborazione multi-agente in cui ogni agente ha un ruolo 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;
    }
}

Analizzatore-ottimizzatore

Il criterio di ottimizzazione dell'analizzatore associa un agente generatore a un agente di valutazione in un ciclo di perfezionamento. Il generatore produce output, l'analizzatore lo valuta rispetto ai criteri di qualità e fornisce feedback, e il ciclo si ripete fino a quando l'output non supera il test o viene raggiunto il numero massimo di iterazioni. Poiché ogni iterazione del ciclo viene registrata, un arresto anomalo dopo tre giri di perfezionamento riusciti non farà perdere i progressi.

Questo modello è particolarmente utile quando la qualità può essere misurata in modo programmatico — ad esempio, la validazione che il codice generato viene compilato correttamente o che una traduzione conserva le entità denominate.

Quando usare: Generazione di codice con revisione automatizzata, traduzione letteraria, perfezionamento del contenuto iterativo, attività di ricerca complesse che richiedono più round di analisi.

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

Cicli dell'agente

In un'implementazione tipica dell'agente di intelligenza artificiale, un LLM viene richiamato in un ciclo, chiamando gli strumenti e prendendo decisioni fino al completamento dell'attività o fino a quando non viene raggiunta una condizione di arresto. A differenza dei flussi di lavoro deterministici, il percorso di esecuzione non è predefinito. L'agente determina le operazioni da eseguire in ogni passaggio in base ai risultati dei passaggi precedenti.

I cicli degli agenti sono ideali per le attività in cui non è possibile stimare il numero o l'ordine dei passaggi. Gli esempi comuni includono agenti di codifica aperti, ricerche autonome e bot di conversazione con funzionalità di chiamata agli strumenti.

Esistono due approcci consigliati per implementare cicli agente con il modello di programmazione Durable Task:

Avvicinarsi Descrizione Quando utilizzare
Basata sull'orchestrazione Scrivere il loop dell'agente come un'orchestrazione durevole. Le chiamate agli strumenti vengono implementate come attività e l'input umano usa eventi esterni. L'orchestrazione controlla la struttura del ciclo, mentre il modello linguistico di grandi dimensioni (LLM) gestisce le decisioni al suo interno. È necessario un controllo granulare sul ciclo, sui criteri di ripetizione dei tentativi per strumento, sul bilanciamento del carico distribuito delle chiamate agli strumenti o sulla possibilità di eseguire il debug del ciclo nell'IDE con punti di interruzione.
Basato su entità Ogni istanza di agente è un'entità durevole. Il framework dell'agente gestisce internamente il ciclo e l'entità fornisce stato persistente e persistenza della sessione. Si usa un framework agente (ad esempio Microsoft Agent Framework) che implementa già il ciclo dell'agente e si vuole aggiungere durabilità con modifiche minime al codice.

Cicli di agenti basati sull'orchestrazione

Un ciclo di agenti basato su orchestrazioni combina diverse funzionalità di Durable Task: orchestrazioni eterne (continue-as-new) per mantenere la memoria limitata, fan-out/fan-in per l'esecuzione parallela degli strumenti e eventi esterni per l'interazione umana nel ciclo. Ogni iterazione del ciclo:

  1. Invia il contesto di conversazione corrente all' LLM tramite un'attività o un'entità con stato.
  2. Riceve la risposta del modello LLM, che può includere richieste agli strumenti.
  3. Esegue qualsiasi chiamata di strumento come attività (distribuita tra le risorse di calcolo disponibili).
  4. Facoltativamente, attende l'input umano usando eventi esterni.
  5. Continua il ciclo con lo stato aggiornato o si completa quando l'agente segnala che è finito.
[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.";
    }
}

Cicli di agenti basati su entità

Se si usa un framework agente che implementa già il proprio ciclo di agenti, è possibile eseguirne il wrapping in un'entità durevole per aggiungere durabilità senza riscrivere la logica del ciclo. Ogni istanza di entità rappresenta una singola sessione dell'agente. L'entità riceve messaggi, delega internamente al framework dell'agente e mantiene lo stato della conversazione attraverso le interazioni.

Il vantaggio principale di questo approccio è la semplicità: scrivere l'agente usando il framework preferito e aggiungere durabilità come problema di hosting anziché riprogettare il flusso di controllo dell'agente. L'entità funge da wrapper durevole, gestendo automaticamente la persistenza e il ripristino della sessione.

Gli esempi seguenti illustrano come incapsulare un SDK dell'agente esistente come entità durevole. L'entità espone un'operazione message che viene chiamata dai client per inviare l'input dell'utente. Internamente, l'entità delega al framework dell'agente, che gestisce il proprio ciclo di chiamate agli strumenti.

// 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'estensione Durable Task per Microsoft Agent Framework usa questo approccio. Esegue l'incapsulamento degli agenti di Microsoft Agent Framework come entità durevoli, fornendo sessioni persistenti, checkpoint automatici ed endpoint API integrati con una singola riga di configurazione.

Passaggi successivi