Vercel AI SDK — streaming chatbot w Next.js w 30 minut
Jak zbudować chatbota AI w Next.js z Vercel AI SDK? Streaming odpowiedzi, integracja z OpenAI/Anthropic/Claude, React hooks useChat i useCompletion — kompletny tutorial krok po kroku.
Vercel AI SDK to otwartoźródłowa biblioteka TypeScript do budowy aplikacji AI w Next.js, React, Svelte i Vue, ujednolicająca API różnych dostawców modeli (OpenAI, Anthropic, Google, Mistral) pod jednym interfejsem. to fantastyczna, stworzona przez deweloperów biblioteka, która zdejmuje
z Twoich barków tony żmudnej roboty podczas wrzucania sztucznej inteligencji do
aplikacji w Next.js. Zapomnij o męczeniu się z "ręcznym" czytaniem i
zarządzaniem strumieniem bajtów płynącym z API, parsowaniem tzw. chunków czy
ręcznym doklejaniem starych wypowiedzi do historii rozmowy.
Ten pakiet to świetnie przemyślany zestaw zabawek: od wygodnych hooków ułatwiających budowę interfejsu (takich jak useChat czy useCompletion), aż po metody serwerowe załatwiające za nas brudną robotę z backendem (na czele z streamText i generateText). Co więcej, biblioteka nie faworyzuje absolutnie nikogo. Bez zająknięcia współpracuje z modelami od OpenAI, Anthropic, Google, Mistrala czy nawet z Twoimi lokalnymi modelami puszczonymi na Ollamie.
W wielkim skrócie: wpadłeś na pomysł, by zaimplementować fajnego chatbota lub zautomatyzować pisanie treści na swojej platformie opartej o Next.js? Łap za Vercel AI SDK. Odpalasz useChat na widoku, na zapleczu wpinasz streamText i w drogę. Na start bierz coś taniego jak gpt-4o-mini i pamiętaj o zapinaniu na kłódkę swoich zapytań poprzez limity czasowe. Obiecuję, że funkcjonalny pierwowzór będziesz miał gotowy jeszcze zanim wystygnie Ci kawa — w około 30 minut.
Krok po kroku: zaczynamy instalację
Nie ma na co czekać, zaciągnijmy potrzebne paczki prosto z NPM:
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
Od zera do bohatera, czyli chatbot oparty o dwa proste pliki
Budujemy zaplecze (Route Handler — backend)
Zacznijmy od małego, ale sprytnego silnika, który obsłuży strzały na zapleczu Twojej aplikacji:
Code
// Twój plik w app/api/chat/route.tsimport { 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()}
Robimy przyjemny widok (Client Component — frontend)
Teraz pora złożyć nasz interfejs wizualny w komponencie klienckim:
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> )}
I to w zasadzie tyle! Obecne odsłony AI SDK świetnie opierają się na standardzie ujednoliconych wiadomości (mają zgrabne obiekty parts w tablicy messages), a backend ładnie wysyła informacje w świat dzięki funkcji toUIMessageStreamResponse().
Zaglądamy pod maskę strumieniowania
Zastanawiałeś się kiedyś, dlaczego literki "pływają" na ekranie jak w ChatGPT? Odpowiada za to funkcja streamText(). Błyskawicznie buduje ona odpowiedź wysyłając ją świat w świat porcjami w formie pojedynczych tokenów, z których korzysta klasyczny mechanizm Server-Sent Events. Na froncie Twój hook useChat niczym wytrawny siatkarz łapie te podania i sekundę po sekundzie klei z nich aktualizowany, ciągły wpis w tablicy historii wiadomości. Efekt po stronie użytkownika powala natychmiastowym czasem reakcji.
A co gdy znudzi Ci się OpenAI i wolisz zaprzęgnąć do działania Claude'a?
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()}
Zero przepisywania skomplikowanych klas. Masz tu idealny dowód, dlaczego Vercel SDK po prostu rządzi na rynku!
Nadajemy AI mocnego charakteru i konkretnych reguł (System prompt)
To jak wychowasz swojego asystenta, zależy od tego jak dobrym promptem systemowym uświadomisz model co do tego na jakim podwórku operuje:
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 prosto w formularz do podstrony /kontakt.- 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 — asystent do pojedynczych rzutów
Świetnie sprawdza się w momentach, gdy chcesz zrobić drobnostkę jak tłumaczenie w panelu w locie, czy po prostu "Wygeneruj fajny opis tego telefonu na dwa zdania do mojego sklepu". Żadnej historii – tylko jedno polecenie:
// 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 — gdy potrzebujesz cichego wykonawcy z tyłu sceny (Server Actions)
Co w przypadku gdy odpalamy coś w locie i nie chcemy żadnego efekciarskiego strumieniowania? Tak się to robi przy ukrytym generowaniu np meta tagów podczas budowania zaplecza na widoku!
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 — wyciągamy mocno sformatowane bloki za pomocą JSON
AI ma czasem ten dziwny odruch by gadać o wiele za dużo, rozwalając wyjścia z kodu pod aplikację i tworząc miazgę interfejsu klienta. Do porządków stworzono super potężną wstawkę generateObject i zaprzyjaźniono ją z najlepszym ochroniarzem typów na rynku – Zodem!
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 portfela: jak ochronić API przed oszustami i limitami kosztów
Pamiętaj — nikt nie da Ci dostępu do zasobów sztucznej inteligencji za darmowe uśmiechy w sieci! Musisz obwarować swoje serwery, chroniąc pieniądze na karcie w zapleczu panelu dostawców!
Code
// app/api/chat/route.tsimport { 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()}
Słowo na zakończenie
Biblioteka Vercel AI SDK potraktowała proces integracji narzędzi potężnej, inteligentnej komunikacji tak, jak my dziś wrzucamy proste, trywialne formularze dla opcji logowań w systemach. To fantastyczne uczucie – gdy pragniesz potężnych, czatowych wtyczek korzystasz z dobrodziejstw z gotowca w postaci useChat; jak masz ochotę na opcje jednorazowych, wrzucanych by wyprodukować na zawołanie np podpowiedzi tekstów chwytasz w obroty useCompletion; uderzając np z tyłu w mechanice z wykorzystaniem systemowych zaciągnieć bierzesz generateText, no a jeżeli API po stronie frontendu wymaga mocnych restrykcji, to ratuje Cię JSON wraz z wymogami generateObject!
To co absolutnie kluczowe — to tarcza przed bankructwem (zabezpiecz ścieżki i zapnij na mocny rate limiting, ucina "wagę" przesyłanej po serwerach dawnej historii użytkownika i nie żałuj długich, konkretnych reguł narzucających z góry "od czego asystent w systemie po prostu JEST").
Często zadawane pytania
Pracuję z tym zawodowo.
Jeśli chcesz sensownie wdrożyć AI do codziennej pracy zespołu, uporządkować narzędzia i wyciągnąć z nich realną przewagę zamiast chaosu, skontaktuj się ze mną. Pomagam łączyć AI z praktyką developmentu, analityki i procesu produktowego.
Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.
Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.
Next.js vs WordPress w 2026 — obiektywne porównanie dla firm, freelancerów i agencji. Wydajność, SEO, bezpieczeństwo, koszty, łatwość edycji — kiedy który wybrać i dlaczego.
Praktyczne porównanie Claude, ChatGPT i Gemini z perspektywy dewelopera. Kodowanie, analiza, API, prywatność i workflow — kiedy które narzędzie ma sens.
React 19 Actions zmieniają sposób pisania formularzy. Przewodnik po useActionState, useOptimistic, useFormStatus i akcjach na atrybucie form action. Z przykładami i migracją z onSubmit.