Przejdź do treści

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

Twoje dane + LLM = baza wiedzy, która odpowiada na pytania. Jak zbudować RAG w Next.js z pgvector i wyszukiwaniem semantycznym?

Maciej Sala

Founder StriveLab

4 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 31 maja 2026)

łata największą bolączkę potężnych modeli językowych: nie znają Twojej własnej bazy wiedzy. Zapytany o szczegół z firmowej dokumentacji, model bez kontekstu zacznie zgadywać. RAG zamienia to w kontrolowany proces — najpierw szukasz właściwego fragmentu, potem każesz modelowi odpowiedzieć wyłącznie na jego podstawie.

Zanim przejdziemy do implementacji — warto wiedzieć, że RAG nie zawsze wymaga pisania kodu. Google NotebookLM to gotowy produkt zbudowany na tej samej zasadzie: wgrywasz dokumenty, a przy każdym pytaniu system sam odnajduje odpowiednie fragmenty i podaje je modelowi jako kontekst. Różnica polega na tym, że w NotebookLM nie masz wglądu w to, jak działa ten proces — nie możesz zmienić sposobu cięcia tekstu, wybrać innego modelu ani wpiąć tego w swoją aplikację. Ten artykuł jest dla sytuacji, gdy potrzebujesz RAG wewnątrz własnego produktu, a nie jako osobne narzędzie.

  • Tnij długie teksty na fragmenty po ok. 400–800 tokenów.
  • Zamień fragmenty na (np. modelem OpenAI).

  • Zapisz wektory w bazie danych (np. pgvector na start).

  • Przy zapytaniu użytkownika wyszukaj najbliższe fragmenty.

  • Sklej znalezione fragmenty w prompt z pytaniem i podaj taniemu LLM (np. Claude Haiku 4.5).

  • Streamuj odpowiedź do klienta przez Vercel AI SDK.

Architektura RAG w Next.js: jak płynie zapytanie?

Code
Użytkownik pyta: "Jak zresetować hasło w firmowym systemie?"

1. Zapytanie zamieniamy na wektor (embedding) przez API OpenAI — ciąg 1536 liczb

2. Szukamy w bazie najbliższych fragmentów (np. wpis 'reset-hasla.mdx')

3. Doklejamy znalezione fragmenty jako kontekst do promptu i wysyłamy do LLM

4. Model odpowiada na podstawie kontekstu i streamuje wynik do chatu

Stack RAG w Next.js: jakie narzędzia wybrać?

  • Next.js — front i backend ( routes) w jednym projekcie.
  • Vercel AI SDK — spoiwo do strumieniowania odpowiedzi do przeglądarki, bez ręcznego zarządzania połączeniem.
  • OpenAI Embeddings — tanie i skuteczne generowanie wektorów z tekstu.
  • Baza wektorowa — Supabase z pgvector na MVP; Upstash Vector albo Pinecone przy większym ruchu produkcyjnym.
  • Zod — walidacja i typowanie danych wejściowych.

Etap 1: chunking i obróbka dokumentów dla RAG

Długich artykułów nie wciśniesz w całości do prompt, dlatego tekst tniemy na mniejsze fragmenty (chunki):

Code
// lib/rag/chunker.ts
interface Chunk {
  content: string
  metadata: {
    source: string
    title: string
    chunkIndex: number
  }
}
 
export function chunkText(
  text: string,
  source: string,
  title: string,
  chunkSize: number = 500, // domyślnie ok. 500 słów na fragment,
  overlap: number = 100, // nachodzenie fragmentów, by nie ucinać kontekstu w pół zdania,
): Chunk[] {
  const words = text.split(/\s+/)
  const chunks: Chunk[] = []
  let i = 0
  let chunkIndex = 0
 
  while (i < words.length) {
    const chunk = words.slice(i, i + chunkSize).join(' ')
    chunks.push({
      content: chunk,
      metadata: { source, title, chunkIndex },
    })
    i += chunkSize - overlap
    chunkIndex++
  }
 
  return chunks
}

Etap 2: generowanie embeddingów w Next.js

Code
// lib/rag/embeddings.ts
import { openai } from '@ai-sdk/openai'
import { embed, embedMany } from 'ai'
 
export async function generateEmbedding(text: string): Promise<number[]> {
  const { embedding } = await embed({
    model: openai.embedding('text-embedding-3-small'), // tani model w zupełności wystarcza do bazy wiedzy
    value: text,
  })
  return embedding
}
 
export async function generateEmbeddings(texts: string[]): Promise<number[][]> {
  const { embeddings } = await embedMany({
    model: openai.embedding('text-embedding-3-small'),
    values: texts,
  })
  return embeddings
}

Etap 3: baza wektorowa Supabase pgvector

Baza musi umieć przechowywać i przeszukiwać wektory. Supabase robi to przez rozszerzenie vector (pgvector):

Code
-- Uruchom w edytorze SQL w Supabase:
create extension if not exists vector;
 
create table documents (
  id bigserial primary key,
  content text not null,
  metadata jsonb,
  embedding vector(1536) -- wymiar zgodny z text-embedding-3-small
);
 
-- Indeks ivfflat przyspiesza wyszukiwanie po podobieństwie
create index on documents using ivfflat (embedding vector_cosine_ops)
  with (lists = 100);
 
-- Funkcja zwracająca najbliższe fragmenty dla zapytania
create or replace function match_documents(
  query_embedding vector(1536),
  match_threshold float default 0.7,
  match_count int default 5
)
returns table (
  id bigint,
  content text,
  metadata jsonb,
  similarity float
)
language sql stable
as $$
  select
    id,
    content,
    metadata,
    1 - (embedding <=> query_embedding) as similarity
  from documents
  where 1 - (embedding <=> query_embedding) > match_threshold
  order by embedding <=> query_embedding
  limit match_count;
$$;

Etap 4: indeksowanie dokumentów w bazie wiedzy RAG

Skrypt, który raz przepuszcza Twoje treści przez chunker i embeddingi, a wynik zapisuje do bazy:

Code
// scripts/index-documents.ts — uruchamiasz raz, by zasilić bazę
import { chunkText } from '@/lib/rag/chunker'
import { generateEmbeddings } from '@/lib/rag/embeddings'
import { createClient } from '@supabase/supabase-js'
import fs from 'fs'
import path from 'path'
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
)
 
async function indexDocuments() {
  // Bierzemy gotowe artykuły .mdx z dysku
  const articlesDir = path.join(process.cwd(), 'content/blog')
  const files = fs.readdirSync(articlesDir).filter((f) => f.endsWith('.mdx'))
 
  for (const file of files) {
    const content = fs.readFileSync(path.join(articlesDir, file), 'utf-8')
    const title = file.replace('.mdx', '')
 
    // Tniemy na fragmenty
    const chunks = chunkText(content, file, title)
    console.log(`Plik ${file}: ${chunks.length} fragmentów.`)
 
    // Generujemy embeddingi dla wszystkich fragmentów naraz
    const embeddings = await generateEmbeddings(chunks.map((c) => c.content))
 
    // Zapisujemy do bazy
    const records = chunks.map((chunk, i) => ({
      content: chunk.content,
      metadata: chunk.metadata,
      embedding: embeddings[i],
    }))
 
    const { error } = await supabase.from('documents').insert(records)
    if (error) console.error(`Błąd indeksowania pliku ${file}:`, error)
  }
 
  console.log('Baza wektorowa gotowa.')
}
 
indexDocuments()

Etap 5: endpoint API RAG ze streamingiem odpowiedzi

Główny kontroler: przyjmuje pytanie, znajduje kontekst i streamuje odpowiedź modelu:

Code
// app/api/rag/route.ts
import { openai } from '@ai-sdk/openai'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
import { generateEmbedding } from '@/lib/rag/embeddings'
import { createClient } from '@supabase/supabase-js'
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
)
 
export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json()
  const lastMessage = messages.at(-1)
  const lastMessageText =
    lastMessage?.parts
      .filter((part) => part.type === 'text')
      .map((part) => part.text)
      .join('\n') ?? ''
 
  if (!lastMessageText.trim()) {
    return Response.json({ error: 'Puste zapytanie.' }, { status: 400 })
  }
 
  // 1. Zamieniamy pytanie użytkownika na wektor
  const queryEmbedding = await generateEmbedding(lastMessageText)
 
  // 2. Szukamy najbliższych fragmentów w bazie
  const { data: documents } = await supabase.rpc('match_documents', {
    query_embedding: queryEmbedding,
    match_threshold: 0.7,
    match_count: 5,
  })
 
  // 3. Sklejamy znalezione fragmenty w kontekst
  const context = documents
    ?.map((doc: any) => `[Źródło: ${doc.metadata.title}]\n${doc.content}`)
    .join('\n\n---\n\n')
 
  // 4. Każemy modelowi odpowiedzieć wyłącznie na podstawie kontekstu
  const result = streamText({
    model: openai('gpt-4o'),
    system: `Jesteś asystentem bazy wiedzy StriveLab. Odpowiadaj wyłącznie na podstawie kontekstu poniżej. Jeśli odpowiedzi nie ma w kontekście, napisz wprost, że nie znalazłeś informacji. Nie zmyślaj.
 
KONTEKST:
${context || 'Brak pasującego kontekstu w bazie.'}`,
    messages: await convertToModelMessages(messages),
  })
 
  return result.toUIMessageStreamResponse()
}

Etap 6: frontend chatu dla bazy wiedzy RAG

Code
'use client'
 
import { useState } from 'react'
import { DefaultChatTransport } from 'ai'
import { useChat } from '@ai-sdk/react'
 
export default function KnowledgeBase() {
  const [input, setInput] = useState('')
 
  // Vercel AI SDK obsługuje połączenie z endpointem RAG
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: '/api/rag' }),
  })
  const isLoading = status === 'submitted' || status === 'streaming'
 
  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    if (!input.trim()) return
 
    sendMessage({ text: input })
    setInput('')
  }
 
  return (
    <div className="mx-auto max-w-2xl p-6">
      <h1 className="mb-2 text-2xl font-bold">
        Zapytaj asystenta bazy wiedzy StriveLab
      </h1>
      <p className="mb-6 text-gray-500">
        Pytaj o technologie, SEO i usługi opisane na stronie.
      </p>
 
      <div className="mb-6 min-h-[200px] space-y-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={`rounded-lg p-4 ${
              m.role === 'user' ? 'ml-8 bg-blue-50' : 'mr-8 bg-gray-50'
            }`}
          >
            {m.parts.map((part, index) =>
              part.type === 'text' ? (
                <p key={`${m.id}-${index}`} className="whitespace-pre-wrap">
                  {part.text}
                </p>
              ) : null,
            )}
          </div>
        ))}
      </div>
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={(event) => setInput(event.target.value)}
          placeholder="O czym jest ta strona w kontekście SEO?"
          className="flex-1 rounded-lg border px-4 py-3"
        />
        <button
          type="submit"
          disabled={isLoading}
          className="rounded-lg bg-blue-600 px-6 py-3 text-white disabled:opacity-50"
        >
          Zapytaj
        </button>
      </form>
    </div>
  )
}
Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.
Automatyzacja AI

Często zadawane pytania

Czym jest RAG (Retrieval-Augmented Generation)?

RAG to architektura aplikacji, która ogranicza halucynacje modelu językowego. Zanim system odpowie na pytanie, najpierw przeszukuje wektorową bazę danych w poszukiwaniu najtrafniejszych fragmentów Twojej dokumentacji, artykułów albo FAQ, a dopiero potem przekazuje je modelowi jako kontekst. Dzięki temu LLM odpowiada na podstawie Twojej wiedzy firmowej, bez kosztownego i powolnego fine-tuningu.

RAG czy fine-tuning — co wybrać?

Jeśli Twoje dane stale się zmieniają (piszesz bloga, dodajesz FAQ, rozbudowujesz bazę wiedzy), wybierz RAG. Możesz w każdej chwili dodać lub usunąć dokument, a zmiana jest widoczna od razu. Fine-tuning zostaw na sytuacje, w których modelowi brakuje wrodzonej wiedzy — specyficznego słownictwa branżowego albo tonu marki. RAG jest tańszy, prostszy we wdrożeniu i transparentny: widzisz, z jakiego źródła pochodzi odpowiedź. W 2026 to RAG jest podstawą, a fine-tuning dodatkiem.

Na jakim stacku postawić RAG w Next.js w 2026?

Trzy poziomy. (1) Budżetowe MVP: Next.js na froncie, Vercel AI SDK, tanie OpenAI Embeddings (text-embedding-3-small) i wektorowa baza Supabase (pgvector) — koszt od zera do ~50 USD/mies. (2) Produkcja: zamieniasz bazę na Pinecone albo Upstash Vector i opcjonalnie dokładasz Cohere Rerank dla trafniejszych wyników. (3) Enterprise: wyszukiwanie hybrydowe (BM25 + wektory), własne algorytmy chunkowania, pipeline ewaluacji (np. Ragas) i monitoring (np. LangSmith). Zacznij od MVP i skaluj w miarę potrzeb.

Ile kosztuje utrzymanie RAG?

Trzy składniki. (1) Embeddingi — grosze: text-embedding-3-small kosztuje ok. 0,02 USD za milion tokenów. (2) Wektorowa baza — Supabase i Pinecone mają darmowe tiery wystarczające na start. (3) Model odpowiadający (główny koszt) — średnie użycie Claude Haiku 4.5 albo GPT-4o-mini to od ułamków centa do centa za zapytanie z kontekstem. Przy ruchu rzędu 10 tys. pytań miesięcznie zapłacisz orientacyjnie 10–100 USD.

Jakiej wielkości chunki (fragmenty tekstu) działają najlepiej?

Złoty środek to zwykle 200–800 tokenów. Krótsze (200–400) dają wyższą precyzję wyszukiwania, ale gubią szerszy kontekst. Dłuższe (do 1000) lepiej oddają wywód, ale trudniej je trafnie dopasować do zapytania. Dla dokumentacji i FAQ sprawdza się przedział 400–600 tokenów, z overlapem (nachodzeniem końcówek fragmentów) rzędu 50 tokenów. Warto eksperymentować na własnych danych.

Kiedy odświeżać i przeindeksowywać wektory w bazie?

Najlepiej po każdej istotnej zmianie treści źródłowej. Dwie taktyki. (1) Webhook — gdy zapisujesz artykuł w CMS (np. Sanity), zapis wyzwala ponowne wygenerowanie wektora przez Server Action; to najefektywniejszy tryb. (2) Pełna przebudowa (cron) — np. raz dziennie regenerujesz embeddingi dla wszystkich dokumentów; kosztuje więcej tokenów, ale ratuje przy niespójnej bazie. W praktyce webhook do bieżących zmian plus okresowy cron jako siatka bezpieczeństwa.

Czym są embeddingi i dlaczego są kluczowe w RAG?

Embedding to zakodowanie tekstu jako wektora liczb (np. 1536 wymiarów), który reprezentuje znaczenie semantyczne. Teksty o podobnym sensie mają wektory blisko siebie w przestrzeni. Dzięki temu pytanie „Jak zresetować hasło?" matematycznie zbliża się do fragmentu dokumentacji „Proces resetowania poświadczeń w firmie", nawet jeśli nie mają wspólnych słów. Zwykły Ctrl+F nigdy by tego nie znalazł — embeddingi szukają po znaczeniu, nie po dosłownym dopasowaniu.

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
AI Search w Next.js: Wyszukiwanie semantyczne z Upstash Vector

Tradycyjne wyszukiwanie LIKE, full-text search szuka dokładnych słów. Użytkownik wpisuje „jak przyspieszyć stronę", nie znajdzie artykułu pt. „Optymalizacja Core Web Vitals", ponieważ nie ma wspólnych słów kluczowych. W tym artykule budujemy wyszukiwanie semantyczne w Next.js: zamieniamy treść na embeddingi, wrzucamy je do Upstash Vector i pokazujemy, jak aplikacja dopasowuje wyniki po znaczeniu zapytania, a nie po dosłownych słowach.

Maciej Sala

Maciej Sala

Founder StriveLab

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

Vercel AI SDK zdejmuje z Twoich barków większość żmudnej roboty przy dodawaniu AI do aplikacji w Next.js. Zapomnij o ręcznym czytaniu i zarządzaniu strumieniem bajtów z API, parsowaniu chunków czy doklejaniu poprzednich wypowiedzi do historii rozmowy. W tym tutorialu zbudujesz działającego, streamującego chatbota w mniej więcej 30 minut. Potrzebujesz dwóch plików aplikacji, konfiguracji dostawcy i klucza API.

Maciej Sala

Maciej Sala

Founder StriveLab

Integracja Supabase z Next.js App Router

Supabase to otwartoźródłowa platforma backendowa zbudowana wokół PostgreSQL . W jednym projekcie dostajesz bazę danych, Auth, Storage, Realtime, Edge Functions i automatycznie generowane API. Dla aplikacji w Next.js to często najkrótsza droga do sensownego MVP bez osobnego backendu pisanego od zera.

Maciej Sala

Maciej Sala

Founder StriveLab