Суть
Чат-бот: триггер → ответ → конец (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 — петля починки: там же четвёртый сигнал застревания, счётчик ошибок