Статья про то, как AI-агенту дали знание о дизайн-системе, чтобы он перестал придумывать несуществующие компоненты и начал верстать из того, что реально есть в проекте.Закрываем очень практичную боль: агент видит макет, умеет писать код, но не понимает, какими компонентами из вашего проекта этот экран нужно собирать.Внутри: Figma MCP, свой DS MCP, React, Jetpack Compose и SwiftUI, поиск по компонентам, ChromaDB, RAG, embeddings, препроцессор для описания дизайн-системы, грабли с чтением файлов, шумом в контексте, устаревающим SKILL.md и честное сравнение подходов по tool calls, токенам и результату.Заходите, читайте и рассказывайте, как вы решаете эту проблему у себя ❤️ Читать далее
Привет, Хабр! На связи Николай Сычев. Я старший разработчик в X5 Tech. Работаю с большими проектами — много фич, много платформ, много кода. В какой-то момент начал активно использовать AI-агентов для генерации верстки и столкнулся с повторяющейся проблемой.
Агенты научились писать код, но когда просишь сверстать экран, то получаешь код с компонентами, которых нет в проекте. Агент знает Material UI, знает Bootstrap, знает что угодно из интернета, но не знает ваш CardList, ваш PrimaryButton и ваши токены темы.
Я решил это исправить.
Попросите любого агента сверстать форму авторизации. Он справится, но результат будет написан так, будто в вашем проекте нет ни одного готового компонента.

Агент не знает вашу дизайн-систему
Агент не виноват. У него нет контекста о вашей дизайн-системе. Проблема усиливается, если платформ несколько: один проект на React, другой на Jetpack Compose, третий на SwiftUI и у каждого своя дизайн-система, свои компоненты, свои соглашения.
Давать контекст вручную в каждый промпт долго. Копировать компоненты в чат тоже громоздко и не надёжно. В итоге агент действительно экономит время на написание кода, но создаёт дополнительную работу по его адаптации, и нужен дополнительный инструмент.
Figma MCP даёт агенту структуру макета, отступы, layout, но не знает, какими компонентами из вашего кода этот макет реализовывать. Поэтому нужен ещё один инструмент, который закроет вопрос «чем это верстать».
Первая реакция была решить проблему в лоб. Дать исходники дизайн-системы агенту.
Самый очевидный вариант: дать агенту инструменты read_file и list_directory, пусть сам разберётся в структуре проекта.
На практике это выглядит так: агент запрашивает список файлов в папке компонентов и получает 200+ позиций. Дальше пытается угадать нужные позиции по именам файлов и читает их по одному. Иногда угадывает, иногда открывает не тот файл, читает, понимает что не то, ищет дальше.

Для экрана с 10 компонентами это выливается в:
25–40 tool calls,
~12–18k токенов в контексте,
покрытие 60–70% — часть компонентов агент просто не находит.
Корень проблемы: имя файла FilterChip.kt ничего не говорит агенту о том, подходит ли этот компонент для задачи. Агент вынужден читать файл, чтобы это понять. Дизайн-систему из 200+ компонентов физически не обойти.
Тогда появилась следующая идея. Раз агент не может обойти все файлы, нужно помочь ему искать. Загрузим все исходники в ChromaDB, пусть делает similarity search.
В результате поиск стал быстрее, tool calls сократились до 5–8 поисковых вызовов. Но появилась другая проблема — качество результатов.
Векторный поиск строит embedding по смыслу текста. Но у задачи агента и у исходного кода смысловая близость текста устроена по-разному. Агент формулирует запрос на языке интерфейса и поведения, например, ищет «кнопка для подтверждения действия в форме». В индексе лежит onClick, variant, disabled, JSX-разметка. Для модели это не одно и то же. Расстояние между таким запросом и реальным фрагментом кода слишком большое, потому что слов «подтверждение» и «форма» в коде нет.

Семантическая пропасть между языком задач и языком кода
В итоге поиск возвращал топ-10 результатов целыми исходниками файлов, ~800 токенов каждый. Агент получал по 40–60 тысяч токенов в контексте, из которых релевантными были только 2–3 компонента. Остальное — шум.
Третья идея выглядела более элегантно. Не городить векторную базу, не давать агенту инструменты для поиска, а просто распарсить всю дизайн-систему в структурированный датасет, сгенерировать из него один большой .md файл с описанием всех компонентов и положить его агенту в контекст через SKILL.md или аналогичный механизм. Например: .cursorrules, system prompt или memory файл.

Агент один раз читает файл при старте сессии и «знает» всю дизайн-систему. Никаких tool calls, никакой инфраструктуры, никакого MCP.
Если в дизайн-системе 20–30 компонентов и они хорошо описаны, подход вполне жизнеспособен. Но у него есть потолок, и он довольно быстро достигается.
Проблема 1 — размер контекста
Описание одного компонента вместе с props и примером использования занимает ~500-1000 токенов. Если в дизайн-системе 100 компонентов, то одна документация разрастается до 50-100 тысяч токенов. И это ещё до того, как агент увидел задачу, макет и файл, который нужно редактировать.
Формально современные модели поддерживают большие контексты, но между «поддерживает» и «эффективно использует» есть разница. Исследования показывают деградацию качества при заполненном контексте. Модель хуже извлекает нужное из середины большого документа (эффект «lost in the middle»). Весь этот объём приходится передавать при каждом запросе, даже если для конкретной задачи агенту не нужны все 100 компонентов.
Проблема 2 — актуальность
SKILL.md — это статичный файл, а дизайн-система живёт и постоянно меняется. В неё добавляются компоненты, меняются props, появляются новые паттерны. Каждый раз нужно вручную или через скрипт пересобирать файл и не забыть обновить его в нужных местах. Это операционная нагрузка, которая со временем приводит к дрейфу. Агент работает с устаревшим описанием дизайн-системы, и никто этого не замечает.
Проблема 3 — нет фильтрации по платформе и контексту
Если в команде несколько платформ, например React, Compose, SwiftUI, то либо нужен отдельный файл для каждой, либо всё сваливается в один общий. В первом случае мы умножаем операционную нагрузку на количество платформ. Во втором агент получает в контекст компоненты для всех платформ сразу, хотя для конкретной задачи нужна только одна.
Проблема 4 — нет поиска, есть только чтение
MCP с векторным поиском даёт агенту релевантные компоненты под конкретную задачу. А SKILL.md наоборот всё сразу и нужное, и ненужное. Агент сам должен разобраться что из 100 компонентов применимо к текущему экрану. А это дополнительная когнитивная нагрузка на модель, которая снижает качество итогового результата.
Когда подход оправдан
Если дизайн-система маленькая и стабильная. Например, одна платформа, 30–40 компонентов и редкие изменения. То SKILL.md вполне рабочее решение с минимальным порогом входа. Никакой инфраструктуры, никакого сервера. Распарсил, сложил в файл и подключил.
Но как только дизайн-система начинает расти, подход начинает тормозить.
Условие: сверстать карточку матча по Figma-макету. Четыре подхода запущены на одной и той же задаче.
Метрика | Подход 1 — Ручной поиск файлов | Подход 2 — ChromaDB RAG | Подход 3 — DS Spec документ | Подход 4 — DS MCP |
|---|---|---|---|---|
Figma MCP вызовы | 1 | 1 | 1 | 1 |
Чтение файлов (Glob/Grep/Read) | ~18 | 0 (ноль!) | 1 (spec doc) | 0 (ноль!) |
RAG-запросы | 4 batches (~28 queries) | |||
DS MCP вызовы | 6 | |||
Всего | ~22 | 7 | 3 | 8 |
DS-компоненты использованы | 6 + 6 иконок | 6 + 6 иконок | 6 + 6 иконок | 6 + 6 иконок |
Расход токенов (оценка) | ~40–50k | ~25–30k | ~15–20k | ~15–18k |
Шагов агента | 6 turns | 4 turns | 2 turns | 3 turns |
Качество кода | Идентичный результат | Идентичный результат | Идентичный результат | Идентичный результат |
Качество кода одинаковое для всех подходов. Но 4 подход DS MCP даёт оптимальный баланс. У него минимальный расход токенов ~15–18k. Он не требует чтения файлов и RAG-инфраструктуры.
Зато подход 3 DS Spec самый экономный по tool calls, но при этом требует ручного обновления.

Результаты эксперимента: сравнение четырёх подходов. Задача: сверстать карточку по Figma-макету
Но все эти подходы упираются в одну и ту же проблему — между кодом и смыслом нет моста.
Значит нужно что-то, что будет переводить реализацию обратно в язык задач.
Идея простая: если агент не знает вашу дизайн-систему, её нужно ему дать. Но не в ручную или промпте, а как отдельный инструмент через MCP.
Model Context Protocol (MCP) — это стандарт для подключения внешних инструментов к агентам. Агент вызывает tools → получает нужный контекст → использует при генерации.
Ключевое отличие от предыдущих подходов в том, что между кодом и ChromaDB добавляется препроцессор. Он генерирует человекочитаемое описание каждого компонента — назначение, use cases, смысл props, ограничения. В индексе появлялся текст в одном семантическом пространстве с запросом агента, и similarity search начинал работать как надо.
Систему удобно разделить на две независимые части.
Шаг 1. Merge в main
В репозиторий с дизайн-системой прилетает изменение — новый компонент, обновлённые props, исправленная документация. Триггер — merge в main ветку.
Шаг 2. Webhook забирает изменения
CI/CD поднимает webhook, который отслеживает изменения в папке дизайн-системы и запускает пайплайн индексации только для изменившихся файлов, а не для всей базы.
Шаг 3. Парсинг и сборка датасета
Парсер обходит исходники компонентов и извлекает структурированную информацию: имя компонента, props с типами и дефолтными значениями, JSDoc-комментарии, зависимости от других компонентов дизайн-системы, категорию и путь к файлу.
Шаг 4. Препроцессор генерирует описания
Сырая структура компонента уходит в препроцессор. Тот генерирует человекочитаемое текстовое описание через LLM: назначение компонента, типичные use cases, смысл каждого prop, ограничения и антипаттерны.
Это ключевой шаг — именно здесь код переводится в язык задач.
Шаг 5. Embedding и сохранение в ChromaDB
Текстовое описание прогоняется через embedding model и получается вектор. В ChromaDB сохраняется пакет данных по каждому компоненту: вектор описания, метаданные (имя, платформа, категория, путь к файлу), семантическое описание и исходный код компонента.
Шаг 6. Агент формирует запрос
Агент получает задачу на верстку и вызывает tool searchComponents. В запрос передаёт usecase, то есть описание того, что нужно: «Большая кнопка с текстом» — и опциональный контекст, где это будет использоваться: «Экран, Диалог».
Шаг 7. Usecase → вектор
Строка запроса прогоняется через ту же embedding model, что использовалась при индексации компонентов.
Это важный момент — модель должна быть одна и та же, иначе сравнение теряет смысл.
Шаг 8. Поиск в ChromaDB
Вектор запроса сравнивается с векторами всех компонентов в базе. ChromaDB возвращает top-N компонентов с наименьшим косинусным расстоянием, то есть самым семантически близких к запросу.
Шаг 9. Результат возвращается агенту в формате .md
Агент получает список релевантных компонентов. Для каждого есть описание, props, контекст использования, код. Всё в читаемом .md формате, который можно сразу использовать как готовый контекст для генерации верстки.

Сервис написан на Node.js. В качестве векторной базы используется ChromaDB. Её легко поднять локально через Docker, и для этого не нужна отдельная инфраструктура.
Сервис поддерживает три платформы: React, Jetpack Compose, SwiftUI. В него можно добавить свой проект, указать путь к исходникам и запустить индексацию. Отдельно можно добавить гайдлайны в markdown, чтобы они тоже попадали в контекст агента. Работает локально как MCP-сервер.

Цель парсера не в том, чтобы выполнить код или проверить типы. Его цель – извлечь структурированное описание компонента для последующей индексации. Нас интересует: имя компонента, список props с типами и дефолтными значениями, JSDoc-комментарии, зависимости от других компонентов дизайн-системы, категория.
Для этого с помощью простого скрипта (который тоже написан с помощью ИИ) парсим компоненты через регулярные выражения, извлекая типы из интерфейсов:
пример кодаdef extract_components(file: Path, type_map: Dict[str, Any]) -> List[Dict[str, Any]]:
text = file.read_text(encoding="utf-8")
results = []
patterns = [
r"function\s+([A-Z]\w*)\s*\(([^)]*)\)",
r"const\s+([A-Z]\w*)\s*=\s*\(([^)]*)\)\s*=>",
]
for pat in patterns:
for m in re.finditer(pat, text):
name, params = m.groups()
props = []
if ":" in params:
p_name, p_type = params.split(":", 1)
p_type = p_type.strip()
props = type_map.get(p_type, {}).get("fields", [])
results.append({
"name": name,
"framework": "react",
"kind": infer_kind(name),
"props": props,
"source_file": str(file)
})
return resultsJetpack Compose — парсинг @Composable функций с извлечением параметров и типов:
def extract_composables_from_file(
file_path: Path,
type_map: Dict[str, Any],
name_counts: Dict[str, int]
) -> List[Dict[str, Any]]:
lines = file_path.read_text(encoding="utf-8").splitlines()
package = extract_package(lines)
results = []
for i, line in enumerate(lines):
if "@Composable" not in line:
continue
for j in range(i + 1, len(lines)):
fun_line = lines[j].strip()
if not fun_line.startswith("fun "):
continue
match = re.match(
r"(public|private|internal)?\s*fun\s+(?:(\w+)\.)?(\w+)",
fun_line
)
if not match:
break
name = match.group(3)
if name[0].islower() or name.lower().endswith("preview"):
break
annotations = extract_annotations(lines, i)
param_block = fun_line[fun_line.find("("):]
# ... собираем многострочный блок параметров
params = parse_parameters(param_block[1:-1], type_map)
body_flags = analyze_body(lines, j)
kind = infer_kind(name, annotations, body_flags)
results.append({
"name": name,
"package": package,
"kind": kind,
"parameters": params,
"annotations": annotations,
"calls_material": body_flags["calls_material"],
"source_file": str(file_path)
})
break
return resultsSwiftUI — парсинг struct * : View с извлечением @State/@Binding свойств.
def extract_views(file: Path) -> List[Dict[str, Any]]:
text = file.read_text(encoding="utf-8")
results = []
for m in re.finditer(r"struct\s+(\w+)\s*:\s*View\s*\{", text):
name = m.group(1)
params = extract_properties(text)
kind = infer_kind(name)
results.append({
"name": name,
"framework": "swiftui",
"kind": kind,
"parameters": params,
"source_file": str(file)
})
return results
def extract_properties(text: str) -> List[Dict[str, Any]]:
props = []
for m in re.finditer(
r"@(?:State|Binding|Environment)?\s*var\s+(\w+)\s*:\s*([\w<>]+)",
text
):
props.append({"name": m.group(1), "type": m.group(2)})
return propsРезультат парсинга — это структура данных. Её ещё нельзя класть в ChromaDB напрямую, но препроцессор принимает эту структуру и генерирует текстовое описание.
Вариант 1 — Шаблонный
Он детерминированный, дешёвый и быстрый. Мы берём имя + props и собираем описание по шаблону. Работает хорошо, если в компоненте есть JSDoc.
Вариант 2 — LLM-генерация
Отправляем структуру + исходник в LLM, просим описать компонент. Дороже, медленнее, но работает даже без документации в коде. Запускается один раз при индексации. На регулярной работе агента это не сказывается.
Шаблонный препроцессор — функция composeDocument генерирует текст для embedding:
function composeDocument(c: NormalizedComponent): string {
const readableName = c.name
.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase()
.replace(/^./, char => char.toUpperCase())
return `${readableName} or ${c.name} (in code) is a ${c.kind} component
from the ${c.projectId} design system.
It is a ${c.dsLevel}-level component.
Typical roles: ${c.roles.join(SEPARATOR)}.
It can be used in the following contexts:
${c.allowedContexts.join(SEPARATOR)}.
This component ${c.wrapsCompose
? 'wraps Jetpack Compose primitives'
: 'does not wrap Compose primitives'}
and ${c.callsMaterial ? 'uses Material components'
: 'does not rely on Material components'}.
Configurable parameters include:
${c.parameters.map(p =>
`${p.name}:${p.type ?? 'any'}`
).join(SEPARATOR) || 'no parameters'}.`
}Итоговый документ для ChromaDB — функция asData собирает всё в VectorizedItemData. Помимо document для embedding, формируется parametersDescription — человекочитаемое описание каждого параметра с указанием обязательности, nullable и дефолтных значений.
function asData(components: NormalizedComponent[]): VectorizedItemData[] {
return components.map(c => {
const parametersDescription = c.parameters
.map(p => {
const type = p.type instanceof Object
? 'object' : p.type ?? 'any'
const def = p.default
? ` and default value ${p.default}` : ''
return `${p.default ? 'Optional' : 'Required'}`
+ ` parameter: '${p.name}'`
+ ` with ${p.nullable ? 'nullable' : 'not null'}`
+ ` type '${type}'${def}`
})
.join(SEPARATOR)
return {
id: c.id,
document: composeDocument(c),
metadata: {
name: c.name,
projectId: c.projectId,
kind: c.kind,
roles: c.roles.join(SEPARATOR),
dsLevel: c.dsLevel,
package: c.package,
visibility: c.visibility,
wrapsCompose: c.wrapsCompose,
callsMaterial: c.callsMaterial,
allowedContexts:
c.allowedContexts.join(SEPARATOR),
parametersCount: c.parameters.length,
parametersDescription,
parameters: JSON.stringify(c.parameters),
}
}
})
}
Препроцессор: вход и выход
Препроцессор с LLM-генерацией — это костыль, который закрывает отсутствие документации. Если в вашей дизайн-системе компоненты задокументированы, то индексация работает значительно лучше с минимальными усилиями.
JSDoc, Storybook-описания и README над компонентами можно индексировать напрямую. Это дополнительный аргумент в пользу документирования дизайн-системы в команде.
Хорошая документация работает сразу на двух потребителей — человека и агента.
Агент получает доступ к семи инструментам. Каждый отвечает за свой тип контекста.
Tool | Что делает |
|---|---|
| Возвращает контекст проекта: стек, платформа, структура DS |
| Возвращает гайдлайны DS — правила, соглашения, ограничения |
| Находит один наиболее подходящий компонент по описанию |
| Возвращает список компонентов по релевантности: имя, |
| Полное описание компонента по |
| Возвращает токены темы: цвета, отступы, типографику |
| Возвращает доступные иконки |
Покажу на примере, как агент получает задачу «сверстай форму авторизации».
getProject → понимает платформу и стек
getGuidelines → знает правила дизайн-системы: что можно, что нельзя, какие паттерны приняты
searchComponents("input, button, error message") → получает список компонентов: имя, id, краткое описание
getComponent(id) × N → полное описание каждого нужного компонента: props, контекст, код
getTheme → берёт цвета и отступы из токенов
Генерирует верстку — из компонентов, которые реально есть в проекте

Как агент работает с DS MCP. Пример: «сверстай форму авторизации»
Теперь мы наконец можем вернуться к Figma MCP. Она даёт агенту макет, а DS MCP даёт агенту компоненты. Вместе они закрывают полный цикл.
Figma MCP: структура экрана, layout, отступы, визуальная иерархия — «как это выглядит»
DS MCP: реальные компоненты, токены темы, иконки, гайдлайны — «чем это верстать»
Агент получает оба контекста и генерирует верстку, которая соответствует и макету, и дизайн-системе

Figma MCP + DS MCP = полный цикл
До внедрения связки Figma MCP + DS MCP верстка экрана занимала кучу времени разработчика. Причём его значительная доля уходила не на написание кода, а на адаптацию сгенерированного и замену выдуманных компонентов.
После внедрения агент сразу генерирует код из реальных компонентов дизайн системы. При этом ручная адаптация сгенерированного кода минимальна. В основном точечные правки там, где агент не угадал намерение.
Этот эффект особенно заметен при масштабировании. Чем больше экранов, тем ощутимее разница. При одном экране экономия небольшая, но с большим потоком задач на месяц разница становится ощутимой в человеко-часах.

Результат: до и после DS MCP
На одном экране. Было - 8 часов. Стало - 2-3 часа. Экономия времени на экран: 62-75%. В среднем - около 68%.
На команде из трёх разработчиков. Было - 3 экрана в день. Стало - от 8 до 12. В среднем - почти 10. Ускорение на команде: ×3.2 по фактическому замеру.
На команде из тридцати разработчиков. Было - 30 экранов в день. Стало - от 80 до 120. Тот же порядок ускорения, погрешность +-25%.

Производительность вёрстки: до и с DS MCP. Базовая метрика: 1 экран = 1 рабочий (8ч) без MCP. Замер на 3 разработчиках, экстраполяция на 30.
Месяц работы превращается в неделю. Квартал в месяц. Релиз, на который раньше закладывали половину года, - теперь укладывается в один месяц. И это - без расширения штата. Без переобучения. Без переписывания дизайн-системы.
Только за счёт того, что агент наконец-то знает, с чем он работает.
MVP работает, но до production не хватает нескольких вещей:
Актуальность индекса. Из-за того, что дизайн-система постоянно меняется, нужна автоматическая переиндексация. CI/CD хук на изменения в DS-репозитории, инкрементальное обновление только изменившихся компонентов, чтобы не переиндексировать всё каждый раз.
Evaluation pipeline. Сейчас качество поиска оценивается субъективно. Поэтому нужен тестовый набор запросов с эталонными компонентами, метрики precision@k и recall@k, регрессионное тестирование при изменении схемы индексации или embedding-модели.
Выбор embedding-модели. Дефолтная модель ChromaDB не оптимальна для технических описаний. Code-specific embeddings могут дать лучший результат, стоит проверить на своих данных.
Безопасность. MCP-сервер имеет доступ к исходникам дизайн-системы. В командном или облачном деплое нужны: аутентификация запросов к MCP, изоляция по проектам, контроль что именно попадает в индекс.
Мультипроектность. Единый реестр проектов, возможность агенту самому выбрать нужный проект по контексту задачи, версионирование: DS версии 2.x и 3.x могут существовать параллельно.
Observability. Какие компоненты агент находит? Какие запросы дают плохие результаты? Без логирования tool calls и результатов поиска улучшать систему сложно. Нужен минимальный трекинг: запрос → результаты → использовано ли в итоговом коде.
Автоматизация при добавлении компонента. Разработчик дизайн-системы пишет компонент → CI автоматически генерирует описание через LLM → кладёт в PR как артефакт → после merge автоматически переиндексируется. Разработчик дизайн-системы не думает про индексацию.
MCP + RAG — реальный способ дать агенту знание о вашем проекте прямо сейчас. Не нужно ждать пока LLM обучат на вашей дизайн-системе. Можно сделать это самому на существующем коде.
Качество RAG определяется качеством текста в индексе. Сырой код не работает. Нужна семантика: описания, документация, препроцессинг. Это ещё и аргумент в пользу документирования компонентов в команде — хорошая документация работает на двух потребителей сразу.
Figma MCP + DS MCP = полный цикл генерации верстки. Каждый инструмент закрывает свою половину задачи. Вместе они дают агенту достаточно контекста для качественного результата.
Если в вашем проекте есть дизайн-система и вы используете AI-агентов для верстки, вы можете попробовать этот подход у себя.
Буду рад вопросам и обратной связи в комментариях.
Также можно прочитать статьи о том, как AI-инструменты, дизайн-процессы и контекст проекта встраиваются в инженерную работу:
– Как я подключил Obsidian к Claude и Codex: домашний сервер, CouchDB, MCP и баг, который съедал заметки
– От vibe coding к Spec-Driven Development: как приручить скорость ИИ и довести проект до продакшена