Почти любой агент, который вы сегодня запустите, внутри устроен одинаково: модель думает, выбирает действие, получает результат, думает снова. Схема называется ReAct — от reasoning and acting — и её популярность объясняется тем, что она реализуется примерно за десять строк вокруг обычного вызова модели.
Проблема в том, что эти десять строк создают свойство, которого не было в исходном вызове: контекст растёт с каждым шагом. Причём растёт не линейно по смыслу, а по объёму текста, который вы сами туда добавляете. На третьем шаге это незаметно. На двенадцатом задача уже вытеснена из окна собственными наблюдениями.
#Короткий ответ
ReAct — это цикл из трёх повторяющихся элементов:
- Мысль — модель вслух рассуждает, что делать дальше;
- Действие — модель печатает вызов инструмента;
- Наблюдение — ваш код выполняет вызов и дописывает результат в контекст.
Цикл повторяется, пока модель не решит, что готова отвечать, или пока не сработает лимит. Каждое наблюдение остаётся в контексте до конца задачи — это и есть источник силы схемы и её главного ограничения одновременно.
#Схема цикла
┌──────────────────────────────────┐
│ контекст: цель + всё, что было │
└──────────────┬───────────────────┘
▼
[ вызов модели ]
│
┌────────────┴───────────┐
│ │
есть вызов вызова нет
инструмента │
│ ▼
▼ ответ готов
выполнить вызов
│
▼
дописать наблюдение
в контекст ────────────▶ назад наверх
Условий выхода два, и второе часто забывают реализовать: модель перестала звать инструменты — либо счётчик шагов упёрся в предел. В боевой конфигурации второе срабатывает регулярно, поэтому обработка «упёрлись в лимит» должна возвращать частичный результат, а не молчание.
#Как выглядит трассировка
Реальный прогон на задаче «найди в моих файлах, где я записывал пароль от роутера, и покажи строку». Формат — как он ложится в лог.
шаг 1 МЫСЛЬ надо посмотреть, какие вообще есть файлы
ДЕЙСТВИЕ list_dir({"path": "."})
НАБЛЮДЕНИЕ [ok] содержимое: (корень)
FILE notes.md 18420b
FILE setup.md 4110b
DIR archive
шаг 2 МЫСЛЬ «setup» звучит подходяще, читаю
ДЕЙСТВИЕ read_file({"path": "setup.md"})
НАБЛЮДЕНИЕ [ok] setup.md
…4110 байт содержимого…
шаг 3 МЫСЛЬ пароля тут нет, но есть отсылка к архиву
ДЕЙСТВИЕ list_dir({"path": "archive"})
НАБЛЮДЕНИЕ [ok] содержимое: archive
FILE router-2024.md 890b
шаг 4 МЫСЛЬ читаю
ДЕЙСТВИЕ read_file({"path": "archive/router-2024.md"})
НАБЛЮДЕНИЕ [ok] router-2024.md
…890 байт…
шаг 5 МЫСЛЬ нашёл, отвечаю
ОТВЕТ строка 12 файла archive/router-2024.md
Два наблюдения по этому логу, которые не видны в финальном ответе.
Первое. На шаге 2 модель прочитала файл целиком — все 4110 байт легли в контекст, хотя нужна была одна строка. Это типичный расход: инструмент отдаёт всё, что у него есть, а фильтровать некому.
Второе. Правильный путь был найден со второй попытки. Шаг 2 — это тупик, но он остался в контексте навсегда и участвует во всех последующих вызовах модели, увеличивая счёт и уменьшая свободное окно.
Чтение трассировки — основной навык отладки агентов: ошибка почти всегда видна на первом неверном наблюдении, а не в финальном ответе. Разбор конкретно зацикливаний — в статье про зацикливание и предохранители.
#Рост контекста по шагам
Посчитаем на том же прогоне. Возьмём условные объёмы: постановка задачи и описания инструментов — назовём это базой, каждое наблюдение — свой размер.
| Шаг | Что добавилось | Что видит модель на входе |
|---|---|---|
| 1 | — | база |
| 2 | список файлов | база + список |
| 3 | 4110 байт файла | база + список + файл |
| 4 | список архива | база + список + файл + список |
| 5 | 890 байт файла | всё вышеперечисленное |
К пятому шагу модель обрабатывает вход, который в разы больше исходного запроса, и заметная его часть — тупиковая ветка со второго шага. Отсюда три практических следствия.
Стоимость растёт быстрее числа шагов. Агент на пять шагов стоит не впятеро дороже одного вызова, а больше: каждый следующий вызов длиннее предыдущего.
Точность падает в середине. Модели хуже находят информацию в середине длинного входа, чем в начале и в конце. Наблюдение с третьего шага, оказавшись в середине, может быть фактически не использовано; механика этого эффекта разобрана в материале про контекстное окно и потерю в середине.
Окно кончается раньше, чем задача. На локальной модели с небольшим окном это происходит на пятом-седьмом шаге при работе с файлами обычного размера.
Лечится это не «увеличить окно», а тремя приёмами: обрезать результат инструмента до полезного размера, не давать инструментам возвращать всё содержимое целиком и заканчивать цепочку раньше, разбивая задачу. В коде чата Persona стоят прямые ограничения: не более 4000 символов на один результат инструмента, не более 24000 суммарно за ход, не более 16 вызовов и не более шести раундов цикла — восьми в режиме углублённого ответа.
#Что из этого следует на практике
Лимит шагов — не защита от бесконечности, а бюджет. Он должен быть меньше, чем «сколько влезет», и рассчитываться от размера типичного наблюдения.
Обрезка результата важнее выбора модели. Инструмент, возвращающий выдержку вместо целого файла, продлевает жизнь цепочки сильнее, чем переход на модель с вдвое большим окном.
Тупиковые ветки надо уметь выбрасывать. Простой приём — не хранить в контексте наблюдения, которые модель сама пометила как бесполезные. Реализуется сложнее, чем звучит, но эффект заметный.
Дедупликация вызовов обязательна. Модель, видя в контексте свой предыдущий вызов, часто печатает его снова. Без сравнения по точному тексту вызова один инструмент отработает трижды.
ReAct — не единственная схема. Для задач, где шаги известны заранее, план с фиксированными этапами дешевле и надёжнее; сравнение подходов — в разборе планирования и декомпозиции задач агентом.
#Где интуиция подводит
«Мысль в ReAct — это рассуждение модели, как в chain of thought». Похоже, но роль другая. В цепочке рассуждений промежуточные шаги нужны, чтобы получить лучший финальный текст, и их можно скрыть. В ReAct мысль — это выбор следующего действия, и она напрямую влияет на то, какие данные попадут в контекст. Разница разобрана в справке про chain of thought.
«Чем больше шагов разрешить, тем лучше результат». До определённого предела — да. После него растёт вероятность блуждания, а качество падает из-за переполненного контекста. На практике цепочки длиннее восьми-десяти шагов почти всегда означают, что задачу стоило разбить.
«Наблюдение — это истина». Наблюдение — это то, что вернул инструмент. Инструмент мог вернуть пустоту, ошибку в человекочитаемом виде или устаревшие данные, а модель воспримет всё это как факт и будет рассуждать дальше уверенно и неправильно.
«Если агент ошибся, надо улучшить финальный промпт». Почти всегда ошибка возникла на первом неверном наблюдении, а дальше модель рассуждала логично на неверных данных. Улучшать надо инструмент или его описание.
#Когда ReAct не нужен
Если последовательность шагов известна заранее и не зависит от результатов — напишите её кодом. Цикл нужен ровно тогда, когда следующее действие невозможно назвать, не увидев результата предыдущего. Во всех остальных случаях он добавляет стоимость, задержку и способ ошибиться.
Общая рамка того, где агентность оправдана, — в базовой статье о том, что такое ИИ-агент.
#Частые вопросы
#Чем ReAct отличается от простого вызова инструмента?
Числом итераций и тем, кто их инициирует. Одиночный вызов — это один ход: модель попросила, код выполнил, человек прочитал. В ReAct результат возвращается модели автоматически, и она делает следующий шаг сама, пока не остановится по собственному решению или по лимиту.
#Сколько шагов разумно разрешать агенту?
Начинать стоит с пяти и увеличивать, только если логи показывают, что цепочки регулярно упираются в предел по делу, а не из-за блуждания. Цепочки длиннее восьми-десяти шагов на практике почти всегда сигнализируют, что задачу нужно было разбить на несколько.
#Почему агент повторяет один и тот же вызов?
Потому что видит его в контексте и продолжает текст по образцу. Лечится дедупликацией по точному тексту вызова на стороне кода и тем, чтобы ошибки инструментов возвращались осмысленным текстом, из которого понятно, что повторять бессмысленно.
#Что делать, если контекст кончается на середине задачи?
Уменьшать не число шагов, а объём наблюдений: обрезать результаты инструментов, отдавать выдержки вместо целых файлов, выбрасывать наблюдения из тупиковых веток. Увеличение окна модели откладывает проблему, но не устраняет её.
#Подходит ли ReAct для локальной модели?
Для коротких цепочек в два-три шага — да. Дальше начинают накладываться два ограничения: небольшое окно быстро заполняется наблюдениями, а соблюдение синтаксиса вызова у компактных моделей ухудшается по мере роста контекста. Практический выход — дробить задачу на несколько коротких прогонов.
#Вывод
ReAct стоит десять строк кода и приносит с собой одно свойство, о котором в этих десяти строках не написано: контекст растёт с каждым наблюдением, и вместе с ним растут стоимость и вероятность потерять важное в середине.
Если вы строите цикл — сразу закладывайте обрезку результатов, дедупликацию вызовов и внятную обработку упирания в лимит. Если последовательность шагов известна заранее — не стройте цикл вовсе, напишите её кодом.