Близорукость пошагового агента
ReAct реактивен: у него нет общей картины, он каждый раз решает «что сделать прямо сейчас». На задаче в 8–10 шагов это выливается в проблемы: агент теряет нить, повторяет действия, не замечает, что половину уже сделал, и тратит мощную модель на каждое мелкое решение. Человек так сложную задачу не решает — он сначала набрасывает план, а потом идёт по пунктам.
Plan-and-Execute переносит это в агента и разделяет «думать стратегически» и «делать» на разные шаги. Появляется явный артефакт — план, список шагов, который виден, проверяем и обновляем. Агент держит цель перед глазами и не блуждает.
ReAct — реактивный агент: решение на каждом шаге по обстановке. Plan-and-Execute — совещательный (deliberative): сначала обдумал маршрут целиком, потом исполняет. Первый гибче в неопределённости, второй устойчивее и дешевле на длинных предсказуемых задачах.
Три роли: planner, executor, replanner
Паттерн состоит из трёх узлов, каждый со своей задачей:
- Planner — по запросу строит план: упорядоченный список шагов. Здесь нужна сильная модель: качество плана определяет всё.
- Executor — берёт первый невыполненный шаг и исполняет его (часто это маленький ReAct-агент с инструментами). Возвращает результат шага.
- Replanner — смотрит на исходную задачу, план и уже сделанное; решает: обновить план (убрать сделанное, добавить новое) или выдать финальный ответ, если цель достигнута.
Зачем перепланировать
Самая частая ошибка — построить план один раз и тупо исполнить до конца. Так работает хрупкий агент: реальность отклоняется от плана почти всегда. Шаг вернул неожиданный результат, оказался не нужен, открылась новая подзадача — статичный план этого не учитывает. Replanning — то, что отличает рабочий паттерн от наивного.
После каждого исполненного шага репланировщик пересматривает остаток плана с учётом того, что уже узнали:
- выкинуть шаги, ставшие ненужными (ответ уже получен раньше);
- добавить шаги, о которых стало известно только сейчас;
- переформулировать следующий шаг под реальные данные;
- завершить — если цель достигнута, выдать ответ вместо нового плана.
Именно репланировщик отвечает за выход из цикла: пока он возвращает новый план — агент исполняет дальше; как только возвращает финальный ответ — граф идёт в END. Без этого решения цикл execute↔replan не остановится. И, как всегда с циклами, держи recursion_limit как страховку.
Состояние и узлы
Опишем состояние и роли. Для надёжного парсинга плана используем структурированный вывод (Pydantic — урок про state schema): модель обязана вернуть именно список шагов или ответ, а не свободный текст.
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 допишет
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 из прошлого урока с нужными инструментами.
Типичные ошибки
Построить план один раз и слепо исполнить — хрупко: реальность отклонится, и агент будет делать ненужное или упустит новое. Replanner после каждого шага — суть паттерна.
Если репланировщик всегда возвращает план и никогда — ответ, цикл не остановится. Дай ему выбор «план ИЛИ финальный ответ» (структурированный union) и явно проверяй response.
Парсить шаги из произвольного ответа модели хрупко. Используй структурированный вывод (Pydantic-схему Plan) — гарантированно получишь список шагов.
Для короткого вопроса три роли и цикл планирования — оверкилл и лишняя задержка. Берут паттерн на длинные многошаговые задачи; для простых хватает ReAct.
Шпаргалка
# Состояние: задача + план + сделанное + ответ
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 с инструментом поиска
- Опиши состояние
input / plan / past_steps / responseи три узла. Дляplanиспользуй структурированный выводPlan(steps: list[str]). - Сделай
executorчерезcreate_react_agentс инструментом поиска (можно заглушкой) и заставь его исполнятьplan[0]. - В
replanверни unionPlan | Response; заверши, когда задача решена. Свяжи циклexecute → replan → (execute|END). - Дай многошаговую задачу (например, «найди X, затем посчитай Y на основе X») и через
streamпосмотри, как меняетсяplanот итерации к итерации. - Добавь
recursion_limitи убедись, что он ловит случай, когда replanner «застрял» и не завершает. - Со звёздочкой: сделай planner/replanner на сильной модели, а executor — на дешёвой, и сравни стоимость с чистым ReAct на той же задаче.