Przejdź do treści

Vercel AI SDK i budowa strumieniującego chatbota w Next.js

Strumieniujący chatbot z Vercel AI SDK w aplikacji Next.js. Zobacz jak przejść od zera do działającego interfejsu z OpenAI lub Claude bez zbędnego kodu boilerplate.

Maciej Sala

Founder StriveLab

7 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 24 lipca 2026)

Czym jest Vercel AI SDK w Next.js?

to biblioteka TypeScript, która zdejmuje z dewelopera 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 główne warstwy narzędzi. Po stronie klienta są hooki useChat i useCompletion, które obsługują stan oraz aktualizacje

. Po stronie serwera znajdują się streamText i generateText, które komunikują się z modelem i zwracają wynik w ustandaryzowanym formacie. Adaptery pozwalają zachować podobną architekturę przy OpenAI, Anthropic, Google czy Mistralu. Nie oznacza to jednak identycznego wsparcia wszystkich możliwości przez każdy model.

W czacie opartym na AI SDK 7 interfejs operuje na UIMessage[]. Ten format zawiera identyfikatory i tablicę parts, dzięki czemu może reprezentować tekst, pliki, dane oraz wywołania narzędzi. Model otrzymuje prostszy format ModelMessage[], dlatego Route Handler używa asynchronicznego convertToModelMessages().

Instalacja Vercel AI SDK w projekcie Next.js

Instalujesz paczkę rdzeniową, integrację React i adapter wybranego dostawcy. Warto przypiąć główne wersje w package.json, ponieważ kolejne wydania SDK mogą zmieniać API:

Code
npm install ai@^7 @ai-sdk/openai@^4 @ai-sdk/react@^4 zod
 
# Wariant z Anthropic
npm install ai@^7 @ai-sdk/anthropic@^4 @ai-sdk/react@^4 zod

AI SDK 7 wymaga Node.js 22 lub nowszego i nie obsługuje CommonJS require(). Do nowego projektu produkcyjnego warto wybrać aktualną wersję LTS Node.js i sprawdzić tę samą wersję w środowisku lokalnym, CI oraz na hostingu.

Adaptery domyślnie odczytują klucze z serwerowych zmiennych środowiskowych. Dla OpenAI utwórz lokalny plik .env.local:

Code
OPENAI_API_KEY=sk-proj-...

Nie dodawaj prefiksu NEXT_PUBLIC_, ponieważ udostępniłby sekret kodowi klienckiemu. Plik .env.local nie powinien trafić do repozytorium. Dla Anthropic odpowiednią zmienną jest ANTHROPIC_API_KEY.

Streaming chatbot w Next.js w dwóch plikach

Route Handler jako backend chatbota AI

Code
// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai'
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  safeValidateUIMessages,
  streamText,
  toUIMessageStream,
} from 'ai'
 
export const maxDuration = 30
 
export async function POST(request: Request) {
  const body: unknown = await request.json().catch(() => null)
 
  if (
    typeof body !== 'object' ||
    body === null ||
    !('messages' in body) ||
    !Array.isArray(body.messages) ||
    JSON.stringify(body).length > 100_000
  ) {
    return Response.json(
      { error: 'Nieprawidłowe dane wejściowe' },
      { status: 400 },
    )
  }
 
  const validation = await safeValidateUIMessages({
    messages: body.messages,
  })
 
  if (
    !validation.success ||
    validation.data.length > 50 ||
    validation.data.some((message) => message.role === 'system')
  ) {
    return Response.json(
      { error: 'Nieprawidłowa historia rozmowy' },
      { status: 400 },
    )
  }
 
  const messages = validation.data
  const inputCharacters = messages.reduce(
    (total, message) =>
      total +
      message.parts.reduce(
        (sum, part) => sum + (part.type === 'text' ? part.text.length : 0),
        0,
      ),
    0,
  )
 
  if (inputCharacters > 30_000) {
    return Response.json(
      { error: 'Historia rozmowy jest zbyt długa' },
      { status: 413 },
    )
  }
 
  const result = streamText({
    model: openai('gpt-5-mini'),
    instructions:
      'Jesteś pomocnym asystentem. Odpowiadaj konkretnie i wyłącznie po polsku.',
    messages: await convertToModelMessages(messages),
    maxOutputTokens: 800,
    abortSignal: request.signal,
    timeout: { totalMs: 25_000, chunkMs: 10_000 },
  })
 
  const uiStream = toUIMessageStream({
    stream: result.stream,
    sendReasoning: false,
    onError(error) {
      console.error('Generowanie odpowiedzi nie powiodło się', {
        errorName: error instanceof Error ? error.name : 'UnknownError',
      })
      return 'Nie udało się wygenerować odpowiedzi.'
    },
  })
 
  return createUIMessageStreamResponse({ stream: uiStream })
}

safeValidateUIMessages() sprawdza strukturę wiadomości, ale limit biznesowy nadal należy do aplikacji. W przykładzie ograniczamy rozmiar body, liczbę wiadomości, łączną długość tekstu, długość odpowiedzi i czas oczekiwania na model. Odrzucamy też wiadomości z rolą system, ponieważ instrukcje mogą pochodzić wyłącznie z zaufanego kodu serwerowego. request.signal pozwala przerwać wywołanie po rozłączeniu klienta lub użyciu przycisku Stop. maxDuration ustawia limit funkcji na hostingu, natomiast timeout ogranicza samą operację generowania. Ustawienie sendReasoning: false zapobiega przesyłaniu części reasoning do przeglądarki, jeśli wybrany model je zwraca.

Client Component jako frontend chatbota

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('')
 
  const { messages, sendMessage, status, error, regenerate, stop } = useChat()
  const isLoading = status === 'submitted' || status === 'streaming'
 
  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const text = input.trim()
    if (!text || isLoading) return
 
    void sendMessage({ text })
    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">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`rounded-lg p-4 ${
              message.role === 'user'
                ? 'ml-12 bg-blue-100'
                : 'mr-12 bg-gray-100'
            }`}
          >
            <p className="mb-1 text-sm font-medium">
              {message.role === 'user' ? 'Ty' : 'Asystent'}
            </p>
            {message.parts.map((part, index) =>
              part.type === 'text' ? (
                <p
                  key={`${message.id}-${index}`}
                  className="whitespace-pre-wrap"
                >
                  {part.text}
                </p>
              ) : null,
            )}
          </div>
        ))}
 
        {status === 'submitted' && (
          <div className="mr-12 animate-pulse rounded-lg bg-gray-100 p-4">
            <p className="text-sm text-gray-500">Czekam na odpowiedź...</p>
          </div>
        )}
 
        {error && (
          <div role="alert" className="rounded-lg bg-red-50 p-4 text-red-700">
            <p>Nie udało się pobrać odpowiedzi.</p>
            <button type="button" onClick={() => void regenerate()}>
              Spróbuj ponownie
            </button>
          </div>
        )}
      </div>
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <label htmlFor="chat-input" className="sr-only">
          Wiadomość
        </label>
        <input
          id="chat-input"
          value={input}
          onChange={(event) => setInput(event.target.value)}
          placeholder="Napisz wiadomość..."
          className="flex-1 rounded-lg border px-4 py-3"
          maxLength={4000}
        />
        {isLoading ? (
          <button type="button" onClick={stop}>
            Stop
          </button>
        ) : (
          <button
            type="submit"
            disabled={!input.trim()}
            className="rounded-lg bg-blue-600 px-6 py-3 text-white disabled:opacity-50"
          >
            Wyślij
          </button>
        )}
      </form>
    </div>
  )
}

To cały rdzeń prototypu. SDK opiera interfejs na wiadomościach z tablicą parts, a backend przekształca wynik przez toUIMessageStream() i zwraca Response z createUIMessageStreamResponse(). Komponent obsługuje też błąd, ponowienie odpowiedzi i anulowanie generowania. Odpowiedzi serwera pozostają ogólne, aby nie ujawniać użytkownikowi komunikatów dostawcy ani szczegółów infrastruktury.

Jak działa streaming odpowiedzi w Vercel AI SDK?

streamText() nie czeka na kompletny wynik. Zwraca strumień zdarzeń, a toUIMessageStream() przekształca zdarzenia modelu na protokół UI Message Stream. createUIMessageStreamResponse() koduje go w formacie SSE. Po stronie klienta useChat odbiera fragmenty tekstu i aktualizuje tablicę wiadomości. Fragment nie musi odpowiadać jednemu tokenowi modelu, dlatego określenie „token po tokenie” jest uproszczeniem.

Streaming poprawia czas do pierwszego widocznego fragmentu odpowiedzi, ale nie przyspiesza samego modelu. Po drodze proxy i middleware nie mogą buforować całej odpowiedzi. Jeśli lokalnie widzisz tekst na bieżąco, a po wdrożeniu dopiero na końcu, sprawdź kompresję i buforowanie na warstwie pośredniej.

Zmiana dostawcy modelu AI jedną linią kodu

Code
// app/api/chat/route.ts
import { anthropic } from '@ai-sdk/anthropic'
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
  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'),
    instructions:
      'Jesteś inżynierem Next.js ze znajomością SEO. Odpowiadaj po polsku.',
    messages: await convertToModelMessages(messages),
  })
 
  const uiStream = toUIMessageStream({
    stream: result.stream,
    sendReasoning: false,
  })
  return createUIMessageStreamResponse({ stream: uiStream })
}

W podstawowym czacie reszta kodu pozostaje bez zmian. Przy zmianie modelu sprawdź jednak obsługę obrazów, narzędzi, structured output, limit kontekstu i opcje reasoning. Wspólny interfejs SDK nie wyrównuje różnic między API dostawców.

System prompt dla chatbota w Next.js

Prompt systemowy opisuje rolę modelu, styl odpowiedzi i zakres tematyczny. Dla asystenta biznesowego może wyglądać tak:

Code
const systemPrompt = `Jesteś asystentem StriveLab.
 
Zasady odpowiedzi:
- odpowiadaj wyłącznie po polsku;
- pisz konkretnie i bez zbędnych wstępów;
- odpowiadaj tylko na pytania o inżynierię webową i tworzenie oprogramowania;
- gdy użytkownik pyta o wycenę, skieruj go do formularza kontaktowego.
 
Fakty, z których możesz korzystać:
- StriveLab specjalizuje się w Next.js, technicznym SEO i stronach konwersyjnych;
- biuro znajduje się w Krakowie;
- adres kontaktowy to kontakt@strivelab.pl.
 
Jeśli nie znasz odpowiedzi, powiedz o tym wprost. Nie wymyślaj faktów.`
 
const result = streamText({
  model: openai('gpt-5-mini'),
  instructions: systemPrompt,
  messages: await convertToModelMessages(messages),
})

System prompt wpływa na zachowanie modelu, ale nie stanowi mechanizmu autoryzacji. Użytkownik może próbować go obejść przez prompt injection. Dostęp do danych i możliwość wykonania operacji trzeba kontrolować w kodzie narzędzia oraz na podstawie sesji użytkownika.

useCompletion do generowania tekstu bez historii rozmowy

useCompletion przydaje się tam, gdzie nie potrzebujesz kontekstu rozmowy. Przykładami są jednorazowe tłumaczenie, opis produktu lub szybka sugestia. Hook zarządza jednym promptem i jednym wynikiem:

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>
 
      {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
import { openai } from '@ai-sdk/openai'
import {
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
} from 'ai'
 
export async function POST(request: Request) {
  const body: unknown = await request.json().catch(() => null)
 
  if (
    typeof body !== 'object' ||
    body === null ||
    !('prompt' in body) ||
    typeof body.prompt !== 'string' ||
    body.prompt.length > 4000 ||
    JSON.stringify(body).length > 10_000
  ) {
    return Response.json({ error: 'Nieprawidłowy prompt' }, { status: 400 })
  }
 
  const result = streamText({
    model: openai('gpt-5-mini'),
    instructions:
      'Tworzysz rzeczowe opisy produktów po polsku. Nie dodawaj niepotwierdzonych cech.',
    prompt: `Napisz opis produktu w 2 lub 3 zdaniach na podstawie tych danych:\n\n${body.prompt}`,
    maxOutputTokens: 250,
    abortSignal: request.signal,
  })
 
  const uiStream = toUIMessageStream({
    stream: result.stream,
    sendReasoning: false,
  })
  return createUIMessageStreamResponse({ stream: uiStream })
}

generateText w Server Actions bez streamingu

Gdy potrzebujesz gotowego wyniku do dalszego przetworzenia, generateText zwraca kompletny tekst po zakończeniu wywołania. Dotyczy to na przykład generowania metadanych, tłumaczenia wsadowego i podsumowania:

Code
// actions/ai-actions.ts
'use server'
 
import { openai } from '@ai-sdk/openai'
import { generateText } from 'ai'
 
export async function generateMetaDescription(content: string) {
  const { text } = await generateText({
    model: openai('gpt-5-mini'),
    instructions:
      'Tworzysz precyzyjne opisy stron. Zwracasz wyłącznie gotowy tekst.',
    prompt: `Napisz meta description o długości do 160 znaków na podstawie tego wstępu:\n\n${content.slice(0, 2000)}`,
    maxOutputTokens: 100,
  })
 
  return text
}
 
export async function translateText(text: string, targetLang: string) {
  const { text: translated } = await generateText({
    model: openai('gpt-5-mini'),
    instructions:
      'Tłumacz wiernie. Zwracaj wyłącznie tłumaczenie bez komentarza.',
    prompt: `Język docelowy: ${targetLang}\n\nTekst:\n${text}`,
    maxOutputTokens: 2000,
  })
 
  return translated
}

Server Action nadal jest publicznym punktem wejścia wywoływanym przez klienta. Przed wysłaniem treści do modelu sprawdź sesję, długość danych i dozwolone wartości, na przykład kod języka z zamkniętej listy. Sam typ TypeScript nie waliduje danych w runtime.

Structured output z walidacją Zod

Funkcje generateObject() i streamObject() są przestarzałe. Aktualny interfejs korzysta z generateText() lub streamText() oraz Output.object(). Schemat Zod opisuje oczekiwany kształt i waliduje wynik:

Code
import { openai } from '@ai-sdk/openai'
import { generateText, NoObjectGeneratedError, Output } from 'ai'
import { z } from 'zod'
 
const productSchema = z.object({
  title: z.string().min(3).max(120),
  description: z
    .string()
    .min(20)
    .max(500)
    .describe('Opis oparty wyłącznie na danych wejściowych'),
  tags: z.array(z.string().min(2).max(40)).min(1).max(5),
  category: z.enum(['elektronika', 'dom', 'sport', 'inne']),
})
 
export async function generateProductData(rawDescription: string) {
  try {
    const { output } = await generateText({
      model: openai('gpt-5-mini'),
      output: Output.object({ schema: productSchema }),
      prompt: `Przygotuj dane produktu na podstawie tego opisu:\n\n${rawDescription.slice(0, 4000)}`,
    })
 
    return output
  } catch (error) {
    if (NoObjectGeneratedError.isInstance(error)) {
      throw new Error('Model nie zwrócił poprawnych danych produktu')
    }
 
    throw error
  }
}

Schemat zmniejsza ryzyko błędnego formatu, ale nie potwierdza prawdziwości danych. Wygenerowaną kategorię, cenę czy parametry produktu nadal trzeba porównać z regułami domenowymi przed zapisem do bazy.

Bezpieczeństwo i kontrola kosztów chatbota AI

Walidacja z pierwszego przykładu nie zastępuje autoryzacji i limitów użycia. W wersji produkcyjnej Route Handler powinien co najmniej identyfikować użytkownika, ograniczać częstotliwość żądań i rejestrować faktyczne użycie modelu:

Code
// app/api/chat/route.ts
import { auth } from '@/auth'
import { rateLimiter } from '@/lib/rate-limit'
import { openai } from '@ai-sdk/openai'
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  safeValidateUIMessages,
  streamText,
  toUIMessageStream,
} from 'ai'
 
export async function POST(request: Request) {
  const session = await auth()
  const userId = session?.user?.id
 
  if (!userId) {
    return Response.json({ error: 'Brak autoryzacji' }, { status: 401 })
  }
 
  const { success } = await rateLimiter.limit(`chat:${userId}`)
  if (!success) {
    return Response.json(
      { error: 'Limit zapytań został przekroczony' },
      { status: 429 },
    )
  }
 
  const body: unknown = await request.json().catch(() => null)
  if (
    typeof body !== 'object' ||
    body === null ||
    !('messages' in body) ||
    !Array.isArray(body.messages) ||
    JSON.stringify(body).length > 100_000
  ) {
    return Response.json(
      { error: 'Nieprawidłowe dane wejściowe' },
      { status: 400 },
    )
  }
 
  const validation = await safeValidateUIMessages({
    messages: body.messages,
  })
 
  if (
    !validation.success ||
    validation.data.length > 50 ||
    validation.data.some((message) => message.role === 'system')
  ) {
    return Response.json({ error: 'Nieprawidłowe wiadomości' }, { status: 400 })
  }
 
  const recentMessages = validation.data.slice(-20)
  const inputCharacters = recentMessages.reduce(
    (total, message) =>
      total +
      message.parts.reduce(
        (sum, part) => sum + (part.type === 'text' ? part.text.length : 0),
        0,
      ),
    0,
  )
 
  if (inputCharacters > 30_000) {
    return Response.json({ error: 'Historia jest zbyt długa' }, { status: 413 })
  }
 
  const result = streamText({
    model: openai('gpt-5-mini'),
    messages: await convertToModelMessages(recentMessages),
    maxOutputTokens: 800,
    abortSignal: request.signal,
    timeout: { totalMs: 25_000, chunkMs: 10_000 },
    onEnd({ usage, finishReason }) {
      console.info('Użycie modelu', {
        userId,
        finishReason,
        inputTokens: usage.inputTokens,
        outputTokens: usage.outputTokens,
      })
    },
  })
 
  const uiStream = toUIMessageStream({
    stream: result.stream,
    sendReasoning: false,
    onError(error) {
      console.error('Błąd streamingu odpowiedzi', {
        userId,
        errorName: error instanceof Error ? error.name : 'UnknownError',
      })
      return 'Nie udało się wygenerować odpowiedzi.'
    },
  })
 
  return createUIMessageStreamResponse({ stream: uiStream })
}

W publicznym czacie logowanie nie zawsze jest wymagane, ale limit nadal musi być powiązany z wiarygodnym identyfikatorem, na przykład sesją lub odpowiednio przetworzonym adresem IP. Sam adres IP ma ograniczenia związane z NAT, sieciami mobilnymi i prywatnością. Limit liczby żądań warto uzupełnić dziennym budżetem tokenów na użytkownika lub organizację.

Nie wysyłaj do logów treści promptów ani pełnych odpowiedzi bez świadomej decyzji dotyczącej danych osobowych i retencji. Rejestrowanie liczby tokenów, czasu odpowiedzi, modelu, kodu zakończenia i identyfikatora żądania zwykle wystarcza do kontroli kosztów oraz diagnostyki.

Pamięć rozmowy i zapis wiadomości

useChat zarządza stanem bieżącego komponentu, lecz nie tworzy automatycznie trwałej pamięci. W prostym przykładzie przeglądarka przesyła całą aktualną tablicę wiadomości przy kolejnym żądaniu, a odświeżenie strony usuwa historię z interfejsu.

Jeśli rozmowa ma przetrwać odświeżenie, nadaj jej chatId, zapisz UIMessage[] w bazie i sprawdzaj, czy zalogowany użytkownik jest właścicielem rozmowy. Przy zapisie kompletnej odpowiedzi przydaje się onEnd:

Code
const uiStream = toUIMessageStream({
  stream: result.stream,
  sendReasoning: false,
  originalMessages: messages,
  async onEnd({ messages: completedMessages, isAborted }) {
    if (!isAborted) {
      await saveChat({
        chatId,
        userId,
        messages: completedMessages,
      })
    }
  },
})
 
return createUIMessageStreamResponse({ stream: uiStream })

W produkcji bezpieczniej jest przyjąć od klienta identyfikator rozmowy i nową wiadomość, a wcześniejszą historię odczytać z bazy po stronie serwera. Klient może zmienić przesyłaną tablicę, podszyć się pod wiadomość asystenta albo usunąć wcześniejsze instrukcje. Dane z przeglądarki nigdy nie powinny być autorytatywną historią dla operacji mających skutki biznesowe.

Ograniczanie kontekstu bez utraty sensu rozmowy

messages.slice(-20) jest prostym zabezpieczeniem demonstracyjnym, ale nie kontroluje rzeczywistego rozmiaru kontekstu. Dwadzieścia krótkich pytań i dwadzieścia wiadomości z dużymi załącznikami mają zupełnie inny koszt.

W aplikacji produkcyjnej ustal budżet tokenów dla wejścia, zawsze zachowaj niezbędne instrukcje systemowe i najnowszą część rozmowy, a starsze fragmenty podsumowuj lub pobieraj selektywnie. Limity dostawcy są granicą techniczną. Własny, niższy budżet kontekstu jest granicą kosztową i jakościową.

Czego system prompt nie zabezpiecza

System prompt może ograniczyć styl i zakres odpowiedzi, lecz model nadal jest podatny na prompt injection oraz błędne wnioski. Jeśli dodasz narzędzia do wysyłania wiadomości, odczytu CRM albo wykonywania płatności, ich funkcje execute muszą niezależnie sprawdzać sesję, uprawnienia, dane wejściowe i zakres operacji. Dla działań nieodwracalnych dodaj potwierdzenie użytkownika.

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 otwartoźródłowy zestaw bibliotek TypeScript do budowania funkcji opartych na modelach AI. AI SDK Core ujednolica komunikację z dostawcami modeli, AI SDK UI dostarcza hooki takie jak useChat i useCompletion, a osobne adaptery integrują OpenAI, Anthropic, Google i innych dostawców.

Z jakimi modelami dogaduje się Vercel AI SDK?

SDK ma adaptery między innymi dla OpenAI, Anthropic, Google, Mistral, Cohere, xAI, Groq i Amazon Bedrock. Dostępne są też integracje społecznościowe z modelami lokalnymi. Prosty czat tekstowy zwykle wymaga tylko zmiany adaptera i identyfikatora modelu, ale obsługa narzędzi, reasoning, obrazów oraz structured output różni się między dostawcami i wymaga sprawdzenia macierzy funkcji.

Jak to wygląda kosztowo w utrzymaniu?

Sam SDK jest otwartoźródłowy. Płacisz za użycie modelu oraz infrastrukturę, na której działa Route Handler. Koszt zależy od liczby tokenów wejściowych i wyjściowych, długości przesyłanej historii, użycia reasoning oraz wywołań narzędzi. Nie warto podawać jednej ceny rozmowy, ponieważ cenniki i zachowanie modeli się zmieniają. Mierz rzeczywiste użycie w onEnd, ustaw alerty budżetowe i limity wyjścia.

Kiedy łapać za useChat, a kiedy za useCompletion?

Jeśli Twój cel to klasyczny, konwersacyjny chatbot z pamięcią poprzednich wiadomości, wybierz 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 będzie prostszy. Do danych strukturalnych w AI SDK 7 użyj generateText z Output.object() i schematem Zod. Hook useObject pozostaje przydatny, gdy obiekt ma być przesyłany strumieniowo do interfejsu.

Czy streaming danych od AI będzie śmigał z Edge Runtime?

SDK może działać w środowiskach Node.js, Edge i Cloudflare Workers, ale adapter dostawcy oraz użyte zależności muszą wspierać wybrany runtime. W Next.js nie trzeba wybierać Edge tylko po to, aby strumieniować odpowiedź. Na Vercelu Edge musi zacząć wysyłać odpowiedź w określonym czasie, a maksymalny czas funkcji Node.js zależy od planu i konfiguracji Fluid Compute. maxDuration jest limitem hostingu, nie timeoutem zapytania do modelu.

Jak ochronić takiego chatbota przed trollami i zjadaczami budżetu?

Wymagaj autoryzacji tam, gdzie czat nie jest publiczny, stosuj rate limiting, ogranicz rozmiar body i liczbę wiadomości, ustaw maxOutputTokens oraz timeout. Historię ograniczaj według budżetu tokenów, a nie wyłącznie liczby wiadomości. Klucze API przechowuj tylko po stronie serwera. Jeśli model może uruchamiać narzędzia, każdą operację autoryzuj niezależnie, ponieważ system prompt nie jest granicą bezpieczeństwa.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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.

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

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Serwer MCP w TypeScript: bezpieczny przewodnik dla Next.js

Model Context Protocol standaryzuje sposób, w jaki aplikacja AI odkrywa narzędzia, odczytuje zasoby i korzysta z gotowych promptów. W tym przewodniku zbudujesz lokalny serwer w TypeScript SDK v2, zabezpieczysz operację zapisu i poznasz wymagania zdalnego wdrożenia przez Streamable HTTP.

Maciej Sala

Maciej Sala

Founder StriveLab

RAG w Next.js — budujemy inteligentną bazę wiedzy z AI

Modele językowe pokroju ChatGPT czy Claude potrafią być niesamowicie pożyteczne, ale nie mają pojęcia, co kryje się w Twojej wewnętrznej dokumentacji, bazie wiedzy czy FAQ. Zapytane o szczegóły, których nie znają, zaczynają halucynować . RAG rozwiązuje ten problem: zanim model odpowie, podsuwasz mu właściwe fragmenty Twoich danych i mówisz wprost: „Odpowiadaj na podstawie tego, a nie z pamięci". Ten artykuł pokazuje, jak zbudować taki system w Next.js — krok po kroku, od cięcia tekstu po gotowy chat.

Maciej Sala

Maciej Sala

Founder StriveLab

Generowanie treści z AI w CMS opisy i meta tagi

Wyobraźmy sobie sklep internetowy. Ma on 500 produktów i potrzebuje 500 unikalnych opisów, 500 meta descriptions, 500 alt tekstów do zdjęć i 500 zestawów tagów SEO. Ręczne pisanie brzmi koszmarnie, ale jest możliwość, że AI wygeneruje je w minuty. Jak zrobić, żeby automatyzacja zadziała, nie utracić jakości i by Google je "zaakceptował"?

Maciej Sala

Maciej Sala

Founder StriveLab