Шаблоны агентических приложений

Существует два общих подхода к созданию агентических приложений с помощью искусственного интеллекта:

  • Детерминированные рабочие процессы — код определяет поток управления. Последовательность шагов, ветвления, параллелизма и обработки ошибок выполняется с помощью стандартных конструкций программирования. LLM выполняет работу внутри каждого шага, но не контролирует общий поток.
  • Рабочие процессы, направленные агентом (циклы агента), — LLM управляет потоком управления. Агент решает, какие средства следует вызывать, в каком порядке и когда задача завершена. Вы предоставляете средства и инструкции, но агент определяет путь выполнения во время выполнения.

Оба подхода получают преимущества устойчивого выполнения и могут быть реализованы с помощью модели программирования устойчивых задач. В этой статье показано, как создать каждый шаблон с готовыми примерами кода в C#, Python, JavaScript или TypeScript и Java.

Подсказка

Prerequisites: Перед использованием этих шаблонов настройте пакеты SDK системы Durable Tasks или Устойчивые функции. Ознакомьтесь с обзором SDK или Устойчивые функции, чтобы приступить к работе.

На этой странице:

Подсказка

Эти шаблоны соответствуют дизайнам агентных рабочих процессов, описанным в Building Effective Agents от Anthropic. Модель программирования устойчивых задач естественно сопоставляется с этими шаблонами: оркестрации определяют поток управления рабочим процессом и автоматически устанавливаются контрольные точки, в то время как действия инкапсулируют недетерминированные операции, такие как вызовы LLM, вызовы инструментов и запросы API.

Выбор подхода

В следующей таблице показано, когда следует использовать каждый подход.

Используйте детерминированные рабочие процессы, когда... Используйте циклы агентов, когда...
Последовательность шагов известна заранее. Задача открыта и шаги не могут быть предсказаны.
Вам нужны явные ограничения на поведение агента. Вы хотите, чтобы LLM решил, какие средства следует использовать и когда.
Для соответствия требованиям или аудита требуется проверяемый поток управления. Агент должен адаптировать свой подход на основе промежуточных результатов.
Вы хотите объединить несколько платформ ИИ в одном рабочем процессе. Вы создаете конверсционного агента с возможностями вызова инструментов.

Оба подхода обеспечивают автоматическое создание контрольных точек, политики повторных попыток, распределенное масштабирование и взаимодействие с человеком с помощью устойчивым выполнением.

Детерминированные шаблоны рабочих процессов

В детерминированном рабочем процессе код управляет путем выполнения. LLM вызывается как шаг в рабочем процессе, но не решает, что произойдет дальше. Модель программирования устойчивых задач естественно сопоставляется с этим подходом:

  • Оркестрации определяют поток управления рабочим процессом (последовательность, ветвление, параллелизм, обработка ошибок) и автоматически создают контрольные точки.
  • Действия охватывают недетерминированные операции, такие как вызовы LLM, инструменты и запросы API. Действия могут выполняться на любом доступном вычислительном экземпляре.

В следующих примерах используется Устойчивые функции, который работает на Функции Azure с бессерверным размещением.

В следующих примерах используются переносимые пакеты SDK для задач Durable Task SDK, которые выполняются на любом узле, включая Контейнеры приложений Azure, Kubernetes, виртуальные машины или локально.

Шаблон последовательного формирования запросов

Цепочка запросов является самым простым агентным шаблоном. Сложная задача разбивается на ряд последовательных взаимодействий LLM, где выход каждого шага передается на вход следующего шага. Поскольку каждый вызов действия автоматически фиксируется в контрольной точке, сбой в середине конвейера не заставляет вас перезапустить с нуля, что предотвратит ненужное расходование дорогостоящих токенов LLM — выполнение возобновляется с последнего завершенного шага.

Можно также вставить программные шлюзы проверки между шагами. Например, после создания структуры можно убедиться, что она соответствует ограничению длины или тематическому ограничению перед передачей к этапу написания.

Этот шаблон сопоставляется непосредственно с шаблоном цепочки функций в модели программирования устойчивых задач.

Когда следует использовать: Конвейеры создания контента, многофакторная обработка документов, последовательное обогащение данных, рабочие процессы, требующие промежуточных шлюзов проверки.

Замечание

Во всех следующих примерах состояние оркестрации автоматически выполняется на каждой await/yield инструкции. Если процесс узла терпит сбой или виртуальная машина перезапускается, оркестрация возобновляется с последнего завершенного шага, вместо того, чтобы начинаться заново.

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

Маршрутизация

Маршрутизация использует шаг классификации, чтобы определить, какой подчиненный агент или модель должны обрабатывать запрос. Оркестрационный процесс сначала вызывает действие классификатора, а затем переходит к соответствующему обработчику на основе результата. Такой подход позволяет независимо настраивать запрос, модель и инструментарий каждого обработчика — например, направлять вопросы о выставлении счетов специализированному агенту с доступом к платёжным API, в то время как общие вопросы отправляются в облегчённую модель.

Когда следует использовать: Процесс поддержки клиентов, классификация намерений для специализированных агентов, выбор динамической модели на основе сложности задачи.

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

Распараллеливание

При наличии нескольких независимых подзадач их можно отправить в виде параллельных вызовов действий и ждать всех результатов, прежде чем продолжить. Планировщик долговременных задач автоматически распределяет эти действия по всем доступным вычислительным экземплярам, что означает, что добавление дополнительных рабочих напрямую сокращает общее время выполнения.

Распространенный вариант — это голосование с несколькими моделями: вы отправляете один и тот же запрос нескольким моделям (или одной и той же модели с разными температурами) параллельно, а затем агрегируете или выбираете из ответов. Поскольку каждая параллельная ветвь контролируется независимо, временный сбой в одной ветви не влияет на другие.

Этот шаблон сопоставляется непосредственно с шаблоном разветвление/объединение в Durable Task.

Когда следует использовать: Пакетный анализ документов, вызовы параллельных инструментов, оценка нескольких моделей, модерация содержимого с несколькими рецензентами.

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

Шаблон оркестратор-воркеры

В этом шаблоне центральный оркестратор сначала вызывает LLM (через действие) для планирования работы. На основе выходных данных LLM оркестратор затем определяет необходимые подзадачи. Затем оркестратор отправляет эти подзадачи в специализированные рабочие оркестрации. Ключевое отличие от параллелизации заключается в том, что набор подзадач не фиксирован во время разработки; оркестратор определяет их динамически во время выполнения.

В этом шаблоне используются суб-оркестрации, которые являются независимыми дочерними рабочими процессами. Каждая рабочая оркестрация может содержать несколько шагов, повторных попыток и вложенный параллелизм.

Когда следует использовать: Конвейеры глубокого исследования, рабочие процессы агента программирования, изменяющие несколько файлов, совместную работу с несколькими агентами, где каждый агент имеет отдельную роль.

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

Шаблон оценщика-оптимизатора

Шаблон оценщика-оптимизатора связывает агент-генератор с агентом-оценщиком в процессе уточнения. Генератор выдает выходные данные, оцениватель оценивает их по критериям качества и предоставляет обратную связь, а цикл повторяется до тех пор, пока выходные данные не проходят или не достигнуто максимальное число итераций. Поскольку каждая итерация цикла сохраняется в контрольной точке, авария после трех успешных этапов доработки не приведет к потере прогресса.

Этот шаблон особенно полезен, когда качество можно измерять программным способом — например, проверяя, что созданный код компилируется, или что перевод сохраняет именованные сущности.

Когда следует использовать: Создание кода с автоматизированным просмотром, литературным переводом, итеративным уточнением контента, сложными задачами поиска, требующими нескольких раундов анализа.

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

Циклы агента

В типичной реализации агента ИИ LLM вызывается в цикле, при этом используются инструменты и принимаются решения до тех пор, пока задача не будет завершена или не будет достигнуто условие остановки. В отличие от детерминированных рабочих процессов, путь выполнения не определен. Агент определяет, что делать на каждом шаге на основе результатов предыдущих шагов.

Циклы агентов хорошо подходят для задач, в которых невозможно спрогнозировать количество шагов или порядок шагов. Распространенные примеры включают открытые агенты кодирования, автономные исследования и диалоговые боты с возможностями вызова инструментов.

Существует два рекомендуемых подхода к реализации циклов агента с помощью модели программирования устойчивых задач:

Подход Описание Когда использовать
На основе оркестрации Цикл агента — это устойчивая оркестрация. Вызовы инструментов — это действия; входные данные человека используют внешние события. Детальное управление, политики повторных попыток для каждого инструмента, распределенное выполнение инструментов, отладка интегрированной среды разработки.
На основе сущностей Каждый экземпляр агента является устойчивой сущностью. Платформа агента управляет циклом; сущность предоставляет сохраняемость сеанса. Вы используете платформу (например, Microsoft Agent Framework), которая уже имеет цикл агента, и вы хотите добавить устойчивость с минимальными изменениями.

Циклы агента на основе оркестрации

Цикл агента на основе оркестрации объединяет несколько возможностей устойчивых задач: вечные оркестрации (продолжить как новый) для обеспечения ограниченной памяти, разветвление/сведение для параллельного выполнения инструментов и внешних событий для взаимодействия с человек-в-петле. Каждая итерация цикла:

  1. Отправляет текущий контекст беседы в LLM через действие или сущность с отслеживанием состояния.
  2. Получает ответ LLM, который может включать вызовы инструментов.
  3. Выполняет любые вызовы средства в виде действий (распределенных по доступным вычислительным ресурсам).
  4. По желанию ожидает ввода человека с помощью внешних событий.
  5. Продолжает цикл с обновленным состоянием или завершается, когда агент сигнализирует об окончании.
[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.";
    }
}

Циклы агента на основе сущностей

Если вы используете платформу агента, которая уже реализует свой собственный цикл агента, ее можно упаковать в устойчивую сущность , чтобы добавить устойчивость без перезаписи логики цикла. Каждый экземпляр сущности представляет один сеанс агента. Сущность получает сообщения, делегирует их внутренней агентной платформе и сохраняет состояние беседы между взаимодействиями.

Ключевым преимуществом этого подхода является простота: вы пишете своего агента, используя предпочитаемую платформу, и добавляете устойчивость как задачу размещения, а не перепроектируете управляющую логику агента. Сущность выступает в качестве устойчивой оболочки, обрабатывает сохраняемость сеансов и восстановление автоматически.

В следующих примерах показано, как упаковать существующий пакет SDK агента в качестве устойчивой сущности. Сущность предоставляет message операцию, которую клиенты вызывают для отправки пользовательского ввода. Внутренне сущность делегирует в агентный фреймворк, который управляет собственным циклом вызова инструментов.

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

Расширение задачи Durable для платформы агента Microsoft использует этот подход. Он оборачивает агентов Microsoft Agent Framework в устойчивые сущности, обеспечивая постоянные сеансы, автоматическое создание контрольных точек и встроенные API-конечные точки с помощью одной строки конфигурации.

Дальнейшие действия