13 KiB
Les 15 — 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 14 — Agents Aansluit op: Les 16 — MCP servers
Slide 1: Title
Les 15 — RAG + Embeddings
Visual:
- Background: CREAM
- "Les 15" 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 15: 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
vectordatatype 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 unpdfvoor 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:
pnpm create next-app pdf-qa+pnpm add ai @ai-sdk/openai @supabase/supabase-js unpdf- Supabase: activeer
pgvectorextension (één SQL command) - Schema:
chunkstabel metvector(1536)kolom + HNSW index 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; }- Test:
console.logeen 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:
app/api/index/route.ts— POST endpoint accepteert file uploadunpdf: extract text per page- Chunking: split text in ~500 char chunks met 50 char overlap
embedMany: batch embed alle chunks (sneller dan loop)- Insert in Supabase met
embeddingals 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],
}))
);
- Demo: upload Polderfest 2027 line-up PDF — 30 chunks in DB
- 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:
app/api/ask/route.ts— POST endpoint accepteert vraag- Embed vraag (zelfde model als index!)
- 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}`,
});
- Supabase function
match_chunksdefinië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;
$$;
- 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 15 (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 (
useChatvan Les 12) - C: RAG-tool in een ToolLoopAgent
- D:
RAG.mdmet:- 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-rerankof 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 16): MCP — Model Context Protocol
- 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 15 + 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
- AI SDK Embeddings: https://ai-sdk.dev/docs/ai-sdk-core/embeddings
- OpenAI Embeddings: https://platform.openai.com/docs/guides/embeddings
- pgvector: https://github.com/pgvector/pgvector
- Supabase pgvector: https://supabase.com/docs/guides/database/extensions/pgvector
- unpdf: https://github.com/unjs/unpdf
- Anthropic — contextual retrieval: https://www.anthropic.com/news/contextual-retrieval
- LlamaIndex RAG concepts: https://docs.llamaindex.ai/en/stable/getting_started/concepts/