Агентный SDLC на внутренней LLM: инженерия вместо промптов

от автора

Большинство публичных материалов об агентной разработке описывает работу с frontier-моделями. В корпоративной среде frontier-модель часто недоступна: отправка кода внешним LLM-провайдерам запрещена политикой безопасности, работа идет через внутренний шлюз, и его модели заметно слабее передовых, а добиться от такой модели соблюдения процесса промптами не удается. В статье описан подход, к которому удалось прийти в этих условиях, — структурная инженерия: запреты в таблицах, которые модель сверяет, а не помнит; правила, приходящие свежим чтением в момент, когда нужны; изоляция агентов через файлы; детерминированные блокировки кодом там, где цена ошибки высока. Дальше — устройство харнеса и осечки, из которых он вырос.

1. Ограничения — условие задачи

Работа над обычным тикетом складывается из известных шагов: ознакомиться с постановкой, при необходимости уточнить детали, найти нужное место в коде, написать реализацию, покрыть тестами, прогнать статанализ, сборку и тесты — скорее всего, не по одному разу, — закоммитить и оформить MR.

Часть этих шагов можно отдать агенту.

Вводные:

  • Крупный нативный Android-проект: Kotlin, смесь Jetpack Compose (новый UI) и View-легаси, строгий статанализ, множество модулей.

  • Внешние модели запрещены: политика безопасности не допускает отправку кода внешним LLM-провайдерам.

  • Есть внутренний LLM-шлюз с несколькими моделями; frontier-уровня среди них нет. Основной рабочей выбрана самая сильная из доступных — текстовая, картинок не видит; остальные еще слабее. Vision-модели в шлюзе тоже есть — из них так же выбрана самая сильная (слабее основной).

  • Агент — opencode, открытый CLI-агент, который можно направить на свой шлюз: инструмент внешний, модельный трафик — внутренний. Выбор не принципиален: все, что описано ниже, переносится на любой CLI-агент с поддержкой правил, субагентов и плагинов.

Эти ограничения мы приняли как условие задачи. Цель с самого начала была практической: провести реальный тикет от постановки до готового диффа.

2. Словарь

Базовые понятия подробно описаны во множестве статей — здесь только краткий словарь.

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

Токен — единица, которой модель измеряет текст. В токенах измеряется и объем ввода, и объем ответа.

Контекст — рабочая память модели жесткого размера — контекстное окно. Все проектное — код, правила, переписку — приходится приносить туда явно, и это бюджет: каждый файл и каждая реплика его тратят.

Агент — LLM, которой дали инструменты и запустили в цикл: модель решает вызвать инструмент, результат кладется обратно в контекст, модель смотрит снова — и так до результата. Вся «агентность» — это цикл плюс инструменты.

MCP (Model Context Protocol) — стандартный «разъем», через который агенту подключают внешние системы: трекер задач, вики, Figma, эмулятор. Инструменты сервера появляются у агента как обычные вызовы.

Субагент — отдельный агент со своей ролью, своим промптом и своим контекстным окном, которого главная сессия вызывает как инструмент.

Скилл — записанная процедура с триггером: описание, по которому агент понимает, когда ее применять, и шаги. Процедура, которую раньше объясняли каждому новичку устно — например, как провести фичефлаг через все слои проекта, — записывается один раз и дальше срабатывает по короткой фразе.

3. Устройство харнеса

Харнесом мы называем всю обвязку вокруг модели: конфигурацию, правила, агентов и защиты. Дальше — конкретика на opencode, но аналогичные механизмы есть в любом CLI-агенте.

Основа — конфиг (в opencode это opencode. json): записи моделей (основная текстовая + vision), MCP-серверы (трекер, вики, Figma, эмулятор, документация библиотек), permissions — какие действия агент выполняет сам, а какие требуют подтверждения человека, — и короткий список правил, которые всегда в контексте.

Рядом — ядро инструкций, файл AGENTS. md: «конституция» проекта, она в контексте каждой сессии без исключений. Внутри: жесткие правила (ветки только от develop, формат коммитов и т. д.), принципы (минимальный дифф, не соглашаться по умолчанию и т. д.) и индекс остальных правил.

Все остальное — модульные сущности: правила, субагенты, скиллы, команды, плагины и рабочая папка задачи. Про ключевые — ниже.

Ленивая подгрузка и recency

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

Поэтому правила разделены на два режима. Always-on — несколько главных файлов, которые есть в каждой сессии. Lazy — все остальные: в AGENTS. md лежит индекс таблицей «файл — когда читать», и модель читает правило целиком в тот момент, когда оно становится нужным. Перед написанием тестов агент читает правило тестирования; пока тесты не нужны, этот файл не тратит ни токена. Recency здесь работает на нас: свежепрочитанное правило соблюдается заметно надежнее, чем висящее в контексте с начала сессии.

Плагины

Плагин (hooks) — это код, который встраивается в цикл агента. Два примера:

ast-index расширяет способность. Обычный grep по крупному проекту возвращает сотни строк совпадений, которые расходуют контекст; слабая модель к тому же плохо фильтрует шум. CLI ast-index ищет не по строкам, а по символам кода — классам, методам, полям: точные попадания вместо сотен строк. Но для его работы нужно индексировать проект и следить за актуальностью индекса. Плагин поддерживает индекс автоматически: инкрементальное обновление при старте плюс фоновый watch-демон.

Плагин-блокировщик ограничивает поведение. Он перехватывает git-команды до выполнения и физически блокирует опасные: ветвление от master, коммит или push в защищенные ветки. Что бы модель ни решила, команда не выполнится — это проверка в коде, а не просьба в тексте.

Плагин, таким образом, может и дать агенту новую способность, и отнять у него возможность ошибиться.

Три уровня защиты

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

  1. Правила (промпт): «не коммить в develop». Дешево, но вероятностно.

  2. Permissions: опасные команды требуют подтверждения человека. Что бы модель ни предложила, запуск подтверждаете вы.

  3. Плагины: детерминированная блокировка кодом. Ветвление от master не пройдет независимо от того, что модель «думала».

Формула уровня простая: чем страшнее последствие, тем ниже должна жить защита.

Субагенты: зачем несколько

Причин две. Первая — изоляция контекста: поиск по проекту порождает большой объем промежуточного текста, и если делать его в главной сессии, ее окно заполняется лишними данными. Субагент ищет в своем отдельном окне, а наружу отдает выжимку. Благодаря этому при оркестрации через главную сессию удается тратить гораздо меньше токенов.

Вторая — специализация роли. У каждого агента своя роль, свой набор знаний и свое поведение. Ревьюер не должен видеть рассуждения автора кода: если показать ему, как автор думал, он может унаследовать его ошибки — согласится с логикой вместо того, чтобы проверить результат.

Субагенты, которые будут упоминаться в статье: аналитик (тикет — бинарные критерии приемки + открытые вопросы), имплементер (техзадание — код + тесты + гейты), ревьюер (дифф — независимое ревью) и распознаватель дизайна — про него отдельно.

Роутинг по способности

Наша основная на текущий момент модель не видит картинок, а тикеты могут приходить с изображениями. Решение — роутинг по способности: vision-модель подключена к агенту — распознавателю дизайна. Любая картинка сначала проходит через него и превращается в текстовую спецификацию — размеры, отступы, цвета, состояния, — а дальше все остальные агенты работают с текстом, который они понимают.

Для макетов из Figma у нас используется MCP, но если макетов нет (например, баг с шагами воспроизведения, в котором прикреплено изображение ошибки на экране или Figma недоступна), тогда мы используем vision-модель для распознавания деталей на изображении.

4. Оркестрация: команда /ticket от и до

Теперь все это в работе — один тикет от постановки до готовых изменений. Вход — команда /ticket с номером задачи. Команда при этом не «знает» флоу — она читает правило оркестрации свежим чтением в момент запуска. Тот же recency: мы не рассчитываем, что модель помнит процесс, а даем ей процесс ровно тогда, когда он нужен.

По сути весь флоу — это известный паттерн spec-driven development: сначала спецификация (критерии приемки, тест-план), потом код.

Этап 1. Триаж. Первая строка ответа — вердикт: ветка сложности и маршрут, какие агенты в каком порядке будут работать. До этой строки модели запрещены любые действия, кроме чтения тикета и связанных с ним страниц. Сначала план — потом действия. ![](https://habrastorage. org/webt/66/59/e9/6659e98f9125ccbeb1f5aafc8f586a8e. png)

Этап 2. Воркспейс задачи. Система заводит задаче рабочую папку, и в ней — файл статуса: что сделано, на каком этапе триажа мы находимся, что решено, какие ответы дал человек.

Ведет файл только главная сессия — субагенты в него не пишут. Зачем: сессии обрываются — терминал закрылся, нужно переключиться. Файл на диске это переживает: отдельная команда /resume читает статус и продолжает с того же места, не перезапуская пройденные шаги. Прием — «файл вместо памяти». ![](https://habrastorage. org/webt/6a/4b/7a/6a4b7a5fe5ed649a6ee3f04f8551d6a8.png)

Этап 2а. Если в тикете дизайн. Каналов два. Основной — ссылка на Figma: через MCP агент забирает точные структурные данные макета в числах, без визуального распознавания. Фоллбек — vision-распознавание, если ссылки нет, а есть картинка (или это баг со скриншотом ошибки без макета). В обоих случаях результат — файл спецификации в воркспейсе.

Этап 3. Аналитик. Разбирает тикет на бинарные критерии приемки — такие, на которые можно ответить только «да» или «нет». Бинарность позволяет агенту самому проверить, что задача сделана. Ниже критериев — открытые вопросы.

Этап 4. Round-trip вопросов. Главная сессия пытается ответить на открытые вопросы сама (из кода, вики, связанных страниц); что не смогла — приносит человеку одним пакетом. Вопросы с пометкой «блокер» останавливают флоу до ответа. Субагент при этом никуда не пишет сам — только секция в его отчете.

Этап 5. Тест-план. Для нетривиальных задач скилл генерирует план из двух секций: unit — это напишет агент вместе с кодом, manual — чек-лист для проверки руками на эмуляторе.

Этап 6. Имплементер и гейты. Главная сессия передает задачу пакетом: критерии дословно, пути к файлам, ссылки на правила.

Имплементер пишет код и тесты и сам прогоняет гейты — статанализ, компиляцию, тесты — до зеленого. Мы видим только итог. В статье описывается базовая реализация, с которой можно начать: имплементер совмещает три роли — пишет код, пишет тесты и сам гоняет проверки. Для одного агента это слишком широкая зона ответственности — стоит разделить ее на агента, который пишет код, агента, который пишет тесты, и агента, который проверяет результат за обоими — чтобы исполнитель не проверял сам себя: тесты, написанные автором кода, легко подгоняются под реализацию вместо критериев.

Этап 7. Спот-чек. Главная сессия перепроверяет за имплементером: перегоняет новые тесты сама, просматривает дифф, подводит итог с блоком «следующие шаги». Флоу на этом заканчивается.

Код-ревью, коммит и оформление MR во флоу команды /ticket не входят — они выполняются отдельно.

5. Осечка — структурный фикс

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

История 1. Незапрошенный коммит и вердикт из примера

Первый случай: имплементер закончил, гейты зеленые — и главная сессия сама создала ветку, закоммитила и пошла оформлять MR; никто ее об этом не просил. Шаги «коммит — ревью — MR» стояли в цепочке оркестрации как обычные — модель просто следовала процессу. Второй: в правиле оркестрации был пример триаж-строки, модель скопировала его дословно вместе с маршрутом — и по чужому маршруту пропустила имплементера.

Причина общая: слабая модель исполняет написанное буквально. Инструкция «не коммить без разрешения» не спасла бы — в конфликте с планом, где коммит — следующий шаг, модель выбирает план: он конкретнее и свежее. А пример без пометки для нее не иллюстрация, а шаблон. Фиксы структурные: шаги коммита и MR убраны из цепочки — флоу заканчивается на спот-чеке, дальше действует человек отдельными командами; формат триаж-строки отделен от примеров — формат описан правилом (с инвариантом «маршрут реализации всегда кончается имплементером»), а примеры помечены явным «иллюстрация, не выводить дословно».

Вывод: не проси не делать шаг — убери шаг из плана; а пример без пометки — это шаблон.

История 2. Два шага цепочки параллельно

Тикет с дизайном: модель запустила распознавателя дизайна и аналитика одновременно — и аналитик отработал вслепую, без спецификации дизайна, которую должен был получить на вход. Порядок шагов в правиле был, и формально модель его не нарушила — она решила, что параллельно будет быстрее. Фикс: явный запрет запускать шаги цепочки параллельно, с объяснением причины — выход шага N это вход шага N+1. Показательное продолжение: после запрета модель перестала выполнять параллельно вообще все, включая независимый поиск по коду, — запрет пришлось уточнить («касается только шагов цепочки между собой»). Слабая модель читает запреты шире, чем написано.

Вывод: конвейер с зависимостями должен быть прописан как конвейер, а не подразумеваться порядком списка.

История 3. Вопросы — комментарием в рабочий трекер

Аналитик разобрал тикет, собрал открытые вопросы — и попытался запостить их комментарием в боевой тикет трекера от имени автора. Просьба «не пиши во внешние системы» соблюдается в большинстве случаев, но внешнее действие необратимо — одного исключения достаточно. Фикс двухслойный: запись во внешние системы запрещена всем субагентам на уровне permissions — deny, а не просьба; и вопросам дан легальный канал — секция «Открытые вопросы» в отчете аналитика, которую главная сессия приносит человеку. Запрет без канала оставил бы потребность нереализованной — модель искала бы обходной путь.

Вывод: права надежнее вежливой просьбы; и рядом с каждым запретом полезен канал, куда направить поведение.

История 4. Субагент умер молча

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

Фикс — правило для главной сессии: «пустой ответ без отчета = BLOCKED, не успех», с процедурой восстановления — проверить состояние рабочей копии, затем либо переделегировать остаток работы, либо догнать гейты самой, явно сказав, что выбрала. Здесь же оправдал себя воркспейс: состояние задачи лежало в файлах, а не в памяти умершего агента.

Вывод: харнес должен переживать смерть агента — считайте ее не исключением, а штатным событием.

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

6. Сводная таблица приемов

Прием

Когда применять

Запрет стоит в точке решения, а не в общих инструкциях

Когда правило записано, но нарушается: запрет вдали от места решения модель не применяет

Примеры в правилах помечены явным «иллюстрация, не выводить дословно»

Любой образец формата или строки в правиле: слабая модель воспроизводит пример как шаблон

Recency: правило приходит свежим чтением в момент запуска (команда читает файл), а не висит в контексте

Для процессов и флоу: все, что модель должна соблюдать точно и целиком

Конвейер прописан явно: выход шага N — вход шага N+1, параллельный запуск шагов запрещен

Многошаговые цепочки, где результат одного шага нужен следующему

Субагенты не общаются между собой: обмен — через главную сессию и файлы на диске

Всегда, когда агентов больше одного: контроль, воспроизводимость, устойчивость к обрыву

Permissions — ask: необратимое и внешнее подтверждает человек

Внешние системы, дорогие сборки

Плагин-гейт: детерминированная блокировка кодом

Когда цена ошибки не допускает «в большинстве случаев»: защищенные ветки, опасные команды

У файла состояния один писатель — главная сессия

Любое разделяемое состояние (статус задачи, журнал)

Отсутствие отчета = сбой + процедура восстановления

Любая делегация субагенту: смерть агента — штатное событие

Общий знаменатель у всех строк один: не убеждать модель, а строить среду, в которой правильное поведение — единственное доступное или хотя бы самое простое.

7. Заключение

Со слабой моделью работает не убеждение — структура:

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

Все это ради одного: агент становится достаточно надежным, чтобы взять на себя часть рутины. Промпт просит — структура заставляет действовать в ожидаемых рамках.

Если вы работаете со слабой или средней моделью в корпоративных ограничениях — расскажите в комментариях: как вы заставляете ее соблюдать процесс?

ссылка на оригинал статьи https://habr.com/ru/articles/1064140/