Модель исполнения: контекст, инструменты, цикл наблюдение-действие
Когда инженер впервые видит, как агент сам читает файлы, запускает тесты и правит код, возникает ощущение собеседника, который «работает в проекте». Это ощущение — самый дорогой источник ошибок: оно заставляет доверять словам вместо вывода команд, удивляться «забывчивости» на сороковом шаге и не понимать, почему счёт за час вырос в разы.
Лечится оно разбором модели исполнения — как и любое магическое мышление про технологию.
Программист на C держит в голове модель памяти, фронтендер — event loop, бэкендер — модель
транзакций своей СУБД. Модель исполнения агента проще всех трёх: это цикл while вокруг
функции без памяти. Глава отвечает на три вопроса: что физически уезжает в модель на
каждом ходу, кто превращает текст модели в изменение файла и почему цена растёт быстрее,
чем кажется. Про отличие агента от чата было в главе
«Агент и чат»; здесь — механика того, что там
называлось «агент действует сам».
Три вещи, которые называют словом «агент»
Слово «агент» покрывает три разные сущности. Пока они слиты в одну, невозможно даже сформулировать, что сломалось.
| Сущность | Что это | Держит ли состояние | Что ломается на этом уровне |
|---|---|---|---|
| Модель | Функция «текст на входе → текст на выходе». Ничего не помнит между вызовами, ничего не исполняет | Нет | Неверное рассуждение, выдуманный API, неправильный вывод из верных данных |
| Харнесс (Claude Code, Cursor, Codex CLI, Aider, ваш скрипт) | Программа: собирает запрос, парсит ответ, исполняет инструменты, ведёт транскрипт, решает, когда остановиться | Да, весь | Обрезка вывода, компакция, права, ограничения инструментов, зацикливание |
| Среда | Репозиторий, оболочка, сеть, БД, CI | Да, это и есть истина | Расхождение между тем, что в контексте, и тем, что на диске |
Дальше «агент» — связка всех трёх. Но когда что-то пошло не так, первым делом спросите: модель ошиблась в выводе, харнесс потерял данные или среда изменилась под ногами? Три диагноза, три разных исправления. И сразу — важная честность: у модели нет состояния. Ощущение непрерывного разговора создаёт харнесс, пересылая всю историю заново на каждом ходу; это не деталь реализации, а причина половины наблюдаемых странностей. Механику токенов и окна разбирает трек основ — «Токены, контекстное окно и память диалога».
Один ход цикла целиком
Разберём итерацию по секундам. Инженер просит: «почини падающий тест test_reserve_idempotent».
контракт проекта, весь предыдущий транскрипт Х->>М: HTTP-запрос со всем этим текстом М-->>Х: Текст ответа плюс структурный блок вызова инструмента Note over М: Модель ничего не запустила.
Она напечатала намерение в заданном формате Х->>Х: Проверяет схему, права, лимиты Х->>С: bash pytest -k test_reserve_idempotent С-->>Х: stdout, stderr, код возврата Х->>М: Тот же запрос плюс новая пара вызов/наблюдение
(вывод обрезан до лимита) М-->>Х: Следующий шаг рассуждения и следующий вызов Note over Х,С: Цикл повторяется, пока модель не перестанет
запрашивать инструменты либо не сработает лимит Х-->>И: Итоговый ответ и diff
Три вещи из этой диаграммы стоит запомнить дословно. Модель ничего не исполняет. Она печатает структурированный текст — «хочу вызвать
bash с аргументом pytest -k ...». Исполняет харнесс: между «модель решила» и «в системе
что-то произошло» стоит обычный код с валидацией, правами и таймаутами, и именно туда
встраиваются ограничения из главы «Безопасность».
Наблюдение — то, что харнесс положил обратно в транскрипт, а не то, что случилось в среде. Если вывод обрезан на четырёх килобайтах, реальности за этой границей для модели не существует.
Запрос уезжает целиком каждый раз. На двадцатом шаге модель получает те же инструкции, те же схемы и все девятнадцать предыдущих пар «вызов/наблюдение» — это корень экономики цикла.
Анатомия того, что уезжает в модель
Верхние блоки стабильны в пределах сессии — кандидаты на кэширование префикса (prompt caching у Anthropic, аналоги у других провайдеров). Следствие: правка в начале контекста обесценивает кэш всего, что за ней следует, — подставлять «текущее время» в системный промпт дорого. Средний блок растёт линейно с числом шагов, нижний — единственное, что действительно ново.
Главная строчка — внизу картинки. Транскрипт устроен как кэш среды без инвалидации:
файл, прочитанный на четвёртом шаге, лежит в контексте таким, каким был тогда. Если на
девятом его изменил агент, коллега или git pull, модель об этом не узнает — она видит
старую копию и уверенно рассуждает по ней. Отсюда целый класс отказов.
Цикл в псевдокоде и в коде
Псевдокод агентского цикла помещается на экран; всё остальное в харнессах — обвязка.
вход: задача T, набор инструментов Tools, лимиты (шаги, токены, время)
выход: результат либо признак остановки
транскрипт ← [сообщение пользователя T]
для шага i от 1 до max_steps:
ответ ← вызвать_модель(системный_промпт, схемы(Tools), транскрипт)
добавить ответ в транскрипт
вызовы ← извлечь_вызовы_инструментов(ответ)
если вызовов нет:
вернуть Готово(текст ответа)
для каждого вызова c из вызовов:
наблюдение ← исполнить(c) # исполняет харнесс, не модель
добавить наблюдение в транскрипт # в том числе ошибки и таймауты
если размер(транскрипт) > порог:
транскрипт ← компактировать(транскрипт)
вернуть Остановлено(исчерпан лимит шагов)
Реализация на Python — тот же цикл с честной обработкой отказов. call_model оставлена
заглушкой намеренно: имена полей и форма ответа отличаются между провайдерами и версиями
API, их надо смотреть в актуальной документации, а не вспоминать.
import subprocess
from dataclasses import dataclass
MAX_OBSERVATION_CHARS = 8_000 # лимит одного наблюдения в транскрипте
@dataclass
class Done:
text: str
steps: int
@dataclass
class Stopped:
reason: str
steps: int
def run_bash(command: str, cwd: str, timeout: float = 120.0) -> str:
"""ВСЕГДА возвращает наблюдение, в том числе про собственный отказ:
падение инструмента — это факт, который модель должна увидеть."""
try:
proc = subprocess.run(
["bash", "-lc", command],
cwd=cwd, capture_output=True, text=True, timeout=timeout,
)
except subprocess.TimeoutExpired:
return f"exit_code=timeout\nкоманда не завершилась за {timeout:.0f} с"
body = (proc.stdout or "") + (proc.stderr or "")
if len(body) <= MAX_OBSERVATION_CHARS:
return f"exit_code={proc.returncode}\n{body}"
# Маркер обрезки обязателен: без него модель считает хвост концом вывода.
return (f"exit_code={proc.returncode}\n{body[:MAX_OBSERVATION_CHARS]}\n"
f"[обрезано: показано {MAX_OBSERVATION_CHARS} из {len(body)} символов]")
def agent_loop(task: str, tools: dict, cwd: str,
max_steps: int = 25, token_budget: int = 150_000):
transcript = [{"role": "user", "content": task}]
for step in range(1, max_steps + 1):
reply = call_model(SYSTEM_PROMPT, schemas(tools), transcript) # заглушка
transcript.append({"role": "assistant", "content": reply.content})
calls = [b for b in reply.content if b["type"] == "tool_use"]
if not calls:
return Done(reply.text, steps=step)
results = []
for call in calls:
handler = tools.get(call["name"])
# Галлюцинация имени инструмента — тоже наблюдение, а не крах.
observation = (handler(**call["input"], cwd=cwd) if handler
else f"ошибка: инструмента {call['name']} не существует")
results.append({"type": "tool_result",
"tool_use_id": call["id"], "content": observation})
transcript.append({"role": "user", "content": results})
if estimate_tokens(transcript) > token_budget:
transcript = compact(transcript) # разбор компакции — в главе 04
return Stopped("исчерпан лимит шагов", steps=max_steps)
Сложность. Пусть $n$ — число шагов, $B$ — размер стабильного префикса в токенах (системный промпт, схемы инструментов, контракт проекта), $\bar{o}$ — средний прирост транскрипта за шаг (рассуждение модели плюс наблюдение). Тогда суммарное число prompt-токенов за сессию:
$$T_{\text{prompt}}(n) = \sum_{i=1}^{n}\bigl(B + (i-1)\bar{o}\bigr) = nB + \bar{o}\frac{n(n-1)}{2}$$
То есть $O(n^2)$ по входным токенам при $O(n)$ по объёму хранимого транскрипта; выходных токенов линейно, но они обычно дороже за штуку. Кэширование префикса уменьшает константу при $nB$, но не трогает квадратичный член: история на каждом шаге новая. Прикинуть порядок можно без всяких прайсов — подставив свои цифры:
def prompt_tokens(n: int, base: int, per_step: int) -> int:
"""Суммарные prompt-токены за n шагов. base и per_step измеряются по своей сессии."""
return n * base + per_step * n * (n - 1) // 2
for n in (5, 10, 20, 40):
print(n, prompt_tokens(n, base=12_000, per_step=1_500))
# 5 → 75 000; 10 → 187 500; 20 → 525 000; 40 → 1 650 000
base и per_step здесь — иллюстрация формулы, а не измерение: свои снимите со счётчиков
харнесса. Но соотношение от параметров почти не зависит: рост числа шагов в 8 раз даёт
рост prompt-токенов примерно в 22 раза. Поэтому «пусть агент сам поразбирается подольше»
— не бесплатное терпение, а покупка на квадрате. Денежная сторона — в главе
«Цена работы с агентом».
Инструмент — это контракт наблюдения
Инструмент состоит из трёх частей, и путать их нельзя.
- Схема — то, что видит модель: имя, описание, JSON Schema аргументов. Это часть промпта, она занимает токены и влияет на решения.
- Исполнитель — обычная функция в харнессе: валидация, права, таймаут, обрезка.
- Наблюдение — строка, вернувшаяся в транскрипт. Именно она, а не реальный эффект, формирует картину мира у модели.
{
"name": "run_tests",
"description": "Запускает pytest по выбранному пути. Возвращает код возврата, число собранных тестов и последние 200 строк вывода. Использовать для проверки поведения кода, а не для чтения файлов.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Путь к файлу или каталогу тестов"},
"keyword": {"type": "string", "description": "Выражение для -k, необязательно"}
},
"required": ["path"]
}
}
Три правила проектирования прямо следуют из механики цикла.
Наблюдение должно содержать признак сделанной работы. exit_code=0 от тест-раннера,
не нашедшего ни одного теста, — ложноположительный сигнал, и модель на него купится, как
купился бы человек, глядящий только на цвет плашки. Возвращайте число собранных тестов,
число изменённых файлов, длину diff.
Обрезка должна быть явной. Молчаливое усечение — прямая дорога к выводу «ошибок нет»
по хвосту лога; маркер [обрезано: ...] стоит десяток токенов и снимает класс отказов.
Один инструмент — одно наблюдение. Универсальный bash удобен и одновременно главный
источник шума: одна команда find может съесть больше окна, чем всё полезное содержимое
сессии. Узкие инструменты с предсказуемым размером ответа дешевле. Как отсюда вырастают
MCP и свои интеграции — в главе
«Инструменты и протоколы» и в треке ИИ-инженерии
(«MCP»).
Наблюдение против утверждения
Главный практический вывод главы, и он стоит того, чтобы вынести его отдельно.
Фраза агента «я запустил тесты, всё зелёное» — это выход языковой модели. Она порождается тем же механизмом, что и любой другой текст, и не является свидетельством запуска. Свидетельство — блок
tool_resultв транскрипте с командой и её выводом.
Между «модель написала, что проверила» и «проверка была» стоит ровно один вопрос: есть ли в ленте соответствующее наблюдение. Это не недоверие к инструменту, а следствие механики: генерация текста и вызов инструмента — физически разные события, и первое не влечёт второго.
Отсюда привычка, экономящая часы: просить не выводы, а адреса и прогоны. «Покажи
команду и её вывод», «дай файл и строку», «приложи diff». В пакете шаблонов портала это
оформлено правилом: каждое нетривиальное утверждение несёт маркер verified / derived /
assumed / unchecked, и verified про внешнюю систему принимается только с адресом —
путь и строка, команда и её вывод либо датированный URL
(products/workbench/templates/memory/REASONING-DISCIPLINE.md). Там же зафиксировано, что
воспоминание модели о библиотеке — unchecked по определению, как бы уверенно оно
ни звучало.
Полная типология вранья — в главе «Где агенты врут», процедуры доказательства — в главе «Проверяемость»; здесь только та часть, которая объясняется механикой цикла.
Отказы, которые вытекают прямо из модели исполнения
| Отказ | Что происходит в цикле | Дешёвый способ поймать |
|---|---|---|
| Фантомное наблюдение | В тексте есть вывод о поведении кода, в транскрипте нет соответствующего tool_result |
Поиск по ленте: где вызов? Требование печатать команду и её вывод |
| Устаревшее наблюдение | Файл прочитан на шаге 4, изменён на шаге 9, решение принимается по копии из контекста | Перечитывание перед правкой; инструменты правки, требующие точного совпадения исходного фрагмента, — падение такой правки и есть сигнал расхождения |
| Усечённое наблюдение | Вывод обрезан, вывод «всё чисто» сделан по видимой части | Явный маркер обрезки; агрегаты вместо простыней: grep -c, ... | tail -30, сводка тест-раннера |
| Потеря середины | Нужный факт лежит в середине длинного контекста и вытесняется краями (Liu et al., 2023) | Свежее чтение ключевого места непосредственно перед решением |
| Компакция унесла инвариант | Сводка сохранила «что делали» и выкинула «почему нельзя трогать миграции» | Инварианты живут вне транскрипта: файл контракта, тест, запись в задаче |
| Петля | Один и тот же вызов с теми же аргументами повторяется третий раз | Правило «два одинаковых провала — остановись и спроси»; счётчик повторов в харнессе |
| Успех без эффекта | Код возврата 0 у команды, которая ничего не сделала | Наблюдение обязано содержать метрику работы: число тестов, число файлов, размер diff |
| Подмена цели | Вместо починки кода правится тест или добавляется пропуск | Смотреть diff тестов отдельно от diff кода — всегда |
Ни один из восьми пунктов не «глупость модели»: все восемь — следствия того, что контекст есть снимок прошлого, наблюдения ограничены по объёму, а решение принимается по тексту, а не по среде. Лечатся они инженерно, а не уговорами в промпте. Диагностический маршрут, когда агент отчитался «готово»:
инструмента с выводом?"} B -- "нет" --> N1["Утверждение ничем не подкреплено.
Просить прогон, а не пересказ"] B -- "да" --> C{"Команда проверяет именно то,
что заявлено?"} C -- "нет" --> N2["Пример: линтер вместо тестов,
сборка вместо поведения"] C -- "да" --> D{"Вывод полный или помечен
как обрезанный?"} D -- "обрезан" --> N3["Повторить с агрегатом:
счётчики, хвост, сводка"] D -- "полный" --> E{"Наблюдение получено ПОСЛЕ
последней правки файлов?"} E -- "нет" --> N4["Прогон устарел, правки не проверены"] E -- "да" --> F{"Воспроизводится у вас
на чистом дереве?"} F -- "нет" --> N5["Разница среды: кэш, зависимости,
переменные окружения, незакоммиченные файлы"] F -- "да" --> G["Теперь можно читать код
по существу"]
Последний узел важен: воспроизведение прогона — ещё не ревью кода, а только допуск к нему. Что смотреть дальше — в главе «Ревью кода от агента».
Жизненный цикл одного хода
Состояния хода и переходы между ними полезно держать в голове: по ним понятно, где именно сессия свернула не туда.
Два состояния важнее остальных. Компакция — единственный переход, на котором информация исчезает безвозвратно: всё, что должно её пережить, обязано жить снаружи — в файле, тесте, задаче (подробно в главах «Контекст» и «Память агента»). ВопросЧеловеку — правильный терминал, а не поражение: агент, остановившийся на третьей неудачной попытке, дешевле агента, переписавшего на двадцатой половину модуля. Когда именно останавливаться — в главе «Планирование и декомпозиция».
Управление циклом: что реально работает
Восемь практик — прямые следствия механики выше. Ни одна не «секретный промпт», все проверяемы.
- Короткие итерации вместо длинных заплывов. Не потому, что агент «устаёт», а потому, что расхождение контекста со средой копится, а стоимость растёт на квадрате. Свежая сессия с точной постановкой обычно дешевле продолжения сорокашаговой.
- Детерминированные ворота вместо оценочных суждений. «Прогоняй
make checkпосле каждой правки» работает; «проверь, что всё хорошо» — нет: у него нет наблюдаемого выхода. - Быстрая обратная связь окупается дважды. Восьмиминутный тест агент запустит один раз в конце, восьмисекундный — после каждой правки. Тот же аргумент, что и в юнит-тестировании, только цена медленной петли выражается ещё и в токенах.
- Наблюдения-агрегаты вместо простыней.
pytest -q,git diff --stat,grep -c: экономия контекста и меньший шанс усечения одним действием. - Инварианты — в файл, не в чат. Контракт репозитория (
AGENTS.md,CLAUDE.md, правила проекта) переживает компакцию, реплика в середине сессии — нет. - Контракт до реализации. Сигнатура, предусловия, постусловия, ошибки — до тела
функции; если четыре пункта не формулируются, требование не понято и правильный
следующий шаг — вопрос, а не код (
CODE-01изVERIFIED-CODE.mdв пакете шаблонов). - Маркеры и адреса в отчётах. Отчёт без адресов читается не быстрее, а медленнее: всё приходится перепроверять с нуля.
- Законы вместо примеров, где они есть. Идемпотентность, обратимость, коммутативность
повторов проверяются property-тестом и не требуют веры в рассказ агента о поведении
кода (
COMPOSITION-AND-LAWS.mdтам же).
Пакет лежит в репозитории портала (products/workbench/templates/memory/) и написан под
ту же модель исполнения: каждое правило формулирует наблюдаемое нарушение, видимое
в диффе или в ленте сессии. Правило, нарушение которого нельзя заметить, — украшение.
Когда цикл дороже рук
Честный ответ: часто. Цикл платит фиксированную цену за каждый заход — сборка контекста, ориентация в репозитории, прогон проверок, ваше внимание на ревью. Если механической работы мало, а проверка результата дорога, он не окупается.
Верхняя граница пользы упирается не в модель, а в вашу способность проверить результат. Задача без дешёвого критерия правильности не становится проще от того, что код написал агент, — она становится опаснее: кода больше, и он выглядит убедительно.
Стоит знать и эмпирику, противоречащую интуиции. В рандомизированном исследовании METR (2025) опытные контрибьюторы open-source решали задачи в своих же репозиториях медленнее с ИИ-инструментами, чем без них, при том что субъективно ощущали ускорение (отчёт METR). Это одно исследование, небольшая выборка, конкретные инструменты начала 2025 года: обобщать его на «агенты не помогают» некорректно ровно так же, как обобщать вендорские демо на «агенты пишут проекты». Но один вывод оно поддерживает твёрдо: ощущение скорости и скорость — разные величины, и мерить надо вторую.
Разные харнессы, один цикл
Инструменты отличаются не механикой, а тем, кто владеет циклом и что считается наблюдением. Конкретные возможности меняются между версиями — сверяйтесь с документацией на дату, а не с чужим постом полугодовой давности.
| Класс | Кто владеет циклом | Что считается наблюдением | Чем платите |
|---|---|---|---|
| CLI-агент в терминале (Claude Code, Codex CLI, Aider) | Харнесс в вашей оболочке, ваши права | Вывод команд, файлы, git | Полный доступ к среде требует явных ограничений прав |
| Агент в IDE (Cursor и подобные) | Редактор; часть контекста подставляется автоматически | Файлы, индекс проекта, диагностика языкового сервера | Меньше видно, что именно уехало в контекст |
| Фоновый или облачный запуск | Сервис на своей копии репозитория | CI, тесты, изолированная среда | Задержка обратной связи, расхождение среды с локальной |
| Свой скрипт поверх API | Вы | То, что вы вернули из инструментов | Всё пишете сами, зато всё наблюдаемо |
Ни один вариант не отменяет разбора выше: везде цикл над функцией без памяти, транскрипт — снимок прошлого, наблюдение важнее утверждения. Выбор инструмента — тема главы «Инструменты и практика».
Мини-эксперимент на полчаса
Модель исполнения становится своей, когда её измерили на своём проекте.
- Возьмите задачу, которую умеете сделать руками за 20–30 минут.
- Запустите агента и не помогайте первые десять шагов. Ведите журнал: шаг, инструмент, размер наблюдения, польза.
- Отметьте первый шаг, где решение принято по устаревшему или обрезанному наблюдению. Обычно он есть, и обычно раньше, чем ожидалось.
- Снимите prompt-токены на десятом и на двадцатом шаге и сравните отношение с формулой выше — квадратичный член виден невооружённым глазом.
- Сделайте ту же задачу руками: время, внимание на ревью, качество.
Этот замер полезнее любой чужой статистики — он про ваш репозиторий и вашу скорость, и он же даёт личный порог, ниже которого агента звать не стоит.
Типичные ошибки
- Считать текст модели отчётом о действиях. Отчёт — это транскрипт с наблюдениями.
- Держать одну сессию весь день. Дорого на квадрате, и расхождение со средой копится.
- Давать один универсальный
bashи удивляться шуму в контексте. - Класть инварианты в чат, а не в файл. Компакция их не обязана сохранить.
- Просить «проверь, что всё работает» вместо команды с наблюдаемым выходом.
- Оценивать пользу по ощущению скорости. Мерить надо время до зелёного и время ревью.
- Спорить с агентом текстом. Расхождение решается прогоном, а не третьей формулировкой.
- Не смотреть, что уехало в контекст. Один взгляд экономит час догадок.
Мини-итог
- Агент — цикл харнесса вокруг модели без памяти, действующей на среду через инструменты; на каждом ходу уезжает всё заново: инструкции, схемы, контракт, весь транскрипт. Отсюда $O(n^2)$ по prompt-токенам и прямая цена длинных сессий.
- Контекст — снимок прошлого, кэш среды без инвалидации; устаревшие и обрезанные наблюдения дают больше отказов, чем ошибки рассуждения.
- Модель ничего не исполняет: она печатает намерение, исполняет харнесс. Наблюдение — это то, что харнесс вернул в ленту, а не то, что произошло в системе.
- Утверждение о поведении кода без прогона не стоит ничего: просите адреса и вывод команд
(маркеры
verified/derived/assumed/uncheckedиз пакета шаблонов портала). - Быстрые проверки — главный рычаг качества цикла: превращают мнение в наблюдение на каждом шаге, а не в конце. А если механической работы мало, а проверка дорога — сделайте руками.
Источники
- Yao et al., «ReAct: Synergizing Reasoning and Acting in Language Models» — https://arxiv.org/abs/2210.03629
- Anthropic, «Building Effective Agents» — https://www.anthropic.com/engineering/building-effective-agents
- Anthropic, tool use — https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview, prompt caching — https://docs.claude.com/en/docs/build-with-claude/prompt-caching
- Model Context Protocol — https://modelcontextprotocol.io/
- Liu et al., «Lost in the Middle: How Language Models Use Long Contexts» — https://arxiv.org/abs/2307.03172
- METR, «Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity» — https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/
- SWE-bench — https://www.swebench.com/ ; документация харнессов: Claude Code — https://docs.claude.com/en/docs/claude-code/overview, Aider — https://aider.chat/, Cursor — https://docs.cursor.com/
Что дальше
Механика разобрана: цикл, контекст, инструменты, наблюдения. Дальше — как задавать работу в этом цикле так, чтобы результат принимался или отклонялся по формальным признакам, а не по впечатлению: Промпт как спецификация, а не заклинание.