Agent loop за 30 секунд
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)
continueWhile-цикл. LLM получает весь контекст (системный промпт + история + описания инструментов), решает что делать. Если генерирует text - ответ готов, цикл останавливается. Если tool_use - хост-система выполняет инструмент, результат добавляется в историю, цикл повторяется.
Ключевое: на каждой итерации в LLM отправляется ВСЯ история. Не дельта. Весь промпт, все предыдущие сообщения, все результаты инструментов. К 100-й итерации один API-вызов может весить 500K+ токенов.
Как это выглядит изнутри: одна задача, шаг за шагом
У меня работает AI-агент на базе Claude Opus 4.6. Агент построен на Claude Code с переписанным ядром на базе Nanoclaw - легковесного фреймворка для запуска AI-агентов. Покажу, как он выполняет конкретную задачу: прочитать черновик статьи с сайта и дать критику. Задача простая, но агент проходит через 11 шагов, 3 инструмента и 3 ошибки. На каждом шаге - размер контекстного окна.
Вход: одно сообщение
Я пишу в Telegram:
Перечитай черновик и раскритикуй.
https://zinovev.org/draft/d4fe0588-...Зиновий получает это как текст. Дальше он решает, что делать.
Контекст: 25K токенов (системный промпт + инструменты + память + сообщение)Шаг 1-2. Загрузка скиллов
LLM видит URL сайта и понимает: нужен доступ к серверу. Вызывает два скилла - один про работу с сайтом (как подключиться, где база), второй про хранилище секретов (как получить SSH-ключ).
→ Skill("content-website") → инструкция: SSH на сервер, читай документацию
→ Skill("infra-vault") → инструкция: AppRole auth, endpoint для секретов+ Контекст: 30K токенов (+3K скилл сайта, +2K скилл секретов)До этого момента агент не знал, как подключиться к серверу. Теперь знает. Но за это заплатил 5K токенов контекста - они останутся там до конца сессии.
Шаг 3. Хранилище секретов: получение SSH-ключа
Агент формирует curl для авторизации, получает токен, запрашивает секрет:
→ Bash: curl vault/auth/approle/login → client_token
→ Bash: curl vault/secret/data/bots/zinoviy → ssh_key → KeyErrorОшибка. Ключ называется не ssh_key, а как-то иначе.
+ Контекст: 31.5K токенов (+0.7K вызов + ответ Vault, +0.8K вызов + ошибка)Обратите внимание: ошибка тоже занимает место. Сообщение об ошибке, traceback - всё это токены в контексте.
Шаг 4. Восстановление
LLM не паникует. Следующий вызов - запросить список всех ключей:
→ Bash: curl vault → список ключей → нашёл ssh_private_key
→ Bash: curl vault → получил ключКлюч называется ssh_private_key, не ssh_key. Агент запомнил имя из прошлых сессий неточно. Исправился за один шаг.
+ Контекст: 34.5K токенов (+1K список ключей, +2K содержимое SSH-ключа)Шаг 5. SSH: первая попытка
→ Bash: ssh zinoviy@external-host → Connection closed by remote hostОшибка номер два. Внешний hostname проксируется через Cloudflare Tunnel, SSH через него не пройдёт.
+ Контекст: 35K токенов (+0.5K вызов + ошибка подключения)Шаг 6. SSH: вторая попытка
Агент берёт внутренний адрес из контекста (он есть в описании скилла):
→ Bash: ssh zinoviy@internal-host → подключился
→ Bash: find / -name AGENT-INSTRUCTIONS.md → нашёл+ Контекст: 36K токенов (+0.5K вызов + результат find)Шаг 7. Чтение документации
→ Bash: cat AGENT-INSTRUCTIONS.md → схема БД, SQLite, таблица posts, поле contentТеперь агент знает: черновик лежит в SQLite, нужно сделать SELECT по draft_token.
+ Контекст: 40K токенов (+4K документация с полной схемой БД)Скачок. Один файл документации добавил столько же, сколько два скилла вместе. Большие результаты инструментов - главный источник роста контекста.
Шаг 8. SQLite: прямой доступ
→ Bash: sqlite3 data/zinovev.db "SELECT content..." → command not foundОшибка номер три. На хост-машине нет sqlite3 - сайт крутится в Docker.
+ Контекст: 40.5K токенов (+0.5K вызов + ошибка)Шаг 9. Поиск обходного пути
→ Bash: docker ps → нашёл контейнерВнутри - Node.js с better-sqlite3. Можно выполнить запрос через node.
+ Контекст: 41K токенов (+0.5K вызов + список контейнеров)Шаг 10. Получение статьи
→ 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. Агент выстроил эту цепочку сам, через три ошибки.
+ Контекст: 49K токенов (+8K текст статьи в markdown)Самый большой скачок за всю сессию. 30K символов markdown - это примерно 8K токенов. Статья теперь целиком лежит в контексте, и агент может её анализировать.
Шаг 11. Анализ и ответ
Статья в контексте. LLM читает, анализирует, генерирует критику. Цикл останавливается - ответ типа text, не tool_use.
+ Контекст: 51K токенов (+2K сгенерированный ответ с критикой)Итого: бюджет одной задачи
Старт: 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 API-вызовов отправлял весь накопленный контекст. Но благодаря prompt cache, первые 25K (системный промпт + инструменты) кешировались и не пересчитывались. Новыми на каждом шаге были только дельты - от 0.5K до 8K токенов.
Под капотом: контекст, инструменты, кеш
Контекстное окно - оперативная память агента
У Claude Opus 4.6 контекстное окно - 1 миллион токенов. Это примерно 700 страниц текста. Но ещё до первого сообщения 25-30K уже заняты.
Реальный состав системного промпта:
Системный промпт: 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, выполняет действие, возвращает результат в контекст.
Вот как выглядит описание инструмента в промпте:
{
"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 из рабочей сессии:
{
"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.
Когда loop ломается
Тот же механизм, который позволяет агенту восстанавливаться после ошибок, может его убить.
Агент получил задачу, попробовал, ошибка. Скорректировал, попробовал снова. Опять ошибка. И снова. Каждый retry добавляет в контекст ещё одну попытку, ещё один результат. Контекст растёт. Качество решений падает - модель хуже удерживает ранние детали в разбухшем контексте. К 50-й попытке LLM уже не "помнит", что пробовала то же самое 49 раз. Каждый retry кажется ей первым.
Это retry storm. While-цикл без условия выхода.
Что должно быть, но пока не везде есть:
- Лимит retry. Максимум 5 попыток одного действия, потом эскалация к человеку
- Circuit breaker. 3 одинаковых ошибки подряд - стоп
- Мониторинг контекста. Если контекст растёт быстрее обычного - что-то пошло не так
Как реализовать эти защиты на практике - в статье Как создать ИИ-агента.
Что из этого следует
Автоматизация с AI агентом - это не магия, а инженерная задача. Agent loop - простая конструкция. While-цикл, LLM, инструменты. Вся сложность - в том, что происходит внутри цикла.
Когда loop работает штатно, агент выглядит умным: ошибся с именем ключа - нашёл правильное за один шаг. Попробовал внешний адрес - переключился на внутренний. Это не интеллект. Это перебор с обратной связью: каждая ошибка попадает в контекст и влияет на следующее решение.
Когда loop ломается, тот же механизм работает против тебя. Контекст растёт, внимание размывается, каждый retry кажется первым. Разница между "агент решил задачу за 11 шагов" и "агент крутится в бесконечном цикле" - один circuit breaker.
Если хотите разобраться глубже - у меня есть курс по Claude на Stepik и Telegram-канал, где разбираю такие кейсы. А здесь - кто я и чем занимаюсь.