Промпт вместо кода
Статья о том, как меняется профессия программиста: раньше мастерство измерялось умением писать код, теперь всё больше решает то, как вы формулируете задачу для ИИ в промпте. В статье отдельно разобраны параллели между промпт-инжинирингом и архитектурным мышлением, а также типичные ошибки в промптах.
Что было, что стало, что будет
Что было
Профессионализм разработчика измерялся раньше одним— насколько хорошо вы умеете писать код. Знаете синтаксис, паттерны, умеете разложить задачу на классы и методы, держите в голове архитектуру — вот и весь набор координат, по которому оценивался инженер.
Что стало
Последние год-два всё больше задач я решаю не написанием кода, а написанием текста — того самого, который в обиходе называют промптом (prompt). Я формулирую задачу для искусственного интеллекта (ИИ), описываю контекст, ограничения, ожидаемый результат — и получаю код, который раньше писал бы сам, час или два, а то и три часа, или даже 10 часов. Это не значит, что писать код я разучился или перестал это делать руками там, где это нужно. Это значит, что рядом с навыком писать код появился — и стремительно набирает вес — другой навык: "точно формулировать, что именно нужно сделать".
И вот тут я хочу сразу расставить акценты, потому что вижу вокруг довольно поверхностное отношение к этому навыку. Многие воспринимают написание промптов как что-то второстепенное — набросал пару предложений, получил результат, если не подошло — переформулировал ещё раз. На практике всё устроено иначе: от того, как именно сформулирован промпт, зависит буквально всё, что сгенерирует ИИ — архитектура кода, обработка пограничных случаев (edge cases), соответствие вашим соглашениям в проекте, даже то, будут ли в коде тесты. Как я и говорил раньше, так и сейчас повторю: "Можно написать такой код, который просто невозможно будет покрыть unit-тестами."
Промпт — это такая же инженерная работа, как проектирование интерфейса или контракта между модулями, и относиться к ней стоит с той же серьёзностью.
Что будет
Роль разработчика смещается от «человека, который пишет код» к «человеку, который точно формулирует задачу и проверяет результат». Это не отменяет глубокое знание технологий — наоборот, чтобы написать хороший промпт про архитектуру микросервисов, нужно самому понимать, что такое, скажем, паттерн Outbox или чем чревато нарушение границ контекста (bounded context). Но написание промптов как отдельная дисциплина, со своими приёмами, ошибками и лучшими практиками, — это то, о чём я хочу поговорить в этой статье подробно.
От синтаксиса к смыслу
Раньше путь от идеи до работающего кода выглядел так: вы придумываете решение в голове, а дальше переводите его на язык программирования — расставляете фигурные скобки, вспоминаете сигнатуру нужного метода, следите за синтаксисом. Знание синтаксиса и API было частью профессионального мастерства почти в той же степени, что и само умение решать задачи: забыли, как правильно настроить retry-политику в конкретной библиотеке — теряете время на документацию, а не на суть задачи.
Сейчас между идеей и кодом появился ещё один слой — формулировка на естественном языке. И вот что интересно: сама по себе задача написать код на ASP.NET Core, который публикует событие в RabbitMQ через Rebus с повторными попытками при сбое, для ИИ давно не представляет сложности. Сложность сместилась туда, где раньше её почти не замечали, — в формулировку того, какие именно гарантии вам нужны: at-least-once или ровно один раз (exactly-once), сколько попыток делать при отказе, что писать в ELK при финальном отказе, нужно ли откладывать сообщение в очередь ошибок (dead-letter queue). Раньше это решение принималось по ходу написания кода, часто неявно. Теперь его приходится проговаривать словами ещё до того, как появится первая строка кода — и если вы этого не сделаете, ИИ примет решение за вас, причём не обязательно то, которое устроило бы именно ваш проект.
Здесь легко провести параллель с тем, как языки высокого уровня когда-то избавили нас от необходимости писать на ассемблере: абстракция поднялась на уровень выше, а забота о деталях перешла компилятору. С промптами происходит похожий сдвиг — только «компилятор» здесь недетерминированный, и результат его работы прямо зависит от того, насколько точно и полно вы описали задачу. Компилятору всё равно, назвали вы переменную invoiceId или id — результат будет одинаков. ИИ же чувствителен к каждой детали формулировки: скажете «добавь обработку ошибок» — получите try-catch с логированием в консоль; скажете «добавь обработку ошибок с логированием в ELK и повторной попыткой при временных сбоях, но без повторной попытки при ошибках валидации» — получите совершенно другой код.
Из этого следует не самый очевидный вывод: написание промптов не облегчает работу программиста в смысле «думать меньше». Оно смещает то, где именно вы думаете. Раньше львиная доля инженерного мышления уходила на перевод решения в синтаксис конкретного языка. Теперь эта часть работы почти исчезла, а мышление, которое раньше растворялось в процессе кодирования — про гарантии, пограничные случаи, соответствие архитектуре проекта, — приходится формулировать явно, словами, заранее. И оказывается, что это не проще, а во многом честнее: плохо продуманное решение больше не спрячешь за читаемым кодом, оно сразу видно в самом промпте.
Промпт как спецификация
Когда я пишу промпт для серьёзной задачи, я ловлю себя на том, что делаю ровно то же самое, что делал бы, ставя задачу разработчику в своей команде или описывая контракт (contract) для внешнего сервиса: формулирую, что должно получиться на входе и выходе, какие есть ограничения, что считается успешным результатом, а что — провалом.
Инженерная работа — не написание фигурных скобок, а принятие решений и их точная формулировка
Разница лишь в том, что раньше это делалось на словах или в тикете, а исполнитель — человек — сам додумывал недостающие детали, опираясь на опыт и контекст проекта. ИИ додумывает тоже, но не всегда туда, куда нужно вам, — и чем меньше вы сказали явно, тем выше шанс, что додумает не так.
«Напиши метод, который сохраняет заказ в базу»
Возьмём конкретный пример из моей практики. Промпт «напиши метод, который сохраняет заказ в базу» технически исполним — ИИ сгенерирует рабочий код за секунды. Но в нём нет ни одного из тех решений, которые на самом деле определяют качество результата: нужна ли транзакция (transaction), что делать при конфликте параллельного обновления, нужно ли публиковать доменное событие (domain event) после сохранения, как обрабатывать нарушение уникальности, нужна ли валидация на уровне метода или она уже сделана выше. Промпт как спецификация (specification) выглядит иначе: «сохрани заказ в PostgreSQL через Entity Framework Core в рамках уже открытой unit of work; при конфликте параллельного обновления брось доменное исключение с понятным сообщением, не оборачивай его в generic exception; после успешного сохранения опубликуй событие OrderCreated через Rebus; валидацию входных данных не делай — она уже выполнена в хендлере команды выше по стеку». Это не намного длиннее исходного промпта, но именно эти детали и есть инженерная работа — не написание фигурных скобок, а принятие решений и их точная формулировка.
«сохрани заказ в PostgreSQL через Entity Framework Core в рамках уже открытой unit of work; при конфликте параллельного обновления брось доменное исключение с понятным сообщением, не оборачивай его в generic exception; после успешного сохранения опубликуй событие OrderCreated через Rebus; валидацию входных данных не делай — она уже выполнена в хендлере команды выше по стеку».
Здесь легко провести параллель с тем, как в Domain-Driven Design важен единый язык (ubiquitous language) между разработчиками и предметной областью — если команда называет одну и ту же сущность по-разному в коде и в разговоре, это рано или поздно приводит к ошибкам. С промптами то же самое: если вы называете одну и ту же концепцию по-разному от промпта к промпту — то «событие», то «уведомление», то «сообщение» — для ИИ это может звучать как три разные сущности, и результат будет об этом свидетельствовать. Хороший промпт требует того же дисциплинированного языка, что и хорошая техническая спецификация.
«Напиши тесты для этого метода»
И, пожалуй, главное: критерии приёмки (acceptance criteria) или критерии готовности (definitions of done)) в промпте — это не роскошь, а необходимость. «Напиши тесты для этого метода» — слабая формулировка, потому что не определяет, что считать полным покрытием. «Напиши тесты, которые проверяют: успешное сохранение, конфликт параллельного обновления, попытку сохранить заказ с уже занятым номером» — формулировка, которая заранее описывает, каким должен быть результат, чтобы вы могли его принять, не читая код построчно. Именно способность заранее сформулировать критерии успеха и отличает промпт-инженерию как дисциплину от простого «подбора заклинаний», о котором иногда говорят пренебрежительно.
«Напиши тесты, которые проверяют: успешное сохранение, конфликт параллельного обновления, попытку сохранить заказ с уже занятым номером»
Анатомия хорошего промпта
Если разобрать удачный промпт на составляющие, в нём почти всегда можно найти пять элементов, даже если они не разложены по пунктам явно, а вплетены в текст.
Первый элемент — контекст (context): в каком проекте вы работаете, какой слой архитектуры затрагивает задача, какие соглашения уже приняты в кодовой базе. Без контекста ИИ решает задачу «в вакууме» — предложит EF Core там, где у вас Dapper, или сгенерирует контроллер в духе MVC, хотя ваш проект построен на Vertical Slice. Контекст не обязательно писать заново в каждом промпте — часто достаточно один раз описать его в системном файле проекта (в моём случае, например, файл CLAUDE.md с описанием архитектуры и стека), а дальше ссылаться на уже заданные правила.
Второй элемент — требования и ограничения. Это как раз то, о чём я говорил в разделе «Промпт как спецификация»: не просто «сделай», а «сделай именно так, а вот так не делай». Причём отрицательные ограничения работают не хуже положительных — фраза «не оборачивай исключение в generic Exception» экономит вам куда больше времени, чем можно подумать, потому что без неё ИИ выберет самый общий, а не самый подходящий вариант.
Третий элемент — примеры. Если у вас уже есть похожий код в проекте — обработчик команды, репозиторий, тест — фрагмент такого кода в промпте работает лучше любого текстового описания стиля. Я на практике убедился: проще вставить в промпт готовый пример обработчика на 15 строк и сказать «сделай новый хендлер в такой же манере», чем пытаться словами описать, что «здесь используется MediatR, валидация через FluentValidation, а логирование через ILogger с определённым форматом сообщений». Пример снимает добрую половину двусмысленностей сразу.
Четвёртый элемент — формат ответа. Нужен вам просто код или ещё и объяснение решения? Нужен один файл или сразу с тестами? Нужен diff к существующему коду или новый файл целиком? Этот пункт часто недооценивают, а зря — если не сказать больше, ИИ может вернуть полноценную лекцию с объяснением базовых концепций там, где вам нужен был только код, готовый к вставке.
И пятый, самый важный элемент — критерии успеха (acceptance criteria) или критерии готовности (definitions of done), про которые уже шла речь: что именно должно быть верно в готовом результате, чтобы вы могли сказать «да, это то, что нужно», не перечитывая код построчно в поисках проблем. Хороший промпт для нетривиальной задачи почти всегда содержит фразу вида «результат должен...» или «убедись, что...» — и именно эта фраза чаще всего определяет, получите вы рабочий код с первой попытки или будете переформулировать запрос по кругу.
До и после: один и тот же запрос, два результата
Чтобы не оставлять всё сказанное на уровне теории, покажу на одном и том же сценарии, что происходит на практике.
Задача: метод, который сохраняет заказ и публикует событие о его создании — тот самый пример, что уже звучал в разделе «Промпт как спецификация».
Первый вариант
Первый вариант промпта: «напиши метод, который сохраняет заказ в базу и публикует событие о создании заказа». Формулировка выглядит вполне рабочей, и код по ней действительно получится — примерно такого вида:
public async Task CreateOrderAsync(Order order)
{
try
{
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
await _bus.Publish(new OrderCreated(order.Id));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating order");
throw;
}
}
На первый взгляд всё в порядке: заказ сохраняется, событие публикуется, ошибки логируются. Но если разобрать этот код внимательно, в нём набор скрытых решений, которые ИИ принял за вас, а не вы сами:
- событие публикуется до подтверждения транзакции (transaction) — если после сохранения, но до публикации что-то пойдёт не так, вы получите заказ в базе без события о нём;
- исключение любого типа, от конфликта уникальности до обрыва сети, обрабатывается одинаково — общим catch (Exception);
- в событии OrderCreated передан только идентификатор, хотя подписчикам вниз по цепочке может понадобиться сумма заказа или идентификатор клиента.
Второй вариант
Второй вариант — тот самый промпт-спецификация из раздела "Промпт как спецификация": «сохрани заказ в PostgreSQL через Entity Framework Core в рамках уже открытой unit of work; при конфликте параллельного обновления брось доменное исключение с понятным сообщением, не оборачивай его в generic exception; после успешного сохранения опубликуй событие OrderCreated через Rebus; валидацию входных данных не делай — она уже выполнена в хендлере команды выше по стеку».
Результат на том же самом ИИ получается заметно другим:
public async Task CreateOrderAsync(Order order)
{
try
{
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
throw new OrderConcurrencyException(
$"Заказ {order.Id} был изменён параллельно, повторите операцию", ex);
}
await _bus.Publish(new OrderCreated(order.Id, order.CustomerId, order.TotalAmount));
}
- Разница не в объёме кода — она в количестве решений, которые были приняты осознанно, а не по умолчанию: конкретное исключение вместо общего;
- доменная ошибка вместо технической;
- событие с данными, которые действительно нужны подписчикам;
- отсутствие избыточной валидации там, где она уже сделана.
Ни одна из этих деталей не была бы получена дополнительной просьбой «улучши код» — их нужно было заложить в исходную формулировку.
Отсюда практический вывод, который я вынес для себя: если после получения кода от ИИ вы обнаруживаете, что просите «доработать», «уточнить обработку ошибок» или «добавить ещё вот это» — чаще всего это не недостаток ИИ, а недостаток исходного промпта. Вторая, третья, четвёртая итерации уходят на то, что можно было сформулировать сразу.
Параллели с архитектурным мышлением
Чем дольше я работаю с промптами, тем яснее вижу: настоящая инженерная работа в архитектуре почти никогда не в синтаксисе — она в границах (boundaries). Где заканчивается ответственность одного модуля и начинается другая, что видно снаружи, а что скрыто внутри, какой контракт (contract) связывает两 части системы. Ошибка в синтаксисе стоит копейки — её видно сразу, компилятор укажет строку. Ошибка в границах стоит дорого — она проявляется через месяцы (!), когда изменение в одном месте неожиданно ломает что-то другое, причем никак не связанное по сути.
Плохо проведённая граница в архитектуре — это протекающая абстракция (leaky abstraction)
С промптами работает тот же принцип, только граница проводится не между модулями, а между тем, что вы формулируете явно, и тем, что оставляете на усмотрение ИИ. Плохо проведённая граница в архитектуре — это протекающая абстракция (leaky abstraction), когда клиенту приходится знать детали реализации, которые его не должны касаться. Плохо проведённая граница в промпте — это та же протечка, только наоборот: вы оставляете ИИ додумывать то, что на самом деле является вашим архитектурным решением, а не деталью реализации. В разделе «До и после» я показал именно такой случай — обработка исключений и структура события были архитектурными решениями, а не техническими мелочами, и оставлять их на усмотрение ИИ означало отдавать часть архитектуры в чужие руки.
Плохо проведённая граница в промпте — это та же протечка: вы оставляете ИИ додумывать то, что на самом деле является вашим архитектурным решением, а не деталью реализации.
Здесь же уместна параллель с принципом единственной ответственности (single responsibility) из SOLID. Класс, который делает три несвязанные вещи, тяжело читать, тестировать и менять — это знает любой, кто хоть раз дорабатывал такой класс. Промпт, который просит одновременно «отрефактори сервис, добавь кэширование и заодно поправь логирование», страдает той же болезнью: ИИ либо сделает всё поверхностно, либо сфокусируется на одной части в ущерб остальным. Один промпт — одна задача, так же как один класс — одна причина для изменения.
И ещё одна параллель, пожалуй, самая практичная: в Clean Architecture правило зависимостей (dependency rule) требует, чтобы бизнес-логика не зависела от деталей реализации — от конкретной базы данных, конкретного фреймворка. В промптах я стараюсь следовать похожему разделению: явно и подробно формулирую бизнес-правила и ограничения — то, что действительно важно для вашего домена, — а низкоуровневые технические детали, которые у меня, кстати, тоже есть (как назвать локальную переменную, в каком порядке расположить приватные методы), обычно, оставляю на усмотрение ИИ. Это не лень, а разумное делегирование — то же самое, что вы делаете, ставя задачу разработчику в команде: подробно объясняете бизнес-смысл и жёстко фиксируете инварианты (invariants), но не расписываете каждую строчку кода.
Один промпт — одна задача, так же как один класс — одна причина для изменения.
Из этого следует обнадёживающий вывод для тех, кто, как и я, годами работал архитектором и программистом: навык мыслить границами, контрактами и инвариантами — это тот же самый навык, что нужен для хорошего промпт-инжиниринга, просто применённый к другому материалу. Тем, кто уже умеет проектировать системы, учиться писать хорошие промпты будет заметно проще, чем тем, кто привык мыслить исключительно в терминах отдельных строк кода.
Разбор неудачных промптов
Соберу здесь несколько формулировок, которые выглядят вполне разумно, но на практике почти гарантированно приводят к результату, который придётся переделывать. У каждой — своя причина.
Проблема не в исполнении, а в постановке.
«Сделай сервис для работы с заказами» — классический пример расплывчатой постановки задачи без границ (scope). Непонятно, что входит в «работу с заказами»: только CRUD-операции или ещё и бизнес-логика расчёта скидок, интеграция с оплатой, обработка отмены? ИИ вынужден либо угадывать объём, либо ограничиться минимальным набором методов, который скорее всего не совпадёт с тем, что вы имели в виду. Лечится это просто — явным перечислением того, что входит в задачу, а что сознательно оставлено за скобками.
«Напиши валидацию для email» — пример отсутствия контекста. В проекте на ASP.NET Core валидация может делаться через атрибуты Data Annotations, через FluentValidation, через доменный value object с проверкой в конструкторе — и без указания, какой подход уже принят в вашем проекте, ИИ выберет тот, что статистически чаще встречается в обучающих данных, а не тот, что впишется в вашу кодовую базу.
«Отрефактори этот класс, заодно добавь unit-тесты и поправь баг с датами, который я заметил» — смешение нескольких несвязанных задач в одном промпте, о котором уже шла речь в разделе «Параллели с архитектурным мышлением». Рефакторинг меняет структуру кода, тесты должны фиксировать поведение до изменений, а исправление бага — это вообще отдельная, третья задача с собственными критериями. Свалив всё в одну кучу, вы получаете результат, где непонятно, какая часть изменений относится к рефакторингу, какая — к багфиксу, а тесты в итоге написаны под уже изменённый, а не исходный код.
«Проверь этот код на ошибки» — пример без критериев приёмки. Слово «ошибки» здесь может означать что угодно: синтаксические проблемы, логические баги, нарушения архитектурных принципов, проблемы производительности, уязвимости безопасности. ИИ ответит на что-то одно из этого списка — скорее всего, на то, что бросается в глаза в первую очередь, — а остальное просто не попадёт в поле внимания, потому что вы не сказали, что именно вас интересует.
И последний, менее очевидный случай — «сделай как можно быстрее, но с полной валидацией на каждом шаге и подробным логированием всех операций». Это противоречивые требования: производительность и исчерпывающая валидация с подробным логированием на каждом шаге — конкурирующие цели, и без указания приоритета ИИ либо пожертвует одним в пользу другого произвольно, либо попытается угодить всем сразу и выдаст код, который не оптимален ни по одному из критериев. Если у требований есть конфликт, стоит явно сказать, что важнее: «производительность важнее, логируй только точки входа и ошибки, детальную валидацию делай только для полей, которые приходят от пользователя».
Объединяет все пять случаев одно: ни в одном из них ИИ не «ошибся» в привычном смысле слова — он корректно выполнил именно то, что было сформулировано. Проблема не в исполнении, а в постановке, и это ровно то, о чём вся эта статья.
Заключение
Что было
Возвращаясь к тому, с чего начал: то, что было, — способность писать код руками — никуда не делось и вряд ли обесценится в обозримом будущем. Но рядом с ним выросло новое мастерство, и вес его в профессии продолжает расти.
Что стало
То, что стало, — умение сформулировать задачу так точно, чтобы результат совпал с замыслом с первой попытки, а не после пятой итерации переформулировок.
Я специально уделил столько внимания разбору того, что именно делает промпт "хорошим" или "плохим", потому что хочу развеять миф: будто написание промптов — это что-то несерьёзное, подбор удачных слов методом проб и ошибок. На практике это ровно та же инженерная дисциплина, что стоит за хорошей архитектурой: умение проводить границы, формулировать инварианты, заранее определять критерии успеха. Разница лишь в материале — вместо кода вы работаете с языком, а вместо компилятора — с моделью, которая честно выполнит именно то, что вы попросили, ни больше ни меньше.
Что будет
Я думаю, что умение писать точные, продуманные промпты станет таким же базовым навыком разработчика, как сегодня — умение писать читаемый код или проектировать API. И тем, кто уже привык мыслить контрактами, границами и явными договорённостями — архитекторам, тимлидам, всем, кто годами оттачивал именно эту часть мышления, — здесь есть очевидное преимущество. Остаётся перенести этот же навык на новый материал — и относиться к написанию промптов с той же серьёзностью, с какой вы относитесь к проектированию системы.
Абстракция поднялась на уровень выше.
П.С.: Раньше у меня был лозунг "Пишите правильный код" пора менять на новый логузг "Пишите правильные промпты".
