fix: les
This commit is contained in:
575
Les16-RAG-Embeddings/Les16-Docenttekst.md
Normal file
575
Les16-RAG-Embeddings/Les16-Docenttekst.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# Les 16 — RAG + Embeddings
|
||||
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
|
||||
|
||||
**Les:** 15 van 18
|
||||
**Onderwerp:** RAG (Retrieval-Augmented Generation) — AI laten antwoorden op eigen documenten
|
||||
**Duur:** 180 minuten
|
||||
**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk.
|
||||
**Demo-app:** PDF Q&A from scratch — nieuwe kleine app
|
||||
|
||||
---
|
||||
|
||||
## Hoe deze tekst werkt
|
||||
|
||||
- `[SLIDE X]` — klik naar slide X
|
||||
- `[SCHERM: slides | terminal | editor | browser | supabase]`
|
||||
- **Vertel:** "..." — wat je zegt
|
||||
- `*[stage direction]*` — instructie voor jezelf
|
||||
- 💬 = verwachte studentenvraag
|
||||
|
||||
---
|
||||
|
||||
## VÓÓR DE LES — Setup (45 min)
|
||||
|
||||
### 1. Demo-folder + Supabase
|
||||
|
||||
- Maak vooraf werkende `pdf-qa` lokaal (backup)
|
||||
- Supabase project: pgvector extension AL geactiveerd
|
||||
- Schema (chunks tabel + match_chunks function) al gedraaid
|
||||
- 1 PDF al klaar om te indexeren: `polderfest-lineup-2027.pdf` (genereer met AI of pak echte)
|
||||
|
||||
### 2. Voorbeeld-PDF klaar
|
||||
|
||||
- Print of genereer een PDF over Polderfest 2027 — ~10-20 pagina's
|
||||
- Liefst: line-up, dagschema's, locatie-info
|
||||
- Studenten kunnen deze ook downloaden
|
||||
- Backup-PDF: Wikipedia-artikel over willekeurig onderwerp als geprinte PDF
|
||||
|
||||
### 3. Browser-tabs
|
||||
|
||||
- `localhost:3000` (dev server)
|
||||
- Supabase dashboard (Table Editor + SQL Editor)
|
||||
- https://ai-sdk.dev/docs/ai-sdk-core/embeddings (referentie)
|
||||
- https://platform.openai.com/usage (cost-monitoring)
|
||||
|
||||
### 4. Backup als demo crasht
|
||||
|
||||
- Werkende eindstaat (alle code) op USB
|
||||
- Console-log met "voorbeeld-embedding" als slide-content
|
||||
- Backup-vraag-en-antwoord screenshots
|
||||
|
||||
### 5. Terminals
|
||||
|
||||
- 3 tabs: dev, watch (`watch curl ...`), supabase SQL via CLI als nodig
|
||||
|
||||
---
|
||||
|
||||
# HET SCRIPT — Lees mee tijdens de les
|
||||
|
||||
## BLOK 1 — Welkom + Terugblik (10 min)
|
||||
|
||||
`[SLIDE 1 — Title]` `[SCHERM: slides]`
|
||||
|
||||
**Vertel:** "Welkom bij les 15. Vandaag: RAG. Retrieval-Augmented Generation. AI laten antwoorden op basis van jouw eigen documenten."
|
||||
|
||||
`[SLIDE 2 — Terugblik]`
|
||||
|
||||
**Vertel:** "Lessen 11-14: AI SDK basics, tool calling, agents, externe APIs en deploy. Nu hebben we een complete stack.
|
||||
|
||||
Wat we nog niet hadden: AI laten werken met **jouw documenten**. Stel je hebt 200 pagina's productdocumentatie of een dik handboek of een lange whitepaper. Hoe stel je daar vragen aan?
|
||||
|
||||
Twee naïeve oplossingen. Eén: alles meesturen in context. Werkt voor 5 pagina's, niet voor 500. Te duur. Twee: AI zelf laten zoeken. Werkt voor publieke info, niet voor jouw private data.
|
||||
|
||||
RAG is de derde oplossing. In één zin: haal de relevante stukjes uit je documenten op, geef die aan AI, AI antwoordt."
|
||||
|
||||
`[SLIDE 3 — Planning]`
|
||||
|
||||
**Vertel:** "Drie uur. Eerst vijftig minuten theorie — embeddings, similarity, pipeline, pgvector, chunking. Veel om te begrijpen, maar het loont. Daarna vier demos. Pauze rond minuut 100."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 2 — Theorie embeddings (20 min)
|
||||
|
||||
`[SLIDE 4 — Wat is een embedding]`
|
||||
|
||||
**Vertel:** "Een embedding is een vector. Letterlijk een array van getallen. Voor OpenAI's small model: 1536 getallen tussen min-één en plus-één.
|
||||
|
||||
Je stopt tekst erin, krijgt vector terug. Maar het magische zit hier — vergelijkbare betekenissen produceren vergelijkbare vectors.
|
||||
|
||||
`*[Wijs naar slide voorbeeld]*`
|
||||
|
||||
'De kat zit op de mat' en 'Een poes ligt op het tapijt' hebben bijna geen woorden gemeen. Maar betekenis is hetzelfde. Hun embeddings staan dichtbij elkaar in de 1536-dimensionale ruimte. 'Voetbal in Nederland' — totaal andere kant op.
|
||||
|
||||
Dit is fundamenteel anders dan keyword search. Keyword zou de eerste twee zinnen totaal niet matchen — geen woord overlap. Semantic search wel.
|
||||
|
||||
Welke modellen. OpenAI 3-small en 3-large — de standaarden. Open-source alternatieven zoals nomic-embed-text als je lokaal wilt embedden. Voor vandaag: 3-small. Goedkoop, snel, prima kwaliteit voor de meeste apps."
|
||||
|
||||
💬 *Vraag: 'Wat doet zo'n embedding-model eigenlijk?'*
|
||||
|
||||
**Antwoord:** "Het is een neural network, getraind om de betekenis van tekst te 'condenseren' naar een dichte vector. Vergelijkbaar met de eerste lagen van een GPT-model, maar zonder de generative head. Heel veel context distilleren tot 1536 nummers. Wiskundig: een functie die tekst projecteert in een 'semantic space'."
|
||||
|
||||
`[SLIDE 5 — Vector similarity]`
|
||||
|
||||
**Vertel:** "Hoe meet je 'dichtbij'? Drie metrics, één winnaar voor text.
|
||||
|
||||
Cosine similarity — hoek tussen twee vectors. Van min-één tot plus-één. Plus-één betekent identiek georiënteerd, nul ongerelateerd, min-één tegenovergesteld. Voor genormaliseerde vectors (OpenAI's zijn dat) is cosine effectief dot product.
|
||||
|
||||
Voor RAG: cosine. Standaard, robuust, simpel.
|
||||
|
||||
In pgvector schrijf je `<=>` voor cosine distance. Een kleinere afstand = meer similar. Order by `<=>` ascending = top resultaten eerst. Onthoud die operator."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 3 — Theorie RAG pipeline + pgvector (30 min)
|
||||
|
||||
`[SLIDE 6 — RAG pipeline]`
|
||||
|
||||
**Vertel:** "Twee pipelines. Index time en query time.
|
||||
|
||||
Index time gebeurt eenmalig — wanneer je een document uploadt. PDF binnen, parsen, opknippen in chunks van ongeveer vijfhonderd tokens, elke chunk embedden, alles in Postgres.
|
||||
|
||||
Query time gebeurt elke vraag. Vraag binnen, embedden — zelfde model als index, dat is belangrijk — similarity search in Postgres, top vijf chunks terug. Die geef je als context aan LLM, LLM antwoordt.
|
||||
|
||||
Belangrijkste inzicht: LLM ziet nooit de hele DB. Alleen de vijf meest relevante chunks. Schaalbaar tot miljoenen documenten."
|
||||
|
||||
💬 *Vraag: 'Waarom moet het embedding-model hetzelfde zijn?'*
|
||||
|
||||
**Antwoord:** "Embeddings zijn alleen vergelijkbaar binnen hetzelfde model. Model A's vector voor 'kat' staat op een totaal andere plek dan Model B's vector voor 'kat'. Hun coördinatensystemen zijn niet uitwisselbaar. Dus: zelfde model voor index én query — always."
|
||||
|
||||
`[SLIDE 7 — pgvector]`
|
||||
|
||||
**Vertel:** "Hoe sla je vectors op? Postgres heeft een extensie: pgvector. Voegt `vector` datatype toe, plus operators voor cosine, euclidean, dot product. In Supabase: één klik in dashboard om te activeren.
|
||||
|
||||
`*[Toon SQL op slide]*`
|
||||
|
||||
Schema: chunks tabel met content kolom en embedding kolom. `vector(1536)` — dimensie matcht model. Voor performance: een HNSW index. Hierarchical Navigable Small Worlds. Wiskundige magie waardoor je in miljoenen vectors in milliseconden zoekt. Bij minder dan tienduizend rows kun je het skippen — exacte search is dan al snel."
|
||||
|
||||
`[SLIDE 8 — Chunking]`
|
||||
|
||||
**Vertel:** "Hoe knip je een document op. Klinkt simpel — is het niet.
|
||||
|
||||
Drie strategieën. Fixed size — vijfhonderd tokens per chunk, vijftig tokens overlap. Simpel, default. Recursive — knip eerst op paragrafen, dan zinnen, dan woorden. Behoudt structuur beter. Semantic — embed zinnen, group similar. Beste kwaliteit, duurder.
|
||||
|
||||
Sweet spot voor chunks: tweehonderd tot vijfhonderd tokens. Te klein, je verliest context. Te groot, irrelevant info verdunt het signaal. Overlap van tien tot vijftien procent zodat info aan chunk-grenzen niet verdwijnt.
|
||||
|
||||
Vandaag: fixed size, vijfhonderd char chunks, vijftig overlap. Werkt prima voor de meeste docs."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 4 — Demo 1: pgvector setup (25 min)
|
||||
|
||||
`[SLIDE 9 — Wat we bouwen]`
|
||||
|
||||
**Vertel:** "Vandaag bouwen we een PDF Q&A app from scratch. Upload PDF, hij wordt geïndexeerd, je stelt vragen. Klassieke RAG-use-case."
|
||||
|
||||
`[SLIDE 10 — DEMO 1]` `[SCHERM: terminal + editor]`
|
||||
|
||||
**Vertel:** "Klas, kijk mee."
|
||||
|
||||
```bash
|
||||
cd ~/novi/novi-lessons/Les16-RAG-Embeddings
|
||||
pnpm create next-app@latest pdf-qa \
|
||||
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
|
||||
cd pdf-qa
|
||||
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js unpdf
|
||||
cursor .
|
||||
```
|
||||
|
||||
`*[Supabase dashboard]*` `[SCHERM: supabase]`
|
||||
|
||||
**Vertel:** "Eerst pgvector activeren. Database → Extensions → zoek vector → enable. Eén click.
|
||||
|
||||
Of via SQL:"
|
||||
|
||||
```sql
|
||||
create extension if not exists vector;
|
||||
```
|
||||
|
||||
`*[SQL Editor — voer uit:]*`
|
||||
|
||||
```sql
|
||||
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);
|
||||
|
||||
alter table chunks enable row level security;
|
||||
create policy "demo open" on chunks
|
||||
for all to anon using (true) with check (true);
|
||||
```
|
||||
|
||||
**Vertel:** "Tabel, HNSW index, RLS, open policy. Dezelfde regels als alle eerdere demos."
|
||||
|
||||
`*[Editor → lib/embeddings.ts]*`
|
||||
|
||||
```typescript
|
||||
import { embed, embedMany } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export const embedModel = openai.textEmbeddingModel("text-embedding-3-small");
|
||||
|
||||
export async function embedOne(text: string) {
|
||||
const { embedding } = await embed({ model: embedModel, value: text });
|
||||
return embedding;
|
||||
}
|
||||
|
||||
export async function embedBatch(texts: string[]) {
|
||||
const { embeddings } = await embedMany({ model: embedModel, values: texts });
|
||||
return embeddings;
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Twee functies. embedOne voor één string, embedBatch voor veel tegelijk. Batch is veel sneller — één API-call in plaats van een loop."
|
||||
|
||||
`*[Voeg chunkText functie toe]*`
|
||||
|
||||
```typescript
|
||||
export function chunkText(text: string, size = 500, overlap = 50): string[] {
|
||||
const chunks: string[] = [];
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
chunks.push(text.slice(i, i + size).trim());
|
||||
i += size - overlap;
|
||||
}
|
||||
return chunks.filter((c) => c.length > 0);
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Simpelste chunker mogelijk. Slice op chars. Voor productie: gebruik LangChain's RecursiveCharacterTextSplitter — handelt edge cases beter. Voor demo: dit volstaat."
|
||||
|
||||
`*[Test embed]*`
|
||||
|
||||
```typescript
|
||||
// Eenmalig in een test-script of API route
|
||||
const e = await embedOne("De kat zit op de mat");
|
||||
console.log(e.length, e.slice(0, 5));
|
||||
// 1536 [0.012, -0.0345, 0.0123, ...]
|
||||
```
|
||||
|
||||
**Vertel:** "Vector van 1536 nummers. Eerste vijf: kleine getallen tussen min-één en plus-één. Dat is alles."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 5 — Demo 2: Index pipeline (20 min)
|
||||
|
||||
`[SLIDE 11 — DEMO 2]` `[SCHERM: editor + terminal]`
|
||||
|
||||
**Vertel:** "Index endpoint. POST een PDF erin, krijg chunk-count terug."
|
||||
|
||||
`*[app/api/index/route.ts]*`
|
||||
|
||||
```typescript
|
||||
import { extractText } from "unpdf";
|
||||
import { embedBatch, chunkText } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File;
|
||||
if (!file) return Response.json({ error: "No file" }, { status: 400 });
|
||||
|
||||
const buffer = new Uint8Array(await file.arrayBuffer());
|
||||
const { text } = await extractText(buffer, { mergePages: true });
|
||||
|
||||
const chunks = chunkText(text, 500, 50);
|
||||
const embeddings = await embedBatch(chunks);
|
||||
|
||||
const { error } = await supabase.from("chunks").insert(
|
||||
chunks.map((content, i) => ({
|
||||
source: file.name,
|
||||
content,
|
||||
embedding: embeddings[i],
|
||||
}))
|
||||
);
|
||||
|
||||
if (error) return Response.json({ error: error.message }, { status: 500 });
|
||||
return Response.json({ chunks: chunks.length, source: file.name });
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Vier stappen. Eén: ontvang file via formData. Twee: parse met unpdf. Drie: chunk + embed. Vier: insert alles in Supabase."
|
||||
|
||||
`*[Terminal:]*`
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
`*[Tweede terminal:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/index \
|
||||
-F "file=@/path/to/polderfest-lineup.pdf"
|
||||
```
|
||||
|
||||
`*[Wacht 5-10s]*`
|
||||
|
||||
**Vertel:** "Response: chunks: 30. Dertig chunks van onze PDF in de DB."
|
||||
|
||||
`*[Supabase Table Editor]*` `[SCHERM: supabase]`
|
||||
|
||||
**Vertel:** "Daar staan ze. Content kolom met tekst. Embedding kolom — clickbaar voor de vector. Source filename. Klaar voor querying."
|
||||
|
||||
💬 *Vraag: 'Hoe lang duurt embedding van 1000 chunks?'*
|
||||
|
||||
**Antwoord:** "Met embedMany batch: enkele seconden voor duizend chunks. OpenAI's API is snel — duizenden tokens per seconde. Voor één PDF van 200 pagina's: misschien 5-10 seconden totaal. Indexing is goedkoop én snel."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 6 — Pauze (15 min)
|
||||
|
||||
`[SLIDE 12 — Pauze]`
|
||||
|
||||
---
|
||||
|
||||
## BLOK 7 — Demo 3: Query pipeline (20 min)
|
||||
|
||||
`[SLIDE 13 — DEMO 3]` `[SCHERM: editor + terminal + supabase]`
|
||||
|
||||
**Vertel:** "Nu querying. We schrijven een SQL function voor de similarity search."
|
||||
|
||||
`*[Supabase SQL Editor:]*`
|
||||
|
||||
```sql
|
||||
create or replace function match_chunks(
|
||||
query_embedding vector(1536),
|
||||
match_count int default 5
|
||||
)
|
||||
returns table (id bigint, content text, source text, similarity float)
|
||||
language sql stable as $$
|
||||
select chunks.id, chunks.content, chunks.source,
|
||||
1 - (chunks.embedding <=> query_embedding) as similarity
|
||||
from chunks
|
||||
order by chunks.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
$$;
|
||||
```
|
||||
|
||||
**Vertel:** "Postgres function neemt vector + count, returnt top N met similarity score. `1 - (a <=> b)` converteert distance terug naar similarity score."
|
||||
|
||||
`*[app/api/ask/route.ts]*`
|
||||
|
||||
```typescript
|
||||
import { embedOne } from "@/lib/embeddings";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { question } = await req.json();
|
||||
const embedding = await embedOne(question);
|
||||
|
||||
const { data: chunks } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
|
||||
if (!chunks?.length) {
|
||||
return Response.json({ answer: "Geen relevante info gevonden." });
|
||||
}
|
||||
|
||||
const context = chunks
|
||||
.map((c: any, i: number) => `[Source ${i + 1}: ${c.source}]\n${c.content}`)
|
||||
.join("\n\n---\n\n");
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
prompt: `Beantwoord de vraag op basis van deze context. Als geen antwoord in de context staat, zeg dat eerlijk.
|
||||
|
||||
CONTEXT:
|
||||
${context}
|
||||
|
||||
VRAAG: ${question}
|
||||
|
||||
ANTWOORD:`,
|
||||
});
|
||||
|
||||
return Response.json({ answer: text, sources: chunks });
|
||||
}
|
||||
```
|
||||
|
||||
**Vertel:** "Vier stappen. Eén: embed de vraag. Twee: RPC call naar match_chunks — top 5 terug. Drie: bouw context-string met source-labels. Vier: generateText met prompt-template."
|
||||
|
||||
`*[Terminal:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question": "Wie zijn de headliners op Polderfest 2027?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
`*[Wacht 2-3s]*`
|
||||
|
||||
**Vertel:** "Antwoord: 'De headliners op Polderfest 2027 zijn... blah blah, gebaseerd op de line-up uit het document.'
|
||||
|
||||
Plus: sources. Welke chunks waren de top vijf. Similarity scores. Volledig traceerbaar."
|
||||
|
||||
`*[Tweede vraag voor demo:]*`
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/ask \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question": "Op welk podium spelen de jazz acts?"}' \
|
||||
| jq
|
||||
```
|
||||
|
||||
**Vertel:** "Andere vraag, andere chunks opgehaald, ander antwoord. Werkt."
|
||||
|
||||
💬 *Vraag: 'Wat als vraag niet in document staat?'*
|
||||
|
||||
**Antwoord:** "Twee dingen. Eén: similarity scores zijn laag — top vijf is alsnog niet relevant. Twee: LLM merkt dat context het niet bevat, en zegt 'staat niet in document'. Dat tweede dwingen we af met de prompt: 'Als geen antwoord in context staat, zeg dat eerlijk.' Anders zou-ie kunnen hallucineren."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 8 — Demo 4: RAG-tool in agent (15 min)
|
||||
|
||||
`[SLIDE 14 — DEMO 4]` `[SCHERM: editor]`
|
||||
|
||||
**Vertel:** "Laatste demo. Combineer Les 13 met Les 16. Een RAG-tool in een ToolLoopAgent."
|
||||
|
||||
`*[lib/agent.ts]*`
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, tool, stepCountIs } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { embedOne } from "./embeddings";
|
||||
import { supabase } from "./supabase";
|
||||
import { z } from "zod";
|
||||
|
||||
const ragSearch = tool({
|
||||
description:
|
||||
"Zoek in geüploade documenten op basis van semantic similarity. " +
|
||||
"Gebruik voor inhoudelijke vragen over de documenten.",
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe("Wat je wilt vinden"),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
const embedding = await embedOne(query);
|
||||
const { data } = await supabase.rpc("match_chunks", {
|
||||
query_embedding: embedding,
|
||||
match_count: 5,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
export const docAgent = new ToolLoopAgent({
|
||||
model: openai("gpt-4o-mini"),
|
||||
system: `Je beantwoordt vragen over geüploade documenten. Werkwijze:
|
||||
1. Zoek met ragSearch voor relevante info
|
||||
2. Lees de chunks
|
||||
3. Eventueel: tweede ragSearch met andere query voor meer context
|
||||
4. Antwoord met bronvermeldingen.
|
||||
|
||||
Verzin niets - alleen wat in de chunks staat.`,
|
||||
tools: { ragSearch },
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
```
|
||||
|
||||
**Vertel:** "Agent met één tool: ragSearch. System prompt vertelt hem multi-step te werken — zoek, lees, evalueer of nog een query nodig is.
|
||||
|
||||
Het voordeel boven simple RAG: voor complexe vragen kan agent meerdere searches doen. 'Vergelijk de jazz- en rock-headliners' — agent doet twee searches, één voor jazz, één voor rock, dan vergelijkt."
|
||||
|
||||
`*[Quick test in script of route:]*`
|
||||
|
||||
```typescript
|
||||
const result = await docAgent.generate({
|
||||
prompt: "Vergelijk de jazz- en rock-headliners op Polderfest 2027.",
|
||||
});
|
||||
console.log(result.text);
|
||||
console.log("Steps:", result.steps.length);
|
||||
```
|
||||
|
||||
`*[Run]*`
|
||||
|
||||
**Vertel:** "Antwoord: vergelijking van beide. Steps: drie of vier — twee searches, dan reasoning, dan finale text. Agent doet zijn werk."
|
||||
|
||||
💬 *Vraag: 'Wanneer agent vs simple RAG?'*
|
||||
|
||||
**Antwoord:** "Simple RAG voor single-shot vragen. 'Wie is X?' → één search → antwoord. Agent voor vragen die multiple lookups vereisen. Vergelijkingen, multi-hop, of als gebruiker breed vraagt en je wilt dat AI verfijnt. Trade-off: agent kost 3-5x meer en duurt langer. Begin met simple, upgrade als nodig."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 9 — Wanneer RAG (10 min)
|
||||
|
||||
`[SLIDE 15 — Wanneer wel/niet]`
|
||||
|
||||
**Vertel:** "Niet alles is RAG. Wanneer wel: veel documenten, ze veranderen, semantic search nodig, privacy belangrijk, bronvermelding gewenst.
|
||||
|
||||
Wanneer niet: klein document — stop in prompt. Exacte data — gebruik tool-calls of SQL. Structured data — SQL is beter dan vector search. Code-base — gebruik grep en tree-sitter.
|
||||
|
||||
In productie zie je vaak hybrid. Semantic search voor concept-vragen, keyword search voor exacte termen, metadata filter voor structured criteria. Drie technieken in één query.
|
||||
|
||||
En: re-ranking. Top-twintig ophalen via vector, dan re-rank met Cohere of LLM-as-judge naar top-vijf. Twintig procent betere kwaliteit. Voor productie: doen. Voor demo: skip."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 10 — Lesopdracht + Huiswerk (10 min)
|
||||
|
||||
`[SLIDE 16 — Lesopdracht + Huiswerk]`
|
||||
|
||||
**Vertel:** "Lesopdracht: zelfde app als ik liet zien. Half uur. PDF Q&A, pgvector, index, ask. Voorbeeld-PDF deel ik in de groep.
|
||||
|
||||
Huiswerk: drie dingen.
|
||||
|
||||
Eén: echte PDF van minimaal twintig pagina's, eigen interesse. UI met file upload + chat-interface.
|
||||
|
||||
Twee: RAG-tool in een ToolLoopAgent. UI laat tool-invocations zien zoals Les 12.
|
||||
|
||||
Drie: RAG.md schrijven. Document-info, vijf werkende vragen met sources, één fail-case waar RAG het verkeerd had, één chunking-experiment, één observatie.
|
||||
|
||||
Bonus: hybrid search, streaming met citations, re-ranking, multi-document.
|
||||
|
||||
Tien punten, voldoende zes."
|
||||
|
||||
---
|
||||
|
||||
## BLOK 11 — Afsluiting (5 min)
|
||||
|
||||
`[SLIDE 17 — Afsluiting]`
|
||||
|
||||
**Vertel:** "Wat hebben we vandaag gedaan. Embeddings als vectors. Cosine similarity. RAG pipeline — index + query. pgvector. Chunking. RAG-tool in een agent. Wanneer wel/niet RAG.
|
||||
|
||||
Volgende les: Multimodal. Voice, vision, image generation. Whisper voor transcriptie. GPT-4o voor foto-analyse. Flux of DALL-E voor afbeeldingen genereren in je app. Heel visueel.
|
||||
|
||||
Daarna: les 17 over MCP — Model Context Protocol — eigen MCP server bouwen. Les 18: browser automation en Computer Use. Spannende lessen om mee af te sluiten.
|
||||
|
||||
Vragen?"
|
||||
|
||||
`*[Vragenronde — minstens 5 min over laten]*`
|
||||
|
||||
---
|
||||
|
||||
## NA DE LES — Wrap-up
|
||||
|
||||
- Push working `pdf-qa` repo naar GitHub als referentie
|
||||
- Deel voorbeeld-PDF voor lesopdracht in Brightspace
|
||||
- Brightspace: huiswerk-instructies + repo URL
|
||||
- Voor Klas B: check of zelfde stack werkt (Supabase free tier limiet pgvector?)
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende fouten tijdens live coding
|
||||
|
||||
| Fout | Oplossing |
|
||||
|------|-----------|
|
||||
| `extension "vector" does not exist` | Supabase: enable in dashboard |
|
||||
| Embedding dimension mismatch | Check `vector(1536)` matcht `text-embedding-3-small` |
|
||||
| `match_chunks function not found` | SQL function niet aangemaakt — kopieer uit slide |
|
||||
| RLS error op insert | Open policy nog niet uitgevoerd |
|
||||
| `unpdf` geen text uit PDF | Image-based PDF (scan) — gebruik OCR of andere PDF |
|
||||
| HNSW index trager dan exact | Voor <10k rows: skip index of `set hnsw.ef_search = 100` |
|
||||
| Antwoord is generic, mist details | Top-k te laag of chunks te klein — experimenteer |
|
||||
| Hallucination | Strengere prompt: "Als info ontbreekt, zeg dat" |
|
||||
|
||||
---
|
||||
|
||||
## Mentale model voor de klas
|
||||
|
||||
Als studenten verward zijn over wat embeddings doen:
|
||||
|
||||
> Een embedding is een **adres** in de betekenis-ruimte. Twee adressen dichtbij elkaar = twee zinnen met vergelijkbare betekenis. RAG werkt door: vertaal vraag naar adres, vind alle adressen in de buurt, lees wat daar woont, geef dat aan AI.
|
||||
|
||||
En RAG vs Agent:
|
||||
|
||||
> RAG = bibliotheek met index. Eén vraag, één bezoek, top-vijf boeken, antwoord.
|
||||
> RAG-in-Agent = bibliothecaris die voor jou zoekt. Stelt eerst sub-vragen, doet meerdere zoekrondes, vergelijkt, antwoordt.
|
||||
Reference in New Issue
Block a user