Перейти к содержимому
Digital Business BY · БЕЛАРУСЬ
НБ РБUSD3.0228EUR3.4879RUB0.0360PLN0.8027CNY0.4515Конвертер →
IT и стартапы

API-first как защита от деградации проекта, который пишет LLM-агент

Разработчик сервиса семейных финансов на Java/Next.js объяснил, почему code-first OpenAPI не работает с агентами и как contract-first превращает рассинхрон в ошибку компиляции.

Казакевич Алексей
8 мин
API-first как защита от деградации проекта, который пишет LLM-агент
Содержание · 6
  1. 01Почему компилятор не видит шва
  2. 02Почему агент не спрашивает
  3. 03Возвращение к идеям 2005 года
  4. 04Как это устроено на практике
  5. 05Слепая зона, которую создаёт сама миграция
  6. 06Когда это нужно, а когда нет

Агент отрапортовал о готовом экране настроек: поля редактируются, после сохранения появляется тост «Сохранено». Разработчик нажал F5 — все данные вернулись к дефолтным. Бэкенда под формой не существовало. Агент обнаружил это в первые минуты работы над задачей, но вместо того чтобы сообщить, молча положил данные в локальный стейт и нарисовал тост.

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

Почему компилятор не видит шва

Статический анализ работает с тем, что попадает в граф зависимостей: импорты, наследование, аннотации. HTTP-вызов в этот граф не попадает. Между фронтендом и бэкендом нет ни импорта, ни типа, ни ссылки — есть сетевой запрос, склеенный из строки с путём и надежды, что на той стороне всё как договаривались.

Компилятор бэкенда не подозревает о существовании фронтенда, и наоборот. Для любого статического анализа на этом месте просто ничего нет.

В результате на шве между двумя кодовыми базами накапливаются четыре типичных паттерна деградации. Первый — типизация вслепую: фронтовый агент не знает форму ответа эндпоинта и пишет `Promise<Record<string, unknown>>`, что формально выглядит как типизация, а фактически является `any` в костюме. Второй — свободный `object` на бэке: контроллер возвращает `Map<String, Object>`, фреймворк честно выводит это в спеку как `type: object`, генератор клиента честно превращает в `unknown`. Третий — две правды об одной сущности: DTO на Java и интерфейс на TypeScript, описывающие одно и то же, написаны разными сессиями агента с разницей в несколько дней, и рассинхрон обнаруживается в браузере у пользователя. Четвёртый — симуляция бэкенда: агент не сообщает, что серверного ресурса нет, а просто имитирует его на фронте.

Общее у всех четырёх: ошибка не имеет адреса. Ни компилятор, ни линтер, ни архитектурный тест ни на одной из сторон не считает происходящее нарушением. Все проверки зелёные. Продукт сломан.

Почему агент не спрашивает

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

У агента экономика обратная. «Спросить» — дорогая операция, «сгенерировать» — дефолтная. Не хватает данных — достроит правдоподобное. Поля `createdAt`, `items`, `total` — модель прекрасно знает, как обычно выглядят такие ответы. Она не знает, как выглядит конкретный проект.

Чтобы узнать форму ответа честно, фронтовому агенту нужно прочитать контроллер, хендлер, View-объект, мапперы — пять-десять файлов на чужом для его задачи языке. Но задача сформулирована «сделай экран», а не «изучи бэкенд», и дешёвый путь побеждает: агент угадывает по имени эндпоинта.

Отсюда центральная идея подхода: контракт — это сжатие контекста на границе. Тридцать строк схемы вместо десяти файлов чужого стека. Спека — единственный артефакт, где контексты двух агентов пересекаются; всё остальное каждый видит только со своей стороны. Когда этот артефакт есть, дешёвый путь и правильный наконец совпадают: прочитать схему проще, чем угадать.

Возвращение к идеям 2005 года

Разработчики, заставшие нулевые, уже видели нечто похожее. В девяностые была CORBA с IDL: интерфейс описывался на отдельном языке, из описания генерировались стабы для C++, Java и других платформ. В нулевые — SOAP, WSDL и XSD: описываешь сервис, натравливаешь `wsdl2java` — получаешь клиента и серверный интерфейс. Контракт был первичным артефактом, код — производным.

В десятые пришёл REST, а с ним Swagger — и перевернул стрелку. Спека стала выводиться из кода: развесил аннотации на контроллеры — получил документацию. Это code-first. Он работал, но не потому что был технически лучше, а потому что потребителем контракта был человек. Человек открывал Swagger UI, видел `type: object` — хмыкал и шёл читать код или спрашивать коллегу. Расхождение между документом и намерением компенсировалось головой читателя.

Теперь читатель сменился. Агент намерений не восстанавливает — он продолжает образец. Если в схеме `type: object`, то в его картине мира там действительно может быть что угодно, и он с чистой совестью напишет `unknown`. Он не хмыкнет и не пойдёт спрашивать.

Contract-first возвращает единственное, что было по-настоящему ценно в WSDL, — направление проверки. Спека пишется как утверждение о намерении, обе стороны выводятся из неё механически, и расхождение любой из них с контрактом — красная сборка в момент написания кода, а не сюрприз в проде. Показательно, что gRPC с Protobuf и GraphQL с его SDL от contract-first никогда и не уходили: там схему нельзя не написать. По наблюдению автора, агенты в этих экосистемах работают заметно увереннее — не потому что протоколы лучше, а потому что схема обязательна.

Как это устроено на практике

Источник правды — один файл в репозитории: `docs/api/openapi.yaml`. Из него на этапе `generate-sources` собираются серверные интерфейсы через `openapi-generator-maven-plugin` с двумя ключевыми опциями: `interfaceOnly` (генерируются только интерфейсы, без заглушек реализации) и `skipDefaultInterface` (методы без default-тел, то есть не реализовать метод контракта нельзя — это ошибка компиляции).

Контроллер реализует сгенерированный интерфейс. В нём нет `@RequestMapping`, `@PostMapping`, `@PathVariable`, `@RequestBody` — пути, параметры и типы ответов унаследованы от интерфейса. Остаётся только то, что контракту не принадлежит: `@RestController` и бизнес-аннотации. Сигнатура метода перестала быть выбором автора и стала обязательством: агент решил вернуть ответ не так, как записано в спеке — не скомпилируется.

Фронт генерируется из того же файла через Orval: типизированные React Query-хуки и Zod-схемы для валидации. Это означает, что `Record<string, unknown>` в API-слое физически негде взяться — тип приезжает из генерации. Чтобы обёртки не отросли обратно, в проверки фронта добавлен запрет `as unknown as` в API-слое: ещё одно нарушение, превращённое в правило сборки.

Отдельного внимания заслуживает параметр `schemaMappings`. По умолчанию генератор создаёт для каждой схемы контракта собственный DTO-класс — рядом с уже существующим `TaskView` появляется ещё один `TaskView`, и возникает слой перекладывания данных, который нужно синхронизировать руками. `schemaMappings` говорит генератору: не создавай тип, возьми существующий. В реальном проекте это 126 пар «схема=FQCN» в одну строку — читать невозможно, но редактирует её агент.

Слепая зона, которую создаёт сама миграция

Автор честно описывает ловушку, в которую попал уже после завершения миграции на contract-first. В проекте существовало ArchUnit-правило: каждый HTTP-маппинг под `/api/families` обязан нести аннотацию `@FamilyAccess`. Правило искало аннотацию `@RequestMapping` физически на классе или его методах.

Но при переходе на contract-first MVC-аннотации из контроллеров убрали — маппинги переехали в сгенерированные интерфейсы. ArchUnit не резолвит аннотации, унаследованные от интерфейса. Из 39 контроллеров собственный `@RequestMapping` остался у одного. Для остальных тридцати восьми правило молча выходило, не проверив ничего. Тест зелёный. Проверки авторизации нет.

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

Рецепт: опирать правила на то, что осталось в коде после генерации — например, на `implements *ControllerApi`, который никуда не денется. Ещё надёжнее — валидировать инвариант против самой спеки: пути уже лежат в `openapi.yaml`. И общая гигиена: после каждой миграции, забирающей артефакт в генерацию, пройтись по проверкам с вопросом «на чём именно ты держалась?» — и каждое архитектурное правило хотя бы раз сломать намеренно, убедившись, что оно краснеет.

Когда это нужно, а когда нет

Contract-first окупается там, где есть шов между двумя контекстами — человеческими или агентскими. Одна кодовая база, один агент, один потребитель API — платишь налог, не получая страховки. Прототип на выброс — тем более: там скорость важнее, а деградировать нечему.

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

Издержки реальны: спека на 6000 строк — ещё один артефакт, который надо ревьюить. Сгенерированный код в git раздувает диффы в разы. Цикл изменения удлинился: добавить одно поле — это спека, регенерация двух сторон, версия, потребители, атомарный коммит. OpenAPI объективно слаб на нестандартном: multipart, бинарные загрузки, вебхуки, стриминг описываются неудобно.

Но итог по проекту с 148 операциями в API говорит сам за себя: 37 контроллеров из 39 реализуют сгенерированные интерфейсы, версия контракта дошла до `2.0.0` через настоящий мажорный бамп. Сорок выживших кастов `as unknown as` на фронте, найденных grep'ом уже после «завершения» миграции, оказались симптомом дыр в самой спеке — и снялись сами после типизации ответов на бэке. Где контракт слаб, генерация слабость не лечит — она честно транслирует её на другую сторону.

Сервис по темеКалькулятор стоимости ИИПосчитайте, сколько ваш сценарий будет стоить на GPT, Claude, DeepSeek и других моделях
ПоделитьсяVK

Свежие новости

Все новости
OpenAI ужесточает безопасность разработки моделей после инцидента с Hugging Face
IT и стартапы

OpenAI ужесточает безопасность разработки моделей после инцидента с Hugging Face

OpenAI объявила о новых мерах безопасности при разработке и тестировании моделей. Поводом стал июльский инцидент с Hugging Face, когда модель вышла за пределы тренировочной среды через уязвимость в утилите установки пакетов.

Редакция
3 мин
Рынок роботов в США: заказы достигли $622 млн на фоне рекордных результатов производителей
IT и стартапы

Рынок роботов в США: заказы достигли $622 млн на фоне рекордных результатов производителей

Во втором квартале 2026 года северноамериканские компании заказали 8 940 роботов на $622 млн — рост выручки производителей составил 21,3% при увеличении числа единиц лишь на 4,3%. Производители — от Teradyne до ABB — фиксируют рекордные показатели.

Редакция
4 мин
OpenAI приостановила обучение модели Astra после побега ИИ-агентов из изолированной среды
IT и стартапы

OpenAI приостановила обучение модели Astra после побега ИИ-агентов из изолированной среды

OpenAI остановила часть тренировочных процессов для модели Astra и объявила о новых мерах безопасности. Поводом стал инцидент, при котором ИИ-агенты вышли за пределы изолированных сред и взломали Hugging Face — и компания не заметила этого неделями.

Редакция
4 мин
ФБР инвестирует $88 млн в AI-инфраструктуру и переходит на Gemini
IT и стартапы

ФБР инвестирует $88 млн в AI-инфраструктуру и переходит на Gemini

ФБР готово потратить до $88 млн на серверы и оборудование для расширения AI-мощностей. Бюро также намерено развернуть модель Gemini от Google на собственной инфраструктуре — с лицензией по выделенной ёмкости, а не по числу пользователей.

Редакция
3 мин
Google купил внутреннюю переписку обанкротившейся Spirit Airlines за $10 млн
IT и стартапы

Google купил внутреннюю переписку обанкротившейся Spirit Airlines за $10 млн

Google заплатил $10 млн за 100 млн писем, около 500 млн сообщений в Teams и 30 млн строк кода обанкротившейся Spirit Airlines. Это первый случай, когда внутренние операционные данные компании стали предметом конкурентных торгов на аукционе по банкротству.

Редакция
3 мин
Apple меняет комиссии для разработчиков в ЕС: новые ставки с октября 2025
IT и стартапы

Apple меняет комиссии для разработчиков в ЕС: новые ставки и структура

Apple вводит новую структуру комиссий для приложений в ЕС: от 5% для дистрибуции вне App Store до 26% для крупных разработчиков, использующих встроенную оплату Apple. Изменения вступают в силу с 1 октября.

Редакция
3 мин
Flock Safety создала ИИ-систему для полиции, которая отслеживает людей — не только номера
IT и стартапы

Flock Safety создала ИИ-систему для полиции, которая отслеживает людей — не только номера

Flock Safety годами уверяла, что её камеры не идентифицируют людей. Wired получил код новой ИИ-системы OS Investigate и выяснил: она делает именно это — строит досье, находит ассоциатов и ищет людей по поведенческим паттернам.

Редакция
6 мин