479 lines
13 KiB
Markdown
479 lines
13 KiB
Markdown
# 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
|
||
|
||
```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 15 (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 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/
|