Инженерия с ИИ-агентами Модель исполнения: контекст, инструменты, цикл наблюдение-действие
0%

Модель исполнения: контекст, инструменты, цикл наблюдение-действие

Модель исполнения: контекст, инструменты, цикл наблюдение-действие

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

Лечится оно разбором модели исполнения — как и любое магическое мышление про технологию. Программист на C держит в голове модель памяти, фронтендер — event loop, бэкендер — модель транзакций своей СУБД. Модель исполнения агента проще всех трёх: это цикл while вокруг функции без памяти. Глава отвечает на три вопроса: что физически уезжает в модель на каждом ходу, кто превращает текст модели в изменение файла и почему цена растёт быстрее, чем кажется. Про отличие агента от чата было в главе «Агент и чат»; здесь — механика того, что там называлось «агент действует сам».

Три вещи, которые называют словом «агент»

Слово «агент» покрывает три разные сущности. Пока они слиты в одну, невозможно даже сформулировать, что сломалось.

Сущность Что это Держит ли состояние Что ломается на этом уровне
Модель Функция «текст на входе → текст на выходе». Ничего не помнит между вызовами, ничего не исполняет Нет Неверное рассуждение, выдуманный API, неправильный вывод из верных данных
Харнесс (Claude Code, Cursor, Codex CLI, Aider, ваш скрипт) Программа: собирает запрос, парсит ответ, исполняет инструменты, ведёт транскрипт, решает, когда остановиться Да, весь Обрезка вывода, компакция, права, ограничения инструментов, зацикливание
Среда Репозиторий, оболочка, сеть, БД, CI Да, это и есть истина Расхождение между тем, что в контексте, и тем, что на диске

Дальше «агент» — связка всех трёх. Но когда что-то пошло не так, первым делом спросите: модель ошиблась в выводе, харнесс потерял данные или среда изменилась под ногами? Три диагноза, три разных исправления. И сразу — важная честность: у модели нет состояния. Ощущение непрерывного разговора создаёт харнесс, пересылая всю историю заново на каждом ходу; это не деталь реализации, а причина половины наблюдаемых странностей. Механику токенов и окна разбирает трек основ — «Токены, контекстное окно и память диалога».

Один ход цикла целиком

Разберём итерацию по секундам. Инженер просит: «почини падающий тест test_reserve_idempotent».

Три вещи из этой диаграммы стоит запомнить дословно. Модель ничего не исполняет. Она печатает структурированный текст — «хочу вызвать 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 раза. Поэтому «пусть агент сам поразбирается подольше» — не бесплатное терпение, а покупка на квадрате. Денежная сторона — в главе «Цена работы с агентом».

Инструмент — это контракт наблюдения

Инструмент состоит из трёх частей, и путать их нельзя.

  1. Схема — то, что видит модель: имя, описание, JSON Schema аргументов. Это часть промпта, она занимает токены и влияет на решения.
  2. Исполнитель — обычная функция в харнессе: валидация, права, таймаут, обрезка.
  3. Наблюдение — строка, вернувшаяся в транскрипт. Именно она, а не реальный эффект, формирует картину мира у модели.
{
  "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 кода — всегда

Ни один из восьми пунктов не «глупость модели»: все восемь — следствия того, что контекст есть снимок прошлого, наблюдения ограничены по объёму, а решение принимается по тексту, а не по среде. Лечатся они инженерно, а не уговорами в промпте. Диагностический маршрут, когда агент отчитался «готово»:

Последний узел важен: воспроизведение прогона — ещё не ревью кода, а только допуск к нему. Что смотреть дальше — в главе «Ревью кода от агента».

Жизненный цикл одного хода

Состояния хода и переходы между ними полезно держать в голове: по ним понятно, где именно сессия свернула не туда.

Два состояния важнее остальных. Компакция — единственный переход, на котором информация исчезает безвозвратно: всё, что должно её пережить, обязано жить снаружи — в файле, тесте, задаче (подробно в главах «Контекст» и «Память агента»). ВопросЧеловеку — правильный терминал, а не поражение: агент, остановившийся на третьей неудачной попытке, дешевле агента, переписавшего на двадцатой половину модуля. Когда именно останавливаться — в главе «Планирование и декомпозиция».

Управление циклом: что реально работает

Восемь практик — прямые следствия механики выше. Ни одна не «секретный промпт», все проверяемы.

  1. Короткие итерации вместо длинных заплывов. Не потому, что агент «устаёт», а потому, что расхождение контекста со средой копится, а стоимость растёт на квадрате. Свежая сессия с точной постановкой обычно дешевле продолжения сорокашаговой.
  2. Детерминированные ворота вместо оценочных суждений. «Прогоняй make check после каждой правки» работает; «проверь, что всё хорошо» — нет: у него нет наблюдаемого выхода.
  3. Быстрая обратная связь окупается дважды. Восьмиминутный тест агент запустит один раз в конце, восьмисекундный — после каждой правки. Тот же аргумент, что и в юнит-тестировании, только цена медленной петли выражается ещё и в токенах.
  4. Наблюдения-агрегаты вместо простыней. pytest -q, git diff --stat, grep -c: экономия контекста и меньший шанс усечения одним действием.
  5. Инварианты — в файл, не в чат. Контракт репозитория (AGENTS.md, CLAUDE.md, правила проекта) переживает компакцию, реплика в середине сессии — нет.
  6. Контракт до реализации. Сигнатура, предусловия, постусловия, ошибки — до тела функции; если четыре пункта не формулируются, требование не понято и правильный следующий шаг — вопрос, а не код (CODE-01 из VERIFIED-CODE.md в пакете шаблонов).
  7. Маркеры и адреса в отчётах. Отчёт без адресов читается не быстрее, а медленнее: всё приходится перепроверять с нуля.
  8. Законы вместо примеров, где они есть. Идемпотентность, обратимость, коммутативность повторов проверяются 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 Вы То, что вы вернули из инструментов Всё пишете сами, зато всё наблюдаемо

Ни один вариант не отменяет разбора выше: везде цикл над функцией без памяти, транскрипт — снимок прошлого, наблюдение важнее утверждения. Выбор инструмента — тема главы «Инструменты и практика».

Мини-эксперимент на полчаса

Модель исполнения становится своей, когда её измерили на своём проекте.

  1. Возьмите задачу, которую умеете сделать руками за 20–30 минут.
  2. Запустите агента и не помогайте первые десять шагов. Ведите журнал: шаг, инструмент, размер наблюдения, польза.
  3. Отметьте первый шаг, где решение принято по устаревшему или обрезанному наблюдению. Обычно он есть, и обычно раньше, чем ожидалось.
  4. Снимите prompt-токены на десятом и на двадцатом шаге и сравните отношение с формулой выше — квадратичный член виден невооружённым глазом.
  5. Сделайте ту же задачу руками: время, внимание на ревью, качество.

Этот замер полезнее любой чужой статистики — он про ваш репозиторий и вашу скорость, и он же даёт личный порог, ниже которого агента звать не стоит.

Типичные ошибки

  • Считать текст модели отчётом о действиях. Отчёт — это транскрипт с наблюдениями.
  • Держать одну сессию весь день. Дорого на квадрате, и расхождение со средой копится.
  • Давать один универсальный bash и удивляться шуму в контексте.
  • Класть инварианты в чат, а не в файл. Компакция их не обязана сохранить.
  • Просить «проверь, что всё работает» вместо команды с наблюдаемым выходом.
  • Оценивать пользу по ощущению скорости. Мерить надо время до зелёного и время ревью.
  • Спорить с агентом текстом. Расхождение решается прогоном, а не третьей формулировкой.
  • Не смотреть, что уехало в контекст. Один взгляд экономит час догадок.

Мини-итог

  • Агент — цикл харнесса вокруг модели без памяти, действующей на среду через инструменты; на каждом ходу уезжает всё заново: инструкции, схемы, контракт, весь транскрипт. Отсюда $O(n^2)$ по prompt-токенам и прямая цена длинных сессий.
  • Контекст — снимок прошлого, кэш среды без инвалидации; устаревшие и обрезанные наблюдения дают больше отказов, чем ошибки рассуждения.
  • Модель ничего не исполняет: она печатает намерение, исполняет харнесс. Наблюдение — это то, что харнесс вернул в ленту, а не то, что произошло в системе.
  • Утверждение о поведении кода без прогона не стоит ничего: просите адреса и вывод команд (маркеры verified / derived / assumed / unchecked из пакета шаблонов портала).
  • Быстрые проверки — главный рычаг качества цикла: превращают мнение в наблюдение на каждом шаге, а не в конце. А если механической работы мало, а проверка дорога — сделайте руками.

Источники

Что дальше

Механика разобрана: цикл, контекст, инструменты, наблюдения. Дальше — как задавать работу в этом цикле так, чтобы результат принимался или отклонялся по формальным признакам, а не по впечатлению: Промпт как спецификация, а не заклинание.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов