ReAct

ReAct — паттерн работы агента в виде цикла: Goal → Observation → Reasoning → Action → Feedback → Correction → repeat. Цикл повторяется 5–200+ раз, пока цель не достигнута. Именно это отличает «reasoning loop» агента от «reactive loop» чат-бота.

Суть

Чат-бот: триггер → ответ → конец (reactive loop, без памяти о цели). Агент: крутит петлю, где на каждом шаге наблюдает результат, рассуждает, действует и при необходимости корректируется.

Зачем это нужно

Даёт агенту адаптивность и самокоррекцию: если шаг упал или цель пока недостижима — агент переосмысливает и пробует иначе, а не отдаёт один фиксированный ответ.

Как работает

  • На каждом шаге агент получает ground truth (эталонные данные) из окружения: результаты инструментов, вывод кода, ответы API.
  • Может встать на checkpoint и запросить помощь человека (см. Human in the Loop).
  • Опирается на компоненты из Agent Anatomy (мозг рассуждает, инструменты действуют, память хранит прогресс).
  • Риск: без лимита числа итераций цикл может «зациклиться» и сжечь бюджет → нужен max_iterations; само зацикливание ловится не только лимитом, но и детектором «No Progress» — отсутствие прогресса между шагами (см. Agent CostControl).
  • В коде это «master loop» (OODA: Observe-Orient-Decide-Act): while True опрашивает граф задач → вызывает LLM → обновляет стейт → self-healing retry при ошибке. Anthropic называет это «boring orchestration» — управляющий слой намеренно глуп и предсказуем (см. реализацию в Agent Architecture).
  • В LangGraph этот цикл — минимальный граф agent ↔ ToolNode, замкнутый условным ребром по tool_calls; высокоуровневая create_agent() сама написана на LangGraph (см. LangGraph ReAct Loop, LangGraph vs LangChain).

Пример

goal = "собрать отчёт по конкуренту X"
while not done and steps < max_iterations:
    obs   = run_tool(action)      # Observation (ground truth)
    think = llm.reason(goal, obs) # Reasoning
    action, done = think.next()   # Action / Feedback

Как отлаживать цикл: сначала без модели

Приём, проходящий сквозь весь практический материал: цикл собирают и запускают до подключения LLM. Вместо модели ставят детерминированную политику — функцию, которая по состоянию возвращает следующий шаг. В разных примерах это planner на условиях, «агенты» как обычные функции и FakeListLLM, отдающий заранее заданные ответы по очереди.

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

Из этого же следует главное свойство: при подстановке настоящей модели архитектура не меняется. Меняется содержимое одной функции. Если замена LLM требует переписать цикл, значит логика управления протекла в промпт.

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

Шаг как структура данных

Второй приём оттуда же: шаг цикла описывают не логом, а типом.

@dataclass
class ToolCall:
    name: str
    args: dict

@dataclass
class StepTrace:
    thought: str                        # что агент решил на этом шаге
    tool_call: ToolCall | None = None   # None — «мысленный» шаг без действия
    observation: Any = None             # что вернул инструмент

Три поля — это буквально ReAct в типизированном виде: рассуждение, действие, наблюдение. Агент возвращает не только ответ, но и список таких шагов, и по нему видно, как он к ответу пришёл.

Отдельного внимания стоит tool_call = None. Это шаг, на котором агент ничего не вызвал, а только зафиксировал вывод в состоянии — например, определил, что «прошлая неделя» означает конкретный диапазон дат. Без такой возможности каждый шаг обязан быть действием, и рассуждение приходится либо прятать внутри вызова, либо терять.

Разница с внешней трассировкой (Agent Observability) в том, чья это структура. Внешний трейс собирает наблюдатель, и агент о нём не знает; StepTrace — часть возвращаемого значения, то есть контракт самого агента. Одно другого не заменяет: первое нужно для эксплуатации, второе — чтобы вызывающий код мог принять решение по траектории, а не только по финальному ответу.

Где проходит граница с подменой модели в тестах. Детерминированная политика вместо модели — приём для отладки самого цикла: он проверяет, что оркестрация, передача состояния и обработка результатов инструмента работают, и остаётся честным тестом ровно до тех пор, пока проверяется код вокруг модели. Как только тест начинает утверждать что-то про качество ответа, подмена превращается в самообман — вы проверили заглушку. Ту же роль на уровне стенда оценки играют синтетические профили (Agent Evals): слабый профиль обязан блокироваться гейтом, сильный — проходить, и это проверка гейта, а не модели.

Лимит итераций и детекция застревания

Универсального max_iterations нет, и подбирать его «по ощущению» бессмысленно. Значение выводится из двух величин: глубины декомпозиции типовой задачи (сколько шагов нужно, чтобы дойти до ответа честно) и потолка стоимости одного прогона (Agent CostControl). Лимит ставится с запасом к первому и проверяется вторым; если запас упирается в потолок, проблема не в лимите, а в том, что задачу надо резать иначе.

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

  • повтор вызова — тот же инструмент с теми же аргументами, что и на предыдущем шаге;
  • одинаковые observations — инструмент отвечает то же самое, значит новая информация не поступает;
  • отсутствие изменений в state — шаг не добавил в состояние ничего нового.

Четвёртый сигнал нужен там, где каждый шаг что-то меняет. Три признака выше ловят застревание на месте: тот же вызов, тот же ответ, то же состояние. Петля починки в них не попадает — каждый круг даёт новый кандидат, новые ошибки и новое состояние, так что все три молчат, пока прогресса нет. Ловит это объективный счётчик, например число оставшихся ошибок валидации: продолжать, пока счётчик берёт новый минимум, и останавливаться после двух кругов подряд без нового минимума (Validation Loops).

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

Шаг «мысль» теряется при переходе на вызов инструментов

В исходной схеме мысль и действие чередуются по построению: модель пишет рассуждение, потом действие. В реализации через вызов инструментов рассуждение формально остаётся, но на практике схлопывается: модель переходит от вызова к вызову, а текст между ними вырождается в пересказ того, что она собирается сделать.

Обходят это неожиданным способом — заводят инструмент, который ничего не делает: принимает текст рассуждения, возвращает подтверждение, не имеет побочных эффектов. Вся его работа в том, что вызов инструмента — обязательный шаг, который модель не пропускает, и в этот шаг помещается рефлексия. Промпт предписывает вызывать его после каждого содержательного действия и отвечать на конкретные вопросы: что нашлось, чего не хватает, хватит ли этого для ответа.

Приём выглядит костылём и им является, но лечит настоящий дефект: без принудительного места для рефлексии агент не останавливается оценить результат, а сразу планирует следующий вызов — и именно так набираются круги, которые потом ловит детектор застревания выше.

Бюджет вызовов как аффорданс, а не только как предохранитель

Лимит итераций из раздела выше — предохранитель в коде: он срабатывает независимо от модели и после того, как бюджет сожжён. Рядом с ним ставят вторую конструкцию — бюджет, объявленный модели в промпте, и она работает раньше:

  • градация по сложности запроса: простому — один-два вызова, обычному — два-три, очень сложному — до пяти, и жёсткая остановка после пяти в любом случае;
  • условия остановки, сформулированные как достаточность, а не как исчерпание: «можешь ответить исчерпывающе», «набрал три и более релевантных источника», «последние два вызова вернули то же самое».

Второй пункт того же списка — это наш сигнал «одинаковые observations», но высказанный модели как правило поведения, а не проверяемый снаружи. Обе формы нужны: объявленная в промпте останавливает дёшево и вовремя, но принуждается только чтением; проверка в коде принуждается всегда, но срабатывает позже (Harness Optimization — лестница средств по надёжности).

Что измерено в исходной статье — и почему «ReAct лучше CoT» неверно

Учебные изложения подают ReAct как безусловно лучший способ рассуждения. Статья, которая его ввела (Yao et al., 2210.03629), измеряет иначе, и различие практическое.

На чистых вопросно-ответных задачах ReAct сам по себе проигрывает. HotpotQA, exact match: CoT-SC — 33,4%, CoT — 29,4%, standard — 28,7%, ReAct — 27,4%, Act — 25,7%. Выигрывает не ReAct, а связка: ReAct→CoT-SC даёт 35,1%. На FEVER картина обратная (ReAct 60,9% против CoT 56,3%), и это подсказывает, где проходит граница: проверка факта требует внешнего обращения к источнику, а многошаговый вывод по уже известному — нет.

Настоящий отрыв — на интерактивных задачах, где действие меняет среду:

Бенчмарк ReAct Act Прежний метод
ALFWorld 71% 45% BUTLER 37%
WebShop 40,0% 30,1% IL 29,1%, IL+RL 28,7%

Числа здесь — успех за один проход, без повторных попыток. Это важно, потому что в базе есть другое число по тому же ALFWorld: 130 задач из 134 у Reflexion, то есть около 97%. Противоречия нет — там измеряется результат после двенадцати попыток с разбором неудач между ними, здесь однократное прохождение. Сравнивать их напрямую бессмысленно: разница в 26 пунктов куплена двенадцатикратным бюджетом.

Разбор ошибок объясняет, почему обмен именно такой. Среди успешных прогонов доля тех, где рассуждение и факты действительно верны: ReAct 94%, CoT 86% — то есть у CoT каждый седьмой «успех» получен на выдуманных фактах. Среди неудачных прогонов галлюцинация как причина: CoT 56%, ReAct 0%. Заземление на наблюдения убирает выдумывание практически полностью.

Платит за это ReAct тем же местом: среди его неудач 47% — ошибки рассуждения, против 16% у CoT. Авторы называют причину прямо: жёсткая структура «мысль → действие → наблюдение» ограничивает свободу формулировать шаги рассуждения. Ещё 23% неудач — плохой результат поиска, то есть цена зависимости от внешнего источника.

Отсюда два вывода для проектирования. Первый: выбор между ReAct и CoT — это выбор, какой класс ошибок вы предпочитаете, а не выбор лучшего. Там, где выдуманный факт дороже кривого вывода (справки по документам, финансы, медицина), ReAct; там, где задача замкнута в контексте и нужна свобода рассуждения, — CoT, и лучше с самосогласованностью. Второй: характерная неудача ReAct — повторяющиеся мысль и действие по кругу, и это ровно тот режим, который ловит детектор отсутствия прогресса из раздела выше. Он не страховка на всякий случай, а реакция на задокументированный основной режим отказа паттерна.

Альтернативный взгляд: агент как State Machine

ReAct-цикл можно описывать не как while-петлю, а как конечный автомат: узлы (рассуждение, действие) и рёбра (переходы) поверх единого состояния. Эта рамка превращает цикл в явный объект, который можно прерывать, сохранять и переигрывать (см. LangGraph, LangGraph Time Travel) — то, что в виде голого while True недоступно. Лимит итераций при этом становится не steps < max, а условным ребром на выход (см. LangGraph Reliability).

Связано с

  • AI Agent — кто исполняет этот цикл
  • Agent Anatomy — компоненты, задействованные на каждом шаге
  • Human in the Loop — пауза на checkpoint за подтверждением
  • Agent Architecture — master loop / OODA / self-healing как реализация цикла в коде
  • LangGraph ReAct Loop — тот же цикл, выраженный как граф LangGraph
  • Agent CostControl — лимит итераций и детектор No Progress как защита от зацикливания
  • Agent Observability — внешняя трассировка; StepTrace дополняет её изнутри агента
  • Tool Calling — что происходит на шаге действия и через какой гейт проходит вызов
  • Reflexion — внешний цикл поверх этого: разбор неудачной попытки переносится в следующую
  • Validation Loops — петля починки: там же четвёртый сигнал застревания, счётчик ошибок