Близорукость пошагового агента

ReAct реактивен: у него нет общей картины, он каждый раз решает «что сделать прямо сейчас». На задаче в 8–10 шагов это выливается в проблемы: агент теряет нить, повторяет действия, не замечает, что половину уже сделал, и тратит мощную модель на каждое мелкое решение. Человек так сложную задачу не решает — он сначала набрасывает план, а потом идёт по пунктам.

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

ℹ️ Deliberative против reactive

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

Три роли: planner, executor, replanner

Паттерн состоит из трёх узлов, каждый со своей задачей:

  • Planner — по запросу строит план: упорядоченный список шагов. Здесь нужна сильная модель: качество плана определяет всё.
  • Executor — берёт первый невыполненный шаг и исполняет его (часто это маленький ReAct-агент с инструментами). Возвращает результат шага.
  • Replanner — смотрит на исходную задачу, план и уже сделанное; решает: обновить план (убрать сделанное, добавить новое) или выдать финальный ответ, если цель достигнута.
100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
START planner строит план execute шаг плана replan обновляет план готово? END план результат да → ответ нет → следующий шаг (план обновлён) State: input · plan: [шаг1, шаг2, …] · past_steps: [(шаг, результат)] · response

Зачем перепланировать

Самая частая ошибка — построить план один раз и тупо исполнить до конца. Так работает хрупкий агент: реальность отклоняется от плана почти всегда. Шаг вернул неожиданный результат, оказался не нужен, открылась новая подзадача — статичный план этого не учитывает. Replanning — то, что отличает рабочий паттерн от наивного.

После каждого исполненного шага репланировщик пересматривает остаток плана с учётом того, что уже узнали:

  • выкинуть шаги, ставшие ненужными (ответ уже получен раньше);
  • добавить шаги, о которых стало известно только сейчас;
  • переформулировать следующий шаг под реальные данные;
  • завершить — если цель достигнута, выдать ответ вместо нового плана.
⚠️ Replanner решает и про остановку

Именно репланировщик отвечает за выход из цикла: пока он возвращает новый план — агент исполняет дальше; как только возвращает финальный ответ — граф идёт в END. Без этого решения цикл execute↔replan не остановится. И, как всегда с циклами, держи recursion_limit как страховку.

Состояние и узлы

Опишем состояние и роли. Для надёжного парсинга плана используем структурированный вывод (Pydantic — урок про state schema): модель обязана вернуть именно список шагов или ответ, а не свободный текст.

Состояние, planner и executor
python
from typing import Annotated, TypedDict
import operator
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

llm = ChatOpenAI(model="gpt-4o-mini")

class State(TypedDict):
    input: str
    plan: list[str]                                  # оставшиеся шаги
    past_steps: Annotated[list[tuple], operator.add] # (шаг, результат) — копится
    response: str                                    # финальный ответ

# --- Planner: структурированный список шагов ---
class Plan(BaseModel):
    steps: list[str]

planner = llm.with_structured_output(Plan)

def plan_step(state: State) -> dict:
    plan = planner.invoke(f"Составь пошаговый план для задачи:\n{state['input']}")
    return {"plan": plan.steps}

# --- Executor: исполняет ПЕРВЫЙ шаг (здесь — мини ReAct-агент) ---
executor = create_react_agent(llm, tools=[])      # сюда дают инструменты

def execute_step(state: State) -> dict:
    task = state["plan"][0]
    result = executor.invoke({"messages": [{"role": "user", "content": task}]})
    answer = result["messages"][-1].content
    return {"past_steps": [(task, answer)]}        # reducer допишет
Replanner и сборка цикла
python
from typing import Union
from langgraph.graph import StateGraph, START, END

# Replanner возвращает ЛИБО новый план, ЛИБО финальный ответ
class Response(BaseModel):
    response: str

class Act(BaseModel):
    action: Union[Plan, Response]      # план = продолжить, ответ = закончить

replanner = llm.with_structured_output(Act)

def replan_step(state: State) -> dict:
    out = replanner.invoke(
        f"Задача: {state['input']}\n"
        f"Исходный план: {state['plan']}\n"
        f"Уже сделано: {state['past_steps']}\n"
        f"Обнови план оставшихся шагов. Если задача решена — верни ответ."
    )
    if isinstance(out.action, Response):
        return {"response": out.action.response}   # закончили
    return {"plan": out.action.steps}              # новый план

def should_end(state: State) -> str:
    return "end" if state.get("response") else "execute"

builder = StateGraph(State)
builder.add_node("planner", plan_step)
builder.add_node("execute", execute_step)
builder.add_node("replan", replan_step)

builder.add_edge(START, "planner")
builder.add_edge("planner", "execute")
builder.add_edge("execute", "replan")
builder.add_conditional_edges("replan", should_end,
    {"execute": "execute", "end": END})            # план → исполнять / ответ → END

graph = builder.compile()
result = graph.invoke({"input": "Кто тренер победителя Australian Open 2024 у мужчин?"})
print(result["response"])

Plan-and-Execute против ReAct

Критерий ReAct Plan-and-Execute
Горизонт планирования один шаг (реактивно) весь маршрут наперёд
Длинные многошаговые задачи теряет цель, блуждает держит план перед глазами
Стоимость планирующей модели сильная модель на КАЖДОМ шаге сильная — только в planner/replan
Гибкость в полной неопределённости выше — решает по ситуации план может устаревать (спасает replan)
Прозрачность цепочка действий явный план — видно намерение
Экономия на разделении ролей

Классический приём: planner и replanner — сильная модель (хорошо думают стратегически), а executor — дешёвая и быстрая (его шаги простые и конкретные). Так платишь за «ум» только там, где он нужен, а массовое исполнение делаешь дёшево. Часто executor — это create_react_agent из прошлого урока с нужными инструментами.

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

Ошибка 1: план без перепланирования

Построить план один раз и слепо исполнить — хрупко: реальность отклонится, и агент будет делать ненужное или упустит новое. Replanner после каждого шага — суть паттерна.

Ошибка 2: replanner не умеет завершать

Если репланировщик всегда возвращает план и никогда — ответ, цикл не остановится. Дай ему выбор «план ИЛИ финальный ответ» (структурированный union) и явно проверяй response.

Ошибка 3: план как свободный текст

Парсить шаги из произвольного ответа модели хрупко. Используй структурированный вывод (Pydantic-схему Plan) — гарантированно получишь список шагов.

Ошибка 4: Plan-and-Execute на простую задачу

Для короткого вопроса три роли и цикл планирования — оверкилл и лишняя задержка. Берут паттерн на длинные многошаговые задачи; для простых хватает ReAct.

Шпаргалка

Plan-and-Execute — всё в одном месте
python
# Состояние: задача + план + сделанное + ответ
class State(TypedDict):
    input: str
    plan: list[str]
    past_steps: Annotated[list[tuple], operator.add]
    response: str

# 3 роли:
#  planner  → строит план  (структурированный вывод: Plan.steps)
#  execute  → исполняет plan[0]  (часто create_react_agent с инструментами)
#  replan   → План ИЛИ Response  (Union — продолжить или закончить)

def should_end(state):
    return "end" if state.get("response") else "execute"

# Граф: planner → execute → replan → (execute | END)
builder.add_edge(START, "planner")
builder.add_edge("planner", "execute")
builder.add_edge("execute", "replan")
builder.add_conditional_edges("replan", should_end,
    {"execute": "execute", "end": END})

# Правила:
#  • ОБЯЗАТЕЛЬНО replanning после каждого шага
#  • replanner умеет вернуть финальный ответ (иначе вечный цикл)
#  • план — структурированный (Pydantic), не свободный текст
#  • planner/replan — сильная модель, executor — дешёвая
#  • паттерн для ДЛИННЫХ задач; для простых — ReAct

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

Собери планирующего агента:

Задание: Plan-and-Execute с инструментом поиска

  1. Опиши состояние input / plan / past_steps / response и три узла. Для plan используй структурированный вывод Plan(steps: list[str]).
  2. Сделай executor через create_react_agent с инструментом поиска (можно заглушкой) и заставь его исполнять plan[0].
  3. В replan верни union Plan | Response; заверши, когда задача решена. Свяжи цикл execute → replan → (execute|END).
  4. Дай многошаговую задачу (например, «найди X, затем посчитай Y на основе X») и через stream посмотри, как меняется plan от итерации к итерации.
  5. Добавь recursion_limit и убедись, что он ловит случай, когда replanner «застрял» и не завершает.
  6. Со звёздочкой: сделай planner/replanner на сильной модели, а executor — на дешёвой, и сравни стоимость с чистым ReAct на той же задаче.

Что дальше