Skip to content
Skaarberg Digital
Back to blog
·8 min read·
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.

Interested in a project?

Want to discuss something from the article, or have a project you'd like to build?

Get in touch