AI-агент дважды дошёл до лимита в 50 шагов. Я пыталась чинить prompt — проблема оказалась в runtime
Когда агент начинает зацикливаться, первое подозрение обычно падает на модель.
Плохой prompt.
Неудачная инструкция.
Модель «не понимает», что нужно сделать.
У меня в одном эксперименте всё выглядело именно так. Я собирала минимальный agent harness, чтобы буквально увидеть изнутри цикл:
LLM → code → tool → observation → next step → final_answer()
Задача была простая: агент должен был изучить содержимое ограниченного workspace и выдать краткое описание архитектуры проекта. Вместо этого он дошёл до максимального лимита в 50 шагов и так и не завершил задачу. Самое интересное: tools при этом действительно выполнялись. А агент почти ничего из их результатов не видел.
Что происходило в первом запуске
На первом шаге агент вызвал:
list_dir(path=".")
Инструмент отработал. Но в observation вернулось:
Executed successfully (no output)
Следующий шаг:
list_dir(path="/.../workspace")
Снова:
Executed successfully (no output)
Потом:
ls -la
И опять:
Executed successfully (no output)
Со стороны модели это выглядело так, будто она обращается к инструментам, инструменты формально исполняются, но никакого полезного результата назад не возвращают.
Агент начинает пробовать другие пути:
ls -R pwd ls -a
Пытается выйти за workspace — guardrail блокирует. Пытается использовать неразрешённую команду — снова блокировка. Но главная проблема остаётся: успешный tool call не превращается в полезное observation для следующего шага.
После 50 шагов:
Max steps (50) reached without final_answer()
То есть система не упала. Она работала. Просто агент фактически действовал почти вслепую.
Первая реакция была логичной: исправить prompt
Я добавила в system prompt явное правило:
Tool return values are not automatically visible on the next step. If you need a tool result for later reasoning, assign it and print it. Do not call an observation-producing tool only as a bare expression.
То есть вместо:
list_dir(".")
агент должен был делать:
result=list_dir(".") print(result)
На уровне логики это выглядело разумно.
Если проблема в том, что модель не выводит результат, значит нужно жёстче объяснить ей, как работать с observation.
Я запускаю второй эксперимент.
Результат:
снова FAIL после 50 шагов.
Prompt стал лучше. Поведение системы — нет. И вот здесь для меня как раз началась самая полезная часть эксперимента.
Если критически важное поведение держится только на prompt — это слабое место системы
После второго запуска стало понятно, что я пытаюсь исправить архитектурную проблему инструкцией модели. Это разные уровни.
Модель может:
- забыть правило;
- интерпретировать его иначе;
- сгенерировать код без print();
- выбрать другой паттерн вызова;
- просто не выполнить инструкцию идеально.
Но observation — это не косметическая часть ответа. Это данные, на которых строится следующий reasoning step. Если агент не увидел результат tool call, следующий шаг уже опирается на неполную картину.
То есть цепочка:
tool executed → result exists → model did not expose it → runtime reports "no output" → next step receives no evidence
И дальше ошибка начинает размножаться сама.
Это уже не prompt problem.
Это runtime contract problem.
Что я поменяла в третьем запуске
Я перенесла ответственность за observation из prompt-level behavior в сам harness. В runtime появилась отдельная логика исполнения agent code.
Если последняя строка сгенерированного кода — expression, harness сам вычисляет её значение:
expression_result=execute_agent_code( code, exec_globals, )
И если результат не None, runtime автоматически превращает его в observation:
ifexpression_resultisnotNoneandnotdone: print(clip(expression_result))
То есть теперь даже если агент пишет:
list_dir(".")
а не:
print(list_dir("."))
результат всё равно может быть захвачен runtime. Это небольшое изменение в коде. Но оно меняет сам контракт системы: критическое observation больше не зависит полностью от дисциплины модели.
Что произошло после этого
Третий запуск завершился за 9 шагов. На первом шаге агент увидел содержимое workspace:
['.codex/', 'README.md', 'app.py', 'hooks/']
На втором прочитал README.
На третьем — app.py.
Потом посмотрел hooks/, send_event.py, block_dangerous.py, конфигурацию hooks.
И на девятом шаге вызвал:
final_answer(...)
То есть было:
RUN 01 → FAIL / 50 steps RUN 02 → FAIL / 50 steps RUN 03 → PASS / 9 steps
Причём задача, модель и общий сценарий не превратились внезапно во что-то другое. Ключевое изменение произошло между tool result и observation.
Почему это важнее конкретного бага
Можно посмотреть на этот случай как на мелкую техническую ошибку: забыли вывести return value. Но мне кажется, здесь есть более общий принцип. В agent system есть несколько разных уровней: модель инструкция tool runtime observation state следующий шаг.И если ошибка находится на одном уровне, попытка чинить другой может вообще ничего не дать.
В моём случае:
симптом: агент не понимает workspace первая гипотеза: плохая инструкция модели реальная причина: runtime теряет tool return value как observation
Prompt engineering здесь не устранял корневую проблему. Он только пытался заставить модель компенсировать поведение runtime.
Это очень похоже на обычные бизнес-системы
Мне вообще всё больше нравится смотреть на AI-агентов не как на «умную модель с инструментами», а как на обычную распределённую систему со своими контрактами.
Например: API вернул данные
ещё не значит:следующий компонент их получил
Точно так же: tool успешно выполнился
не значит: модель увидела результат
Между этими событиями есть слой передачи. Если он работает неправильно, сверху можно бесконечно улучшать prompt и всё равно не получить стабильную систему.
Что ещё показал эксперимент
Важно, что во время неудачных запусков safety boundaries продолжали работать. Harness был ограничен workspace.
Попытка обратиться к / блокировалась:
Path escapes workspace
Неразрешённые shell-команды тоже блокировались.
Запись в файлы была отключена:
ALLOW_WRITE = False
А целостность frozen workspace проверялась отдельно через SHA-256. То есть FAIL агента не означал, что «всё сломалось». Наоборот, это хороший пример того, что разные свойства системы нужно оценивать отдельно:
task success = FAIL safety boundary = PASS workspace integrity = PASS
Для меня это тоже важный вывод. Финальный ответ агента — только одна часть качества системы.
Почему я теперь осторожнее отношусь к «просто улучшим prompt»
Prompt важен.
Но есть вещи, которые я бы вообще не оставляла на уровне model compliance, если они критичны для исполнения.
Например:
- передача observation;
- access control;
- tool permissions;
- approval boundary;
- idempotency;
- state transition;
- blocking опасного действия.
Если система обязана что-то делать всегда, лучше чтобы это обеспечивал runtime или application layer, а не надежда на то, что модель каждый раз правильно выполнит инструкцию.
Практический вывод
Если агент зацикливается, повторяет действия или не завершает простую задачу, я бы теперь проверяла проблему примерно в таком порядке:
1. Что реально вернул tool? 2. Попал ли результат в observation? 3. Что именно увидела модель на следующем шаге? 4. Не потерялось ли состояние между шагами? 5. Только после этого — prompt и модель. Потому что иногда агент выглядит «глупым» не потому, что модель плохо рассуждает. А потому что система не дала ей увидеть собственный результат.
И в моём эксперименте разница между:
FAIL после 50 шагов
и
PASS за 9 шагов
оказалась не в более мощной модели и не в ещё одном prompt. Она оказалась в одном маленьком, но критичном слое: observation должен быть гарантией runtime, а не просьбой к модели.