Проблема: когда реактивности недостаточно

Попросите ReAct-агента написать аналитический отчёт: «Сравни продажи нашей компании по кварталам за 2024 и 2023 годы, выяви тренды, найди лучший и худший продукт, дай рекомендации». Что происходит:

  • Агент не знает, сколько шагов потребуется — и вы тоже не знаете
  • Каждый запрос данных выполняется последовательно, хотя Q1–Q4 за оба года можно было запросить параллельно
  • Если агент зашёл в тупик на шаге 7 из 10, непонятно, сколько работы уже сделано
  • Нельзя показать пользователю прогресс-бар или дать оценку времени
  • Если план изначально неоптимален — поздно это заметить

Корень проблемы: ReAct смешивает планирование и исполнение. Каждый шаг — одновременно и «решение, что делать дальше», и «само действие». Для исследовательских задач это достоинство. Для задач с известной структурой — ограничение.

Plan-and-Execute разделяет эти обязанности. Планировщик думает о задаче целиком и создаёт список шагов. Executor просто выполняет каждый шаг, не думая о стратегии. Результат: предсказуемость, параллелизм, возможность ревизии плана до старта.

📄 Откуда паттерн

Plan-and-Execute (также называемый Plan-then-Act) появился в нескольких работах 2023 года как ответ на ограничения чистого ReAct. В частности, «Plan-and-Solve Prompting» (Wang et al., 2023) показал, что явное планирование перед исполнением снижает ошибки при многошаговых математических и логических задачах.

Архитектура: две роли, две фазы

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

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
① ПЛАНИРОВАНИЕ — один LLM-вызов создаёт полный план Задача user message 🧠 Планировщик один LLM-вызов tool_choice: "tool" → структурный план Структурный план ① Получить данные по продажам 2024 ② Получить данные по продажам 2023 ③ Рассчитать рост и выявить тренды ② ИСПОЛНЕНИЕ — отдельный executor для каждого шага ⚙️ Executor LLM + инструменты Результат ① данные продаж за 2024 год ⚙️ Executor контекст: Результат ① Результат ② данные продаж за 2023 год ⚙️ Executor контекст: Результаты ①② Результат ③ рост, тренды, рекомендации ③ СИНТЕЗ — финальный LLM-вызов ✅ Финальный ответ синтез всех результатов шагов

Две роли — два разных промпта

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

  • Планировщик — думает стратегически. Не нужен доступ к инструментам. Нужна хорошая способность к декомпозиции. Промпт: «Разбей задачу на конкретные, независимые шаги». Может использовать более лёгкую модель.
  • Executor — действует тактически. Видит один шаг + накопленный контекст. Нужен доступ к инструментам. Реализует mini-ReAct для выполнения шага. Промпт: «Выполни конкретный шаг, используй инструменты».
  • Синтезатор — (опционально) отдельный LLM-вызов, который объединяет все результаты в финальный ответ. Получает: исходную задачу + результаты всех шагов.

Разделение ролей означает, что каждый LLM-вызов решает одну узкую задачу — и решает её лучше, чем если бы он делал всё сразу.

💡 Можно использовать разные модели

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

Фаза планирования: структурированный вывод

Планировщик должен вернуть не просто текст, а машиночитаемый список шагов. Иначе придётся парсить свободный текст — хрупко и ненадёжно. Решение: использовать tool_choice: "tool", чтобы Claude заполнил JSON Schema плана.

Планировщику нужно знать:

  • Что нужно сделать (задача)
  • Какие инструменты доступны (чтобы создавать реалистичные шаги)
  • Ограничения (максимум шагов, что можно параллелить)

JSON Schema плана с зависимостями

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

python
"""plan_and_execute.py — Plan-and-Execute агент"""
import anthropic
from dataclasses import dataclass, field

client = anthropic.Anthropic()

# ── Схема плана ──────────────────────────────────────────────────────────────

# Инструмент-планировщик: модель заполняет структуру, мы читаем block.input
PLAN_TOOL = {
    "name": "create_plan",
    "description": "Создаёт структурированный план выполнения задачи шагами",
    "input_schema": {
        "type": "object",
        "properties": {
            "goal": {
                "type": "string",
                "description": "Цель задачи — одно предложение"
            },
            "steps": {
                "type": "array",
                "description": "Шаги для достижения цели, в порядке исполнения",
                "items": {
                    "type": "object",
                    "properties": {
                        "id": {
                            "type": "integer",
                            "description": "Порядковый номер шага (начиная с 1)"
                        },
                        "description": {
                            "type": "string",
                            "description": "Что конкретно нужно сделать в этом шаге"
                        },
                        "tool_hint": {
                            "type": "string",
                            "description": "Какой инструмент, вероятно, потребуется (необязательно)"
                        },
                        "depends_on": {
                            "type": "array",
                            "items": {"type": "integer"},
                            "description": "Номера шагов, результаты которых нужны для этого шага"
                        }
                    },
                    "required": ["id", "description", "depends_on"]
                }
            }
        },
        "required": ["goal", "steps"]
    }
}

@dataclass
class Step:
    id: int
    description: str
    depends_on: list[int]
    tool_hint: str = ""
    result: str = ""

@dataclass
class Plan:
    goal: str
    steps: list[Step]
python
# ── Планировщик ──────────────────────────────────────────────────────────────

PLANNER_SYSTEM = """Ты — планировщик задач для AI-агента.

Разбивай задачу на конкретные, выполнимые шаги. Каждый шаг:
- Решает одну конкретную подзадачу
- Описан как действие: «Получить...», «Рассчитать...», «Найти...»
- Имеет явные зависимости (depends_on) от других шагов

Доступные инструменты агента: search (поиск), calculate (вычисления), get_data (данные).
Указывай depends_on: [] если шаг не зависит от других — это позволит запустить его параллельно."""

def create_plan(task: str) -> Plan:
    """Планировщик: один LLM-вызов → структурированный план."""
    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=2048,
        system=PLANNER_SYSTEM,
        tools=[PLAN_TOOL],
        tool_choice={"type": "tool", "name": "create_plan"},
        messages=[{"role": "user", "content": task}],
    )

    for block in response.content:
        if block.type == "tool_use" and block.name == "create_plan":
            raw = block.input
            steps = [
                Step(
                    id=s["id"],
                    description=s["description"],
                    depends_on=s.get("depends_on", []),
                    tool_hint=s.get("tool_hint", ""),
                )
                for s in raw["steps"]
            ]
            return Plan(goal=raw["goal"], steps=steps)

    raise RuntimeError("Планировщик не вернул план")

После вызова create_plan() у вас есть полный план ещё до начала исполнения. Его можно показать пользователю, залогировать или проверить вручную перед запуском.

Вот как выглядит сгенерированный план для задачи «Сравни продажи 2024 и 2023»:

Сгенерированный план · 4 шага · цель: анализ динамики продаж
Получить данные о продажах по кварталам за 2024 год
depends_on: []
Получить данные о продажах по кварталам за 2023 год
depends_on: []
Рассчитать процентный рост по каждому кварталу, найти лучший и худший
depends_on: [1, 2]
calculate
Сформулировать выводы и рекомендации на основе анализа
depends_on: [3]
синтез

Обратите внимание: шаги ① и ② не зависят друг от друга (depends_on: []). Это значит их можно выполнить параллельно — и сократить время вдвое.

Фаза исполнения: executor для каждого шага

Executor — это mini-ReAct агент, который видит только один шаг из плана и накопленный контекст. Он не думает о стратегии всей задачи — только о своём шаге.

Executor как mini-ReAct

Каждый шаг может потребовать нескольких вызовов инструментов. Поэтому executor — не одиночный LLM-вызов, а маленький цикл:

python
# ── Инструменты (заглушки) ───────────────────────────────────────────────────

TOOLS = [
    {
        "name": "search",
        "description": "Поиск информации в интернете",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"]
        }
    },
    {
        "name": "calculate",
        "description": "Вычисляет математическое выражение Python",
        "input_schema": {
            "type": "object",
            "properties": {"expression": {"type": "string"}},
            "required": ["expression"]
        }
    }
]

def execute_tool(name: str, args: dict) -> str:
    if name == "calculate":
        try:
            return str(eval(args["expression"]))  # noqa: S307
        except Exception as e:
            return f"Ошибка: {e}"
    if name == "search":
        return f"[Заглушка] Результаты поиска: {args['query']}"
    return f"Неизвестный инструмент: {name}"


# ── Executor ──────────────────────────────────────────────────────────────────

EXECUTOR_SYSTEM = """Ты — исполнитель конкретного шага в многошаговой задаче.

Тебе даётся один шаг и контекст выполненных предыдущих шагов.
Выполни ТОЛЬКО этот шаг, используя инструменты при необходимости.
Когда шаг выполнен — дай чёткий, конкретный ответ."""

def execute_step(step: Step, context: str, max_iterations: int = 5) -> str:
    """
    Executor: выполняет один шаг плана.
    Это mini-ReAct: LLM ↔ инструменты до получения результата.
    """
    task_prompt = f"Выполни шаг: {step.description}"
    if context:
        task_prompt += f"\n\nКонтекст предыдущих шагов:\n{context}"

    messages = [{"role": "user", "content": task_prompt}]

    for _ in range(max_iterations):
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=2048,
            system=EXECUTOR_SYSTEM,
            tools=TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            # Шаг выполнен — вернуть текст ответа
            for block in response.content:
                if hasattr(block, "text") and block.text:
                    return block.text.strip()
            return "(шаг выполнен)"

        # Выполнить инструменты
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result,
                })
        messages.append({"role": "user", "content": tool_results})

    return "(достигнут лимит итераций для шага)"

Накопление контекста между шагами

Каждый следующий шаг видит результаты всех предыдущих. Это критично: шаг «рассчитать рост» не может работать без данных из шагов «получить продажи 2024» и «получить продажи 2023».

Вот как выглядит трасса исполнения для нашего примера:

Получить данные о продажах за 2024 год
💭 Использую инструмент поиска для получения данных...
⚙️ search("продажи компании Q1 Q2 Q3 Q4 2024")
→ Q1: 1.2M, Q2: 1.5M, Q3: 1.8M, Q4: 2.1M. Итого: 6.6M
Получить данные о продажах за 2023 год
⚙️ search("продажи компании Q1 Q2 Q3 Q4 2023")
→ Q1: 1.0M, Q2: 1.2M, Q3: 1.4M, Q4: 1.6M. Итого: 5.2M
Рассчитать рост по кварталам
💭 Контекст: 2024 = [1.2, 1.5, 1.8, 2.1]M, 2023 = [1.0, 1.2, 1.4, 1.6]M
⚙️ calculate("(1.2-1.0)/1.0*100")20.0%
⚙️ calculate("(1.5-1.2)/1.2*100")25.0%
⚙️ calculate("(6.6-5.2)/5.2*100")26.9%
→ Рост Q1: +20%, Q2: +25%, Q3: +28.6%, Q4: +31.3%. Итого: +26.9%

Фаза синтеза: объединение результатов

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

python
def synthesize(task: str, plan: Plan) -> str:
    """Синтез: собирает результаты всех шагов в финальный ответ."""
    steps_summary = "\n\n".join(
        f"Шаг {s.id}: {s.description}\nРезультат: {s.result}"
        for s in plan.steps
    )

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=4096,
        system="Ты — аналитик. Синтезируй финальный ответ на основе результатов всех шагов.",
        messages=[{
            "role": "user",
            "content": (
                f"Задача: {task}\n\n"
                f"Цель плана: {plan.goal}\n\n"
                f"Результаты шагов:\n{steps_summary}\n\n"
                f"Сформулируй полный финальный ответ."
            )
        }]
    )

    for block in response.content:
        if hasattr(block, "text") and block.text:
            return block.text.strip()
    return "(нет ответа)"

Синтез — отдельный LLM-вызов без инструментов. Это важно: к этому моменту все данные уже собраны, синтезатору нужно только грамотно их структурировать.

Полная реализация: оркестратор

Собираем три фазы в единый оркестратор:

python
def plan_and_execute(task: str) -> str:
    """
    Plan-and-Execute оркестратор.
    Фаза 1: создать план → Фаза 2: выполнить шаги → Фаза 3: синтезировать ответ.
    """
    # ── Фаза 1: Планирование ─────────────────────────────────────────────────
    print("🗺️  Планирование...")
    plan = create_plan(task)
    print(f"  Цель: {plan.goal}")
    print(f"  Шагов: {len(plan.steps)}")
    for s in plan.steps:
        dep_str = f" (зависит от: {s.depends_on})" if s.depends_on else " (независимый)"
        print(f"  {s.id}. {s.description}{dep_str}")

    # ── Фаза 2: Исполнение ───────────────────────────────────────────────────
    print("\n⚙️  Исполнение шагов...")
    context_parts: list[str] = []

    for step in plan.steps:
        print(f"\n  Шаг {step.id}: {step.description}")
        # Передаём контекст: результаты всех предыдущих шагов
        context = "\n\n".join(context_parts) if context_parts else ""
        step.result = execute_step(step, context)
        context_parts.append(f"Шаг {step.id} [{step.description}]:\n{step.result}")
        print(f"  ✓ {step.result[:120]}{'...' if len(step.result) > 120 else ''}")

    # ── Фаза 3: Синтез ───────────────────────────────────────────────────────
    print("\n📝 Синтез финального ответа...")
    answer = synthesize(task, plan)
    return answer


if __name__ == "__main__":
    task = (
        "Сравни продажи компании по кварталам за 2024 и 2023 годы. "
        "Вычисли рост по каждому кварталу, выяви тренд."
    )
    print(f"Задача: {task}\n")
    result = plan_and_execute(task)
    print(f"\n{'═' * 60}")
    print(f"✅ Финальный ответ:\n{result}")
Три LLM-вызова уровня задачи + N executor-вызовов

На уровне оркестратора: 1 вызов планировщика + N вызовов executor (один на шаг) + 1 вызов синтезатора. Внутри каждого executor-а может быть несколько вызовов (mini-ReAct). Контролируйте глубину через max_iterations в execute_step().

Репланирование: адаптация плана в процессе

Что если шаг вернул неожиданный результат — например, данных за 2023 год нет в системе, и агент нашёл только агрегированные данные за год? Первоначальный план (сравнение по кварталам) больше не работает. Нужно перепланировать.

Репланирование — опциональный шаг: после выполнения каждого N-го шага (или при обнаружении критического отклонения) запускаем планировщик снова с актуальным контекстом.

python
REPLAN_TOOL = {
    "name": "update_plan",
    "description": "Обновляет оставшиеся шаги плана с учётом полученных данных",
    "input_schema": {
        "type": "object",
        "properties": {
            "needs_replanning": {
                "type": "boolean",
                "description": "True если план нужно изменить"
            },
            "reason": {
                "type": "string",
                "description": "Почему нужно перепланировать"
            },
            "updated_steps": {
                "type": "array",
                "description": "Обновлённые оставшиеся шаги (если needs_replanning=True)",
                "items": {
                    "type": "object",
                    "properties": {
                        "id": {"type": "integer"},
                        "description": {"type": "string"},
                        "depends_on": {"type": "array", "items": {"type": "integer"}}
                    },
                    "required": ["id", "description", "depends_on"]
                }
            }
        },
        "required": ["needs_replanning"]
    }
}

def maybe_replan(
    original_task: str,
    plan: Plan,
    completed_steps: list[Step],
    remaining_steps: list[Step],
) -> list[Step]:
    """
    Проверяет, нужно ли обновить план.
    Возвращает либо оригинальные оставшиеся шаги, либо обновлённые.
    """
    context = "\n".join(
        f"Шаг {s.id}: {s.description} → {s.result[:300]}"
        for s in completed_steps
    )
    remaining = "\n".join(
        f"{s.id}. {s.description}" for s in remaining_steps
    )

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=1024,
        system="Оцени, нужно ли изменить оставшиеся шаги плана с учётом новых данных.",
        tools=[REPLAN_TOOL],
        tool_choice={"type": "auto"},  # модель сама решает, вызывать ли инструмент
        messages=[{
            "role": "user",
            "content": (
                f"Задача: {original_task}\n\n"
                f"Выполненные шаги:\n{context}\n\n"
                f"Оставшиеся шаги:\n{remaining}\n\n"
                f"Нужно ли изменить план? Вызови update_plan если да."
            )
        }]
    )

    for block in response.content:
        if block.type == "tool_use" and block.name == "update_plan":
            data = block.input
            if data.get("needs_replanning") and data.get("updated_steps"):
                print(f"  ↻ Репланирование: {data.get('reason', '')}")
                return [
                    Step(
                        id=s["id"],
                        description=s["description"],
                        depends_on=s.get("depends_on", []),
                    )
                    for s in data["updated_steps"]
                ]

    return remaining_steps  # план не изменился
⚠️ Не репланируйте после каждого шага

Репланирование — дорогой LLM-вызов. Делайте его только при значимых отклонениях: после первого шага, после шага, который нашёл данных значительно меньше ожидаемого, или при явной ошибке выполнения. Триггер «каждые N шагов» — хорошее правило.

Параллельное исполнение независимых шагов

Главное преимущество явного плана с depends_on — возможность параллельного исполнения. Шаги ① и ② из нашего примера не зависят друг от друга и могут выполняться одновременно.

Независимые шаги (depends_on: [])
  • Запускаются параллельно через asyncio.gather()
  • Время выполнения = max(шаги) вместо sum(шаги)
  • Типичный пример: сбор данных из нескольких источников
Зависимые шаги (depends_on: [...])
  • Ждут результатов указанных шагов
  • Получают в контексте только нужные результаты
  • Типичный пример: расчёт, анализ, синтез
python
import asyncio

async def execute_step_async(step: Step, context: str) -> str:
    """Async-обёртка executor-а для параллельного запуска."""
    return await asyncio.to_thread(execute_step, step, context)


async def plan_and_execute_parallel(task: str) -> str:
    """
    Plan-and-Execute с параллельным исполнением независимых шагов.
    Использует depends_on для определения порядка.
    """
    plan = create_plan(task)
    completed: dict[int, str] = {}  # id шага → результат

    # Обрабатываем шаги волнами: каждая волна — шаги, чьи зависимости выполнены
    while len(completed) < len(plan.steps):
        # Найти все шаги, которые можно запустить сейчас
        ready = [
            s for s in plan.steps
            if s.id not in completed
            and all(dep in completed for dep in s.depends_on)
        ]

        if not ready:
            # Зависимый граф нарушен — запустить оставшиеся принудительно
            ready = [s for s in plan.steps if s.id not in completed]

        print(f"  Запуск параллельно: {[s.id for s in ready]}")

        # Сформировать контекст для каждого шага из его зависимостей
        async def run_with_context(step: Step) -> tuple[int, str]:
            ctx = "\n\n".join(
                f"Шаг {dep_id}: {completed[dep_id]}"
                for dep_id in step.depends_on
                if dep_id in completed
            )
            result = await execute_step_async(step, ctx)
            return step.id, result

        # Выполнить все готовые шаги параллельно
        results = await asyncio.gather(*[run_with_context(s) for s in ready])
        for step_id, result in results:
            completed[step_id] = result
            # Сохранить результат в шаг для синтеза
            for s in plan.steps:
                if s.id == step_id:
                    s.result = result

    return synthesize(task, plan)


# Запуск: asyncio.run(plan_and_execute_parallel(task))

Алгоритм «волн» автоматически определяет, какие шаги можно запускать параллельно. Сначала выполняются все шаги без зависимостей, затем те, чьи зависимости выполнены, и так далее — до завершения всего плана.

💡 Реальный выигрыш от параллелизма

В задаче из примера: 4 шага, шаги ①② занимают по 2–3 секунды каждый. Последовательно: 4–6 с. Параллельно: 2–3 с. При более сложных планах с 6–8 независимыми шагами выигрыш ещё больше. Это особенно важно для агентов с реальными поисковыми инструментами.

Plan-and-Execute vs ReAct: когда что применять

Характеристика Plan-and-Execute ReAct
Стратегия Сначала план целиком, потом исполнение Реактивно: следующий шаг из текущего результата
Параллелизм Легко — независимые шаги явно помечены Сложно — нет знания о будущих шагах
Прогресс Видим — «шаг 3 из 5 выполнен» Непредсказуем — неизвестно, сколько шагов осталось
Гибкость Средняя — план фиксирован, нужен реплан Высокая — адаптируется к каждому результату
Дебаг Проще — план можно проверить до исполнения Сложнее — видно только в ходе работы
Кол-во LLM-вызовов Больше — план + N executor + синтез Меньше — один цикл без лишних вызовов
Задачи Структурированные с понятными этапами Исследовательские, непредсказуемые
Plan-and-Execute
  • Задача явно делится на подзадачи заранее
  • Нужно показывать прогресс пользователю
  • Есть независимые этапы для параллелизма
  • Важно проверить план перед запуском
  • Нужны разные модели/конфигурации для разных шагов
ReAct
  • Структура задачи неизвестна заранее
  • Каждый следующий шаг зависит от предыдущего результата
  • Задача исследовательская, открытая
  • Нужно минимизировать число LLM-вызовов
  • Задача решается за 2–4 шага (накладные расходы не оправданы)

Типичные ошибки

1. Планировщик не знает о доступных инструментах
Если планировщику не передать список инструментов, он создаёт шаги типа «проверить базу данных» или «вызвать внешний API», которые executor не сможет выполнить реальными инструментами. Несоответствие плана реальным возможностям — источник бесполезных шагов.
✓ Включите в системный промпт планировщика описание доступных инструментов: названия и что они делают.
2. Слишком гранулированный или слишком укрупнённый план
Шаги «получить данные» + «проверить данные» + «сохранить данные» — три шага там, где достаточно одного. Или наоборот: «написать полный отчёт» — один шаг там, где нужны пять. Оба варианта ломают ценность планирования.
✓ В системный промпт добавьте: «Каждый шаг — один конкретный инструментальный вызов или одна логическая единица работы». Укажите желаемый диапазон шагов.
3. Игнорирование зависимостей при параллелизме
Запуск всех шагов параллельно без учёта depends_on — шаг «рассчитать рост» получает пустой контекст, потому что данные за 2024 и 2023 ещё не собраны. Результат — галлюцинации или ошибки расчётов.
✓ Используйте алгоритм «волн»: сначала все независимые шаги, затем те, чьи зависимости выполнены.
4. Контекст всех шагов — не всегда нужен
Некоторые реализации передают executor-у весь накопленный контекст всех предыдущих шагов. Для шага 8 из 10 это может быть огромный текст, где большая часть нерелевантна. Контекст растёт, стоимость растёт.
✓ Передавайте executor-у только результаты шагов, указанных в depends_on, а не весь накопленный контекст.
5. Синтезатор без исходного задания
Если синтезатор получает только результаты шагов без исходной задачи, он иногда формулирует ответ на то, что делали шаги, а не на то, что хотел пользователь. Особенно заметно, когда план был перепланирован и исходный вопрос стал неочевиден.
✓ Всегда включайте исходную задачу и цель плана (plan.goal) в промпт синтезатора.

Шпаргалка

Три фазы Plan-and-Execute:

  • Планировщик — один LLM-вызов с tool_choice: "tool", возвращает JSON список шагов с depends_on
  • Executor — mini-ReAct для каждого шага; видит шаг + контекст зависимостей
  • Синтез — финальный LLM-вызов без инструментов; получает задачу + все результаты

Параллелизм:

  • Шаги с depends_on: [] — независимые, запускать параллельно через asyncio.gather()
  • Алгоритм «волн»: находить все готовые шаги → выполнить параллельно → повторить
  • Executor-у передавать только результаты шагов из depends_on, не весь контекст

Системные промпты:

  • Планировщик: знает инструменты, мыслит стратегически, создаёт конкретные шаги-действия
  • Executor: знает только свой шаг и контекст, действует тактически
  • Синтезатор: получает цель + все результаты, формулирует финальный ответ

Plan-and-Execute лучше ReAct, когда: структура задачи известна заранее, есть независимые этапы для параллелизма, нужен прогресс-бар или предсказуемое время работы.

Практическое задание

Задача 1. Реализуйте create_plan() и запустите его на трёх разных задачах: (а) «напиши письмо-извинение клиенту», (б) «найди топ-5 библиотек для работы с PDF в Python», (в) «составь план обучения Django за 2 недели». Сравните планы: сколько шагов, есть ли независимые, разумны ли они? Настройте системный промпт планировщика, пока планы не станут качественными.

Задача 2. Добавьте в оркестратор прогресс-репортинг: перед запуском выведите план пользователю с предложением «Продолжить? [y/N]». После каждого выполненного шага выводите: «✓ Шаг N/M: [описание] — завершён». Это превратит агента в интерактивный инструмент с понятным статусом.

Задача 3 (продвинутая). Реализуйте механизм репланирования: после каждого 2-го выполненного шага вызывайте maybe_replan(). Создайте тест с задачей, где одна из «зависимостей данных» намеренно недоступна (инструмент возвращает «данные не найдены»). Проверьте, что maybe_replan() корректно предлагает адаптированный план: либо пропустить шаг, либо заменить источник данных.