AI-агент дважды дошёл до лимита в 50 шагов. Я пыталась чинить prompt — проблема оказалась в runtime

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, а не просьбой к модели.

11