Vercel AI SDK — streaming chatbot w Next.js w 30 minut

Opublikowano
10 kwietnia 2026
Aktualizacja
31 maja 2026
Czas czytania
4 min czytania

Czym właściwie jest Vercel AI SDK?

to biblioteka TypeScript, która zdejmuje z developera większość żmudnej roboty przy dodawaniu AI do aplikacji w Next.js: ręczne zarządzanie strumieniem bajtów, parsowanie chunków, doklejanie historii rozmowy do kolejnych zapytań.

Pakiet dostarcza dwie warstwy narzędzi. Po stronie klienta to hooki jak useChat i useCompletion, które obsługują stan i aktualizacje . Po stronie serwera to streamText i generateText, które komunikują się z modelem i odsyłają wynik w ustandaryzowanym formacie. Biblioteka jest w pełni agnostyczna — bez zmiany architektury podepniesz OpenAI, Anthropic, Google, Mistral albo lokalne modele przez Ollamę.

Jeśli chcesz zbudować chatbota lub zautomatyzować generowanie treści w Next.js, schemat jest zawsze ten sam: useChat na froncie, streamText na zapleczu, tani model jak gpt-4o-mini na start i kilka prostych zabezpieczeń przed niekontrolowanymi kosztami. Działający prototyp zajmuje około 30 minut.

Instalacja

Instalujesz jedną paczkę rdzeniową i adapter dla wybranego dostawcy modelu:

Code
npm install ai @ai-sdk/openai @ai-sdk/react
 
# Wolisz mądrości od Claude'a zamiast OpenAI? Proszę bardzo:
npm install ai @ai-sdk/anthropic @ai-sdk/react

Chatbot w dwóch plikach

Route Handler — backend

Code
// Twój plik w app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
 
export const maxDuration = 30 // Trzymamy lejce: ucinamy watek, gdy rozmowa potrwa powyżej 30 sekund!
 
export async function POST(req: Request) {
  // Wyciągamy na stół nasze wpisane wiadomości
  const { messages }: { messages: UIMessage[] } = await req.json()
 
  // Wskazujemy konkretny model i zachowanie
  const result = streamText({
    model: openai('gpt-4o'),
    system:
      'Jesteś przemiłym i pomocnym asystentem. Nie lej wody, odpowiadaj tylko w języku polskim bardzo konkretnie i na temat.',
    messages: await convertToModelMessages(messages),
  })
 
  // Odpowiadamy ustandaryzowanym strumieniem z pakietu
  return result.toUIMessageStreamResponse()
}

Client Component — frontend

Code
// Twój plik w app/chat/page.tsx
'use client'
 
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
 
export default function ChatPage() {
  const [input, setInput] = useState('')
 
  // Cała magia SDK mieści się w wyciągnięciu tych parametrów z hooka
  const { messages, sendMessage, status } = useChat()
  const isLoading = status === 'submitted' || status === 'streaming'
 
  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    if (!input.trim()) return
 
    // Wysyłamy i od razu czyścimy okienko
    sendMessage({ text: input })
    setInput('')
  }
 
  return (
    <div className="mx-auto max-w-2xl p-4">
      <h1 className="mb-6 text-2xl font-bold">Pogawędka z AI</h1>
 
      <div className="mb-6 space-y-4">
        {/* Tu "wypluwamy" rozmowę pętlą z gotowej, pilnowanej przez SDK tablicy */}
        {messages.map((m) => (
          <div
            key={m.id}
            className={`rounded-lg p-4 ${
              m.role === 'user' ? 'ml-12 bg-blue-100' : 'mr-12 bg-gray-100'
            }`}
          >
            <p className="mb-1 text-sm font-medium">
              {m.role === 'user' ? 'Ty' : 'Sztuczna Inteligencja'}
            </p>
            {m.parts.map((part, index) =>
              part.type === 'text' ? (
                <p key={`${m.id}-${index}`} className="whitespace-pre-wrap">
                  {part.text}
                </p>
              ) : null,
            )}
          </div>
        ))}
 
        {isLoading && (
          <div className="mr-12 animate-pulse rounded-lg bg-gray-100 p-4">
            <p className="text-sm text-gray-500">
              Robot mocno myśli nad odpowiedzią...
            </p>
          </div>
        )}
      </div>
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
          placeholder="Śmiało, zapytaj o coś..."
          className="flex-1 rounded-lg border px-4 py-3"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="rounded-lg bg-blue-600 px-6 py-3 text-white disabled:opacity-50"
        >
          Start
        </button>
      </form>
    </div>
  )
}

To cały core. SDK opiera się na ustandaryzowanym formacie wiadomości z obiektami parts w tablicy messages, a backend odsyła strumień przez toUIMessageStreamResponse().

Jak działa strumieniowanie

streamText() buduje odpowiedź token po tokenie i odsyła ją przez Server-Sent Events zamiast czekać na kompletny wynik. Po stronie klienta useChat odbiera kolejne fragmenty i aktualizuje tablicę wiadomości w locie — stąd efekt "pisania na żywo" znany z ChatGPT. Użytkownik widzi pierwszą odpowiedź niemal natychmiast, a nie po kilku sekundach oczekiwania na pełny tekst.

Zmiana dostawcy modelu — jedna linia kodu

Code
// app/api/chat/route.ts
// Po prostu zmieniasz paczkę z której zaciągasz moduł i to wszystko!
import { anthropic } from '@ai-sdk/anthropic'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
 
export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json()
 
  const result = streamText({
    model: anthropic('claude-sonnet-4-6'), // Podpinasz wybranego Claude'a
    system:
      'Wyobraź sobie, że jesteś inżynierem Next.js połączonym ze specjalistą od sprawdzania widoczności (SEO). Używaj języka polskiego i fachowych rad.',
    messages: await convertToModelMessages(messages),
  })
 
  return result.toUIMessageStreamResponse()
}

Reszta kodu pozostaje bez zmian — to praktyczny dowód agnostyczności biblioteki.

System prompt — zakres i charakter asystenta

Prompt systemowy określa, w jakich granicach działa model i jak ma się zachowywać. Dla asystenta biznesowego wygląda to tak:

Code
const result = streamText({
  model: openai('gpt-4o'),
  system: `Jesteś oficjalnym asystentem na platformie internetowej StriveLab — potężnego hubu tworzącego witryny szyte na miarę na wdrożeniach Next.js.
 
Pamiętaj o twardych zasadach z naszej firmy:
- Wszystkie Twoje odpowiedzi muszą być wygenerowane w języku polskim.
- Musisz pisać w punkt, wyrzucaj lanie wody do kosza.
- Jeżeli wpadnie zapytanie z prośbą o wyceny — Twoim celem jest przekierować klienta grzecznie i profesjonalnie do podstrony [Automatyzacja AI](/uslugi/automatyzacja-procesow-ai/).
- Jeżeli zaczną wypytywać o tajniki pracy dewelopera — wyjaśniaj im w punkt jakie moce skrywa praca w najnowszym Next.js.
- Bądź jak głaz: nie dyskutuj absolutnie na tematy które nie dotykają inżynierii web lub tworzenia oprogramowania.
 
Twoje podręczne suche fakty o StriveLab:
- Nasz głowny konik (Specjalizacja): pisanie szybkiego Next.js, dopinanie logik SEO i dedykowane witryny konwersyjne (Landing Pages).
- Baza główna biura: Miasto Kraków
- Główna skrzynka pocztowa: kontakt@strivelab.pl`,
  messages,
})

useCompletion — generowanie bez historii

Przydaje się tam, gdzie nie potrzebujesz kontekstu rozmowy: jednorazowe tłumaczenie, generowanie opisu produktu, szybka sugestia. Żadnej historii wiadomości — tylko prompt i wynik:

Code
'use client'
 
import { useCompletion } from '@ai-sdk/react'
 
export function DescriptionGenerator() {
  const { completion, input, handleInputChange, handleSubmit, isLoading } =
    useCompletion({
      api: '/api/generate',
    })
 
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <textarea
          value={input}
          onChange={handleInputChange}
          placeholder="Wklej coś krótkiego o produkcie..."
          className="w-full rounded-lg border p-3"
          rows={3}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="mt-2 rounded-lg bg-blue-600 px-4 py-2 text-white"
        >
          {isLoading ? 'Rozgrzewam zwoje...' : 'Magia! Generuj tekst'}
        </button>
      </form>
 
      {/* Jeśli mamy wynik, wypluwamy go prosto pod przyciskiem */}
      {completion && (
        <div className="mt-4 rounded-lg bg-gray-50 p-4">
          <p className="whitespace-pre-wrap">{completion}</p>
        </div>
      )}
    </div>
  )
}
Code
// app/api/generate/route.ts
// Na zapleczu dla opcji Completion też ustawiasz to banalnie szybko!
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
 
export async function POST(req: Request) {
  const { prompt } = await req.json()
 
  const result = streamText({
    model: openai('gpt-4o-mini'),
    prompt: `Oto Twój cel: Stwórz chwytliwy i krótki (na 2 do maks 3 zdań) opis wdrożeniowy dla tekstu od użytkownika po haśle: "${prompt}". Skoncentruj się na tym by był w pełni optymalny dla czytających w google!`,
  })
 
  return result.toUIMessageStreamResponse()
}

generateText — Server Actions bez strumieniowania

Gdy nie potrzebujesz efektu pisania na żywo, tylko gotowego wyniku do dalszego przetworzenia — generowanie meta tagów, tłumaczenie wsadowe, podsumowanie — generateText zwraca kompletny tekst po zakończeniu wywołania:

Code
// actions/ai-actions.ts
'use server'
 
import { openai } from '@ai-sdk/openai'
import { generateText } from 'ai'
 
export async function generateMetaDescription(content: string) {
  // Pamiętaj, wyciągamy tylko sam suchy 'text' po zakończeniu przeliczania u modelu
  const { text } = await generateText({
    model: openai('gpt-4o-mini'),
    prompt: `Napisz świetnie czytający się tekst z opisu wpisu dla SEO (maks 160 znaków). Tekst będzie tworzony z następującego wstępu do bloga: "${content.slice(0, 500)}"`,
  })
 
  return text
}
 
export async function translateText(text: string, targetLang: string) {
  const { text: translated } = await generateText({
    model: openai('gpt-4o-mini'),
    prompt: `Przetłumacz co do joty poniższy wsad dla rynku w języku ${targetLang}. Wyrzuć jakikolwiek wstęp z Twojej strony, zwróć do mojego narzędzia wyłącznie sam przetłumaczony dokument:\n\n${text}`,
  })
 
  return translated
}

Structured output — generateObject z walidacją Zod

Model języka domyślnie zwraca tekst — co jest problemem, gdy potrzebujesz danych do aplikacji. generateObject wymusza określoną strukturę JSON przez schemat Zod i gwarantuje, że wynik będzie typowany i gotowy do użycia:

Code
import { openai } from '@ai-sdk/openai'
import { generateObject } from 'ai'
import { z } from 'zod'
 
// Określamy ścisły garnitur reguł, przez który model musi przejść by poprawnie zakończyć wynik:
const productSchema = z.object({
  title: z
    .string()
    .describe(
      'Szykowny i dopracowany pod pozycjonowanie zarys nazwy w sklepie',
    ),
  description: z
    .string()
    .describe('Esencja produktu mieszcząca się w zgrabnych dwóch zdaniach'),
  tags: z
    .array(z.string())
    .describe('Zestawienie około 3 do 5 kluczowych znaczników dla asortymentu'),
  category: z
    .string()
    .describe('Ogólne określenie segmentu z którego pochodzi ten towar'),
})
 
export async function generateProductData(rawDescription: string) {
  // Wrzucasz Zoda jako formę sztywnego odlewu do funkcji generateObject:
  const { object } = await generateObject({
    model: openai('gpt-4o-mini'),
    schema: productSchema,
    prompt: `Spójrz na to rozlane słowotwórstwo: "${rawDescription}" — przeanalizuj je i zwróć poprawny obiekt dla moich maszyn pod ustandaryzowaną sprzedaż.`,
  })
 
  return object // Wychodzi perfekcyjnie wylany obiekt typu JSON dla dalszej aplikacji na sklepie: { title: string, description: string, tags: string[], category: string }
}

Zabezpieczenia przed nadużyciem i niekontrolowanymi kosztami

Bez ograniczeń jeden użytkownik może wygenerować rachunek, który zrujnuje budżet projektu. Trzy warstwy ochrony, które warto wdrożyć od pierwszego dnia produkcji:

Code
// app/api/chat/route.ts
import { rateLimiter } from '@/lib/rate-limit'
import { auth } from '@/auth'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
import { openai } from '@ai-sdk/openai'
 
export async function POST(req: Request) {
  // Po 1: Nikt przypadkowy z zewnątrz nie ma tu wstępu, wymuś w sesji zalogowanie w firmie
  const session = await auth()
  if (!session) {
    return new Response('O nie, nie! Najpierw się zaloguj.', { status: 401 })
  }
 
  // Po 2: Ustalamy ostry kaganiec - limit na wciśnięcia wywołań np 20 chatów by uchronić Twoje portfele
  const { success } = await rateLimiter.limit(`chat:${session.user.id}`)
  if (!success) {
    return new Response(
      'Poczekaj! Przeładowałeś silniki asystenta — wróc za chwilę ze swoimi pytaniami.',
      { status: 429 },
    )
  }
 
  const { messages }: { messages: UIMessage[] } = await req.json()
 
  // Po 3: Genialny trik inżyniera — bez sensu wysyłać modelowi ostatnie osiemset stron dyskusji przy każdym wywołaniu! Zostawiaj ogon najmłodszej pamięci np na 20 wymian zdań!
  const recentMessages = messages.slice(-20)
 
  const result = streamText({
    model: openai('gpt-4o-mini'), // Mały model to małe spalanie gotówki na starcie!
    messages: await convertToModelMessages(recentMessages),
    maxOutputTokens: 1000, // Mocno narzucona dętka dla wielomównej odpowiedzi!
  })
 
  return result.toUIMessageStreamResponse()
}
Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Automatyzacja AI

Często zadawane pytania

Czym jest Vercel AI SDK?

Vercel AI SDK to potężna, otwartoźródłowa biblioteka w TypeScript, która niesamowicie upraszcza wrzucanie funkcji AI do aplikacji opartych o Next.js, React, Svelte czy Vue. Daje Ci do ręki gotowe hooki (jak useChat, useCompletion, useObject) oraz serwerowe pomoce (streamText, generateText, generateObject). Koniec z rzeźbieniem własnego kodu do zarządzania streamingiem, rozbijania danych na kawałki czy żmudnego pilnowania historii czatu!

Z niemal wszystkimi! Biblioteka jest w pełni agnostyczna. Bez problemu podepniesz OpenAI (GPT-4o), Anthropic (Claude Opus 4.8, Sonnet 4.6, Haiku 4.5), Google Gemini, a także Mistral, Cohere, xAI Grok, Groq czy Perplexity. Działa nawet z lokalnymi modelami przez Ollamę i LM Studio. Najlepsze jest to, że by zmienić "mózg" aplikacji, podmieniasz zazwyczaj tylko jedną linijkę importu.

Sam SDK jest w 100% darmowy. Płacisz wyłącznie za "spalone" tokeny u dostawcy modelu. Średnio jedna rozmowa z modelem typu GPT-4o-mini to koszt rzędu 0,001–0,005 USD. Claude Sonnet wyniesie Cię ok. 0,003–0,015 USD, a najpotężniejszy Claude Opus to już około 0,015–0,075 USD. Ostateczny rachunek bardzo mocno zależy od długości utrzymywanego kontekstu (historii) i skomplikowania samego promptu bazowego. Jeśli masz skromny budżet, odpalaj na start gpt-4o-mini albo claude-haiku-4-5 i regularnie zaglądaj do zakładek z użyciem w panelu dostawcy.

Jeśli Twój cel to klasyczny, konwersacyjny chatbot z pamięcią poprzednich wiadomości (coś w stylu okienka ChatGPT) — idziesz w useChat. Jeżeli potrzebujesz prostej, jednorazowej strzały bez kontekstu (np. klikasz guzik i generuje się opis przedmiotu do sklepu albo robisz szybkie tłumaczenie na żywo) — useCompletion sprawdzi się idealnie. Chcesz wyciągnąć od AI konkretne dane poskładane w poprawny JSON (pilnowany przez Zod)? Użyj z kolei generateObject lub hooka useObject.

Jasne! Vercel AI SDK świetnie wspiera środowiska Edge, Node.js oraz Cloudflare Workers. Jedyne na co musisz uważać, to limity czasu nałożone przez hosting (na darmowym koncie w Vercelu na Edge masz 30 sekund, w pakiecie Pro już 300 sekund). Jeśli wiesz, że odpowiedź AI potrwa dłużej, wymuś wyższy czas życia skryptu w Route Handlerze, np. przez export const maxDuration = 60.

Postaw na trzy solidne filary. Po pierwsze: zmuś użytkowników do logowania (np. używając auth() z paczki NextAuth lub Clerk). Po drugie: zablokuj limit zapytań (tzw. rate limiting) na głowę, używając chociażby Upstash Redis i @upstash/ratelimit (ustaw np. 20 wiadomości na godzinę). Po trzecie: ucinaj historię! Podawaj do AI tylko ostatnie wiadomości (np. messages.slice(-20)) i nakładaj sztywne ramy limitu na maxOutputTokens. Nie zapomnij też o alertach ustawionych na panelach billingowych dostawcy API.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów