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:
- Tar spørsmålet ditt
- Finner de mest relevante avsnittene fra en kunnskapsbase
- Sender dem som kontekst til GPT-4o-mini
- 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 embeddingsSteg 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?