fix: update lessons
This commit is contained in:
478
Les14-RAG-Embeddings/Les14-Slide-Overzicht.md
Normal file
478
Les14-RAG-Embeddings/Les14-Slide-Overzicht.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# 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 — Cursor + Vercel deploy
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```sql
|
||||
-- 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:**
|
||||
|
||||
```sql
|
||||
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`:
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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],
|
||||
}))
|
||||
);
|
||||
```
|
||||
|
||||
6. Demo: upload Polderfest 2027 line-up PDF — 30 chunks in DB
|
||||
7. 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
|
||||
|
||||
```typescript
|
||||
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}`,
|
||||
});
|
||||
```
|
||||
|
||||
4. Supabase function `match_chunks` definiëren (SQL):
|
||||
|
||||
```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;
|
||||
$$;
|
||||
```
|
||||
|
||||
5. 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):
|
||||
|
||||
```typescript
|
||||
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): Cursor + Vercel deploy**
|
||||
- Externe APIs in Next.js Server Components
|
||||
- Cursor Composer + Background Agents
|
||||
- Deploy naar Vercel productie + preview per branch
|
||||
- GitHub Actions CI (lint + build)
|
||||
|
||||
**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/
|
||||
Reference in New Issue
Block a user