Создаём первый Generative UI (генеративный UI) с Vercel AI SDK
Пошаговое руководство по созданию первого AI-интерфейса с потоковыми компонентами.
Предварительные требования
Прежде чем начать, убедитесь, что у вас есть:
- Node.js 18+
- Проект на Next.js 14+ с App Router
- API-ключ OpenAI (или Anthropic — SDK поддерживает оба)
- Базовые знания React Server Components
Если вы ещё не работали с RSC, потратьте 15 минут на изучение официальной документации Next.js по Server Components. Функция streamUI из Vercel AI SDK построена на RSC, и всё встанет на своё место, как только вы разберётесь с этой моделью.
Что мы строим
Мы создадим простого AI-ассистента, который генерирует интерактивный UI на основе промптов пользователя. По завершении руководства у вас будет рабочая функция Generative UI, которая:
- Принимает текстовый промпт от пользователя
- Стримит React-компоненты с сервера
- Отрисовывает интерактивные карточки и графики на основе решений AI
Для примера возьмём финансового ассистента, умеющего показывать котировки акций и данные о погоде — достаточно просто, чтобы быстро разобраться, и достаточно сложно, чтобы показать реальные паттерны.
⚠️ AI SDK RSC и
streamUIпомечены Vercel как experimental. Для production-проектов Vercel рекомендует AI SDK UI (useChatиз@ai-sdk/react). Эта статья показывает рабочий паттерн RSC-стриминга для прототипов, демо и контролируемых сред; для продакшна оцените trade-off и см. раздел «Когда Vercel AI SDK — НЕ ваш выбор». Migration guide RSC → UI.
Шаг 1: Установка зависимостей
npm install ai@^4 @ai-sdk/openai@^1 zod
Pin v4 — последняя серия с RSC API в форме parameters: и импортом ai/rsc. На v5+ см. примечание о различиях в конце статьи.
Пакет ai — это ядро Vercel AI SDK. @ai-sdk/openai — провайдер OpenAI (замените на @ai-sdk/anthropic, если предпочитаете Claude). zod отвечает за валидацию параметров инструментов — именно с его помощью вы описываете, какие параметры AI может передавать каждому компоненту.
Добавьте API-ключ в .env.local:
OPENAI_API_KEY=sk-...
Шаг 2: Создаём библиотеку компонентов
Определите компоненты, которые AI сможет генерировать. Это обычные React-компоненты — никакой AI-специфики в них нет. Ключевой принцип проектирования: создавайте компоненты, полезные сами по себе, и тогда AI сможет свободно их комбинировать.
// components/weather-card.tsx
interface WeatherCardProps {
city: string;
temperature: number;
conditions: string;
humidity: number;
}
export function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<h3 className="text-lg font-semibold">{city}</h3>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-4xl font-bold">{temperature}°C</span>
<span className="text-muted-foreground">{conditions}</span>
</div>
<p className="mt-2 text-sm text-muted-foreground">
Humidity: {humidity}%
</p>
</div>
);
}
// components/stock-ticker.tsx
interface StockTickerProps {
symbol: string;
price: number;
change: number;
changePercent: number;
}
export function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {
const isPositive = change >= 0;
const sign = isPositive ? '+' : '';
const color = isPositive ? 'text-green-600' : 'text-red-600';
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-xl font-bold">{symbol}</h3>
<span className={`text-sm font-medium ${color}`}>
{sign}{changePercent.toFixed(2)}%
</span>
</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold">${price.toFixed(2)}</span>
<span className={`text-sm ${color}`}>
{sign}{change.toFixed(2)} today
</span>
</div>
</div>
);
}
// components/loading-skeleton.tsx
export function CardSkeleton({ height = 'h-32' }: { height?: string }) {
return (
<div className={`animate-pulse rounded-lg bg-muted ${height} w-full`} />
);
}
Шаг 3: Определяем инструменты AI (Server Action)
Это сердце Generative UI. Создайте серверный экшен, который связывает ваши компоненты с AI в виде «инструментов» — функций, которые модель может вызывать по собственному решению:
// app/actions.tsx
'use server';
export const runtime = 'edge';
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/weather-card';
import { StockTicker } from '@/components/stock-ticker';
import { CardSkeleton } from '@/components/loading-skeleton';
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai('gpt-4o'),
system: `You are a helpful financial and information assistant.
Use the available tools to display information visually
whenever possible. Prefer showing components over text responses.
When asked about weather or stocks, always use the appropriate tool.`,
prompt,
tools: {
showWeather: {
description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',
parameters: z.object({
city: z.string().describe('The city name, e.g. "Paris" or "New York"'),
temperature: z.number().describe('Current temperature in Celsius'),
conditions: z.string().describe('Weather description, e.g. "Partly cloudy"'),
humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),
}),
generate: async function* (params) {
// Yield a skeleton immediately while data "loads"
yield <CardSkeleton height="h-36" />;
// In a real app, you would fetch live weather data here
return <WeatherCard {...params} />;
},
},
showStock: {
description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\'s shares.',
parameters: z.object({
symbol: z.string().describe('Stock ticker symbol, e.g. "AAPL" or "TSLA"'),
price: z.number().describe('Current stock price in USD'),
change: z.number().describe('Price change today in USD'),
changePercent: z.number().describe('Percentage price change today'),
}),
generate: async function* (params) {
yield <CardSkeleton height="h-32" />;
return <StockTicker {...params} />;
},
},
},
});
return result.value;
}
В этом коде важно понимать три вещи:
Функция generate — это асинхронный генератор. Ключевое слово yield немедленно отправляет скелетон — ещё до того, как AI завершит определение параметров. return отдаёт финальный компонент. Именно так работает стриминг в Generative UI.
Описания инструментов — это инструкции для AI. Поля description — это то, что модель читает, чтобы решить, какой инструмент вызвать. Пишите их чётко, указывая, когда инструмент следует использовать, а когда — нет.
Zod-схемы закрепляют контракт. Если вы задаёте строгие Zod-схемы, AI не сможет передать невалидные параметры. Ошибки валидации перехватываются до рендеринга компонента.
Шаг 4: Строим интерфейс
// app/page.tsx
'use client';
import { useState } from 'react';
import { generateUI } from './actions';
const EXAMPLE_PROMPTS = [
"What's the weather like in Tokyo?",
"Show me Apple's current stock price",
"Compare the weather in London and New York",
"How is Tesla stock doing?",
];
export default function Home() {
const [prompt, setPrompt] = useState('');
const [messages, setMessages] = useState<Array<{ prompt: string; ui: React.ReactNode }>>([]);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
setLoading(false);
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="text-3xl font-bold">Generative UI Demo</h1>
<p className="mt-2 text-muted-foreground">
Ask about weather or stocks — watch the AI generate the right interface.
</p>
{/* Example prompts */}
<div className="mt-4 flex flex-wrap gap-2">
{EXAMPLE_PROMPTS.map(p => (
<button
key={p}
onClick={() => setPrompt(p)}
className="rounded-full border px-3 py-1 text-sm hover:bg-muted"
>
{p}
</button>
))}
</div>
{/* Prompt input */}
<form onSubmit={handleSubmit} className="mt-6 flex gap-2">
<input
value={prompt}
onChange={e => setPrompt(e.target.value)}
placeholder="Ask anything..."
className="flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<button
type="submit"
disabled={loading || !prompt.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
>
{loading ? 'Generating...' : 'Ask'}
</button>
</form>
{/* Generated UI output */}
<div className="mt-8 space-y-6">
{messages.map((msg, i) => (
<div key={i}>
<p className="mb-2 text-sm font-medium text-muted-foreground">
"{msg.prompt}"
</p>
{msg.ui}
</div>
))}
</div>
</main>
);
}
Шаг 5: Запускаем и тестируем
npm run dev
Попробуйте эти промпты по порядку, чтобы увидеть разное поведение:
- "What's the weather in Paris?" — одна карточка WeatherCard
- "Show me Apple stock" — один тикер StockTicker
- "Compare the weather in London and New York" — AI вызывает
showWeatherдважды и генерирует две карточки рядом - "How's Tesla doing and what's the weather in San Francisco?" — AI вызывает оба инструмента и генерирует компоненты разных типов
Третий промпт — ключевая демонстрация: без какого-либо дополнительного кода модель сама комбинирует несколько компонентов, чтобы ответить на составной вопрос.
Что происходит под капотом
Когда вы отправляете промпт:
- Клиент вызывает серверный экшен
generateUI streamUIотправляет промпт и определения инструментов в OpenAI API- Модель решает, какие инструменты вызвать и с какими параметрами
- Функция
generateкаждого инструмента немедленно отдаёт скелетон - AI заканчивает определять параметры, и возвращается финальный компонент
- React заменяет скелетон на готовый компонент
Протокол стриминга RSC — это то, что делает всё это возможным. Сервер сериализует деревья React-компонентов и передаёт их клиенту порционно. Это принципиально отличается от JSON API — клиент получает готовые компоненты, а не сырые данные.
Обработка ошибок
Генерируемые компоненты могут падать там, где вручную написанные компоненты не падают. При этом важно понимать: React error boundary ловит только ошибки рендеринга. Сбой стрима (потеря сети, таймаут OpenAI, ошибка вызова инструмента на сервере) error boundary не перехватит — этот сценарий нужно обрабатывать явно в handleSubmit.
Защита в два слоя — try/catch вокруг стрима и error boundary вокруг отрисованного UI:
// components/genui-error-boundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }
export class GenUIErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
<p className="text-sm text-destructive">
This component could not render. The AI may have passed unexpected data.
</p>
</div>
);
}
return this.props.children;
}
}
А ошибки самого стрима ловите в клиентском обработчике:
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!prompt.trim() || loading) return;
const currentPrompt = prompt;
setPrompt('');
setLoading(true);
try {
const ui = await generateUI(currentPrompt);
setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
} catch (err) {
// Сетевые ошибки, таймауты OpenAI, отказ серверного экшена — всё сюда
setMessages(prev => [...prev, {
prompt: currentPrompt,
ui: <div className="text-sm text-destructive">Stream failed. Try again.</div>,
}]);
} finally {
setLoading(false);
}
}
Оберните сгенерированный UI компонентом GenUIErrorBoundary на странице — он подхватит ошибки рендера, а try/catch — всё остальное.
Когда Vercel AI SDK — НЕ ваш выбор
SDK хорош, но он не панацея. Не берите его, если:
- SDK помечен experimental — задокументированные ограничения: невозможно прервать стрим через server actions, компоненты remount-ятся на
.done()(мигание), много<Suspense>boundaries могут крашить страницу,createStreamableUIдаёт квадратичный объём передачи данных. Для production Vercel рекомендует AI SDK UI. - У вас не Next.js.
streamUIпостроен на React Server Components, которые требуют Next.js App Router (или Waku/другой RSC-фреймворк). Для SPA на Vite, Remix без RSC, Vue, Svelte, Angular — смотрите альтернативы ниже. - Нужен фиксированный UI с динамическими данными. Если интерфейс заранее известен и LLM нужна только для данных, используйте обычный
generateObject+ ваш статичный React-код. Generative UI оправдан там, где AI решает какие компоненты показывать. - Жёсткие требования по приватности или on-prem deployment. SDK завязан на провайдеров (OpenAI, Anthropic). Для self-hosted LLM проще написать тонкий слой над
vLLM/Ollamaи собственным реестром компонентов. - Real-time коллаборация или мультиплеер. RSC-стрим — однонаправленный. Для двусторонней синхронизации UI между пользователями нужны WebSocket-решения, не RSC.
- Бюджет на токены — критичен. Каждый рендер — это вызов LLM. На MAU > 10k без агрессивного кэширования счёт за gpt-4o может перешагнуть $1k/мес.
Альтернативы для не-Next.js проектов
| Инструмент | Стек | Когда брать |
|---|---|---|
| Thesys C1 API | Любой (HTTP API) | SaaS, отдаёт готовые UI-блоки по JSON-схеме. Идеально для команд без RSC-экспертизы. |
| CopilotKit | React (Next.js + Vite + Remix) | Если нужны in-app copilots с состоянием и actions. Поддерживает Generative UI через useCopilotAction. |
| Tambo | React (universal) | Каталог компонентов как первоклассная сущность. Работает на Vite, не требует RSC. |
| A2UI | Любой (Google) | Декларативный JSON-формат UI от Google для агентов. Renderer-агностичен, рендерится на любом фронте. |
| assistant-ui | React | Chat-first библиотека, поддерживает tool UIs. Хорошая база для копилотов на любом React-приложении. |
| Свой слой | Любой | Если нужны 2–3 типа компонентов и контроль критичен — реестр + generateObject + ваш switch на клиенте занимает ~150 строк. |
Для Vue/Svelte/Angular на сегодня (май 2026) production-ready решений уровня Vercel AI SDK нет — большинство команд делают тонкий клиент к API, который возвращает JSON-описание компонента, и рендерят на фронте сами.
Деплой на дешёвых платформах
Vercel — очевидный выбор для Next.js, но не единственный. Если бюджет ограничен или вы не хотите завязываться на Vercel:
- Fly.io — $0–5/мес на хобби-планы. Поддерживает Next.js через Dockerfile. Edge-регионы по всему миру. Лимит на free tier — 3 машины × 256MB.
- Render — бесплатный web service засыпает после 15 мин неактивности (первый запрос после сна — ~30 сек). Подходит для демо и пет-проектов, не для продакшна.
- Railway — $5 кредитов в месяц на hobby-плане. Простой деплой из GitHub, отличный DX, но дороже Fly.io при росте.
- Cloudflare Pages + Workers — бесплатно до 100k запросов/день. Требует адаптации под
nodejs_compatruntime, RSC-стриминг работает с оговорками. - Свой VPS + Coolify/Dokploy — от $5/мес (Hetzner, Contabo). Полный контроль, но вы отвечаете за обновления, SSL, мониторинг.
Для большинства pet-проектов Fly.io даёт лучший баланс: бесплатный старт, нормальный production-путь, edge-регионы без vendor lock-in.
Что нужно команде
Прежде чем тянуть Vercel AI SDK в продакшн, оцените готовность стека и людей:
Обязательные навыки:
- React Server Components — без этого
streamUIбудет чёрным ящиком при первом же баге. - TypeScript — Zod-схемы и tool-параметры без типов превращаются в кашу.
- Async generators (
async function*,yield) — не каждый middle-React-разработчик с ними работал. - Prompt engineering — описания инструментов и системный промпт определяют качество выбора компонентов. Это отдельная дисциплина.
Желательные навыки:
- Опыт работы с LLM API (rate limits, retry-стратегии, token accounting).
- Понимание Edge runtime и его ограничений (нет Node.js APIs, лимит на размер бандла).
- Observability — структурированные логи tool-вызовов, трассировка запросов.
Размер команды и TCO (ориентировочно, май 2026):
| Размер | Инженерное время | Затраты на LLM (MAU 1k) | Затраты на LLM (MAU 10k) | Реалистично? |
|---|---|---|---|---|
| Соло (1 чел.) | 2–3 недели на MVP | ~$50/мес (gpt-4o-mini) | ~$500/мес | Да, для side-project |
| Маленькая (2–4) | 4–6 недель на v1 | ~$150/мес (gpt-4o микс) | ~$1.5k/мес | Да, основной use case |
| Средняя (5–15) | 2–3 месяца на полную интеграцию | ~$300/мес | ~$3k–5k/мес | Да, если есть платформа |
| Большая (15+) | 4–6 месяцев + платформенная команда | бюджет согласовывается | $10k+/мес | Стоит оценить self-hosted LLM |
Цифры по LLM — для сценария «1 запрос на сессию, gpt-4o для выбора инструментов, gpt-4o-mini для параметров». Реальные затраты сильно зависят от длины промптов, частоты повторных запросов и стратегии кэширования.
Методология расчёта TCO: цифры рассчитаны при допущениях: средний промпт ~800 input + ~300 output токенов на gpt-4o (или ~$0.001 на gpt-4o-mini), 1 запрос/сессия, OpenAI прайс на 2026-05, MAU ≈ DAU × 30%. Калибруйте под свой workload.
Советы по деплою
Переменные окружения: OPENAI_API_KEY должен быть доступен в вашем production-окружении. На Vercel добавьте его в настройках проекта в разделе Environment Variables.
Edge runtime: Функция streamUI работает на Edge runtime, что существенно сокращает время холодного старта. Добавьте export const runtime = 'edge' в файл серверного экшена.
Rate limiting: Без ограничения частоты запросов один пользователь может сгенерировать тысячи AI-запросов. Добавьте rate limiter перед вызовом streamUI. Пакет @upstash/ratelimit хорошо интегрируется с Next.js.
Выбор модели: gpt-4o даёт наилучшие результаты при выборе компонентов, но стоит дороже. gpt-4o-mini обходится примерно в 15× дешевле по input/output (openai.com/api/pricing, 2026-05) и хорошо справляется с простыми наборами компонентов. Протестируйте оба варианта с вашими конкретными определениями инструментов.
Следующие шаги
В этом руководстве мы разобрали основы. Для production Generative UI:
- Добавляйте новые инструменты — каждый новый компонент в реестре расширяет возможности AI
- Реализуйте кэширование результатов — кэшируйте частые запросы, чтобы снизить задержку и расходы
- Добавьте потоковый текст наряду с UI-компонентами, чтобы AI мог объяснять, что показывает
- Используйте structured outputs для более надёжной генерации параметров
- Настройте observability — логируйте каждый вызов инструмента, его параметры и действия пользователей
Документация Vercel AI SDK подробно описывает все эти паттерны, а в репозитории с примерами есть production-ready шаблоны, которые стоит изучить.
На AI SDK v5/v6
Если вы используете более новые версии SDK, ключевые отличия от кода в этой статье:
parameters:в определении инструмента →inputSchema:- Импорт
import { streamUI } from 'ai/rsc'→import { streamUI } from '@ai-sdk/rsc' - RSC по-прежнему помечен Vercel как experimental — для production рекомендуется AI SDK UI (
useChat).
Хотите внедрить Generative UI в свой продукт? Обсудим ваш кейс — по нашему опыту консалтинга, GenUI-стек хорошо ложится на дашборды и внутренние инструменты; для регулируемых поверхностей и публичных high-traffic страниц trade-off обычно не сходится.
Раскрытие: внешние ссылки на продукты (Thesys, CopilotKit, Tambo, Vercel, Fly.io, Render, Railway, Cloudflare, Upstash) — органические рекомендации; нет аффилиатных программ, нет рекламы (ФЗ-38). Цены актуальны на 2026-05-11.
Alex
Generative UI Engineer & Consultant
Senior-инженер, специализирующийся на AI-интерфейсах и системах Generative UI. Помогаю продуктовым командам шипить быстрее с правильным GenUI-стеком.
Похожие статьи
Κατασκευάζοντας το Πρώτο σας Generative UI με το Vercel AI SDK
Βήμα-βήμα οδηγός για τη δημιουργία της πρώτης σας AI-powered διεπαφής με streaming συστατικά.
Προσβασιμότητα σε Generative UI: Δημιουργία Συμπεριληπτικών AI Διεπαφών
Πρακτικός οδηγός για προσβάσιμα γεννητικά interfaces — screen readers, πλοήγηση με πληκτρολόγιο και συνδυαστικά προβλήματα προσβασιμότητας.
CopilotKit vs Vercel AI SDK vs Thesys: Σύγκριση Frameworks
Μια ειλικρινής σύγκριση των τριών κύριων frameworks Generative UI, με πλεονεκτήματα, μειονεκτήματα και πότε να χρησιμοποιείτε το καθένα.
Будьте в курсе Generative UI
Еженедельные статьи, обновления фреймворков и практические руководства — прямо в почту.
Нужна помощь с реализацией прочитанного?
Записаться на бесплатную консультацию