Туториал

Создаём первый Generative UI (генеративный UI) с Vercel AI SDK

Пошаговое руководство по созданию первого AI-интерфейса с потоковыми компонентами.

A
Alex18 мин чтения

Предварительные требования

Прежде чем начать, убедитесь, что у вас есть:

  • 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, которая:

  1. Принимает текстовый промпт от пользователя
  2. Стримит React-компоненты с сервера
  3. Отрисовывает интерактивные карточки и графики на основе решений 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 вызывает оба инструмента и генерирует компоненты разных типов

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

Что происходит под капотом

Когда вы отправляете промпт:

  1. Клиент вызывает серверный экшен generateUI
  2. streamUI отправляет промпт и определения инструментов в OpenAI API
  3. Модель решает, какие инструменты вызвать и с какими параметрами
  4. Функция generate каждого инструмента немедленно отдаёт скелетон
  5. AI заканчивает определять параметры, и возвращается финальный компонент
  6. 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-экспертизы.
CopilotKitReact (Next.js + Vite + Remix)Если нужны in-app copilots с состоянием и actions. Поддерживает Generative UI через useCopilotAction.
TamboReact (universal)Каталог компонентов как первоклассная сущность. Работает на Vite, не требует RSC.
A2UIЛюбой (Google)Декларативный JSON-формат UI от Google для агентов. Renderer-агностичен, рендерится на любом фронте.
assistant-uiReactChat-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_compat runtime, 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.

ПоделитьсяTwitterLinkedInEmail
vercel-ai-sdkreacttutorialstreaming
A

Alex

Generative UI Engineer & Consultant

Senior-инженер, специализирующийся на AI-интерфейсах и системах Generative UI. Помогаю продуктовым командам шипить быстрее с правильным GenUI-стеком.

Будьте в курсе Generative UI

Еженедельные статьи, обновления фреймворков и практические руководства — прямо в почту.

Мы уважаем вашу конфиденциальность. Отписка в любой момент.

Нужна помощь с реализацией прочитанного?

Записаться на бесплатную консультацию