На экране всё выглядело почти живым. Модель за несколько минут собрала страницу с карточками, статусами и бейджами. Казалось, мне можно закинуть ногу на ногу и допить кофе. Но затем код дошел до ревью. Под аккуратным интерфейсом скрывались styled.div, HEX-цвета и пропсы, которых в нашей дизайн-системе никогда не существовало. Получилось то самое лабораторное чудовище, которое хрипло дышит, но в проекте жить не может.
Поэтому я поставил эксперимент с одной фронтенд-задачей, одной моделью и четырьмя одинаковыми копиями проекта. В первом случае оставил агента без подсказок, во втором дал ему скилл с правилами проекта, в третьем подключил MCP с документацией дизайн-системы, а в четвертом совместил всё сразу. Мне хотелось узнать, какая комбинация превратит генерацию кода в эликсир скорости, а какая породит еще одно чудовище, не способное пережить встречу с typecheck и build.
Привет, Хабр! Меня зовут Егор Штаб, я Fullstack-разработчик в Райффайзен Банке, в команде комплаенс занимаюсь фронтенд-разработкой на React, бэкенд-разработкой на ASP.NET, а также участвую в новой стратегической инициативе банка, в рамках которой создаю harness для проекта. В работе много экспериментирую с ИИ-инструментами для разработки. В какой-то момент мне стало интересно, можно ли научить агента не просто генерировать рабочий интерфейс, а писать код так, будто он уже знает правила моего проекта и дизайн-системы. Об этом эксперименте и расскажу.
Где заканчиваются общие знания модели
Проблема не в том, что модель не понимает, как устроен интерфейс. Она знает, что контейнер можно сделать через flex, статус показать бейджем, а данные разложить по карточкам. Но из общих UI-паттернов нельзя узнать контракт конкретной дизайн-системы.
В банке мы используем FCC UI. Это UI-кит для React с примитивами, карточками, модалками, дроверами и контролами всех мастей. Например, flex-контейнер в FCC выглядит так:
<Flex direction="row" gap="s2" alignItems="center">…</Flex>
Здесь direction отвечает за flex-direction, alignItems за align-items, а gap сохраняет свое привычное имя. Модель без контекста, скорее всего, соберет тот же контейнер самостоятельно:
const StyledContainer = styled.div` display: flex; flex-direction: row; align-items: center; `;
Если звезды сойдутся в особенно причудливую фигуру, мы получим такой результат.
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> Звезды сошлись </div> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}> И мы получили такой результат </div>
То же самое происходит с цветами, размерами, иконками и композицией компонентов. Модель может вспомнить похожий API из другой библиотеки и уверенно подставить его в FCC.
В лучшем случае агент найдет TypeScript-декларации в node_modules и попробует восстановить контракт по ним. Типы подскажут, какие компоненты существуют и какие пропсы они принимают. Но по ним не всегда можно понять, какой компонент выбрать, как его скомпоновать и какой паттерн сейчас считается правильным в проекте. Такой код может работать, но он не учитывает контракт и договоренности конкретного проекта.
Даем агенту доступ к контракту FCC
Чтобы не обсуждать проблему абстрактно, я взял тестовый проект с двумя пакетами. @fcc/ui содержит компоненты интерфейса, а @fcc/icons — иконки дизайн-системы.
Например, задача может требовать показать категорию и статус продукта в бейджах. Для этого недостаточно понять, что нужен зеленый бейдж. Агенту необходимо знать, что в FCC у Badge есть проп color с фиксированным набором значений и semanticColor с семантическими токенами. Затем ему нужно выбрать существующий компонент, правильный проп и допустимое значение.
Доступ к актуальному контракту я дал через MCP-сервер дизайн-системы docs-context. Через него модель может:
найти существующий компонент с помощью list_components;
получить его пропсы и допустимые значения через
get_component_props;посмотреть реальные примеры использования в
get_component_stories;подобрать иконку через
get_icons;найти семантический цветовой токен с помощью
get_colors.
На практике агент сначала находит подходящий компонент, затем проверяет его API и только после этого смотрит примеры композиции. В отличие от обычного поиска по документации, MCP предоставляет структурированные факты о контракте. Модели не приходится угадывать, какие компоненты существуют, какие значения допустимы и какие токены доступны.
И вот, я решил проблему… Или нет?
Зачем MCP скилл
Если модель может спросить, какие компоненты и пропсы существуют, зачем добавлять еще один слой?
Я создал скилл, который заранее задает модели принципы работы с дизайн-системой:
использовать компоненты и иконки FCC;
не выдумывать имена компонентов, пропсов и токенов;
не использовать сторонние UI-библиотеки;
не собирать визуальные примитивы с нуля, если в FCC уже есть подходящий компонент;
не писать inline-стили;
не использовать HEX-цвета вместо токенов;
обращаться к MCP, когда нужно проверить компонент, проп, иконку или цвет.
Скилл не хранит список компонентов и не дублирует MCP. Он задает намерение и границы допустимого поведения. Но одного намерения недостаточно. Даже зная, что пропсы нельзя выдумывать, модель всё равно может ошибиться. Скилл требует проверять контракт, но сам по себе не сообщает точное имя пропса.
Во всех четырех режимах задача оставалась одинаковой. Для каждого я использовал отдельную копию проекта и новую сессию модели. Все данные, продукты и условия вымышлены и не являются реальным предложением.

Теперь самое интересное. Результаты эксперимента.
Четыре режима под микроскопом
Базовый режим без скилла и MCP
реализовала основную бизнес-логику страницы;
не использовала MCP и скилл FCC (логично);
собрала часть интерфейса через
styled.div;захардкодила цвета
#2d9d4cи#888;использовала невалидные пропсы
alignиm0;не прошла проверку типов.
Модель понимает общий UI-паттерн, но не знает контракт конкретной дизайн-системы. Поэтому агенту придется явно указывать, что именно нужно исправить.

Только скилл
не использовала HEX-цвета;
применила семантические токены;
старалась следовать правилам проекта;
использовала компоненты FCC;
ошиблась в пропсе
alignи значенииmargin;не прошла проверку типов.
Скилл задает правильное намерение, но не дает модели точного API. В результате получаем код, который не собирается и требует дополнительной правки.

Только MCP
сделала 11 обращений к MCP;
проверила компоненты, пропсы, стори и цвета;
получила рабочий код, который прошел
typecheckиbuild;реализовала статусы в виде текстовых бейджей, но не добавила более полный визуальный паттерн с иконками.
MCP дал модели точность контракта даже без скилла. Код заработал, но реализация оказалась менее полной.

Скилл и MCP
сделала 9 обращений к MCP;
использовала компоненты и токены FCC;
избежала хардкода и inline-стилей;
добавила иконки для активного и архивного состояний;
прошла
typecheckиbuild.
Скилл добавил проектное намерение, а MCP дал точность. Вместе они обеспечили модели рабочий API и более полный паттерн реализации. Показательно, что в этом режиме модель обращалась к MCP реже, чем при использовании только MCP. Похоже, скилл помог ей заранее структурировать поиск.

На маленькой задаче разница между MCP и комбинацией скилла с MCP заметна только в деталях. На больших задачах с несколькими состояниями, иконками, сложной композицией и несколькими экранами она может стать существеннее. Скилл помогает удерживать единые правила на протяжении всей реализации.
У эликсира нашлись побочки
Но не будем только о хорошем. Я добавил два полезных слоя и получил две новые точки отказа. Во время выполнения задачи MCP становится runtime-зависимостью. Если сервер недоступен, отвечает слишком медленно или возвращает неполные либо противоречивые данные, модель наследует эти проблемы. Она может продолжить работу без ответа, неправильно интерпретировать информацию или сделать несколько повторных вызовов и потратить дополнительный контекст.
Поэтому я не стал считать MCP последней линией обороны. После него всё равно остаются проверка типов, сборка и при необходимости ручное ревью.
Со скиллом обнаружилась другая потенциальная ловушка. Он не обновляется автоматически. Если в дизайн-системе появился новый компонент, изменился пропс или был удален старый паттерн, статичные правила останутся прежними. Модель продолжит следовать правильно сформулированному, но уже неактуальному правилу. MCP может показать текущий контракт, но только если агент действительно обратится к нему.
Кроме того, скилл не проверяет собственные утверждения. Стоит случайно добавить в него несуществующий компонент, неправильный проп или устаревшую рекомендацию, и модель начнет воспринимать эту информацию как правило.
Получается довольно иронично. Скилл, который должен уменьшать количество галлюцинаций, сам может стать их источником. Из этого я вывел для себя простое правило. В скилле лучше хранить только устойчивые принципы поведения:
не выдумывать API;
использовать документацию или MCP как источник правды;
заранее определять приоритет и зоны ответственности источников;
не хардкодить цвета;
проверять неизвестные сущности.
А конкретные сведения о компонентах, пропсах, токенах и иконках лучше получать из актуального MCP-контракта.
Формула жизнеспособного кода
Мой эксперимент показал, что лабораторное чудовище появляется не потому, что модель не умеет писать код. Во всех четырех режимах она поняла бизнес-задачу и собрала интерфейс со списком продуктов, карточками, статусами и условиями. «Чудовище» возникало там, где заканчивались общие знания о фронтенде и начинался контракт конкретной дизайн-системы.
Поэтому для себя я описал надежность агента простой формулой.
Скилл задает намерение и правила проекта. MCP предоставляет точный и актуальный контракт. А проверка типов и сборка отвечают за верификацию результата в реальном коде.
Если одного из этих множителей не хватает, всё снова идет не по плану. Скилл без MCP задает правильное намерение, но не дает модели точного API. MCP без скилла может дать точный, но менее полный результат. А без проверки типов и сборки нет подтверждения, что полученный код вообще работает.
Зато когда все три части работают вместе, код может по-настоящему поселиться в проекте, а разработчик наконец допить свой кофе и перейти к более творческим задачам.

