Каждые несколько лет разработчиков снова хоронят.

Сначала нас должны были заменить визуальные конструкторы. Потом 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 — штука вполне детерминированная, её можно ловить статически, просто пока никто толком не научил. Но пока кто-то должен отключить сеть в девтулзах и посмотреть, что будет. 

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


Читайте также: