# 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/