Традиционные текстовые ответы в чат-интерфейсах обладают ограниченной выразительностью. Современные корпоративные приложения требуют динамического отображения многофункциональных интерактивных виджетов: интерактивных финансовых графиков, таблиц с фильтрацией, карточек оформления заказов и многошаговых форм. При этом прямая генерация и исполнение произвольного React-кода на клиенте создает фатальные риски безопасности (XSS, исполнение вредоносного JS) и приводит к нестабильности верстки.
Концепция Generative UI решает эту фундаментальную проблему путем связывания механизма вызова инструментов (Tool Calling / Function Calling) модели с архитектурой React Server Components (RSC) и протоколом React Flight:
tool_call, запрашивает необходимые данные из доверенных бэкенд-сервисов и рендерит изолированный React Server Component.Вместо монолитного текстового потока клиентский браузер принимает типизированный поток виртуальных узлов React Flight.
Архитектурный шаблон, при котором языковая модель управляет композицией и параметрами предварительно валидированных компонентов интерфейса. Бинарно-текстовый протокол сериализации React Server Components, передающий дерево Virtual DOM и ссылки на клиентские модули без клиентского бандла логики. Детерминированный механизм, позволяющий модели возвращать структурированный JSON с аргументами для вызова внешних функций.[ LLM Inference ] --(Tool Call: { symbol: "AAPL", period: "1M" })--> [ Server Action (Node.js / Edge) ]
|
(Fetch DB + Render RSC)
v
[ Client Browser: React 19 Reconciler ] <--- (React Flight Stream) --- [ <FinancialChart data={...} /> ]Пользователь отправляет запрос через Server Action; LLM сопоставляет интент с зарегистрированными схемами инструментов.
Модель возвращает JSON-аргументы инструмента, прошедшие валидацию через Zod-схему на сервере.
Генератор инструмента вызывает yield для немедленной отправки легковесного компонента-лоадера пользователю.
Сервер считывает данные, формирует дерево Server Component и стримит чанки React Flight в клиентский Reconciler.
Каким образом серверный рантайм передает сгенерированный компонент (RSC) клиенту в концепции Generative UI без выполнения сырого строкового кода в браузере?
Ниже представлен пример реализации Server Action с использованием streamUI из Vercel AI SDK и React Server Components.
import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/WeatherCard';
import { WeatherSkeleton } from '@/components/WeatherSkeleton';
export async function submitUserMessage(message: string) {
'use server';
const result = await streamUI({
model: openai('gpt-4o'),
system: 'Вы интеллектуальный погодный ассистент. Для отображения сводок погоды ОБЯЗАТЕЛЬНО вызывайте инструмент getWeather.',
prompt: message,
text: ({ content }) => <p className="text-gray-800 dark:text-gray-200 leading-relaxed">{content}</p>,
tools: {
getWeather: {
description: 'Отображает структурированный погодный виджет для указанного населенного пункта',
parameters: z.object({
city: z.string().describe('Название города на русском или английском языке'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
generate: async function* ({ city, unit }) {
// 1. Немедленно отдаем оптимистичный скелетон загрузки клиенту
yield <WeatherSkeleton city={city} />;
// 2. Выполняем защищенный асинхронный запрос к источнику данных
const response = await fetch(`https://api.weather.internal/v1/forecast?city=${encodeURIComponent(city)}`);
const data = await response.json();
// 3. Возвращаем скомпилированный Server Component с гидратируемыми данными
return <WeatherCard city={city} temperature={data.temp} unit={unit} humidity={data.humidity} />;
},
},
},
});
return result.value;
}'use client';
import { useState } from 'react';
import { submitUserMessage } from '@/actions/chat';
export default function GenerativeUIChat() {
const [messages, setMessages] = useState<React.ReactNode[]>([]);
const [input, setInput] = useState('');
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isPending) return;
const currentPrompt = input;
setInput('');
setIsPending(true);
// Добавляем сообщение пользователя в UIState
setMessages((prev) => [
...prev,
<div key={`user-${Date.now()}`} className="p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">{currentPrompt}</div>,
]);
try {
// Получаем потоковый React Node от Server Action
const responseNode = await submitUserMessage(currentPrompt);
setMessages((prev) => [...prev, responseNode]);
} catch (err) {
setMessages((prev) => [
...prev,
<div key={`err-${Date.now()}`} className="p-3 bg-red-100 text-red-700 rounded-lg">Сбой генерации интерфейса.</div>,
]);
} finally {
setIsPending(false);
}
};
return (
<main className="max-w-2xl mx-auto p-6 space-y-4">
<div className="space-y-4 min-h-[400px]">
{messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Спросите о погоде в любом городе..."
className="flex-1 px-4 py-2 border rounded-md"
/>
<button type="submit" disabled={isPending} className="px-4 py-2 bg-blue-600 text-white rounded-md">
{isPending ? 'Загрузка...' : 'Отправить'}
</button>
</form>
</main>
);
}Разработчик возвращает из генератора инструмента `streamUI` серверный компонент с кнопкой: `<button onClick={() => trackAnalytics()}>Купить</button>`. К какой ошибке в продакшене это приведет?
Передача callback-функций (например, onClick={() => console.log('click')}) из генератора серверного компонента напрямую в клиентский компонент.
Архитектурное последствие: Немедленная ошибка сериализации React Flight: «Functions cannot be passed directly to Client Components».
Решение: Выносить интерактивные элементы в отдельные файлы с директивой 'use client', передавая из RSC исключительно примитивные данные (числа, строки, JSON).
Использование синхронного return <FinalWidget /> без предварительного yield <SkeletonLoader />.
Архитектурное последствие: Пользователь видит пустой экран в течение нескольких секунд, пока выполняются тяжелые SQL-запросы или сторонние API, что создает ощущение зависшего интерфейса.
Решение: Использование генераторной функции async function* с эмиссией промежуточных скелетонов.
Создание схем инструментов с десятками опциональных вложенных полей, пытаясь заставить модель генерировать полную стилизацию CSS. Архитектурное последствие: Модель часто галлюцинирует, ошибается в синтаксисе вложенных объектов JSON и превышает тайм-ауты вызова инструментов. Решение: Схема инструмента должна содержать только критические идентификаторы и параметры бизнес-логики; вся визуальная верстка инкапсулируется в теле компонента на сервере.