Agent loop за 30 секунд

Text
while True:
    response = llm.send(system_prompt + history + tools)

    if response.type == "text":
        return response.text

    if response.type == "tool_use":
        result = execute_tool(response.tool_name, response.params)
        history.append(response)
        history.append(result)
        continue

While-цикл. LLM получает весь контекст (системный промпт + история + описания инструментов), решает что делать. Если генерирует text - ответ готов, цикл останавливается. Если tool_use - хост-система выполняет инструмент, результат добавляется в историю, цикл повторяется.

Ключевое: на каждой итерации в LLM отправляется ВСЯ история. Не дельта. Весь промпт, все предыдущие сообщения, все результаты инструментов. К 100-й итерации один API-вызов может весить 500K+ токенов.

Agent Loop - основной цикл

Как это выглядит изнутри: одна задача, шаг за шагом

У меня работает AI-агент на базе Claude Opus 4.6. Агент построен на Claude Code с переписанным ядром на базе Nanoclaw - легковесного фреймворка для запуска AI-агентов. Покажу, как он выполняет конкретную задачу: прочитать черновик статьи с сайта и дать критику. Задача простая, но агент проходит через 11 шагов, 3 инструмента и 3 ошибки. На каждом шаге - размер контекстного окна.

Вход: одно сообщение

Я пишу в Telegram:

Text
Перечитай черновик и раскритикуй.
https://zinovev.org/draft/d4fe0588-...

Зиновий получает это как текст. Дальше он решает, что делать.

Text
Контекст: 25K токенов (системный промпт + инструменты + память + сообщение)

Шаг 1-2. Загрузка скиллов

LLM видит URL сайта и понимает: нужен доступ к серверу. Вызывает два скилла - один про работу с сайтом (как подключиться, где база), второй про хранилище секретов (как получить SSH-ключ).

Text
→ Skill("content-website")  → инструкция: SSH на сервер, читай документацию
→ Skill("infra-vault")      → инструкция: AppRole auth, endpoint для секретов
Diff
+ Контекст: 30K токенов (+3K скилл сайта, +2K скилл секретов)

До этого момента агент не знал, как подключиться к серверу. Теперь знает. Но за это заплатил 5K токенов контекста - они останутся там до конца сессии.

Шаг 3. Хранилище секретов: получение SSH-ключа

Агент формирует curl для авторизации, получает токен, запрашивает секрет:

Text
→ Bash: curl vault/auth/approle/login → client_token
→ Bash: curl vault/secret/data/bots/zinoviy → ssh_key → KeyError

Ошибка. Ключ называется не ssh_key, а как-то иначе.

Diff
+ Контекст: 31.5K токенов (+0.7K вызов + ответ Vault, +0.8K вызов + ошибка)

Обратите внимание: ошибка тоже занимает место. Сообщение об ошибке, traceback - всё это токены в контексте.

Шаг 4. Восстановление

LLM не паникует. Следующий вызов - запросить список всех ключей:

Text
→ Bash: curl vault → список ключей → нашёл ssh_private_key
→ Bash: curl vault → получил ключ

Ключ называется ssh_private_key, не ssh_key. Агент запомнил имя из прошлых сессий неточно. Исправился за один шаг.

Diff
+ Контекст: 34.5K токенов (+1K список ключей, +2K содержимое SSH-ключа)

Шаг 5. SSH: первая попытка

Text
→ Bash: ssh zinoviy@external-host → Connection closed by remote host

Ошибка номер два. Внешний hostname проксируется через Cloudflare Tunnel, SSH через него не пройдёт.

Diff
+ Контекст: 35K токенов (+0.5K вызов + ошибка подключения)

Шаг 6. SSH: вторая попытка

Агент берёт внутренний адрес из контекста (он есть в описании скилла):

Text
→ Bash: ssh zinoviy@internal-host → подключился
→ Bash: find / -name AGENT-INSTRUCTIONS.md → нашёл
Diff
+ Контекст: 36K токенов (+0.5K вызов + результат find)

Шаг 7. Чтение документации

Text
→ Bash: cat AGENT-INSTRUCTIONS.md → схема БД, SQLite, таблица posts, поле content

Теперь агент знает: черновик лежит в SQLite, нужно сделать SELECT по draft_token.

Diff
+ Контекст: 40K токенов (+4K документация с полной схемой БД)

Скачок. Один файл документации добавил столько же, сколько два скилла вместе. Большие результаты инструментов - главный источник роста контекста.

Шаг 8. SQLite: прямой доступ

Text
→ Bash: sqlite3 data/zinovev.db "SELECT content..." → command not found

Ошибка номер три. На хост-машине нет sqlite3 - сайт крутится в Docker.

Diff
+ Контекст: 40.5K токенов (+0.5K вызов + ошибка)

Шаг 9. Поиск обходного пути

Text
→ Bash: docker ps → нашёл контейнер

Внутри - Node.js с better-sqlite3. Можно выполнить запрос через node.

Diff
+ Контекст: 41K токенов (+0.5K вызов + список контейнеров)

Шаг 10. Получение статьи

Text
→ Bash: docker exec web-1 node -e "
    const db = require('better-sqlite3')('/app/data/zinovev.db');
    const r = db.prepare('SELECT content FROM posts WHERE draft_token=?')
               .get('d4fe0588-...');
    console.log(r.content)
  "
→ Результат: 30K символов markdown

Четыре системы в одной команде: SSH → Docker → Node.js → SQLite. Агент выстроил эту цепочку сам, через три ошибки.

Diff
+ Контекст: 49K токенов (+8K текст статьи в markdown)

Самый большой скачок за всю сессию. 30K символов markdown - это примерно 8K токенов. Статья теперь целиком лежит в контексте, и агент может её анализировать.

Шаг 11. Анализ и ответ

Статья в контексте. LLM читает, анализирует, генерирует критику. Цикл останавливается - ответ типа text, не tool_use.

Diff
+ Контекст: 51K токенов (+2K сгенерированный ответ с критикой)

Итого: бюджет одной задачи

Text
Старт:                      25K
После загрузки скиллов:      30K   (+5K)
После работы с секретами:    34.5K (+4.5K, из них 0.8K - ошибка)
После SSH (2 попытки):       36K   (+1.5K, из них 0.5K - ошибка)
После чтения документации:   40K   (+4K)
После поиска пути к базе:    41K   (+1K, из них 0.5K - ошибка)
После получения статьи:      49K   (+8K - самый большой кусок)
После анализа и ответа:      51K   (+2K)

За 11 шагов контекст вырос с 25K до 51K. Удвоился. Из 26K новых токенов:

  • ~8K (31%) - текст статьи, ради которого всё затевалось
  • ~5K (19%) - скиллы с инструкциями
  • ~4K (15%) - документация сервера
  • ~2K (8%) - SSH-ключ
  • ~2K (8%) - ответ агента
  • ~1.8K (7%) - ошибки и их traceback
  • ~3.2K (12%) - всё остальное (tool call JSON, мелкие результаты)
Рост контекста: 11 шагов реальной задачи (25K → 51K)

Каждый из 11 API-вызовов отправлял весь накопленный контекст. Но благодаря prompt cache, первые 25K (системный промпт + инструменты) кешировались и не пересчитывались. Новыми на каждом шаге были только дельты - от 0.5K до 8K токенов.


Под капотом: контекст, инструменты, кеш

Контекстное окно - оперативная память агента

У Claude Opus 4.6 контекстное окно - 1 миллион токенов. Это примерно 700 страниц текста. Но ещё до первого сообщения 25-30K уже заняты.

Реальный состав системного промпта:

Text
Системный промпт:            8-15K токенов
  - Персона (кто, как общается)
  - Правила работы с секретами
  - Ограничения (не трогать прод без ок)
  - Список доступных скиллов

Описания инструментов:       5-10K токенов
  - JSON Schema для каждого из 20+ инструментов
  - Bash, Read, Write, Edit, Grep, Glob, WebFetch, Agent...

Память (MEMORY.md):          1-3K токенов
  - Индекс фактов о пользователе
  - Правила из прошлых сессий
  - Ссылки на проектные решения

Скиллы загружаются по требованию. У агента 20+ скиллов, каждый - 1-3K токенов. Если загрузить все разом, это +30-40K к контексту. Поэтому скиллы подгружаются по требованию: пишу "проверь Метрику" - загружается analytics-yandex. Это lazy loading для LLM-контекста. Не столько экономия, сколько качество: чем меньше нерелевантного текста в промпте, тем точнее модель следует нужным инструкциям.

Комфортная зона для длинных сессий - до 350K. После этого качество ответов проседает: внимание модели размывается. Наша задача уложилась в 51K - это 5% от миллионного окна.

Три механизма защиты от переполнения:

Компрессия. На 75% заполнения (750K) система автоматически сжимает старые сообщения. Агент помнит ЧТО делал, но теряет детали - точные параметры, промежуточные результаты.

Новая сессия. На 600K+ лучше перезапустить. Файловая память (MEMORY.md) переживает перезапуск - ключевые факты загрузятся в новый контекст. Потеряются только детали текущего разговора.

Субагенты для изоляции. Тяжёлые операции (парсинг HTML, браузерная автоматизация) выносятся в субагента. Он работает в чистом контекстном окне и возвращает только результат. Основной контекст не засоряется.

Инструменты: как LLM управляет реальным миром

LLM не имеет доступа к терминалу. Она генерирует JSON - имя инструмента и параметры. Хост-система парсит JSON, проверяет permissions, выполняет действие, возвращает результат в контекст.

Вот как выглядит описание инструмента в промпте:

JSON
{
  "name": "Bash",
  "description": "Executes a given bash command and returns its output.",
  "input_schema": {
    "properties": {
      "command": {"type": "string", "description": "The command to execute"},
      "description": {"type": "string", "description": "What this command does"}
    },
    "required": ["command"]
  }
}

20+ таких описаний в промпте = 5-10K токенов постоянной "аренды". LLM выбирает инструмент не через if/else, а через генерацию текста на основе этих JSON Schema. Полная карта всех 50+ инструментов, команд и скрытых фич Claude Code - на ccunpacked.dev.

А вот реальный tool call из рабочей сессии:

JSON
{
  "type": "tool_use",
  "name": "Bash",
  "input": {
    "command": "ssh zinoviy@server 'docker exec web-1 node -e \"const db = require(better-sqlite3)(zinovev.db); db.prepare(SELECT slug, seo_title FROM posts).all().forEach(r => console.log(r.slug, r.seo_title))\"'",
    "description": "Check SEO titles for all posts"
  }
}

SSH на сервер, оттуда в Docker-контейнер, Node.js подключает SQLite, SELECT из базы. Четыре системы в одной команде. Агент сам выстроил эту цепочку на основе контекста. Подробнее про инструмент, в котором это работает - в разборе Claude Code.

Tool Dispatch: от JSON до реального мира

Когда loop ломается

Тот же механизм, который позволяет агенту восстанавливаться после ошибок, может его убить.

Агент получил задачу, попробовал, ошибка. Скорректировал, попробовал снова. Опять ошибка. И снова. Каждый retry добавляет в контекст ещё одну попытку, ещё один результат. Контекст растёт. Качество решений падает - модель хуже удерживает ранние детали в разбухшем контексте. К 50-й попытке LLM уже не "помнит", что пробовала то же самое 49 раз. Каждый retry кажется ей первым.

Это retry storm. While-цикл без условия выхода.

Что должно быть, но пока не везде есть:

  1. Лимит retry. Максимум 5 попыток одного действия, потом эскалация к человеку
  2. Circuit breaker. 3 одинаковых ошибки подряд - стоп
  3. Мониторинг контекста. Если контекст растёт быстрее обычного - что-то пошло не так

Как реализовать эти защиты на практике - в статье Как создать ИИ-агента.


Что из этого следует

Автоматизация с AI агентом - это не магия, а инженерная задача. Agent loop - простая конструкция. While-цикл, LLM, инструменты. Вся сложность - в том, что происходит внутри цикла.

Когда loop работает штатно, агент выглядит умным: ошибся с именем ключа - нашёл правильное за один шаг. Попробовал внешний адрес - переключился на внутренний. Это не интеллект. Это перебор с обратной связью: каждая ошибка попадает в контекст и влияет на следующее решение.

Когда loop ломается, тот же механизм работает против тебя. Контекст растёт, внимание размывается, каждый retry кажется первым. Разница между "агент решил задачу за 11 шагов" и "агент крутится в бесконечном цикле" - один circuit breaker.

Если хотите разобраться глубже - у меня есть курс по Claude на Stepik и Telegram-канал, где разбираю такие кейсы. А здесь - кто я и чем занимаюсь.