Hopp til innhold
Skaarberg Digital
Tilbake til blogg
·8 min lesing·
AIRAGOpenAINext.js

Bygg en RAG-chatbot på én dag

Steg for steg: embeddings med OpenAI, cosine-similarity i minnet og streaming chat i Next.js. Akkurat det vi brukte til AI-assistenten på denne siden.


title: "Bygg en RAG-chatbot på én dag" publishedAt: "2026-05-05" summary: "Steg for steg: embeddings med OpenAI, cosine-similarity i minnet og streaming chat i Next.js. Akkurat det vi brukte til AI-assistenten på denne siden." tags: ["AI", "RAG", "OpenAI", "Next.js"] readingTime: 8

RAG – Retrieval-Augmented Generation – høres akademisk ut, men konseptet er enkelt: gi en LLM tilgang til dine dokumenter, ikke bare treningsdataene. Her er akkurat det vi brukte til AI-assistenten på denne siden.

Hva vi bygger

En chat-assistent som:

  1. Tar spørsmålet ditt
  2. Finner de mest relevante avsnittene fra en kunnskapsbase
  3. Sender dem som kontekst til GPT-4o-mini
  4. Streamer svaret tilbake

Ingen database, ingen vektorDB, bare JSON i minnet. Perfekt for småsider.

Steg 1: Bygg dokumenter

Del opp innholdet ditt i "chunks" – korte, selvstendige avsnitt. Skriv dem i kode slik at de er enkle å holde oppdatert:

// lib/rag/sources.ts
export type Doc = {
  id: string;
  content: string;
};
 
export function buildDocs(): Doc[] {
  return [
    {
      id: "tjenester-tech-health-check",
      content: `Tech Health Check koster 2 490 kr fastpris. 
        Du får en Lighthouse-rapport, tredjeparts-skann og 
        GDPR-vurdering levert innen 3 virkedager.`,
    },
    // ... flere dokumenter
  ];
}

Steg 2: Generer embeddings

Embeddings er vektorer – lister med tall – som representerer meningen av tekst. OpenAIs text-embedding-3-small er billig og god nok.

// scripts/build-embeddings.ts
import OpenAI from "openai";
import { buildDocs } from "../lib/rag/sources.ts";
 
const openai = new OpenAI();
const docs = buildDocs();
 
const embeddings = await Promise.all(
  docs.map(async (doc) => {
    const res = await openai.embeddings.create({
      model: "text-embedding-3-small",
      input: doc.content,
    });
    return {
      id: doc.id,
      content: doc.content,
      vector: res.data[0].embedding,
    };
  })
);
 
// Lagre som JSON – committ til repo
await fs.writeFile(
  "lib/rag/embeddings.json",
  JSON.stringify(embeddings, null, 2)
);

Kjør dette én gang (og på nytt når innholdet endres):

npm run embeddings

Steg 3: Cosine similarity for søk

Når brukeren stiller et spørsmål, embed vi det og finner de k mest like dokumentene:

// lib/rag/search.ts
function cosine(a: number[], b: number[]): number {
  const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
  const magA = Math.sqrt(a.reduce((s, ai) => s + ai * ai, 0));
  const magB = Math.sqrt(b.reduce((s, bi) => s + bi * bi, 0));
  return dot / (magA * magB);
}
 
export function topK(queryVec: number[], k = 4) {
  return KNOWLEDGE_BASE
    .map((doc) => ({ ...doc, score: cosine(queryVec, doc.vector) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, k);
}

Steg 4: Streaming chat-route

// app/api/chat/route.ts
export async function POST(req: Request) {
  const { messages } = await req.json();
  const query = messages.at(-1)?.content ?? "";
 
  // Embed spørsmålet
  const { data } = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: query,
  });
  const context = topK(data[0].embedding)
    .map((d) => d.content)
    .join("\n\n");
 
  // Send til LLM med kontekst
  const stream = openai.beta.chat.completions.stream({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: `Kontekst:\n${context}` },
      ...messages,
    ],
  });
 
  return stream.toReadableStream();
}

Resultatet

Med 17 dokumenter og text-embedding-3-small koster embedding-steget ca. $0.0003 per brukermelding. Vanlig GPT-4o-mini chat koster ca. $0.002–0.005. Totalkost per samtale: neglisjerbar.

Og du slipper en ekstern vektordatabase. For en nettside med opptil noen hundre dokumenter er in-memory JSON mer enn godt nok.

Kompleksitet er en kostnad. Betaler du for den med et resultat du faktisk trenger?

Se kildekoden på GitHub for den fulle implementasjonen.

Nysgjerrig på et prosjekt?

Vil du diskutere noe fra artikkelen, eller har du et prosjekt du vil bygge?

Ta kontakt