Agentbaserade programmönster

Det finns två allmänna metoder för att skapa agentiska program med AI:

  • Deterministiska arbetsflöden – Koden definierar kontrollflödet. Du skriver sekvensen med steg, förgrening, parallellitet och felhantering med hjälp av standardprogrammeringskonstruktioner. LLM utför arbete i varje steg men styr inte det övergripande flödet.
  • Agentstyrda arbetsflöden (agentloopar) – LLM styr kontrollflödet. Agenten bestämmer vilka verktyg som ska anropas, i vilken ordning och när uppgiften är klar. Du tillhandahåller verktyg och instruktioner, men agenten bestämmer exekveringsvägen.

Båda metoderna drar nytta av varaktig körning och kan implementeras med programmeringsmodellen Durable Task. Den här artikeln visar hur du skapar varje mönster med hjälp av kodexempel.

Tips/Råd

Dessa mönster överensstämmer med de agentiska arbetsflödesdesigner som beskrivs i Anthropics Building Effective Agents. Programmeringsmodellen Durable Task mappar naturligt till dessa mönster: orkestreringar definierar arbetsflödeskontrollflödet och kontrolleras automatiskt, medan aktiviteter omsluter icke-deterministiska åtgärder som LLM-anrop, verktygsanrop och API-begäranden.

Välj en metod

Följande tabell hjälper dig att bestämma när du ska använda varje metod.

Använd deterministiska arbetsflöden när... Använd agentloopar när...
Sekvensen med steg är känd i förväg. Uppgiften är öppen och stegen kan inte förutsägas.
Du behöver explicita skyddsräcken för agentbeteende. Du vill att LLM ska bestämma vilka verktyg som ska användas och när.
Efterlevnad eller granskning kräver granskningsbart kontrollflöde. Agenten måste anpassa sin metod baserat på mellanliggande resultat.
Du vill kombinera flera AI-ramverk i ett enda arbetsflöde. Du skapar en konversationsagent med möjligheter att använda verktyg.

Båda metoderna tillhandahåller automatiska kontrollpunkter, återsökningsprinciper, distribuerad skalning och stöd för människa i processen genom varaktig körning.

Deterministiska arbetsflödesmönster

I ett deterministiskt arbetsflöde är det koden som styr exekveringsvägen. LLM anropas som ett steg i arbetsflödet men bestämmer inte vad som händer härnäst. Programmeringsmodellen Durable Task mappar naturligt till den här metoden:

  • Orkestreringar definierar arbetsflödeskontrollflödet (sekvens, förgrening, parallellitet, felhantering) och kontrolleras automatiskt.
  • Aktiviteter omsluter icke-deterministiska åtgärder som LLM-anrop, verktygsanrop och API-begäranden. Aktiviteter kan köras på valfri tillgänglig beräkningsinstans.

I följande exempel används Durable Functions som körs på Azure Functions med serverlös värd.

I följande exempel används portabla Durable Task SDK:er, som körs på valfri värdberäkning, inklusive Azure Container Apps, Kubernetes, virtuella datorer eller lokalt.

Promptkedjning

Kedjning av promptar är det enklaste agentiska mönstret. Du delar upp en komplex uppgift i en serie sekventiella LLM-interaktioner, där varje stegs utdata matas in i nästa stegs indata. Eftersom varje aktivitetsanrop automatiskt kontrollpunktas tvingar en krasch halvvägs genom pipelinen dig inte att starta om från början och återanvända dyra LLM-token – körningen återupptas från det senaste slutförda steget.

Du kan också infoga programmatiska valideringsportar mellan stegen. När du till exempel har genererat en disposition kan du kontrollera att den uppfyller en längd- eller ämnesbegränsning innan du skickar den till utkaststeget.

Det här mönstret mappar direkt till funktionslänkningsmönstret i programmeringsmodellen Durable Task.

När du ska använda: Pipelines för innehållsgenerering, dokumentbearbetning i flera steg, sekventiell databerikning, arbetsflöden som kräver mellanliggande valideringsportar.

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

Anmärkning

Orkestreringens tillstånd kontrolleras automatiskt vid varje await-instruktion. Om värdprocessen kraschar eller om den virtuella datorn återanvänds återupptas orkestreringen automatiskt från det senaste slutförda steget i stället för att starta om.

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

Anmärkning

Orkestreringens tillstånd kontrolleras automatiskt vid varje await-instruktion. Om värdprocessen kraschar eller om den virtuella datorn återanvänds återupptas orkestreringen automatiskt från det senaste slutförda steget i stället för att starta om.

Routing

Routning använder ett klassificeringssteg för att avgöra vilken nedströmsagent eller modell som ska hantera en begäran. Orkestreringen anropar först en klassificeraraktivitet och sedan grenar till lämplig hanterare baserat på resultatet. Med den här metoden kan du skräddarsy varje hanterares fråga, modell och verktyg oberoende av varandra, till exempel genom att dirigera faktureringsfrågor till en specialiserad agent med åtkomst till betalnings-API:er samtidigt som du skickar allmänna frågor till en modell med lägre vikt.

När du ska använda: Kundsupporttriage, avsiktsklassificering för specialiserade agenter, dynamisk modellval baserat på uppgiftskomplexitet.

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

Parallellisering

När du har flera oberoende underaktiviteter kan du skicka dem som parallella aktivitetsanrop och vänta på alla resultat innan du fortsätter. Durable Task Scheduler distribuerar dessa aktiviteter automatiskt över alla tillgängliga beräkningsinstanser, vilket innebär att lägga till fler arbetare direkt minskar den totala tiden för wall-clock.

En vanlig variant är röstning med flera modeller: du skickar samma fråga till flera modeller (eller samma modell med olika temperaturer) parallellt och sammanställer eller väljer sedan bland svaren. Eftersom varje parallell gren är oberoende kontrollpunkt påverkas inte de andra av ett tillfälligt fel i en gren.

Det här mönstret mappar direkt till fan-out/fan-in-mönstret i Durable Task.

När du ska använda: Batchanalys av dokument, parallella verktygsanrop, utvärdering av flera modeller, innehållsmoderering med flera granskare.

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

Orchestrator-arbetare

I det här mönstret anropar en central orkestrerare först en LLM (via en aktivitet) för att planera arbetet. Baserat på LLM:s utdata avgör orkestratorn sedan vilka underaktiviteter som behövs. Orchestrator skickar sedan dessa underaktiviteter till specialiserade arbetarorkestreringar. Den viktigaste skillnaden från parallelism är att uppsättningen med underaktiviteter inte har åtgärdats vid designtillfället. Orkestratorn bestämmer dem dynamiskt vid körning.

Det här mönstret använder underorkestreringar, som är oberoende kontrollpunktsbaserade underordnade arbetsflöden. Varje arbetsorkestrering kan själv innehålla flera steg, återförsök och kapslad parallellitet.

När du ska använda: Djupgående forskningspipelines, kodning av agentarbetsflöden som ändrar flera filer, samarbete med flera agenter där varje agent har en distinkt roll.

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

Utvärderare-optimerare

Mönstret evaluator-optimizer parar ihop en generatoragent med en utvärderaragent i en förfiningsloop. Generatorn producerar utdata, utvärderaren poängsätter den mot kvalitetskriterier och ger feedback och loopen upprepas tills utdata passerar eller ett maximalt iterationsantal har uppnåtts. Eftersom varje loop-iteration är kontrollpunktsäkrad, kommer en krasch efter tre lyckade förfiningsrundor inte att förlora förloppet.

Det här mönstret är särskilt användbart när kvaliteten kan mätas programmatiskt, till exempel genom att verifiera den genererade koden eller att en översättning bevarar namngivna entiteter.

När du ska använda: Kodgenerering med automatiserad granskning, litterär översättning, iterativ innehållsförfining, komplexa sökuppgifter som kräver flera analysrundor.

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

Agentloopar

I en typisk AI-agentimplementering anropas en LLM i en loop, anropar verktyg och fattar beslut tills uppgiften har slutförts eller ett stoppvillkor har nåtts. Till skillnad från deterministiska arbetsflöden är körningssökvägen inte fördefinierad. Agenten avgör vad du ska göra i varje steg baserat på resultat från föregående steg.

Agentslingor passar bra för uppgifter där antalet eller stegens ordning inte kan förutsägas. Vanliga exempel är öppna kodningsagenter, autonom forskning och konversationsrobotar med funktioner för verktygssamtal.

Det finns två rekommenderade metoder för att implementera agentloopar med programmeringsmodellen Durable Task:

Tillvägagångssätt Beskrivning När det bör användas
Orkestreringsbaserad Skriv agentloopen som en varaktig orkestrering. Verktygsanrop implementeras som aktiviteter och mänskliga indata använder externa händelser. Orkestreringen styr loopens struktur, medan LLM fattar besluten inom den. Du behöver detaljerad kontroll över loopen, principer för återförsök per verktyg, distribuerad belastningsutjämning av verktygsanrop eller möjlighet att felsöka loopen i din IDE med brytpunkter.
Entitetsbaserad Varje agentinstans är en varaktig entitet. Agentramverket hanterar loopen internt och entiteten ger varaktigt tillstånd och sessionspersistence. Du använder ett agentramverk (till exempel Microsoft Agent Framework) som redan implementerar agentloopen och du vill lägga till hållbarhet med minimala kodändringar.

Orkestreringsbaserade agentloopar

En orkestreringsbaserad agentloop kombinerar flera durable task-funktioner: eviga orkestreringar (fortsätt som nya) för att hålla minnet begränsat, fläkta ut/fläkta in för parallell verktygskörning och externa händelser för interaktioner mellan människor i loopen. Varje iteration av loopen:

  1. Skickar den aktuella konversationskontexten till LLM via en aktivitet eller tillståndskänslig entitet.
  2. Tar emot LLM:s svar, som kan innehålla verktygsanrop.
  3. Kör alla verktygsanrop som aktiviteter (distribuerade över tillgänglig beräkning).
  4. Det är valfritt att vänta på mänsklig indata med hjälp av externa händelser.
  5. Fortsätter loopen med uppdaterat tillstånd eller slutförs när agenten signalerar att den är klar.
[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.";
    }
}

Entitetsbaserade agentloopar

Om du använder ett agentramverk som redan implementerar en egen agentloop kan du omsluta det i en hållbar entitet för att lägga till hållbarhet utan att skriva om looplogiken. Varje entitetsinstans representerar en enskild agentsession. Entiteten tar emot meddelanden, delegerar till agentramverket internt och bevarar konversationstillståndet mellan interaktioner.

Den största fördelen med den här metoden är enkelhet: du skriver din agent med ditt önskade ramverk och lägger till hållbarhet som ett värdproblem i stället för att designa om agentens kontrollflöde. Entiteten fungerar som ett varaktigt omslag och hanterar sessionsbeständighet och återställning automatiskt.

I följande exempel visas hur du omsluter en befintlig agent-SDK som en varaktig entitet. Entiteten exponerar en message funktion som klienter anropar för att skicka användarinmatning. Internt delegerar entiteten till agentramverket, som hanterar sin egen verktygsanropsloop.

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

Tillägget Durable Task för Microsoft Agent Framework använder den här metoden. Den omsluter Microsoft Agent Framework-agenter som varaktiga entiteter som tillhandahåller beständiga sessioner, automatiska kontrollpunkter och inbyggda API-slutpunkter med en enda konfigurationsrad.

Nästa steg