ИИ‑агенту недостаточно правил: как мы передаём ему инженерный опыт

Сначала нам советовали подробно задавать правила в промпте. Промпты росли и постепенно превращались в:

Ты опытный программист. Пиши хороший код. Плохой код не пиши.

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

Следующим решением стали skills. Теперь перед началом разработки нужно обложить проект.md‑файлами, описать в них архитектуру, правила именования, работу с базой данных, тестирование, логирование и все остальные инженерные практики. Судя по количеству таких файлов в репозиториях, Markdown скоро станет главным языком разработки.

В нашей команде всё это само по себе не заработало.

Не в том смысле, что агент вообще не мог написать работающий код. Мог. Но ни длинный промпт, ни сгенерированная спецификация, ни коллекция скиллов сами по себе не давали код, который мы готовы были принять на ревью.

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

Но компетентность нельзя полностью передать декларативно. Её нельзя заранее исчерпывающе зафиксировать списком правил, документацией или набором skills. О значительной её части мы вспоминаем только в процессе работы: при выборе границ, сравнении вариантов, проверке первой реализации и исправлении ошибок.

Поэтому я не отказался от промптов, документации и skills. Я изменил момент и способ их формирования.

В основе нашего подхода шесть правил:

  1. Агент проходит тот же путь принятия решений, что и разработчик.

  2. Задача декомпозируется до уровня, на котором можно создать эталонное решение.

  3. Агент получает необходимые локальные знания проекта.

  4. Проверенный эталон используется как few‑shot для последующего кода.

  5. Проверенные тесты постепенно становятся самостоятельным этапом приёмки.

  6. Принятые решения превращаются в переиспользуемые skills.

Но прежде чем разбирать эти правила, нужно сделать две оговорки.

1. Речь про инженерную разработку, а не про вайбкодинг

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

Отдельные элементы этого процесса известны как spec‑driven development, context engineering, few‑shot и compounding engineering. Я не предлагаю новый термин. Здесь я описываю, как собираю эти элементы в единый инженерный процесс и в как применяю их на практике.

При этом агенту можно передать не только локальные особенности проекта. Архитектурные паттерны, DDD, TDD, правила именования, тестирования и любые другие инженерные подходы, которыми вы пользуетесь, также можно оформить в виде skills, эталонов и отдельных проверок.

Агент получает не абстрактное требование «писать хороший код», а конкретные правила и примеры того, как эти подходы применяются именно в вашем проекте.

2. Я не работаю в автономном режиме

Я не использую Ralph loop или полностью автономный агентский режим, потому что такой подход становится слишком дорогим по вычислительным затратам и усложняет контроль над качеством решений.

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

Я также не хочу получать 10 000 строк кода, которые нужно сразу рефакторить и разбирать вручную.

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

Правило 1. Агент должен пройти тот же путь, что и программист

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

Часть этого процесса может выполняться почти неосознанно. Разработчик просто «видит», где провести границу или с чего начать. Для агента этот скрытый процесс приходится делать явным.

Мы давно не пишем весь такой код вручную. Сначала описываем контракт в proto или OpenAPI, а затем используем кодогенерацию для получения типов, интерфейсов клиента и серверных заглушек.

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

Проходя путь разработки сервиса или решения задачи наша роль — зафиксировать важные для нас моменты в виде skills или документации, в которых LLM пишет код не так как нам хотелось бы. Постепенно проект обрастает сотнями инструкций от правил написания service provider до правил выявления ошибок, которых мы встретили при генерации. Так мы фиксируем наш инженерный опыт для дальнейшего переиспользования моделью.

Правило 2. Декомпозировать нужно до уровня эталонного решения

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

Например для CRUD, процесс может выглядеть так:

  1. Описать контракты слоёв.

  2. Полностью реализовать один метод репозитория, например Create.

  3. Для остальных методов сделать явные заглушки.

  4. Провести Create через сервисный и транспортный слои.

  5. Добиться компиляции и прохождения первого сквозного теста.

  6. Проверить выбранные границы и контракты.

  7. Последовательно реализовать остальные операции.

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

Если контракт репозитория выбран неверно, это обнаруживается после реализации одного сценария. Откатить такой этап дёшево. Если сразу написать весь CRUD, ошибка размножится по обработчикам, сервисам, запросам и тестам.

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

  • добавляем обработку ошибок;

  • добавляем обёртки для логирования и телеметрии;

  • вводим обёртки над работой с БД и очередями;

  • проверяем ограничения базы данных;

  • добавляем транзакции там, где они нужны;

  • покрываем граничные сценарии;

  • приводим код к правилам проекта.

В результате получается не просто первый работающий метод, а первый достаточный эталон.

Отдельные проходы по качеству

Я не заставляю модель одновременно решать инженерную задачу и безошибочно учитывать десятки правил оформления.

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

  • именования;

  • ответственности функций;

  • уровней абстракции;

  • зависимостей;

  • обработки ошибок;

  • оптимизации сложности;

  • тестируемости

  • и так далее

Разве мы сами не делаем так же? Я, например, учитываю свои знания о том, как лучше написать код, но сначала больше сосредоточен на решении задачи, а уже потом отдельно проверяю «правильность» моего кода.

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

И в этом нет ничего страшного: skill в данном случае не заменяет опыт, а лишь фиксирует его текущее состояние, которое со временем будет уточняться и дополняться.

Однажды мы нашли не критичное снижение производительности, которое возникало из‑за проблемы N+1. LLM её самостоятельно в коде не выявила. Скрыта она была за особенностями легаси ORM. Дополнили skills по рефакторингу. Прогнали старый код с просьбой поиска предложений по улучшению и нашли такие же проблемы в других местах. Забавно что, такую же ошибку допустил в древнем коде и сам автор нового правила рефакторинга.

Правило 3. Нужно передавать локальные знания проекта

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

Поэтому такие знания мы приносим заранее в виде skills и явно обязываем агента их использовать. Skill должен не просто сообщать, что в проекте существует собственный логгер или обёртка над БД. Он должен задавать:

  • в каких случаях используется;

  • как его правильно подключать;

  • какие низкоуровневые зависимости запрещено использовать напрямую;

  • какие обязательные параметры нужно передавать;

  • как решение проверяется;

  • где находится эталон использования.

Иначе первая реализация может оказаться архитектурно аккуратной, но чужой для проекта. А если затем использовать её как few‑shot, агент начнёт масштабировать обход внутренних библиотек и инфраструктурных соглашений на весь последующий код.

Правило 4. Эталон становится few‑shot для следующего кода

После проверки первый элемент перестаёт быть просто частью реализации. Он становится локальной спецификацией на примере. Модель намного лучше продолжает уже принятый паттерн, чем создаёт несколько однотипных решений по абстрактному описанию.

Это работает на разных масштабах:

  • первая функция задаёт уровень абстракции для соседних функций;

  • первый метод показывает обработку ошибок и стиль кода;

  • первый сквозной сценарий определяет взаимодействие слоёв;

  • первый репозиторий становится примером для остальных репозиториев;

  • первый микросервис задаёт устройство следующих сервисов;

  • первый законченный проект может стать основой для новых проектов такого типа.

Skill говорит, что ошибки должны обрабатываться явно. Эталон показывает, как именно в этом проекте устроена модель ошибок.

Skill требует разделять уровни абстракции. Эталон показывает, где проходят реальные границы слоёв.

Skill требует писать проверяемый код. Эталон показывает структуру тестов и принятый уровень изоляции.

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

Также важно явно разделять:

  • обязательные паттерны проекта;

  • особенности конкретной предметной области;

  • решения, которые можно адаптировать;

  • детали, которые нельзя переносить автоматически.

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

Правило 5. Проверенные тесты становятся этапом приёмки

Мы стараемся покрывать тестами весь создаваемый и изменяемый код. Но на первых этапах это не уменьшает объём ревью: разработчик вычитывает и реализацию, и сами тесты.

Агент может одинаково убедительно написать неправильный код и тесты, подтверждающие его неправильное понимание задачи. Поэтому изначально зелёные тесты ничего не доказывают. Сначала человек должен проверить, что они действительно фиксируют принятое поведение и способны обнаружить отклонение от него.

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

При очередной однотипной реализации разработчик в первую очередь проверяет:

  • какое поведение зафиксировано тестами;

  • соответствует ли оно поставленной задаче;

  • сохранились ли прежние гарантии;

  • проходит ли реализация весь набор проверок.

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

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

Правило 6. Результат нужно превращать в переиспользуемые знания

Few‑shot хорошо работает внутри существующей кодовой базы. Но конкретный пример всегда содержит детали своего проекта.

После нескольких реализаций можно отделить повторяемые инженерные правила от случайных особенностей кода и сохранить их в skill.

Это не обучение модели в привычном смысле. Её веса не меняются, и сама по себе она не становится умнее. Растёт система знаний, которую мы построили вокруг неё.

Получается замкнутый цикл:

  1. Декомпозировать задачу.

  2. Передать необходимые локальные знания проекта.

  3. Создать один эталон.

  4. Проверить человеком реализацию и тесты.

  5. Использовать принятый эталон как few‑shot.

  6. После накопления доверия использовать тесты как этап приёмки однотипных решений.

  7. Выделить повторяемые закономерности.

  8. Сохранить их в skills и автоматических проверках.

  9. Использовать накопленные знания в следующей задаче.

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

А как применить этот подход на существующем проекте?

Никак — если под «применить» понимать возможность заранее описать весь старый проект, сформировать для него полный набор skills и сразу получить тот же уровень работы агента, что и в системе, которая развивалась вместе с ним.

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

Кроме того, существующий код не всегда можно считать эталоном. Рядом могут находиться актуальный паттерн, временный компромисс и технический долг. Агент сам не определит, что из этого нужно продолжать, а что давно пора перестать копировать.

Поэтому на старом проекте мы делаем то же самое, но начинаем без накопленного контекстного слоя. И декомпозируем задачи максимально мелко, что бы любое решение можно было быстро вычитать и проверить. Если находим особенности — обязательно фиксируем либо в доку либо в skills., а их в старом легаси будет много.

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

Зачем здесь агент, если программист сделает быстрее?

Первую реализацию опытный программист действительно часто сделает быстрее сам.

Ему не нужно объяснять собственные решения, формализовать привычные действия и проверять, правильно ли модель поняла задачу.

Но это сравнение только стоимости первой реализации.

Реализация

Вручную

С агентом

Первая

Часто быстрее

Требует подготовки эталона

Вторая

Снова требует времени разработчика

Использует готовый few‑shot

Третья и следующие

Опыт остаётся преимущественно в голове

Масштабируют код, правила и проверки

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

Такой процесс дороже

Да, он дороже в токенах, но токены — не единственная единица стоимости. Есть ещё время разработчика. Я сознательно увеличиваю вычислительные расходы, чтобы уменьшить количество собственных часов на повторяющиеся решения.

Поэтому корректнее разделять две цены:

  • оплата токенами становится выше;

  • оплата временем разработчика становится ниже.

Экономика появляется не на первой функции. Она появляется при серии однотипных решений, когда эталоны, skills и проверки начинают переиспользоваться.

А как же развитие разработчика?

После каждого проекта разработчик сохраняет новый опыт. И тут ничего не меняется. Но важнее другое: эти знания редко остаются статичными. Они почти всегда эволюционируют от сервиса к сервису. Работа через агентов не отменяет развитие разработчика. Наоборот, сначала именно разработчик должен распознать удачное решение, отделить общее правило от случайной детали и решить, что можно масштабировать и как именно это решение должно эволюционировать от сервиса к сервису. Меняются интерфейс ввода и способ передачи знаний.

Мы придумали как улучшить часть внутренней библиотеки. Агент всего за час подготовил 20 PR в 20 сервисов. Ещё 2 дня мы потратили на их тестирование, в попытках найти ошибку агента. К счастью или нет — мы ничего не нашли.

Да, моторный навык ручного написания кода может просесть, но разве быстрые пальцы — главный навык программиста?

Вместо заключения

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

То, что раньше я делал за спринт — 10 рабочих дней, — при таком подходе делается за 2–3 дня при наличии большего количество skills. Код на выходе практически неотличим от того, который я написал бы сам. Сделаю оговорку 99% решаемых командой задач технически не сложные, там может быть сложная и важная бизнес логика, большая ответственность перед пользователями и бизнесом, но с точки зрения технологий ничего особенного.

По факту поменялся интерфейс ввода: LLM вместо рук. В зависимости от задачи ручное вмешательство непосредственно во время написания требуется лишь в 5–15% случаев.

А закончу цитатой Линуса Торвальдса:

ИИ — это инструмент повышения продуктивности, точно такой же, какими когда‑то стали компиляторы. Компиляторы в своё время увеличили производительность программирования в 1000 раз. ИИ добавляет ещё примерно 10-кратный прирост поверх этого. Это огромный шаг. Но ведь никто не говорит: «Компилятор написал мой код». Так почему мы говорим это про ИИ?

Всё это — из коммерческого опыта. Было интересно — жду в гости в ТГ‑канале.

Автор: dzahdev

Источник

Оставить комментарий