Каждые несколько лет разработчиков снова хоронят.
Сначала нас должны были заменить визуальные конструкторы. Потом no-code. Потом low-code. Потом автогенерация CRUD. Потом Copilot. Теперь, кажется, пришёл финальный босс в виде больших языковых моделей, которые за вечер могут накидать backend, frontend, Docker Compose, миграции, OpenAPI, Kafka, PostgreSQL, Redis, авторизацию, тесты, README и компилятор на сдачу.
А если открыть любую соцсет, то везде: «Я сделал SaaS за 20 минут», «нейросеть написала мне приложение», «один промпт заменил команду разработки»…Всё выглядит так, будто профессии действительно место в музее рядом с перфокартами и дискетами, так как теперь обычный пользователь интернета может навайбить энтерпрайз ПО, банковское мобильное приложение и агрегатор такси. И всё это только за утро.
Но чем больше я работаю с ИИ-инструментами, тем меньше верю в исчезновение разработчиков. Но не потому что ИИ тупой. Как раз наоборот — ИИ сейчас хорошо пишет код. Но чтобы не рассуждать о теме абстрактно, расскажу одну показательную историю. Специально возьму самый скромный масштаб из возможного: без бэкенда, без Kafka и восьми сервисов. Один фронтенд. Если совсем честно,то это будет один экран поиска. Там всё и увидим.
Сначала я был впечатлён красивым кодом
Однажды коллега показал мне сгенерированное приложение. Там были: Next.js, авторизация, таблицы с фильтрами, формы с валидацией, графики, тёмная тема. Всё поднималось одной командой и выглядело лучше, чем половина того, что я делал руками. Показал, понятно, не просто так. После демонстрации мне было с помпой сказано: «Через год нас тут не будет».
Спорить я не стал, хотя понимал, что он ошибается. Но где именно? Где именно это ломается? Код был написан неплохо, так что здесь я придраться не мог. Тогда я подумал, а где кончается то, что модель делает за меня, и начинается то, что придётся делать мне, сколько бы токенов я ни потратил?
Я взял похожую задачу — интерфейс для бронирования консультаций — и через десяток минут получил работающее приложение. Правда, потом, полдня выяснял, почему поиск иногда показывает результаты не от того запроса, который набран в поле, но мы не об этом. Промпт я писал подробный, потом ещё прогнал его дополнительно через GPT: стек, структура папок, что состояние сервера живёт в react-query, что формы нужны на react-hook-form с использованием zod, что компоненты собираем по таким-то и таким правилам, какие подходы используем и так далее.
На выходе получил поиск экспертов, страница профиля, календарь слотов, форма брони, история этих самых бронирований, страница администратора, скелетоны, пустые состояния, тосты. Всё собралось с первого раза, ошибок типов не было, приложение работало.
Если честно, я был впечатлён.
Руками я потратил бы несколько дней, и то, только имея готовый дизайн и описание бизнес-процессов. Но потом я начал печатать в поле поиска...
И огорчился
Вот код, который сгенерировала модель (чуть-чуть сократил, но механику не трогал):
function ExpertSearch() { const [query, setQuery] = useState(""); const [experts, setExperts] = useState<Expert[]>([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (!query) return; setIsLoading(true); fetch(/api/experts?q=${query}) .then((r) => r.json()) .then((data) => setExperts(data.items)) .catch(() => setExperts([])) .finally(() => setIsLoading(false)); }, [query]); ... }
Нормально же? Есть состояние загрузки, есть обработка ошибки, есть зависимость в useEffect, есть finally. Я бы такое и на ревью пропустил, если бы читал быстро. И пропускал.
Печатаем «мар», «марк», «марке» — уходят три запроса. Ответы приходят в том порядке, в каком ответил сервер, а не в том, в каком спрашивал. Если ответ на «мар» задержался и пришёл последним, то он перезапишет результаты для «марке». В поле — одно, а в списке — другое.
Багу этому лет столько же, сколько useEffect, и он есть в документации React отдельным разделом. Модель про него знает, ведь на вопрос «Нет ли здесь гонки?», она отвечает правильно и сразу.
Жаль, что повода спросить себя об этом у неё не было. Я попросил поиск — я получил поиск. Чего ворчать, да?
Там же рядом ещё setExperts вызовется после того, как пользователь ушёл со страницы. Отменять запросы никто, конечно же, не собирается, так что при быстром наборе в полёте висит по пять штук.
А вот другой пример.
const { data, isPending } = useQuery({ queryKey: ["experts", query], queryFn: ({ signal }) => searchExperts(query, signal), enabled: query.length > 1, placeholderData: keepPreviousData, });
Здесь гонки нет. Вот только это не от того, что мы её починили, а от того, что она стала невозможной, так как результат привязан к ключу запроса, а не к порядку ответов.
И вот это — узнавать не конкретную ошибку, а место, где целый класс ошибок можно сделать ненаступаемым — я считаю главным навыком в работе с генерацией (видеть и знать эти места).
Мы же точно все ещё нужны?
Этот баг я нашёл, отключив сеть в девтулзах.
const handleSubmit = async (form: BookingForm) => { setIsSubmitting(true); const booking = await api.createBooking(form); toast.success("Бронь создана"); setIsSubmitting(false); navigate(/bookings/${booking.id}); };
Если createBooking бросает исключение — setIsSubmitting(false) не выполняется никогда. Кнопка остаётся disabled, спиннер крутится, пользователь ждёт, потом перезагружает страницу и теряет заполненную форму. В консоли висит необработанный reject, который пользователь не видит.
Это самый частый баг в сгенерированном фронтенде из всех, что я встречал.
Модель пишет сценарий, в котором всё получилось: запрос ушёл, ответ пришёл, редирект случился. Состояния, в которые интерфейс попадает сам — сеть отвалилась, юзер нажал дважды, ушёл с экрана на середине, вернулся из фона через двадцать минут с мёртвым токеном, — в этот сценарий не входят.
Лечится либо try/finally, либо useMutation, который сам держит isPending и error.
Но перед тем, как лечить, его надо заметить. А заметно это только если специально ломать.
На отмене брони я, как разработчик, опешил. Модель сделала оптимистичное обновление: клик — и бронь сразу исчезает из списка, запрос уходит в фоне. Красивое.
Но что мы будем делать, если запрос упадёт?
Вернуть бронь на место молча? Пользователь решит, что глючит интерфейс. Показать «уведомляшку»? А если пользователь уже ушел на другую вкладку и не увидит его? Оставить всё как есть и надеяться на авось? Так делать нельзя (но, кстати, почему-то так делают). Спросить «Повторить?» А если не поймет про что речь?
Также вопрос про дебаунс на поиске. Модель решила поставить 300 миллисекунд. А откуда 300? Кто это решил? Для поиска по десяти экспертам можно вообще без дебаунса, а если за ручкой стоит тяжёлый запрос, то и 600 будет мало.
Ни на один из этих вопросов нет ответа в коде. Ответ где-то там, где люди пользуются экраном, где обсуждения, сколько стоит запрос и кого мы готовы «расстроить». Модель хорошо перечисляет варианты (я потом попросил и получил вменяемый список компромиссов), но выбирать между «иногда обделяем пользователя» и «иногда тормозим» — это, простите меня, не техническое решение.
Дальше пошли мелочи, но показательные.
lodash — целиком ради двух функций. На размер бандла это влияет заметно, а в коде выглядит абсолютно нормально: одна строчка импорта. Модалка открывалась без ловушки фокуса и не закрывалась по esc, а половина кликабельных элементов были div с onClick. Проверяется это за 30 секунд — просто пройти по экрану табом. Но это же надо сделать. Это же надо проверить.
После этого вечера у меня сложился порядок
Это последовательность, в которой я открываю файлы. Она сложилась исторически, но так, что большинство реально дорогих ошибок находятся.
Сначала я смотрю, где живёт состояние. Данные сервера в useState? Это будущая боль: придётся синхронизировать руками, и все гонки будут оттуда. Фильтры не в URL? Значит ссылку на отфильтрованный список нельзя отправить коллеге, и это выяснится через полгода.
Потом смотрю на все сайд эффекты и обязательно задаюсь вопросами: «А что, если ответа не будет вообще? А что, если компонент уже размонтирован? А что, если ответ будет НЕ ТОТ, который мы можем ожидать?» Обычно на этом месте и находится всё интересное.
Потом смотрю состояния, кроме loading и success. Модель делает эти два всегда. А пустой результат, частично загруженные данные, ошибка валидации от сервера, второй клик по кнопке — почти никогда.
И в конце обязательно смотрим на bundle, зависимости, конфигурации.
Полтора часа на экран против 3-х дней работы руками — сделка отличная, как по мне! Но это должны быть реально 1,5 часа чтения, а не 5 минут просмотра диффа.
Что реально изменилось
Подешевело создание MVP варианта.
Раньше между «есть идея экрана» и «есть работающий экран, который можно потыкать», лежал день‑два. А теперь — час! Я успеваю просмотреть гораздо больше вариантов, прежде чем на что‑то согласиться. Я стал делать по две‑три раскладки и выбирать, а не защищать первую, потому что на неё уже потрачено время.
А вот цена ошибки в решениях не упала. Скорее выросла.
Раньше 5 способов делать одно и то же было тяжело завести случайно. Теперь можно за вечер сделать свой useFetchрядом с react-query, три разных способа показать ошибку, один компонент кнопки с двенадцатью пропами, потому что каждую генерацию я просил «добавить вариант».
Раньше, чтобы развести бардак, надо было потрудиться, а сейчас бардак генерируется бесплатно. Но разгребать его будет тот же человек, что и раньше.
Итоговый итог
Код пишется быстрее, чем если бы писал его я сам, и, на самом деле, это мега круто. Но ни один из багов, на которые я потратил время, не был багом кода.
Первый был про разницу между «выглядит правильно» и «правильно». Второй про состояния, в которые интерфейс приходит сам, когда никто не смотрит. Третий вообще не решался в редакторе, потому что требовал решить, что именно мы хотим за поведение.
И вот, что мне здесь кажется самым важным. Всё это — один фронтенд: одно приложение, один разработчик, один вечер генерации. Причём фронтенд — это же самая честная часть системы, потому что баг видно глазами. Поиск показал мне не тех людей? Я это заметил. Кнопка умерла? Я это заметил. Мне даже логи были не нужны, хватило девтулзов и собственного любопытства.
А теперь достроим картинку до той, с которой я начал статью. К этому приложению добавляются 8 сгенерированных сервисов.
Между ними Kafka с событиями, которые приходят дважды и не в том порядке.
Сага на три шага, у которой есть состояние «оплата прошла, а бронь уже отменилась по таймауту».
Миграции, идемпотентность команд, распределённые блокировки, которые снимаются на полсекунды раньше, чем нужно.
И всё это тоже сгенерировано за вечер, тоже собирается с первого раза и тоже выглядит абсолютно нормально при чтении.
Только там глазками уже никто ничего не заметит. Гонка в useEffect бьёт по одному пользователю и видна сразу. А гонка между двумя сервисами — это вторая бронь на тот же слот и звонок в поддержку через месяц.
Ошибки в такой системе не складываются, а перемножаются: каждый сервис правдоподобен по отдельности, а взаимодействие, которого нет ни в одном файле, сломано. Читать его негде, ведь оно существует только в голове того, кто держит систему целиком.
Вот эта голова пока и есть наша работа.
Хотя, не думаю, что это навсегда. Модели за год выросли так, что предсказывать я перестал, а гонка в useEffect — штука вполне детерминированная, её можно ловить статически, просто пока никто толком не научил. Но пока кто-то должен отключить сеть в девтулзах и посмотреть, что будет.
Так что самой полезной привычкой за этот год я считаю не умение писать промпты, а не верить работающему демо, пока не попробовал его сломать.
Читайте также:


