Files
novi-lessons/Les14-RAG-Embeddings/Les14-Slide-Overzicht.md
2026-06-07 10:44:05 +02:00

13 KiB
Raw Blame History

Les 14 — RAG + Embeddings

Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)

Lesvorm: Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk. Demo-app: Nieuwe PDF Q&A app from scratch Vervolg op: Les 13 — Agents Aansluit op: Les 15 — Agents


Slide 1: Title

Les 14 — RAG + Embeddings

Visual:

  • Background: CREAM
  • "Les 14" in BLUE
  • "RAG + Embeddings" in BLACK
  • Subtitle: "AI laten antwoorden op basis van jouw eigen documenten"

Slide 2: Terugblik

Waar staan we?

Vorige lessen:

  • Les 12: Tool Calling — AI kiest welke functie
  • Les 13: Agents — autonoom 20-50 stappen
  • Les 14: Externe APIs + Vercel deploy

Het probleem dat we nu oplossen:

AI weet veel, maar weet NIET wat in jouw documenten staat:

  • Interne kennisbank
  • Klantcontracten
  • Productdocumentatie
  • 200-pagina PDF die vandaag binnenkwam

Twee opties:

  • Alle 200 pagina's elke vraag meesturen — niet schaalbaar
  • RAG — alleen de relevante stukjes ophalen per vraag

Visual: Pijl van "alle docs context" naar "RAG → top 5 chunks".


Slide 3: Planning

Vandaag — 180 minuten

Onderwerp Duur
Welkom + Terugblik 10 min
Theorie: Wat is een embedding? 20 min
Theorie: RAG pipeline (index + query) 15 min
Theorie: pgvector + chunking 15 min
Live Demo 1 — pgvector setup + embed pipeline 25 min
Live Demo 2 — PDF upload + index 20 min
Pauze 15 min
Live Demo 3 — Query pipeline + AI antwoord 20 min
Live Demo 4 — RAG-tool in een agent 15 min
Wanneer RAG wel/niet 10 min
Lesopdracht + Huiswerk 10 min
Vragen + Afsluiting 5 min

Slide 4: Wat is een embedding?

Tekst als een punt in de ruimte

Het idee:

  • Stop een stuk tekst in een embedding-model
  • Krijg een vector terug — array van ~1536 nummers
  • Vergelijkbare betekenissen = vergelijkbare vectors
"De kat zit op de mat"      → [0.12, -0.45, 0.88, ...]
"Een poes ligt op het tapijt" → [0.14, -0.41, 0.85, ...]
"Voetbal in Nederland"        → [-0.73, 0.21, -0.32, ...]

Eerste twee zijn dichtbij in vector-space. Derde is ver weg. Niet omdat woorden overlappen — omdat betekenis vergelijkbaar is.

Welk model:

  • OpenAI text-embedding-3-small — 1536 dim, snel + goedkoop
  • OpenAI text-embedding-3-large — 3072 dim, beter maar duurder
  • Open-source: nomic-embed-text, mxbai-embed-large

Visual: 2D plot met 3 punten — twee dichtbij, één ver weg.


Slide 5: Vector similarity

Hoe meet je 'dichtbij'?

Drie metrics:

Metric Wat Wanneer
Cosine similarity Hoek tussen vectors (1 tot 1) Default — werkt op semantic search
Dot product Sum van producten Sneller — als vectors genormaliseerd zijn
Euclidean distance Afstand in ruimte Zelden voor text — meer voor images

Cosine similarity = 1 → exact gelijke betekenis Cosine similarity = 0 → ongerelateerd Cosine similarity = -1 → tegengesteld

-- In pgvector
SELECT * FROM chunks
ORDER BY embedding <=> '[0.12, -0.45, ...]'::vector
LIMIT 5;

<=> is cosine distance. Sorteren op <=> = dichtstbijzijnde eerst.

Visual: Drie vectors met hoeken — kleine hoek = similar.


Slide 6: RAG pipeline

Index time vs query time

Index time (eenmalig of bij upload):

PDF / docs
  ↓
Parse + chunk (500 tokens per chunk)
  ↓
Embed elke chunk
  ↓
Store: chunk_text + embedding in pgvector

Query time (elke vraag):

User vraag
  ↓
Embed vraag
  ↓
Similarity search → top 5 chunks
  ↓
Geef chunks als context aan LLM
  ↓
LLM antwoordt op basis van context

Belangrijkste inzicht: AI ziet nooit de hele DB. Alleen 5 relevante chunks per vraag.

Visual: Twee parallelle flow-diagrammen — index vs query.


Slide 7: pgvector

Postgres met vector-support

Wat is het:

  • Postgres extension — voegt vector datatype toe
  • Cosine, euclidean, dot product operators
  • Schaalbaar tot ~1M+ vectors per tabel
  • Ingebouwd in Supabase — één click activeren

Schema voorbeeld:

create extension if not exists vector;

create table chunks (
  id          bigserial primary key,
  source      text not null,
  page        int,
  content     text not null,
  embedding   vector(1536),
  created_at  timestamp default now()
);

create index on chunks
  using hnsw (embedding vector_cosine_ops);

hnsw index = snelle approximate nearest neighbor search. Voor production met >10k rows.


Slide 8: Chunking strategieën

Hoe knip je een document op?

Drie aanpakken:

Strategie Hoe Wanneer
Fixed size 500 tokens per chunk, 50 overlap Default, simpelste
Recursive Splits op paragraaf → zin → woord Behoudt structuur
Semantic Embed zinnen, group similar Beste kwaliteit, duurder

Wat is een goede chunk:

  • ~200-500 tokens (~1000-2500 chars)
  • Genoeg context maar niet te groot
  • Overlap (10-15%) zodat info aan grenzen niet verdwijnt

Te kleine chunks → fragmentatie, AI mist context Te grote chunks → ruis, irrelevant info verdunt het antwoord

Visual: Document opgeknipt in chunks met overlap.


Slide 9: Wat we vandaag bouwen

PDF Q&A from scratch

Doel: App waar je een PDF uploadt en daarna vragen kunt stellen over de inhoud.

Stack:

  • Next.js 16 + TypeScript + Tailwind
  • Supabase + pgvector
  • AI SDK embedMany + generateText
  • OpenAI text-embedding-3-small
  • unpdf voor PDF parsing

Twee endpoints:

Route Wat Wanneer
POST /api/index PDF upload → chunks → embeddings Eén keer per document
POST /api/ask Vraag → embed → search → answer Per vraag

Bonus: RAG-tool in een ToolLoopAgent — combineer met Les 13.

Visual: Index + query pipelines uitgelegd in 1 diagram.


Slide 10: LIVE DEMO 1 — pgvector + embed pipeline

~25 min

Wat ik laat zien:

  1. pnpm create next-app pdf-qa + pnpm add ai @ai-sdk/openai @supabase/supabase-js unpdf
  2. Supabase: activeer pgvector extension (één SQL command)
  3. Schema: chunks tabel met vector(1536) kolom + HNSW index
  4. lib/embeddings.ts:
    import { embed, embedMany } from "ai";
    import { openai } from "@ai-sdk/openai";
    
    const model = openai.textEmbeddingModel("text-embedding-3-small");
    
    export async function embedText(text: string) {
      const { embedding } = await embed({ model, value: text });
      return embedding;
    }
    
  5. Test: console.log een embedding — array van 1536 nummers tussen -1 en 1

Visual: Console-log met vector + Supabase tabel.


Slide 11: LIVE DEMO 2 — PDF upload + index

~20 min

Wat ik laat zien:

  1. app/api/index/route.ts — POST endpoint accepteert file upload
  2. unpdf: extract text per page
  3. Chunking: split text in ~500 char chunks met 50 char overlap
  4. embedMany: batch embed alle chunks (sneller dan loop)
  5. Insert in Supabase met embedding als vector
const chunks = chunkText(fullText, 500, 50);
const { embeddings } = await embedMany({ model, values: chunks });
await supabase.from("chunks").insert(
  chunks.map((c, i) => ({
    source: filename,
    content: c,
    embedding: embeddings[i],
  }))
);
  1. Demo: upload Polderfest 2027 line-up PDF — 30 chunks in DB
  2. Check Supabase tabel — chunks zichtbaar, embedding kolom gevuld

Slide 12: Pauze

15 minuten


Slide 13: LIVE DEMO 3 — Query pipeline

~20 min

Wat ik laat zien:

  1. app/api/ask/route.ts — POST endpoint accepteert vraag
  2. Embed vraag (zelfde model als index!)
  3. Supabase RPC of raw SQL: cosine similarity search
const { embedding } = await embed({ model, value: question });
const { data: chunks } = await supabase.rpc("match_chunks", {
  query_embedding: embedding,
  match_count: 5,
});

const context = chunks.map((c) => c.content).join("\n\n");
const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: `Beantwoord op basis van deze context:\n\n${context}\n\nVraag: ${question}`,
});
  1. Supabase function match_chunks definiëren (SQL):
create function match_chunks(query_embedding vector(1536), match_count int)
returns table (content text, similarity float)
language sql as $$
  select content, 1 - (embedding <=> query_embedding) as similarity
  from chunks
  order by embedding <=> query_embedding
  limit match_count;
$$;
  1. Test: vraag "Wie zijn de headliners?" → AI geeft accuraat antwoord op basis van PDF

Slide 14: LIVE DEMO 4 — RAG-tool in een agent

~15 min

Wat ik laat zien:

Combineer Les 13 (Agents) + Les 14 (RAG):

import { ToolLoopAgent, tool, stepCountIs } from "ai";

const ragSearch = tool({
  description: "Zoek in geüploade documenten",
  inputSchema: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const { embedding } = await embed({ model, value: query });
    const { data } = await supabase.rpc("match_chunks", {
      query_embedding: embedding,
      match_count: 5,
    });
    return data;
  },
});

const docAgent = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: "Je beantwoordt vragen door eerst in documenten te zoeken.",
  tools: { ragSearch },
  stopWhen: stepCountIs(10),
});

Demo vraag: "Vergelijk de jazz en rock-headliners op Polderfest" — agent doet 2-3 RAG searches, vergelijkt, antwoordt.

Krachtig: RAG + Agent = AI met geheugen + redenering.


Slide 15: Wanneer RAG wel/niet?

Niet alles is RAG

Wanneer WEL RAG:

  • Veel documenten (>50 pagina's totaal)
  • Documenten veranderen (klantcontracten, kennisbank)
  • Semantic search nodig (niet alleen keyword)
  • Privacy: data blijft in jouw DB

Wanneer NIET RAG:

  • Klein document (<10 pagina's) — gewoon meesturen
  • Exacte data (prijzen, IDs) — gebruik tool-calls / DB query
  • Structured data (tabellen, records) — SQL is beter
  • 1 keer per dag — overhead niet de moeite

Alternatieven:

Probleem Beter dan RAG
5-pagina manual Volledige tekst in system prompt
10k product records SQL tool-calls (Les 12)
Live data API tool-call
Code-base navigatie Tree-sitter / grep, geen embeddings

Hybrid is vaak best: semantic search + keyword filter.


Slide 16: Lesopdracht + Huiswerk

PDF Q&A app deployen

Lesopdracht (in-class, 30 min):

  • PDF Q&A app opzetten (volg demo)
  • pgvector in Supabase actief
  • 1 PDF indexed
  • 3 vragen werkend

Huiswerk (voor Les 16):

  • A: Eigen PDF (interesse, studie, hobby) — minimaal 20 pagina's
  • B: UI met chat-interface (useChat van Les 12)
  • C: RAG-tool in een ToolLoopAgent
  • D: RAG.md met:
    • Beschrijving van je PDF + chunks count
    • 5 vragen + antwoorden + welke chunks werden opgehaald
    • 1 vraag waar RAG het fout had — analyse waarom
    • Eén chunking-strategie geprobeerd + resultaat

Bonus:

  • Hybrid search (semantic + keyword via Postgres full-text)
  • Re-ranking met cohere-rerank of LLM
  • Streaming antwoord met source-citations

Slide 17: Volgende les + Afsluiting

Vragen?

Vandaag gezien:

  • Embeddings — tekst als vectors in semantic space
  • Cosine similarity voor 'dichtbij'
  • RAG pipeline — index time vs query time
  • pgvector in Supabase met HNSW index
  • Chunking strategieën
  • RAG-tool in een agent

Volgende les (Les 15): Agents

  • Agent = LLM in een loop met tools
  • ToolLoopAgent — system, tools, stopWhen, prepareStep
  • 4 stop-condities: stepCountIs, hasToolCall, isLoopFinished, custom
  • Combo: RAG-tool in een agent (combo Les 14 + 15)

Daarna in deze leerlijn:

  • Les 16: MCP — eigen Model Context Protocol server bouwen
  • Les 17: Externe APIs in diepte (OAuth, webhooks, paid APIs)
  • Les 18: Supabase Auth + RLS — multi-user apps

Vragen? Feedback?


Slide Summary

# Title Type
1 Title Opening
2 Terugblik Recap
3 Planning 180-min
4 Wat is een embedding Theorie
5 Vector similarity Theorie
6 RAG pipeline Theorie
7 pgvector Theorie
8 Chunking strategieën Theorie
9 Wat we bouwen Intro demo
10 LIVE DEMO 1 — pgvector + embed Demo
11 LIVE DEMO 2 — PDF upload + index Demo
12 Pauze Break
13 LIVE DEMO 3 — Query pipeline Demo
14 LIVE DEMO 4 — RAG-tool in agent Demo
15 Wanneer RAG wel/niet Reflectie
16 Lesopdracht + Huiswerk Praktijk
17 Afsluiting + Les 16 preview Closing

Bronnen