Engineering the Agentic Stack · Часть 1

Циклы ризонинга AI-агентов: ReAct, ReWOO и Plan-and-Execute

Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Цикл ризонинга агента — это поток управления, который определяет, когда модель планирует, вызывает инструмент, читает результат и останавливается. Для инженера, создающего агента, этот выбор — одновременно и бюджет: он определяет, как часто запускается модель, какой объём истории передаётся в каждый вызов и может ли неожиданный результат изменить следующее действие.

В этой статье мы сравним ReAct, ReWOO и Plan-and-Execute на примере Market Analyst Agent на LangGraph, которого я разработал. В итоге у вас будет правило роутинга и варианты реализации, которые можно адаптировать, а не просто три названия для схемы.

Цикл — самый внутренний слой этой серии. Его окружают память, инструменты, безопасность, рантайм и проверки приёмки; они не заменяют сам цикл.

Коротко: выбирайте ReAct, когда каждый результат инструмента может изменить следующий шаг; ReWOO — когда граф зависимостей известен до начала выполнения; Plan-and-Execute — когда большую задачу можно декомпозировать, но отдельным шагам всё ещё нужна обратная связь. Это критерии роутинга, а не универсальный рейтинг скорости: измеряйте их с вашей моделью, инструментами и политикой ретраев.

Краткое сравнение фреймворков см. в статье Best AI Agent Frameworks in 2026.

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

Харнесс — управляющая программа между моделью и машиной. Она собирает промпты из сохранённого состояния (часть 2), определяет действия, которые модель может назвать (часть 3), авторизует вызовы (часть 4) и проверяет доказательства перед объявлением задачи завершённой (часть 6). Это отдельные инженерные задачи, но один ход проходит через все четыре.

Рантайм (часть 5) предоставляет лог сессии, сэндбокс, хранилище чекпоинтов и трейсы, которые переживают один процесс воркера.

Место каждой части серии Engineering the Agentic StackМесто каждой части серии Engineering the Agentic Stack

Каждая статья самостоятельна. Вместе они переходят от цикла к внешним слоям.


Начните с границы отказа

Хороший промпт не решает вопрос потока управления. Паттерны различаются тем, какой объём работы фиксируется до первого вызова инструмента. От этого зависят число вызовов модели, момент обнаружения ошибочного плана и возможность перенаправить выполнение после неожиданного результата инструмента.

Три паттерна ризонинга AI-агентов

Сравнение ReAct, ReWOO и Plan-and-Execute по моменту, когда доказательства могут изменить планСравнение ReAct, ReWOO и Plan-and-Execute по моменту, когда доказательства могут изменить план

ReAct: принимайте решение после каждого наблюдения

ReAct (Yao et al., 2022), сокращение от Reason + Act, держит следующее решение рядом с последним наблюдением:

  1. Thought: агент генерирует «мысль», чтобы разбить цель на части и спланировать следующий шаг.
  2. Action: на основе этой мысли он вызывает инструмент.
  3. Observation: агент читает результат, обновляя своё понимание для следующей мысли.

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

Это даёт ReAct полезные свойства:

  • В ручном примере PaLM-540B из статьи для HotpotQA наблюдения из Wikipedia приводили к меньшему числу галлюцинированных фактов, чем промптинг с chain-of-thought.
  • Агент может менять стратегию на лету на основе только что полученной информации.
  • История вызовов инструментов и наблюдений даёт конкретный трейс выполнения.

У этого цикла есть и издержки:

  • В наивной реализации с полной историей история повторно обрабатывается на каждом шаге, поэтому латентность и стоимость растут вместе с длиной цикла. Суммаризация или усечение могут ограничить рост ценой потери части контекста.
  • Неэффективен, когда вызовы инструментов можно было спланировать заранее, — именно эту нишу занимает ReWOO.
  • Без условия остановки или лимита шагов цикл может выполняться бесконечно.

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

ReWOO: сначала скомпилируйте граф инструментов

ReWOO (Reasoning WithOut Observation) разделяет планирование и выполнение. Планировщик за один проход записывает полную последовательность вызовов инструментов, используя плейсхолдеры для значений, которые появятся только после выполнения.

  1. Plan: один вызов LLM записывает полный план вызовов инструментов, используя плейсхолдеры переменных (#E1, #E2) для ещё не существующих выходных значений.
  2. Worker: экзекьютор без LLM запускает запланированные инструменты и подставляет значения вместо плейсхолдеров. В статье воркер следует плану; приведённая далее реализация добавляет параллельные батчи с учётом зависимостей для готовых шагов.
  3. Solver: финальный вызов LLM получает собранные наблюдения и формирует ответ.

ReWOO строит граф зависимостей до запуска инструментовReWOO строит граф зависимостей до запуска инструментов

Такое разделение даёт следующие преимущества:

  • Меньше повторных вызовов модели по сравнению с ReAct, если исходный план остаётся корректным.
  • Меньше повторяющейся истории в промпте, чем в чередующемся цикле с полной историей. Латентность инструментов всё равно зависит от планировщика вызовов в воркере.
  • Планировщик можно отдельно дообучать без подключения к живому окружению.

Но оно создаёт жёсткую границу. В стресс-тесте HotpotQA из статьи каждый инструмент возвращал No evidence found; ReWOO потерял меньше точности, чем ReAct, поскольку ошибочные наблюдения не отправляли его планировщик в новый цикл. Это относительная устойчивость, а не политика восстановления после сбоев. Реализация всё равно должна решить, становится ли ошибка инструмента свидетельством для solver, запускает ли ретрай или прерывает выполнение. ReWOO подходит для предсказуемых рабочих процессов; сам по себе он не перепланирует выполнение вокруг ошибочного исходного графа.

Используйте его для быстрых снэпшотов, проверки статусов и дашбордов с предсказуемым поведением инструментов.

Plan-and-Execute: декомпозируйте задачу, затем реагируйте локально

Plan-and-Solve prompting описывает метод промптинга, который сначала создаёт план, а затем решает подзадачи. Связанный паттерн оркестрации инструментов обычно называют Plan-and-Execute. Этот паттерн описан в руководстве Plan-and-Execute для LangChain:

  1. Фаза планирования: агент сначала создаёт план, разбивая задачу на небольшие подзадачи.
  2. Фаза выполнения: затем агент выполняет подзадачи одну за другой. Если задействованы инструменты, каждая подзадача обычно запускается как отдельный небольшой цикл ReAct, поэтому экзекьютор может реагировать на возвращаемый инструментом результат, даже если общий план зафиксирован.

Исходная статья была посвящена zero-shot-промптингу. В реализации с инструментами этот паттерн оркестрации может выполнять запланированные шаги последовательно и использовать разные модели для планирования и выполнения. Разделение моделей — инженерное решение, а не результат, установленный статьёй о Plan-and-Solve.

Plan-and-Execute сохраняет общий план, а каждый шаг использует обратную связьPlan-and-Execute сохраняет общий план, а каждый шаг использует обратную связь

Паттерн полезен благодаря следующим свойствам:

  • Иерархический ризонинг, отражающий то, как эксперт разбивает проект на части.
  • Явная ветка перепланирования может приостановить выполнение и запустить повторную оценку после неожиданного результата шага.
  • Специализация моделей. Планировщик может быть дорогим, а экзекьютор — дешёвым.
  • При настроенном чекпоинтере каждый завершённый шаг может стать границей возобновления.

Издержки:

  • Больше обращений к модели, чем у ReWOO, если каждый шаг содержит собственный цикл ReAct.
  • Больше состояния для управления.
  • Избыточен для одношаговых запросов.

Используйте его для сложного анализа и исследований, которым нужен финальный синтез.

Выбирайте по месту, где может сломаться план

ХарактеристикаReAct (2022)Plan-and-Execute (2023)ReWOO (2023)
Основная идеяИмпровизатор: выбирает следующий шаг по последнему результату, по одному вызову за раз.Архитектор: строит полный чертёж, выполняет его, а затем проводит ревью.Оптимизатор: компилирует граф зависимостей, а затем объединяет готовые к запуску вызовы в батчи.
Рабочий процессИтеративный цикл: Thought → Action → Observation.Две стадии: фаза 1 (планирование), фаза 2 (выполнение).Разделённый процесс: Planner записывает граф вызовов инструментов, Worker выполняет их, Solver формирует ответ.
АдаптивностьМаксимальная: может изменить направление после каждого отдельного вызова инструмента.Средняя: обычно перепланирует только после завершения группы шагов.Минимальная: скрипт планировщика выполняется до конца; перепланирования во время выполнения нет.
ЭффективностьНаивный цикл с полной историей повторно передаёт больше входных токенов; управление контекстом может ограничить рост.Перепланирование выполняется периодически, а цикл ReAct каждого шага может начинаться с короткого контекста, а не со всей истории запуска.Меньше вызовов модели; воркер из этой статьи также объединяет инструменты, готовые с учётом зависимостей.
Лучше всего подходитОткрытое исследование или задачи с непредсказуемыми результатами.Долгие задачи, которым нужна стабильная цель (например, написание статьи).Структурированные, повторяемые процессы (например, проверка погоды в 5 городах).

Таблица помогает с роутингом, но не является бенчмарком. Используйте ReAct, когда наблюдение может изменить следующее действие. Выбирайте Plan-and-Execute, когда задачу можно чисто декомпозировать, но каждому шагу всё ещё нужна обратная связь. Используйте ReWOO, когда все зависимости инструментов известны до начала выполнения. В приведённой ниже учебной реализации этот граф зависимостей позволяет запускать параллельные батчи и обнаруживать граф, который не может продвинуться. Вспомогательная функция execute_tool намеренно работает по принципу fail-fast; в production-коде нужна явная политика ретраев, fallback или обработки ошибки как свидетельства. Перед оптимизацией количества вызовов измерьте все три подхода с вашей моделью, латентностью инструментов, набором задач и политикой ретраев.

Практический пример: Market Analyst Agent

Market Analyst Agent делает различие наглядным. Одна кодовая база использует все три паттерна для исследования рынка, а роутер выбирает между маршрутом глубокого исследования и маршрутом краткого флеш-брифинга. Приведённые ниже фрагменты — сокращённые учебные варианты коммита b4e769a: в закреплённом companion-проекте исключение инструмента перехватывается, а строка ошибки передаётся solver; показанный здесь воркер позволяет исключению прервать выполнение и выбрасывает ошибку, если граф зависимостей не может продвинуться. Маршрут и форма состояния одинаковы; политика обработки сбоев здесь намеренно вынесена явно.

Для оркестрации используется LangGraph. Нода — это Python-функция, возвращающая поля для обновления общего состояния. Ребро объявляет следующую ноду и может вызывать функцию роутинга. LangGraph объединяет обновления и может создавать чекпоинты после каждой ноды, делая запуск возобновляемым. Все три паттерна используют один объект состояния, поэтому для роутинга не нужны три отдельные схемы:

Market Analyst Agent направляет один запрос в два цикла ризонинга с общим состояниемMarket Analyst Agent направляет один запрос в два цикла ризонинга с общим состоянием

На схеме показаны роутинг и создание черновика. Общие эвалуатор и publish gate, приведённые далее, опущены, чтобы два цикла ризонинга оставались читаемыми.

Определение состояния

Схема состояния содержит поля, необходимые обоим режимам:

class PlanStep(BaseModel):
    """A single step in the research plan."""
    step_number: int
    description: str
    tool_hint: str | None = None
    completed: bool = False
    result: str | None = None

class UserProfile(BaseModel):
    """Structured user context loaded from long-term memory."""
    risk_tolerance: str | None = None
    investment_horizon: str | None = None

class AgentState(BaseModel):
    """Main state for the Market Analyst Agent graph."""

    # Identity and profile context for memory-backed personalization
    user_id: str
    user_profile: UserProfile = Field(default_factory=UserProfile)

    # Message history with LangGraph's add_messages reducer
    messages: Annotated[list, add_messages] = Field(default_factory=list)

    # Execution mode (set by router)
    execution_mode: ExecutionMode | None = None

    # Plan-and-Execute state
    plan: list[PlanStep] = Field(default_factory=list)
    current_step_index: int = 0

    # ReWOO state
    rewoo_plan: list[ReWOOPlanStep] = Field(default_factory=list)

    # Research results
    research_data: ResearchData | None = None

    # Final report. Both paths write this field, then the graph pauses before
    # publishing (see interrupt_before below), so a human signs off on a draft
    # a fresh-context evaluator has already voted on.
    draft_report: DraftReport | None = None

Паттерн 1: реализация Plan-and-Execute

Plan-and-Execute подходит для многошагового синтеза. Планировщик записывает высокоуровневые шаги, затем цикл ReAct выполняет каждый из них и реагирует на результаты инструментов.

В коде ниже зафиксирована версия claude-sonnet-4-5-20250929 — именно с ней я запускал эти примеры на момент публикации статьи в январе 2026 года. С тех пор поколения моделей сменились. Подставьте актуальную модель и заново проверьте поведение роутинга на собственных задачах — важен паттерн, а не идентификатор модели.

В реализации явно выделены четыре границы:

  1. Одна предварительная фаза планирования. Один вызов LLM создаёт весь план в виде списка описаний шагов.
  2. Выход, управляемый схемой: план валидируется до начала выполнения.
  3. Инструменты пока не выполняются. Планировщик решает только, что делать, но не как.
  4. Понятные человеку шаги. Каждый шаг — текст, который будет интерпретировать экзекьютор.
# System prompt guides the LLM to think like a research analyst
# creating a strategic plan, not immediate tool calls
PLANNER_SYSTEM_PROMPT = """You are a senior investment research analyst.
Break down stock analysis requests into 4-6 research steps covering:
1. Current price and basic metrics
2. Recent news and announcements
3. Competitor analysis (if relevant)
4. Financial health assessment
5. Risk factors
6. Investment thesis synthesis

Output as JSON with step_number, description, and tool_hint."""

# Schema-Guided Reasoning: Enforce structure with Pydantic
class PlanOutput(BaseModel):
    """Structured output for the planner."""

    steps: list[PlanStep] = Field(description="Research steps to execute")
    ticker: str = Field(description="The stock ticker being analyzed")

def planner_node(state: AgentState) -> dict:
    """Generate a research plan from the user's request.

    This is Phase 1 of Plan-and-Execute: creating the high-level strategy.
    """

    # Use a powerful model for strategic planning
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Ask for a typed plan and validate it before execution.
    # The API can still fail, so production code also handles that exception.
    structured_llm = llm.with_structured_output(PlanOutput)

    # Pull the request out of the message history
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    last_user_message = human[-1].content if human else "Analyze the market"

    # Context from long-term memory personalizes the plan
    profile_context = f"""
User Profile:
- Risk Tolerance: {state.user_profile.risk_tolerance}
- Investment Horizon: {state.user_profile.investment_horizon}
"""

    # Single LLM call creates the complete plan
    result: PlanOutput = structured_llm.invoke([
        SystemMessage(content=PLANNER_SYSTEM_PROMPT + profile_context),
        HumanMessage(content=f"Create a research plan for: {last_user_message}"),
    ])

    # State update: Store the plan and initialize tracking
    return {
        "plan": result.steps,           # The sequential steps to execute
        "current_step_index": 0,        # Start at step 0
        "research_data": ResearchData(ticker=result.ticker),  # Initialize data container
    }

Строка llm.with_structured_output(PlanOutput) реализует Schema-Guided Reasoning (SGR), который я разбирал в предыдущей статье. Схема позволяет приложению отклонить некорректный план до того, как LangGraph использует валидированные поля для управления условными рёбрами.

Паттерн 2: выполнение ReAct

После создания плана экзекьютор запускает каждый шаг как собственный цикл ReAct. Это фаза 2: каждый шаг достаточно мал, чтобы цикл Thought-Action-Observation оставался сфокусированным, а агент мог реагировать на любой результат инструмента.

Как устроена часть ReAct:

  1. Итеративное выполнение: по одному шагу за раз, с обратной связью от наблюдений.
  2. Цикл Thought-Action-Observation выполняется внутри create_react_agent.
  3. Результаты предыдущих шагов передаются как контекст для текущего ризонинга.
  4. Агент выбирает инструменты на основе описания шага.
  5. Он может изменить подход в середине шага в зависимости от результата инструмента.
# The five market-data tools the ReAct agent chooses from here. The repo's
# TOOLS list carries four more — a skill loader, two CLI wrappers, and a
# restricted in-process Python evaluator — covering three of the five tool
# modalities Part 3 compares. MCP is the fourth, and it lives in a sidecar
# rather than in this list.
TOOLS = [
    get_stock_snapshot,
    get_price_history,
    search_news,
    search_competitors,
    get_financials,
]

def executor_node(state: AgentState) -> dict:
    """Execute the current step using a ReAct agent.

    This is Phase 2 of Plan-and-Execute: adaptive execution of each planned step.
    Each step runs as a mini ReAct loop until completion.
    """

    # Get the current step from the plan
    current_step = state.plan[state.current_step_index]

    # Build context from what we've learned so far
    # This matters: each step builds on previous observations
    previous_context = ""
    for step in state.plan[:state.current_step_index]:
        if step.result:
            previous_context += f"\nStep {step.step_number}: {step.result}\n"

    # Create a ReAct agent for this step
    # This companion example pins LangGraph's deprecated create_react_agent API.
    # Current LangChain guidance recommends create_agent instead:
    # https://reference.langchain.com/python/langgraph.prebuilt/chat_agent_executor/create_react_agent
    # The factory name does not change the Thought-Action-Observation loop:
    # 1. Agent generates a "thought" about what tool to call
    # 2. Agent calls the tool ("action")
    # 3. Tool returns result ("observation")
    # 4. Agent decides: call another tool or finish
    react_agent = create_react_agent(
        model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
        tools=TOOLS,
    )

    # Invoke the ReAct loop for this single step
    # The agent will loop internally until it completes the step
    result = react_agent.invoke({
        "messages": [
            SystemMessage(content=EXECUTOR_SYSTEM_PROMPT),
            HumanMessage(content=f"""Execute Step {current_step.step_number}:
{current_step.description}

Ticker: {state.research_data.ticker}
Previous findings: {previous_context}"""),
        ]
    })

    # Extract the final answer from the ReAct agent's message history
    # The last message contains the synthesis after all tool calls
    updated_plan = list(state.plan)
    updated_plan[state.current_step_index] = PlanStep(
        step_number=current_step.step_number,
        description=current_step.description,
        completed=True,
        result=result["messages"][-1].content,  # Final synthesized answer
    )

    # State update: Mark step complete and advance to next
    return {
        "plan": updated_plan,
        "current_step_index": state.current_step_index + 1,
    }

Паттерн 3: ReWOO для быстрых снэпшотов

Для быстрого брифинга ReWOO убирает вызовы модели из фазы выполнения. Независимые инструменты запускаются параллельно, а зависимые ждут выполнения своих prerequisites. Планировщик заранее выдаёт граф инструментов, а воркер выполняет его, не спрашивая модель, что делать дальше.

Структура выглядит так:

  1. Три фазы (Planner → Worker → Solver), без циклов.
  2. Вызовы инструментов ссылаются на плейсхолдеры #E1, #E2 для ещё не существующих результатов.
  3. Во время выполнения LLM не вызывается. Воркер просто запускает инструменты.
  4. Независимые инструменты выполняются параллельно.
  5. В конце выполняется один вызов для синтеза всех данных.

Фаза 1: планировщик ReWOO (заранее создаёт полный граф выполнения)

class ReWOOPlanStep(BaseModel):
    """A step in the ReWOO plan with variable placeholders.

    Key difference from Plan-and-Execute's PlanStep:
    - Contains actual tool_name and tool_args (not just description)
    - Uses variable references (#E1) for dependencies
    """
    step_id: str  # e.g., "#E1" - becomes a variable
    description: str
    tool_name: str     # Exact tool to call
    tool_args: dict    # May contain variable refs like {"price": "#E1"}
    depends_on: list[str] = []  # For dependency ordering
    result: str | None = None

class ReWOOPlanOutput(BaseModel):
    """Structured output for ReWOO planner."""
    steps: list[ReWOOPlanStep] = Field(description="Planned tool calls with variables")

def rewoo_planner_node(state: AgentState) -> dict:
    """Generate a complete plan of tool calls upfront.

    This is the key difference from Plan-and-Execute: instead of creating
    human-readable step descriptions, we create EXACT tool calls that
    the worker will execute blindly.
    """

    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)

    # Schema-Guided Reasoning ensures valid tool call specifications
    structured_llm = llm.with_structured_output(ReWOOPlanOutput)

    ticker = state.research_data.ticker if state.research_data else "UNKNOWN"
    human = [m for m in state.messages if isinstance(m, HumanMessage)]
    query = human[-1].content if human else f"Analyze {ticker}"

    # Single LLM call to plan ALL tool executions
    result: ReWOOPlanOutput = structured_llm.invoke([
        SystemMessage(content=REWOO_PLANNER_PROMPT),
        HumanMessage(content=f"""Create a ReWOO plan for: {query}

Ticker: {ticker}

Output tool calls with:
- step_id: Variable name (#E1, #E2, etc.)
- description: What this accomplishes
- tool_name: Exact tool from the list
- tool_args: Dictionary of arguments
- depends_on: List of step_ids this depends on"""),
    ])

    # State update: Store the complete execution plan
    # Worker will execute this without any LLM involvement
    return {"rewoo_plan": result.steps}

Фаза 2: воркер ReWOO (выполняет инструменты без ризонинга LLM)

def rewoo_worker_node(state: AgentState) -> dict:
    """Execute dependency-ready tools in parallel batches (no LLM calls).

    Independent tools share a batch. Dependent tools wait until their
    prerequisites complete. The worker follows the dependency graph and
    does not add LLM calls.
    """

    results = {}        # Results keyed by step_id (e.g., "#E1": "$150.23")
    updated_steps = []  # Plan steps with their result field filled in
    pending = {step.step_id: step for step in state.rewoo_plan}

    # Keep scheduling dependency-ready batches until the graph is complete.
    # This handles chains even when the planner does not list them topologically.
    with ThreadPoolExecutor(max_workers=5) as executor:
        while pending:
            ready = [
                step for step in pending.values()
                if all(dep in results for dep in step.depends_on)
            ]
            if not ready:
                unresolved = ", ".join(pending)
                raise ValueError(f"Unresolvable ReWOO dependencies: {unresolved}")

            futures = {
                executor.submit(execute_tool, step, results): step
                for step in ready
            }
            for future in as_completed(futures):
                step = futures[future]
                results[step.step_id] = future.result()
                updated_steps.append(step.model_copy(update={"result": results[step.step_id]}))
                del pending[step.step_id]

    # State update: restore the planner's order (sorting on step_id would put
    # "#E10" before "#E2") and hand the filled-in plan to the Solver
    plan_order = {s.step_id: i for i, s in enumerate(state.rewoo_plan)}
    return {"rewoo_plan": sorted(updated_steps, key=lambda s: plan_order[s.step_id])}

Фаза 3: solver ReWOO (синтезирует все результаты одним вызовом LLM)

def rewoo_solver_node(state: AgentState) -> dict:
    """Synthesize all tool results into a flash briefing.

    This is the second efficiency gain: Instead of interleaving
    LLM calls with tool execution (like ReAct), we make ONE
    final synthesis call with all gathered data.
    """

    # Build context from ALL tool results at once
    tool_results = []
    for step in state.rewoo_plan:
        if step.result:
            tool_results.append(f"### {step.description}\n{step.result}")

    context = "\n\n".join(tool_results)

    # Single LLM call to synthesize everything
    llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
    structured_llm = llm.with_structured_output(FlashBriefingOutput)
    result = structured_llm.invoke([
        SystemMessage(content=REWOO_SOLVER_PROMPT),
        HumanMessage(content=f"Create a flash briefing from this data:\n\n{context}"),
    ])

    # FlashBriefingOutput and DraftReport carry the same fields; the state
    # schema expects DraftReport, so convert before returning.
    return {"draft_report": DraftReport(**result.model_dump())}

У трёх фаз простой контракт: планировщик создаёт исполнимые вызовы, воркер заполняет их плейсхолдеры, а solver получает готовые результаты. Неразрешимый граф вызывает ошибку до запуска solver; после этого политика ретраев LangGraph или ветка обработки сбоев может решить, нужно ли перепланировать выполнение. Этот контракт эффективен только тогда, когда предположения планировщика выдерживают столкновение с инструментами.

Где каждый паттерн вызывает модель

ПаттернВызовы LLM во время выполненияОбновления состоянияКлючевой паттерн кода
Plan-and-Execute1 для планирования + цикл ReAct для каждого шага (по несколько вызовов) + 1 для отчётаПоследовательное завершение шаговplanner_node() → цикл: executor_node()reporter_node()
ReAct (внутри каждого шага)Несколько на шаг (циклы thought-action)Накапливаемая история сообщенийВ закреплённом companion-проекте используется устаревший create_react_agent()
ReWOO1 для планирования + 0 во время выполнения + 1 для синтезаБатчи инструментов с учётом зависимостейrewoo_planner_node()rewoo_worker_node()rewoo_solver_node()

Ключевое различие — выход планировщика. Он определяет, сколько свободы сохраняется у экзекьютора:

  1. Plan-and-Execute создаёт понятные человеку описания шагов:

    # Planner output (list of PlanStep objects)
    plan = [
        PlanStep(
            step_number=1,
            description="Get current price and key financial metrics",
            tool_hint="get_stock_snapshot"
        ),
        PlanStep(
            step_number=2,
            description="Search for recent news and earnings",
            tool_hint="search_news"
        ),
        # ... more steps
    ]

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

  2. У ReAct нет предварительного плана. Он использует итеративный ризонинг:

    # No planning phase - ReAct works step-by-step with accumulated messages
    messages = [
        HumanMessage(content="Execute Step 1: Get current price"),
        AIMessage(content="I'll call get_stock_snapshot"),
        ToolMessage(tool_call_id="1", content="$132.45"),
        AIMessage(content="Now I need metrics..."),
        # ... agent continues until step complete
    ]

    В этой реализации с полной историей каждый вызов модели заново читает историю, которая растёт на протяжении всей задачи. В production-цикле её можно суммаризировать или усекать. Plan-and-Execute также запускает циклы ReAct, но каждый цикл начинается с описания соответствующего шага и дайджеста предыдущих результатов, а не с полной истории вызовов инструментов.

  3. ReWOO создаёт явные исполнимые спецификации вызовов инструментов:

    # Planner output (list of ReWOOPlanStep objects)
    rewoo_plan = [
        ReWOOPlanStep(
            step_id="#E1",
            tool_name="get_stock_snapshot",
            tool_args={"ticker": "NVDA"}
        ),
        ReWOOPlanStep(
            step_id="#E2",
            tool_name="search_news",
            tool_args={"query": "NVDA earnings", "limit": 5}
        ),
        # ... all tool calls planned upfront
    ]

    Воркер работает вслепую, без участия LLM. Все вызовы модели находятся в планировщике и solver, поэтому их количество предсказуемо.

Поток памяти и состояния:

  • Plan-and-Execute: состояние проходит через plancurrent_step_indexresearch_data.
  • ReAct: состояние накапливается в массиве messages — полной истории диалога.
  • ReWOO: состояние проходит через rewoo_plan, а поля result заполняются воркером.

Подключите оба маршрута к одному графу

В графе есть два пользовательских маршрута поверх одного AgentState: глубокое исследование использует Plan-and-Execute с циклом ReAct внутри каждого шага, а флеш-брифинг — ReWOO. Здесь ReAct — примитив выполнения, а не третий маршрут.

В этой реализации нет replanner: исходный план выполняется до конца. Для добавления перепланирования потребуется ребро от executor обратно к planner и правило, определяющее, когда неожиданный результат оправдывает ещё один вызов модели.

LangGraph сохраняет декларативный характер связей:

def create_graph(checkpointer=None):
    builder = StateGraph(AgentState)

    # Add nodes
    builder.add_node("router", router_node)
    builder.add_node("planner", planner_node)
    builder.add_node("executor", executor_node)
    builder.add_node("reporter", reporter_node)
    builder.add_node("rewoo_planner", rewoo_planner_node)
    builder.add_node("rewoo_worker", rewoo_worker_node)
    builder.add_node("rewoo_solver", rewoo_solver_node)
    builder.add_node("evaluator", evaluator_node)
    builder.add_node("publish", publish_node)

    # Define edges
    builder.add_edge(START, "router")
    builder.add_conditional_edges("router", route_after_router, {
        "planner": "planner",
        "rewoo_planner": "rewoo_planner",
    })

    # Deep Research path
    builder.add_edge("planner", "executor")
    builder.add_conditional_edges("executor", route_after_executor, {
        "executor": "executor",  # Loop back for more steps
        "reporter": "reporter",  # Done with plan
    })
    builder.add_edge("reporter", "evaluator")

    # Flash Briefing path (ReWOO)
    builder.add_edge("rewoo_planner", "rewoo_worker")
    builder.add_edge("rewoo_worker", "rewoo_solver")
    builder.add_edge("rewoo_solver", "evaluator")

    # Both paths converge on the same acceptance check
    builder.add_edge("evaluator", "publish")
    builder.add_edge("publish", END)

    return builder.compile(
        checkpointer=checkpointer,
        # Human-in-the-loop pause: the reporter (or the ReWOO solver) writes a
        # draft, a fresh-context evaluator — a second model session with no
        # history of the run — votes on it, and the graph stops before publish.
        # The human approves a report that already carries an evaluator's
        # verdict rather than adjudicating raw research. The verdict is
        # advisory here — the graph routes to the interrupt either way.
        # Part 6 takes up how an acceptance check decides that a run is done.
        interrupt_before=["publish"],
    )

Автоматический выбор паттерна с помощью роутера

Роутер сопоставляет форму запроса с маршрутом. Schema-Guided Reasoning ограничивает выход классификатора:

class ExecutionMode(str, Enum):
    """Execution mode for the agent."""

    DEEP_RESEARCH = "deep_research"  # Plan-and-Execute + ReAct (thorough)
    FLASH_BRIEFING = "flash_briefing"  # ReWOO (fast, token-efficient)

class RouterOutput(BaseModel):
    """Structured output for the router."""

    mode: ExecutionMode  # DEEP_RESEARCH or FLASH_BRIEFING
    ticker: str
    reasoning: str

ROUTER_SYSTEM_PROMPT = """Classify the user's request:

1. **deep_research**: Complex analysis requiring synthesis
   - Examples: "Analyze strategic risks", "investment thesis"

2. **flash_briefing**: Quick snapshots, simple data retrieval
   - Examples: "quick snapshot", "current price"

Default to deep_research if unclear."""

llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
structured_llm = llm.with_structured_output(RouterOutput)

С этим роутером запрос «текущая цена» направляется в ReWOO, а «инвестиционный тезис» — в Plan-and-Execute. Для неоднозначных запросов используется маршрут глубокого исследования. Прежде чем ставить роутер перед пользователями, прогоните один набор задач через оба маршрута и сравните количество вызовов модели, wall time, сбои и поведение при восстановлении.

Полная companion-реализация, включая роутер и общее состояние, находится в закреплённом коммите Market Analyst Agent.

Следующий слой — память

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

Литература