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

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

ł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 — 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

Jakie narzędzia wziąć

  • 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: cięcie i obróbka danych

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

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 w Supabase

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

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 ze streamingiem

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: front — interfejs chatu

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.

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.

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.

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.

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.

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.

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 — 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