RAG w Next.js — budujemy inteligentną bazę wiedzy z AI
Retrieval-Augmented Generation (RAG) w Next.js w 2026 — jak połączyć własne dane z LLM? Embeddingi, wektorowa baza danych (pgvector, Pinecone), wyszukiwanie semantyczne i odpowiedzi AI z kontekstem.
Do czego właściwie uderza to całe mityczne i szeroko rzucane po IT pojęcie RAG?
Retrieval-Augmented Generation (RAG) to architektura aplikacji AI, w której LLM dostaje kontekst pobrany z zewnętrznej bazy wiedzy (wektorowej) zamiast polegać tylko na wiedzy z pretrainingu. Ogranicza halucynacje, pozwala dodać aktualne i prywatne dane bez fine-tuningu. łata największą i najbardziej irytującą bolączkę znanych, przepotężnych
modeli u twórców narzędzi pokroju od chociażby ChatGPT czy z obozu Anthropic. Są
niesamowicie mądre, ale niestety nie mają krzty pojęcia, co się dzieje za
zamkniętymi i niedostępnymi podstronami u Twoich firmowych dokumentacji bazy!
Zespół stworzył nowatorskie repo z setką funkcji o wpisy FAQ? Owy model by to
pojąć zaczął po prostu na pytania klientów bajdurzyć bez oparcia (halucynować)!
Tu do pracy ruszył twardo wariant od RAG. To jak wyciągniecie grubej i potężnej
instrukcji na stół za krawat od inżynierów i powiedzenie wprost — "Patrz tylko
tu zanim wylejesz do klienta informację bez weryfikacji!".
Rezultat nie pozwala zaistnieć u klienta na rozczarowania przy okienkach na Twoim zintegrowanym narzędziu do obsługi e-sklepu u bazy.
Krótko i żołnierskim sznytem z wiedzy: RAG przy aplikacjach z ekosystemów od Next.js to sześć mocnych przystanków od stacji z produkcją po opublikowanie! Na pętli z wiedzy (1) ciachasz wielkie poradniki na plasterki u wytycznych do wielkości tokenów od ok. 400 do max blisko z okolic od 800, na starcie potem je (2) wrzucasz do potężnych walców od paczek w algorytmach od uderzeń w Embeddingi to wektory liczbowe reprezentujące znaczenie semantyczne tekstu — teksty o podobnym znaczeniu mają wektory blisko siebie w przestrzeni wielowymiarowej. u np u baz uderzeniowych z OpenAi, na wyjściu z kodu lądują uciekając w system bazy danych np we wdrożeniach darmowych pgvector pod opcję np dla MVP by potem za startować pod (4) zapytaniem wyjściowym, co wyciąga je z systemu u zapytania a od strony z (5) zbija w całość prompt po zapytaniu pod ujęciu dla np "Taniego LLM z Claude-haiku z rodziny 4.5" na twardo wysyłając klientom jako gotowe pule pod streaming przez pakiety od "Vercela" w zarysie od kroku (6).
Mapa, jak przepływa Twój pomysł od zaciągania zapytań (Architektura RAG)
Code
Twoi użytkownicy wysyłają uderzenie np "Co robi opcja za reset w firmie u hasła?" ↓1. Rzucasz do maszyn od OpenAI: (Weź uderz i spłaszcz zapytanie o ciąg 1536 liczb wektora) ↓2. Pukamy do bazy (Znalazłem po bliskości z matematycznego odczytu: Uderza pod kątem wpisu oznaczonym 'logowanie_w_systemie_na_koncie_do_resetowania.mdx') ↓3. Łączymy "Odpowiedź na pytania z dokumentacji z wsadzonym u wejścia wektorem i wrzucamy LLM" ↓4. Otrzymujesz poprawną i uśmiechniętą w ekran odpowiedź pod chat u klienta: AI: "Jasne, rzucam u wydań na zapleczu by użyć instrukcji o resetowaniu..."!
Jaki zestaw narzędzi wziąć do piaskownicy w web
Twoje królestwo Next.js — Za odcięciem opcji o backend, ujęciu API z bazą Frontu
Vercel AI SDK — Składny pakiet o strumieniowy streamingu ze starymi gotowcami (bez szarpania się od ręki z SSE)
Złożenia OpenAI Embeddings — Genialny strzał na portfel o uderzeniach z wektoryzacji z tekstów
Platforma pod wektory Supabase we wdrożeniu pgvector za MVP, na ostro weź i Upstash Vector od opcji Pinecone dla ostrych na setki z tysiącach żądań w produkcji
Pilnowanie ucięć i typowań w zarysie - Od Zod na obronie
Pierwsza stacja: Jak siekać i obrabiać dane w procesach
Grube artykuły to przeszkoda, nie wciśniesz 30 tysięcy słów na jedno wdrożenie we frazach modelu, nie płacąc potężnego majątku za zżeranie w tokenach. Ciachaj zarysy inteligentnie pod porcje:
Code
// Z bazy pod lib/rag/chunker.ts rzucamy opcjęinterface Chunk { content: string metadata: { source: string title: string chunkIndex: number }}export function chunkText( text: string, source: string, title: string, chunkSize: number = 500, // Uderza u góry pod puszczenie od ok 500 overlap: number = 100, // Na zakładki, by puszczać po 100 (nie odcięło np kontekst od ujęć z myśli dla zdania pomiędzy by powiązało je do wyszukiwania!)): 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}
Druga stacja: Czas uderzyć na zarysy pod wektory do modelu!
Code
// Uderza z lib/rag/embeddings.tsimport { 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'), // Pakujesz się i ładujesz tu bezczelnie małą taniochę, bo to "wiedza u zarysu dla tekstu dla firmy u asortymentu w wektory" - u wystarcza w zupełnie u wdrożeń we firmowych! 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}
Trzeci kocioł z bazą, by zamienić cyfry dla silnika z wsadów w wejściowym i wyciąganym oparciu!
Baza ma u Ciebie w systemie potrafić czytać dziwactwa. Supabase rozwiązuje to z opcji pod rozszerzeniem na platformy w darmowych ujęciach z "vector"!
Code
-- Rzucaj na edytor Supabase na pulpicie u SQL:create extension if not exists vector;create table documents ( id bigserial primary key, content text not null, metadata jsonb, embedding vector(1536) -- Na te śliczne wymiary od paczki u małych np w "text-embedding-3");-- Pod wydajności odczytowe wrzuć na tło potężnego "ivfflat"!create index on documents using ivfflat (embedding vector_cosine_ops) with (lists = 100);-- Co to u góry narzuciłeś to jedno, z wejścia za wsadami wpisz sztywną funkcję by zrzucić bliskości w matematycznych uderzeniach przy np rzutniku w od zapytaniach!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 stableas $$ 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;$$;
Czwarta paczka z odlewami do wysyłki (Proces z indeksowania na plik)
Przesypmy "fizyczne" informacje dla systemów z chmury:
Code
// Twój proces uruchomiony z scripts/index-documents.ts u zaplecza by rzucić bazy przed wdrożeniemimport { 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() { // Chwytaj np gotowe rzuty o artykułach na dyskach po .MDX 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', '') // Siekamy i kroimy! const chunks = chunkText(content, file, title) console.log( `Bierzemy plik pod ${file}: ułamek wyszedł pod ucięcie z ${chunks.length} kawałków tekstowych.`, ) // Palimy z uderzeń pod ujęcie o API u modelu pod ujęcia o embedding const embeddings = await generateEmbeddings(chunks.map((c) => c.content)) // Wysyłamy i wciskamy do bazy od chłopaków z Supabase z przygotowanego zestawienia wektorów 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( `Oj! Masz błąd u indeksowań na paczkach przy błędem: ${file}:`, error, ) } console.log('Spakowane na wektorową wnękę! Baza dziękuje ze odczyty!')}indexDocuments()
Przepiękny most pod API u Twojego RAG w "Streamingu na widoki"
Zmontujmy "centralny proces ujęty z uderzeń i odbić przy zapytaniu za front-end", czyli nasz główny kontroler:
Code
// W paczce lądują u app/api/rag/route.ts dla serwera!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: 'Hola! Pusto rzucone z konsol.' }, { status: 400 }, ) } // ETAP 1. Z wejścia ładujemy wpisane po "chat" na matematycznie rzucony pod płaszczyzny dla modelu u wektor! const queryEmbedding = await generateEmbedding(lastMessageText) // ETAP 2. Strzał od funkcji Supabase! Pod rzut w poszukiwania co dla modelu w tekstach u wektorów masz ukryte do rzutów! const { data: documents } = await supabase.rpc('match_documents', { query_embedding: queryEmbedding, match_threshold: 0.7, match_count: 5, }) // ETAP 3. Klejenie wyników do odrzuconych z uderzeń o potężny bufor po tekstach z dokumentu const context = documents ?.map( (doc: any) => `[Wskazane Źródło: ${doc.metadata.title}]\n${doc.content}`, ) .join('\n\n---\n\n') // ETAP 4. "Hej Model, Odpowiedz, nie zmyślając!" const result = streamText({ model: openai('gpt-4o'), system: `Oto RAG! Ty jesteś z opcji dla asystenta pod odciecia do pytań od firmy ze StriveLab. Na bazie z opcji na podstawie rzutów na sztywny wektor pod wejściem niżej od kontekstach - odpowiedz na pytanie i zachowuj opcje! Jak nic tu u podstaw dla wektorach np do tekstach niżej nie wpakowano – wywal by rzucił, że opcji nie znalazł za wsadami. Zero bajkopisarstwa!Z rzutowanego pola KONTEKSTOWEGO U BAZIE:${context || 'Niestety na twardo bazy tu pod wsad nie podały.'}`, messages: await convertToModelMessages(messages), }) return result.toUIMessageStreamResponse()}
No i front ze ślicznej architektury na "chat":
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('') // Wrzucasz standard od Vercela bez ceregieli z puszczeń po API na endpoint do Twojego 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 u mądrego asystenta bazy StriveLab </h1> <p className="mb-6 text-gray-500"> Pytaj od technologii pod SEO, o ujęć dla usług firmy na podstronach. </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 powiada paczka dla Next.js np na pozycjonowaniach 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" > Sprawdź u Maszyny </button> </form> </div> )}
Dwa rzuty do zapamiętania, o RAG
By zbudować zgrabnie i wejść do grona systemów firm bez pustych bajkopisarzy z modeli – po prostu tnij u wejścia wektorów, przepuść u chmur (lub we wdrożeniowych na małych od pgvector) a odpowiadaj pod kątem rzucanej od "Prompt-ów ze sztucznym doklejonym kontekstem u bazy z RAG"! Sztywna od Vercel paczka o integracji u rzutów za opcją AI SDK pozwala to wykręcić dla średniej z paczki rzędu pod budżet bez uderzeń i skoków dla tysięcznych rachunków by nie wdrożyć fine-tuning! A Twoje narzędzia dostają opcje u rzutowaniu z profesjonalnych rzutów pod gigabajtów darmowej, sztywnej u bazy po "Firmowym Know-how".
Często zadawane pytania
Pracuję z tym zawodowo.
Jeśli chcesz dobrać narzędzia AI do realnego workflow zespołu, uporządkować pracę z modelami w produkcie albo połączyć AI z frontendem, analityką i procesem produktowym, skontaktuj się ze mną. Pracuję hands-on z React, Next.js i nowoczesnymi narzędziami AI, a ten temat wdrażam nie tylko teoretycznie.
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.
React Compiler stabilny od 2025. Pokazuję, kiedy ręczna memoizacja traci sens, kiedy nadal ma znaczenie i jak realnie wdrożyć Compiler w istniejącym projekcie React.
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.
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.