Циклы ризонинга ИИ-агентов: ReAct, ReWOO, Plan-and-Execute
Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.
Обновление статьи
Первоначально опубликовано 31 января 2026 года. Проверено и обновлено 6 сентября 2026 года. Обновление посвящено новым возможностям моделей и reasoning API; примеры и ссылки на источники переработаны.
Цикл ризонинга агента — это управляющий поток, который определяет, когда модель планирует, вызывает инструмент, читает результат и останавливается. Для инженера, создающего агента, этот выбор — одновременно и бюджет: он определяет, как часто запускается модель, какой объём истории передаётся в каждом вызове и может ли неожиданный результат изменить следующее действие.
В этой статье сравниваются ReAct, ReWOO и Plan-and-Execute на примере LangGraph Market Analyst Agent, которого я разработал. В итоге у вас будет правило роутинга и формы реализации, которые можно адаптировать, а не просто три названия для добавления на диаграмму.
Цикл — самый внутренний слой этой серии. Его окружают память, инструменты, безопасность, рантайм и проверки перед объявлением задачи завершённой; они не заменяют сам цикл.
Краткое сравнение фреймворков см. в статье Лучшие фреймворки для ИИ-агентов в 2026 году.
Цикл ризонинга определяет, что делать дальше. Он не хранит состояние, не выполняет инструменты и не авторизует побочные эффекты.
Харнесс — это управляющая программа между моделью и машиной. Она собирает промпты из сохранённого состояния (часть 2), определяет действия, которые модель может назвать (часть 3), авторизует вызовы (часть 4) и проверяет подтверждения перед объявлением задачи завершённой (часть 6). Это отдельные инженерные задачи, но один ход проходит через все четыре.
Рантайм (часть 5) предоставляет лог сессии, сэндбокс, хранилище чекпоинтов и трейсы, которые переживают один процесс воркера.
Каждая статья самодостаточна. Вместе они движутся от цикла к внешним слоям.
Начинайте с границы отказа
Хороший промпт не решает вопрос управляющего потока. Паттерны различаются тем, какой объём работы фиксируется до первого вызова инструмента. Это определяет количество вызовов модели, момент обнаружения ошибочного плана и возможность перенаправить выполнение после неожиданного результата инструмента.
Три паттерна ризонинга ИИ-агентов
ReAct: принимайте решение после каждого наблюдения
ReAct (Yao et al., 2022), сокращение от Reason + Act, удерживает следующее решение рядом с последним наблюдением:
В оригинальной статье используются явно записанные текстовые блоки Thought, Action и Observation. Современные нативные tool API показывают предлагаемые вызовы и результаты инструментов; отображаемый thought не обязателен. Interleaved thinking — это отдельная возможность модели/API. В историческом паттерне промптинга:
- Thought: агент генерирует «мысль», чтобы разбить цель на части и спланировать следующий шаг.
- Action: на основе этой мысли он вызывает инструмент.
- Observation: агент читает результат, который обновляет его понимание перед следующим thought.
Благодаря этому ReAct обладает следующими полезными свойствами:
- В ручном примере для HotpotQA из статьи, где использовалась PaLM-540B, наблюдения из Wikipedia приводили к меньшему числу галлюцинаций фактов, чем промптинг с chain-of-thought.
- Агент может менять стратегию на лету, основываясь на том, что только что увидел.
- История вызовов инструментов и наблюдений даёт конкретный трейс выполнения.
У этого же цикла есть издержки:
- В наивной реализации с полной историей и без кэширования каждый ход заново обрабатывает растущую историю. Кэширование промптов меняет стоимость и время обработки входных данных, но не заполнение контекстного окна и не устаревшие наблюдения. Измеряйте кэшированные и некэшированные токены отдельно; суммаризация и усечение отбрасывают контекст.
- Он неэффективен, когда вызовы инструментов можно было спланировать заранее; именно эту нишу занимает ReWOO.
- Без условия остановки или лимита шагов цикл может выполняться бесконечно.
Используйте его для исследовательских задач, отладки и работы, где невозможно заранее предсказать следующее действие.
ReWOO: сначала скомпилировать граф инструментов
ReWOO (Reasoning WithOut Observation) разделяет планирование и выполнение. Планировщик за один проход записывает полную последовательность вызовов инструментов, используя плейсхолдеры для значений, которые появятся только после выполнения.
- Plan: один вызов LLM записывает полный план вызовов инструментов, используя плейсхолдеры переменных (
#E1,#E2) для выходных данных, которых пока не существует. - Worker: не-LLM-экзекьютор запускает запланированные инструменты и подставляет значения вместо плейсхолдеров. В статье worker следует плану; реализация далее в этой статье добавляет параллельные батчи с учётом зависимостей для готовых шагов.
- Solver: финальный вызов LLM получает собранные наблюдения и формулирует ответ.
Такое разделение даёт следующие преимущества:
- При сохранении исходного плана актуальным требуется меньше повторных вызовов модели, чем в ReAct.
- История промпта повторяется реже, чем в чередующемся цикле с полной историей. Латентность инструментов по-прежнему зависит от того, как worker планирует вызовы.
- Планировщик можно файн-тюнить отдельно, без работающего окружения.
Однако такое разделение создаёт жёсткую границу. В стресс-тесте HotpotQA из статьи каждый инструмент возвращал No evidence found; ReWOO терял меньше точности, чем ReAct, поскольку ошибочные наблюдения не отправляли его планировщик в новый цикл. Это относительная устойчивость, а не политика восстановления после ошибок выполнения. Реализация всё равно должна определить, станет ли ошибка инструмента свидетельством для solver, запустит ли повторную попытку или прервёт выполнение. ReWOO подходит для предсказуемых workflow, но самостоятельно не перестраивает план вокруг некорректного исходного графа.
Используйте его для быстрых снапшотов, проверки статуса и дашбордов с предсказуемым поведением инструментов.
Plan-and-Execute: декомпозируй задачу, затем реагируй локально
Промптинг Plan-and-Solve описывает метод промптинга, при котором сначала создаётся план, а затем решаются подзадачи. Связанный с ним паттерн оркестрации инструментов обычно называют Plan-and-Execute. В руководстве LangChain по Plan-and-Execute этот паттерн описан так:
- Фаза планирования: агент сначала генерирует план, разбивающий задачу на более мелкие подзадачи.
- Фаза выполнения: затем агент выполняет эти подзадачи одну за другой. Когда задействованы инструменты, каждая подзадача обычно запускается как отдельный небольшой ReAct-цикл, поэтому экзекьютор может реагировать на результат работы инструмента, даже если общий план уже зафиксирован.
В исходной статье основное внимание уделялось zero-shot-промптингу. В реализации с использованием инструментов паттерн оркестрации может выполнять запланированные шаги последовательно и использовать разные модели для планирования и выполнения. Такое разделение моделей — решение на уровне реализации, а не результат, установленный статьёй о Plan-and-Solve.
На схеме показана опциональная ветка перепланирования. Обучающий граф далее в этой статье её не использует: обратная связь может изменить работу внутри одного шага, но не оставшиеся пункты плана.
Этот паттерн полезен по следующим причинам:
- Иерархический ризонинг, отражающий то, как эксперт-человек декомпозирует проект.
- Явная связь перепланирования позволяет приостановиться и пересмотреть план после неожиданного результата шага.
- Специализация моделей. Планировщик может быть дорогим, а экзекьютор — дешёвым.
- При настроенном чекпоинтере каждый завершённый шаг может стать границей возобновления.
Его издержки:
- Больше обращений к модели, чем в ReWOO, если каждый шаг содержит собственный ReAct-цикл.
- Больше состояния, которым нужно управлять.
- Избыточность для однократных запросов.
Используйте его для сложного анализа и исследований, требующих финального синтеза.
Выбирайте по тому, где может сломаться план
| Характеристика | ReAct (2022) | Plan-and-Execute (2023) | ReWOO (2023) |
|---|---|---|---|
| Основная идея | Импровизатор: по одному вызову за раз выбирает следующий шаг на основе последнего результата. | Архитектор: строит полный план, выполняет его, а затем проверяет результат. | Оптимизатор: компилирует граф зависимостей, а затем запускает готовые вызовы пакетами. |
| Воркфлоу | Итеративный цикл: Thought → Action → Observation. | Два этапа: фаза 1 (планирование), фаза 2 (выполнение). | Развязанный: планировщик записывает граф вызовов инструментов; Worker выполняет их; Solver собирает ответ. |
| Адаптивность | Максимальная: может менять направление после каждого отдельного вызова инструмента. | Каждый шаг может учитывать свой результат. Опциональный репланировщик может пересмотреть последующие шаги. | Минимальная: скрипт планировщика выполняется до конца; во время выполнения перепланирование не происходит. |
| Эффективность | Наивный цикл с полной историей повторно передаёт больше входных токенов; контекст-инжиниринг может ограничить этот рост. | Каждый ReAct-цикл шага может начинаться с короткого контекста, а не со всей историей запуска; при включённом репланировщике добавляются вызовы планирования. | Меньше вызовов модели; Worker из этой статьи также запускает готовые по зависимостям инструменты параллельно. |
| Лучше всего подходит для | Исследования без заранее заданного направления или задач с непредсказуемыми результатами. | Долгих задач, требующих стабильной цели (например, написания статьи). | Структурированных, повторяемых воркфлоу (например, проверки погоды в 5 городах). |
Эта таблица помогает с роутингом, но не является бенчмарком. Используйте ReAct, когда результат инструмента может изменить следующее действие. Используйте Plan-and-Execute, когда задача разбивается на шаги, но каждому шагу всё ещё нужна обратная связь. Добавляйте репланировщик только тогда, когда один результат должен изменить последующие шаги; в обучающем графе ниже его нет. Используйте ReWOO, когда все зависимости инструментов известны до начала выполнения. Его граф зависимостей позволяет обучающему Worker запускать готовые вызовы параллельно и обнаруживать граф, который не может продвинуться. С помощью хелпера execute_tool из companion-пакета перехваченные ошибки инструментов превращаются в строки, передаваемые Solver. Исключение, вышедшее за пределы хелпера, прерывает Worker. Ни один из этих вариантов не предоставляет ретраи или перепланирование. Перед оптимизацией количества вызовов измерьте все три подхода на своей модели, с учётом латентности инструментов, набора задач и политики ретраев.
Что меняется в современных моделях и харнессах
Эти паттерны описывают решения за пределами модели. Модели, которая выполняет ризонинг между вызовами инструментов, всё равно нужна программа: она должна выполнять эти вызовы, останавливать цикл и авторизовать побочные эффекты. В Claude Sonnet 5 адаптивное мышление включено по умолчанию, а его текст по умолчанию не возвращается. Пустой блок мышления не означает, что модель пропустила ризонинг. При возврате результатов инструментов сохраняйте полные подписанные блоки мышления: пересборка диалога из видимого текста нарушает эту непрерывность. Сравнивайте усилие на ризонинг и оплачиваемое число выходных токенов, а также количество вызовов модели.
Для новой реализации на LangChain начните с create_agent. Этот автономный пример связывания использует текущий идентификатор Sonnet и локальный fixture-инструмент. Перед его запуском задайте ANTHROPIC_API_KEY и установите langchain вместе с langchain-anthropic; этот запуск выполняет платные запросы к модели.
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
def lookup_fixture_company(ticker: str) -> str:
"""Look up a company name in a tiny local fixture, not live market data."""
return {"NVDA": "NVIDIA", "AMD": "AMD"}.get(
ticker.strip().upper(), "No company in the fixture"
)
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-5", max_tokens=4096),
tools=[lookup_fixture_company],
system_prompt="Use the fixture for company names. Do not invent market data.",
)
result = agent.invoke({
"messages": [{"role": "user", "content": "Which company is NVDA in the fixture?"}]
})
print(result["messages"][-1].content)
Это управляемый наблюдениями цикл работы с инструментом. Для такого одиночного lookup отдельный планировщик не нужен. Я не добавляю в пример переопределения параметров сэмплирования: обновление модели также требует проверки поддерживаемых параметров, блоков ответа и поведения structured output. Пример проверен офлайн на корректность импортов и создания объектов, но не оценивался на живом инференсе.
Если задаче нужны изолированные контексты субагентов, файлы и автоматическое управление контекстом, Deep Agents упаковывает эти возможности вокруг того же агентного цикла. Это выбор харнесса, а не четвёртый алгоритм ризонинга. Сравнивайте одного агента с делегированием на задачах, которые действительно распадаются на независимую работу; учитывайте в результате стоимость координации и потерю контекста.
Практический пример: ИИ-агент Market Analyst
ИИ-агент Market Analyst делает это различие наглядным. Одна кодовая база использует все три паттерна для исследования рынка, а роутер выбирает между путём глубокого исследования и путём краткого брифинга. Приведённые ниже фрагменты — сокращённые учебные варианты из коммита b4e769a: приведённый ниже worker добавляет готовые по зависимостям параллельные батчи и выбрасывает исключение, если граф не может продолжить выполнение. Его execute_tool — вспомогательная функция, которая перехватывает исключения инструментов и возвращает solver строки ошибок. Только исключения, вышедшие за пределы этой функции, прерывают future. В этой учебной адаптации нет ретраев и восстановления.
Для оркестрации используется LangGraph. Node — это Python-функция, которая возвращает поля для обновления общего состояния. Edge объявляет следующую node и может вызывать функцию роутинга. LangGraph объединяет обновления и создаёт чекпоинты на границах super-step: одной node или батча параллельных node. Незавершённые записи сохраняют успешные результаты соседних node, если другая node завершается с ошибкой. Все три паттерна используют один объект состояния, поэтому для роутинга не нужны три отдельные схемы:
Диаграмма изолирует роутинг и создание черновика. В ней не показаны общий эвалуатор и подтверждение человеком перед публикацией — они будут показаны далее, чтобы два цикла ризонинга оставались читаемыми.
Определение состояния
Приведённые ниже блоки Python — это интеграционные фрагменты, а не самостоятельные скрипты: они используют общие типы состояния, хелперы нод и импорты фреймворка из companion-материала. Изолированный раннер примеров в репозитории их пропускает; офлайн-проверки контрактов покрывают состояние, предлагаемые вызовы, ID сообщений и поведение при возобновлении.
Схема состояния содержит поля, необходимые обоим режимам:
from typing import Literal
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
report_approved: bool = False
evaluator_verdict: Literal["pass", "fail", "needs_human"] | None = None
evaluator_reasons: list[str] = Field(default_factory=list)
Паттерн 1: реализация Plan-and-Execute
Plan-and-Execute подходит для многошагового синтеза. Планировщик записывает высокоуровневые шаги, после чего ReAct-цикл выполняет каждый шаг и реагирует на результаты инструментов.
Следующие исторические фрагменты из companion-материала сохраняют claude-sonnet-4-5-20250929 и старый API create_react_agent, чтобы их можно было сопоставить с указанным коммитом. Текущая отправная точка — приведённый выше пример create_agent. Миграция полного графа требует совместно протестировать схемы планировщика, обработку внутренних сообщений, роутинг и поведение при возобновлении; одной заменой строки с именем модели такую миграцию не выполнить.
В реализации явно видны четыре границы:
- Одна предварительная фаза планирования. Один вызов LLM создаёт весь план в виде списка описаний шагов.
- Вывод по схеме, который валидирует структуру ответа. Для выполнения нужна отдельная проверка плана.
- Инструменты пока не выполняются. Планировщик лишь решает, что делать, но не как.
- Шаги, понятные человеку. Каждый шаг — это текст, который будет интерпретировать экзекьютор.
# 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), о котором я писал в предыдущей публикации. Схема отклоняет некорректно сформированные поля. Перед выполнением также нужно требовать непустой список шагов с ограниченным размером, уникальными номерами и пригодными для использования описаниями; эта сокращённая схема такие условия не проверяет. Companion-материал всё ещё может принять пустой план и завершиться ошибкой, когда его экзекьютор обратится к первому шагу.
Паттерн 2: выполнение через ReAct
После создания плана экзекьютор выполняет каждый шаг как отдельный ReAct-цикл. Это Фаза 2: каждый шаг достаточно мал, чтобы цикл Thought–Action–Observation оставался сфокусированным, а агент мог реагировать на любой результат инструмента.
Как устроена часть с ReAct:
- Итеративное выполнение. По одному шагу за раз, с обратной связью через наблюдения.
- Цикл модели и результатов инструментов выполняется внутри
create_react_agent; фабрика не обещает предоставлять наружу транскрипт ризонинга. - Результаты предыдущих шагов передаются как контекст для текущего ризонинга.
- Агент выбирает инструменты на основе описания шага.
- В середине шага он может изменить подход в зависимости от результата инструмента.
# 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 observable loop is a proposed call, its result, and the next decision.
# A visible reasoning block depends on the model and API configuration.
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 убирает вызовы модели из фазы выполнения. Независимые инструменты запускаются параллельно, а зависимые ждут выполнения своих предусловий. Планировщик заранее выдаёт граф инструментов, а воркер выполняет его, не спрашивая модель, что делать дальше.
Структура выглядит так:
- Три фазы (планировщик → воркер → решатель). Воркер не спрашивает у модели, что делать дальше, и не перепланирует.
- Вызовы инструментов ссылаются на плейсхолдеры
#E1и#E2— результаты, которых пока не существует. - Во время выполнения LLM не используется. Воркер просто запускает инструменты.
- Независимые инструменты запускаются параллельно.
- В конце выполняется один synthesis-вызов по всем данным одновременно.
Фаза 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 # Requested tool name; validate it against a registry in production
tool_args: dict # Proposed arguments; may contain refs like {"price": "#E1"}
depends_on: list[str] = [] # Declared ordering; cross-check it against #E placeholders
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, it creates typed proposed tool calls.
Production code validates them before execution; this teaching worker does not.
"""
llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)
# Schema-Guided Reasoning validates the planner response shape.
# It does not validate a tool name, its arguments, or its dependencies.
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"""),
])
# Store the typed proposed-call plan.
# This teaching worker sends ready steps directly to execute_tool.
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: решатель 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())}
Планировщик записывает предполагаемые вызовы, воркер подставляет их плейсхолдеры, а решатель получает результаты. Схема проверяет только форму ответа. Перед диспетчеризацией нужно отклонять пустые или слишком большие планы, дублирующиеся ID, отсутствующие зависимости и циклы. Дублирующиеся ID без предупреждения схлопываются в словаре pending выше. Проверяйте имена инструментов и аргументы по реестру, рекурсивно ищите плейсхолдеры во вложенных списках и объектах, сверяйте их с depends_on и отклоняйте неразрешённые ссылки до запуска затронутого вызова. Этот учебный воркер пропускает эти проверки и отправляет готовые шаги в execute_tool компаньона, включая его поведение с ошибками как свидетельствами. Решатель должен трактовать строку ошибки как отсутствующее свидетельство, а не как успешный результат поиска. Если его граф не может продвинуться, он останавливается до решателя; пути для повторной попытки или перепланирования нет. Повторный запуск в том же состоянии лишь повторяет тот же план, поэтому для перепланирования нужны результат сбоя, условное ребро и ограничение числа попыток.
Где каждый паттерн вызывает модель
| Паттерн | Вызовы LLM во время выполнения | Обновления состояния | Ключевой паттерн кода |
|---|---|---|---|
| Plan-and-Execute | 1 для планирования + ReAct-цикл для каждого шага (по несколько вызовов) + 1 для отчёта | Последовательное завершение шагов | planner_node() → цикл: executor_node() → reporter_node() |
| ReAct (внутри каждого шага) | Несколько на шаг (циклы thought-action) | Только внутренний транскрипт; внешний граф записывает результаты завершённых шагов | Закреплённый компаньон использует устаревший create_react_agent() |
| ReWOO | 1 для планирования + 0 во время выполнения + 1 для синтеза | Пакеты инструментов с учётом зависимостей | rewoo_planner_node() → rewoo_worker_node() → rewoo_solver_node() |
Ключевое различие — вывод планировщика. Он определяет, сколько свободы остаётся у экзекьютора:
-
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-цикл, поэтому шаг стоит нескольких вызовов модели, а не одного.
-
В ReAct нет предварительного плана. Он использует итеративный ризонинг:
# A standalone full-history ReAct loop carries messages from earlier turns. messages = [ HumanMessage(content="Execute Step 1: Get current price"), AIMessage(content="", tool_calls=[{ "id": "1", "name": "get_stock_snapshot", "args": {"ticker": "NVDA"}, "type": "tool_call", }]), ToolMessage(tool_call_id="1", content="$132.45"), AIMessage(content="Now I need metrics..."), # ... agent continues until step complete ]В автономном ReAct-цикле с полной историей каждый вызов модели передаёт историю, которая растёт на протяжении всей задачи. Попадания в кэш могут сократить повторные вычисления и расходы на входные токены; в production-цикле история также может суммироваться или обрезаться. В примере Plan-and-Execute каждый запуск ReAct начинается с текущего шага и дайджеста предыдущих результатов. Завершённый результат сохраняется в
plan, а не во внутренней расшифровке tool call’ов. -
ReWOO создаёт явные типизированные предполагаемые tool call’ы:
# Planner output (list of ReWOOPlanStep objects) rewoo_plan = [ ReWOOPlanStep( step_id="#E1", description="Read the current price and valuation snapshot", tool_name="get_stock_snapshot", tool_args={"ticker": "NVDA"} ), ReWOOPlanStep( step_id="#E2", description="Find recent NVDA earnings news", tool_name="search_news", tool_args={"query": "NVDA earnings", "max_results": 5} ), # ... all tool calls planned upfront ]Воркер работает вслепую, без участия LLM. Все вызовы модели находятся в планировщике и солвере, поэтому количество вызовов модели предсказуемо.
Поток памяти и состояния:
- Plan-and-Execute: состояние передаётся через
plan→current_step_index→research_data. - ReAct внутри этого графа Plan-and-Execute: внутренний агент создаёт расшифровку одного шага. Внешний граф сохраняет
plan,current_step_indexи результаты завершённых шагов. - ReWOO: состояние передаётся через
rewoo_plan, а поляresultзаполняются воркером.
Объединяем оба маршрута в один граф
Граф предоставляет пользователю два маршрута поверх одного AgentState: глубокое исследование использует Plan-and-Execute с ReAct-циклом внутри каждого шага, а экспресс-брифинг — ReWOO. Здесь ReAct — примитив выполнения, а не третий маршрут.
В этой реализации нет репланировщика: она выполняет исходный план до конца. Для добавления репланирования потребуется ребро из executor обратно в planner и правило, определяющее, когда неожиданный результат оправдывает ещё один вызов модели.
LangGraph сохраняет wiring декларативным:
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 run the same evaluator before the human approval step.
builder.add_edge("evaluator", "publish")
builder.add_edge("publish", END)
return builder.compile(
checkpointer=checkpointer,
# Human-in-the-loop pause: the reporter (or ReWOO solver) writes a
# draft. A separate model session reads that draft and records its
# assessment. The graph then stops before publishing, whichever
# assessment it produced, so a human can review both the draft and
# assessment. Part 6 explains how the program decides a run is complete.
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. Если запрос неоднозначен, по умолчанию выбирается глубокое исследование. Прежде чем показывать роутер пользователям, сравните оба маршрута на одном и том же фиксированном workflow, используя одинаковые задачи, инструменты, источники данных и бюджет. Повторяйте стохастические испытания и учитывайте каждую запущенную попытку. Отслеживайте успешность задач, ошибочное принятие результатов, кэшированные и некэшированные токены, wall time, а также восстановление после ошибок инструментов или вводящих в заблуждение наблюдений. Эти паттерны представляют собой варианты управления потоком, а не рейтинг подходов для внедрения; инженерный отчёт Anthropic также рекомендует начинать с простых компонуемых workflow.
Полная сопутствующая реализация, включая роутер и общее состояние, доступна в закреплённом коммите Market Analyst Agent.
Следующий слой — память
В части 2, Архитектура памяти ИИ-агента, возобновляемые чекпоинты отделяются от знаний между сессиями и документов проекта. Без этого слоя состояния расположенные выше роутер и экзекьютор работают только пока живы один процесс и одно контекстное окно.
Ссылки
- ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022)
- ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models (Xu et al., 2023)
- Plan-and-Solve Prompting: Improving Zero-Shot Chain-of-Thought Reasoning by Large Language Models (Wang et al., 2023)
- Репозиторий ИИ-агента — рыночного аналитика