Исторически каждый поставщик LLM (OpenAI, Anthropic, Google Vertex, Mistral, Ollama) развивал собственный закрытый клиентский SDK. В результате в кодовой базе накапливался зоопарк несовместимых библиотек: у одного вендора ответ лежал в response.choices[0].message.content, у второго — в response.content[0].text, а сигнатуры потокового стриминга кардинально различались.
Индустриальным стандартом современной TypeScript-разработки стала библиотека Vercel AI SDK (ai). Это легковесный, типобезопасный фреймворк с открытым исходным кодом, реализующий унифицированную спецификацию Language Model Specification (LanguageModelV1):
openai('gpt-4o'), anthropic('claude-3-7-sonnet') или локальным ollama('llama3') выполняется заменой одной переменной без переписывания бизнес-логики.generateText: блокирующий вызов с синхронным ожиданием полного завершения генерации.streamText: реактивный потоковый конвейер на базе нативного стандарта WebStreams API (ReadableStream).Сравним архитектурное поведение системы при блокирующем и потоковом взаимодействии:
+-----------------------------------------------------------------------------------+
| СИНХРОННЫЙ GENERATETEXT VS ПОТОКОВЫЙ STREAMTEXT |
+-----------------------------------------------------------------------------------+
| СЦЕНАРИЙ А: Блокирующий generateText() |
| Время: 0s 2s 4s 6s (Пользователь ждет) |
| Сервер: [--- Запрос ---> GPU генерирует полный ответ ----> HTTP 200 JSON] |
| Клиент: (Пустой экран / спиннер) ........................ [Появление всего текста]
| TTFT: 6000 мс (высокий риск сброса соединения по таймауту 504 Gateway Timeout)|
+-----------------------------------------------------------------------------------+
| СЦЕНАРИЙ Б: Потоковый streamText() |
| Время: 0s 0.3s 0.6s 0.9s 1.2s ... 6s (Живой интерфейс) |
| Сервер: [Запрос] -> [Токен 1] -> [Токен 2] -> [Токен 3] ... [Stop] |
| Клиент: [Спиннер] -> "Вы" -> "пол" -> "няем" ... [Полный текст] |
| TTFT: 300 мс (мгновенная отзывчивость интерфейса) |
+-----------------------------------------------------------------------------------+Инициализация адаптера (например, @ai-sdk/openai) с передачей API-ключа и выбор конкретной модели: openai('gpt-4o-mini').
Формирование объекта запроса с ролями system и prompt. Функция возвращает объект StreamTextResult с потоком ReadableStream.
Сервер отправляет клиенту заголовки 'Transfer-Encoding: chunked' и 'Content-Type: text/event-stream; charset=utf-8'.
Чтение дельт текста в цикле `for await (const chunk of textStream)`. Каждый чанк немедленно сбрасывается в сетевой буфер.
После отправки токена останова (stop) модель передает объект usage (число входных и выходных токенов), сохраняемый в базе данных.
Команда разрабатывает фоновый сервис ночной генерации аналитических PDF-отчетов по расписанию в очереди BullMQ. Какой метод Vercel AI SDK архитектурно предпочтительнее?
useChat().generateText() внутри очередей задач.fallbackProvider.toDataStreamResponse().Метод generateText блокирует асинхронную функцию до тех пор, пока модель не сгенерирует весь объем токенов или не встретит условие останова:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
export interface SummarizationResult {
summary: string;
tokensUsed: {
prompt: number;
completion: number;
total: number;
};
finishReason: string;
}
export async function summarizeArticle(content: string): Promise<SummarizationResult> {
const result = await generateText({
model: openai('gpt-4o-mini'),
system: 'Ты — профессиональный технический редактор. Сформируй выжимку текста в 3 пунктах.',
prompt: content,
temperature: 0.2,
maxTokens: 500,
});
return {
summary: result.text,
tokensUsed: {
prompt: result.usage.promptTokens,
completion: result.usage.completionTokens,
total: result.usage.totalTokens,
},
finishReason: result.finishReason, // 'stop' | 'length' | 'content-filter'
};
}Для потокового вывода используется streamText. Мы можем итерироваться по чанкам прямо в консоли Node.js с помощью асинхронного итератора for await:
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function streamTechnicalReview(codeSnippet: string): Promise<void> {
const result = streamText({
model: anthropic('claude-3-7-sonnet-20250219'),
system: 'Ты — Senior Staff архитектор. Проведи ревью переданного кода на безопасность.',
prompt: codeSnippet,
temperature: 0.1,
});
process.stdout.write('\n--- НАЧАЛО ПОТОКА ОТВЕТА ---\n');
// Читаем чанки по мере их прибытия по сети
for await (const textDelta of result.textStream) {
process.stdout.write(textDelta);
}
process.stdout.write('\n--- КОНЕЦ ПОТОКА ---\n');
// Метаданные usage доступны через Promise после завершения стрима
const usage = await result.usage;
console.log(`Использовано токенов: ${usage.totalTokens} (Вход: ${usage.promptTokens}, Выход: ${usage.completionTokens})`);
}В Next.js серверный роут транслирует поток модели напрямую в браузерный клиент через Server-Sent Events всего в 5 строк:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export const runtime = 'edge'; // Или 'nodejs'
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
system: 'Ты — корпоративный AI-ассистент платформы WPSKILLS.',
messages,
});
// Преобразует WebStream в совместимый с фронтендом ответ SSE
return result.toDataStreamResponse();
}Vercel AI SDK инициализирует объект ReadableStream, под капотом подключаясь к SSE-эндпоинту API провайдера (OpenAI / Anthropic).
Соединение не блокирует рабочий поток событий Node.js (Event Loop).
Провайдер отправляет HTTP-пакеты с дельтами сгенерированных токенов по мере их выхода с GPU-кластера.
Задержка между чанками составляет всего 15–40 мс.
Метод toDataStreamResponse кодирует текст, ошибки и метаданные в стандартизированный бинарный протокол Vercel Data Stream.
Протокол гарантирует разделение текста, метаданных и вызовов инструментов (Tool Calls).
Клиентский хук распаковывает поток через TextDecoder и плавно обновляет состояние интерфейса с частотой 60 fps.
Пользователь начинает читать текст уже через 200–300 мс после нажатия Enter.
Serverless-функция на Vercel/AWS Lambda имеет жесткий лимит исполнения в 15 секунд. Запрос к LLM на генерацию большой документации занимает 22 секунды. Что произойдет при использовании `generateText` без стриминга?
Разработчик подключает generateText к чату технической поддержки. Пользователь задает сложный вопрос, модель думает и генерирует объемный ответ 8 секунд. Все это время пользователь видит замерший интерфейс, кликает кнопку отправки повторно или закрывает вкладку.
Решение: Для всех пользовательских веб-интерфейсов по умолчанию использовать исключительно streamText.
Синтаксическая ошибка: вызов console.log(result.text) сразу после const result = streamText(...). В отличие от generateText, объект streamText не имеет синхронного свойства text. Попытка прочитать его вызовет ошибку либо вернет undefined.
Решение: Для получения полного текста после стриминга использовать await result.text (это Promise, разрешающийся после закрытия потока).
Если пользователь закрыл вкладку браузера на середине генерации, сокет разрывается. Если бэкенд не слушает событие req.signal.onabort, серверный процесс продолжит получать платные токены от OpenAI в никуда.
Решение: Передавать abortSignal: req.signal в параметры вызова streamText:
const result = streamText({
model: openai('gpt-4o'),
prompt,
abortSignal: req.signal, // Автоматически останавливает генерацию при дисконнекте
});В Next.js App Router при стриминге на старых версиях Node.js без отключения буферизации прокси-сервер (например, Nginx с proxy_buffering on) может накапливать чанки и отдавать их клиенту пачкой раз в 4 КБ, сводя на нет весь эффект стриминга.
Решение: Убедиться, что в Nginx прописан заголовок X-Accel-Buffering: no или используется runtime = 'edge'.