Files
novi-lessons/Les11-AI-SDK/Les11-Docenttekst.md
2026-05-19 18:50:11 +02:00

30 KiB
Raw Blame History

Les 11 — Vercel AI SDK

Docenttekst (Klas A — 3 uur, fysiek, demo-driven)

Les: 11 van 18 Onderwerp: Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset Duur: 180 minuten Format: Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = thuis. Demo-app: Polderfest 2027 — fictief muziekfestival, 500 records in Supabase.


Hoe deze tekst werkt

Dit document is een lopend script. Je kunt 'm letterlijk volgen op je laptop terwijl je lesgeeft.

  • [SLIDE X] — Klik naar slide X op de beamer
  • [SCHERM: slides | terminal | editor | browser | supabase] — Welk scherm op de beamer
  • Vertel: "..." — Letterlijk wat je zegt (mag in eigen woorden)
  • *[stage direction]* — Korte instructie voor jezelf, niet uitspreken
  • Code blocks = wat je typt
  • 💬 = verwachte studentenvraag

VÓÓR DE LES — Setup (60 min)

1. Tools open op je laptop

2. Demo-repo polderfest-demo voorbereiden

cd ~
npx create-next-app@latest polderfest-demo \
  --typescript --tailwind --app --eslint --no-src-dir --turbopack
cd polderfest-demo
npm i @supabase/supabase-js ai @ai-sdk/openai zod dotenv
npm i tsx --save-dev
git init && git add . && git commit -m "init"

3. Nieuwe Supabase project

  • Dashboard → New Project → naam polderfest-demo
  • Wacht ~2 min op deploy
  • Settings → API → kopieer URL + anon key + service role key
  • .env.local:
    NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEY=...
    SUPABASE_SERVICE_ROLE_KEY=...
    OPENAI_API_KEY=sk-proj-...
    

4. Schema runnen

  • Supabase → SQL Editor → plak schema.sql → Run
  • Check Table Editor → bands tabel bestaat, leeg

5. Seed test (verwijder daarna)

  • Plaats seed-polderfest.ts in polderfest-demo/scripts/
  • npx tsx scripts/seed-polderfest.ts → 500 records erin → check Table Editor
  • Wis alle records vóór de les: delete from bands; (zodat je live kunt seeden in demo 2)
  • Verwijder app/chat/page.tsx + app/api/chat/route.ts (maken we live in demo 3)

6. Backup

  • Zip van werkende eindstaat → op USB
  • Check OpenAI usage dashboard — key werkt + credits aanwezig

HET SCRIPT — Lees mee tijdens de les

BLOK 1 — Welkom + Terugblik (10 min)

[SLIDE 1] [SCHERM: slides]

Vertel: "Welkom bij les 11. Vandaag de Vercel AI SDK — eerste keer dat we échte AI features gaan bouwen IN onze apps. Geen ChatGPT openen meer — AI in onze eigen code."

[SLIDE 2 — Terugblik]

Vertel: "Even kort terug: vorige lessen hebben we Supabase geïntegreerd. Tabellen en relaties opgezet. RLS-policies bekeken — wie mag wat lezen en schrijven.

Vandaag iets anders. We gaan niet voortbouwen op QuickPoll. We beginnen een nieuwe demo from scratch. Nieuwe Next.js app, nieuwe Supabase, en dan koppelen we daar de AI SDK aan."

*[Wacht 2 sec, laat het landen]*

[SLIDE 3 — Planning]

Vertel: "Dit is de planning. Drie uur. Eerst theorie — 30 minuten — wat is de AI SDK, welke modellen, welke functies. Daarna vier demo's. Eén: nieuwe app opzetten. Twee: 500 records in Supabase via een seed-script. Pauze. Drie: AI SDK installeren en chatten met de data. Vier: vragen stellen aan die data."

*[Wijs naar de gele rij]*

Vertel: "Belangrijk — dit is een kijk-les. Jullie typen vandaag niet mee. Pak je notitieboek of laptop voor aantekeningen. Thuis bouw je zelf een versie, met je eigen thema. Daar gaan de lesopdracht en huiswerk over."

💬 Verwachte vraag: "Kunnen we niet meedoen?" Antwoord: "Liever niet — als jullie ook typen, gaat 't te langzaam en haakt iedereen op een ander moment af. Vanavond is voor zien-en-snappen. Thuis is doen."


BLOK 2 — Theorie AI SDK (30 min)

[SLIDE 4 — Wat is de AI SDK] [SCHERM: slides]

Vertel: "Wat is de Vercel AI SDK? Een TypeScript-library die één unified API biedt voor alle AI-providers. OpenAI vandaag, Anthropic morgen, Google overmorgen, lokaal Ollama als je dat wil — je code blijft hetzelfde.

Open source. Gemaakt door Vercel — de makers van Next.js. Daarom: naadloze integratie met Server Components, Server Actions en streaming.

Wat zit er in:"

*[Wijs naar de bullets]*

Vertel: "Unified API. Streaming out-of-the-box — geen WebSocket-gedoe. React hooks zoals useChat. Tool Calling — komt volgende les. En type-safe gestructureerde output via Zod."

*[Wijs naar het code-blok rechts]*

Vertel: "Kijk hier. Dit is alle code die je nodig hebt voor één AI-call. Vier regels. En zie je openai('gpt-4o-mini')? Als ik dat morgen wil veranderen naar Anthropic — verander ik dat in anthropic('claude-sonnet-4'). Eén regel. Rest van mijn code blijft hetzelfde. Dat is de waarde."

[SLIDE 5 — Modellen + kosten]

Vertel: "Het modellen-landschap. Loop ik even langs:"

*[Wijs per rij]*

  • "gpt-4o-mini — je default. Snel, goedkoop, $0.15 input / $0.60 output per miljoen tokens. Goed voor 80% van de use cases."
  • "gpt-4o — multimodal, kan plaatjes lezen. 15× duurder dan mini. Pas pakken als nodig."
  • "gpt-4.1 — beste reasoning. Voor agents. Volgende lessen relevant."
  • "claude-sonnet-4 — Anthropic. Beter in coding, 200k context — dus lange documenten."
  • "gemini-2.5-flash — Google. Ultra goedkoop, multimodal."
  • "llama-3.3-70b op Groq — open-source model op snelste inference platform."

Vertel: "Vuistregel: start met gpt-4o-mini. Werkt 't niet goed genoeg? Probeer gpt-4o. Pas daarna iets exotisch. Premature optimization is een valkuil — het is letterlijk één regel veranderen om te wisselen, dus geen reden om voorbarig te kiezen."

*[Wijs naar de blauwe callout]*

Vertel: "Onze hele les vandaag, inclusief Polderfest met 500 bands en 10 vragen? Ongeveer 1 tot 2 cent. Echt. Schaalt prima."

[SLIDE 6 — 4 kern-functies]

Vertel: "De vier kern-functies van de SDK. Deze tabel is je cheat-sheet."

*[Wijs per rij]*

  • "generateText — wachten tot AI klaar is, dan krijg je een string. Voor korte server-only calls."
  • "streamText — streamt karakter voor karakter. Werkt met useChat. Dit gebruiken we vandaag."
  • "useChat — React hook. Complete chat UI in 10 regels. Ook vandaag."
  • "generateObject — type-safe data via Zod schema. Voor database-inserts of classificatie. Vandaag niet — komt later."

Vertel: "Onthoud: streamText en useChat — onze combo voor vandaag. generateObject zien jullie volgende lessen terug. Tool Calling — onderaan — dat is volgende les."


BLOK 3 — Live Demo 1: Next.js + Supabase scaffold (20 min)

[SLIDE 7 — Polderfest concept] [SCHERM: slides]

Vertel: "Voor we gaan coderen — wat bouwen we eigenlijk?

We bouwen Polderfest 2027. Een fictief Nederlands muziekfestival. 500 verzonnen bands. Allemaal namen die niet bestaan. Geen Spotify, geen Pitchfork — pure fantasy."

*[Wijs naar de gele 'Waarom een fictief festival' callout]*

Vertel: "Waarom fictief? Omdat geen enkele LLM dit kan weten. Geen training data over Polderfest 2027 — bestaat niet. En dat is precies wat we willen demonstreren: AI alleen kan dit niet. AI mét onze data, wél."

*[Wijs naar het schema-blok]*

Vertel: "Onze tabel heeft deze velden: naam, genre, sub-genre, stage, dag, starttijd, stad, members, bio, tier, populariteit, ticket-impact. Genoeg variatie voor leuke vragen straks."

*[Wijs naar voorbeeld-vragen]*

Vertel: "Dingen die we straks aan onze AI gaan vragen: welke bands spelen vrijdagavond op de Main Stage? Vat de hip-hop scene samen. Welke acts komen uit Groningen? Allemaal vragen die ChatGPT niet kan beantwoorden — want hij weet niets van Polderfest. Maar onze chat straks wel."

*[Pauze, ademen]*

Vertel: "Goed — laten we 't gaan bouwen. Eerst Next.js en Supabase opzetten."


[SLIDE 8 — LIVE DEMO 1] [SCHERM: slides]

Vertel: "Dit zijn de 6 stappen die we nu gaan doorlopen. Ongeveer 20 minuten. Volg even mee — niet meetypen, kijk."

[SCHERM: terminal]

Vertel: "We beginnen in de terminal."

Stap 1 — Next.js scaffolden

cd ~
npx create-next-app@latest polderfest-demo \
  --typescript --tailwind --app --eslint --no-src-dir --turbopack

*[Druk enter, wacht ~30 sec]*

Vertel terwijl het installeert: "Standaard Next.js 15 met App Router, Tailwind, TypeScript. App Router omdat we Server Components willen. Tailwind voor styling. Niets bijzonders aan deze setup — dit kennen jullie al uit eerdere lessen."

*[Wacht tot install klaar is]*

cd polderfest-demo
code .

[SCHERM: editor]

Vertel: "Editor open. Niets in app/page.tsx — standaard Next.js welkomstpagina. Standaard app/layout.tsx. Tailwind config. Niks bijzonders."

Stap 2 — Supabase project aanmaken

[SCHERM: browser → supabase.com/dashboard]

Vertel: "Nu Supabase. Ik heb nog géén project voor deze demo — we maken er een nieuwe."

*[Klik New Project]*

Vertel: "Naam: polderfest-demo. Database password — kies wat, hoeft niet kopiëren. Region: West Europe. Submit."

*[Wacht ~2 min — gebruik deze tijd voor de uitleg hieronder]*

Vertel: "Terwijl het deployt: waarom een nieuw project? Omdat we van scratch beginnen. Geen vermenging met je QuickPoll-data van eerdere lessen. Clean slate. Voor je eindopdracht en huiswerk geldt: één app = één Supabase project."

Stap 3 — Schema runnen

*[Supabase deploy is klaar]*

[SCHERM: supabase → SQL Editor]

Vertel: "Schema-tijd. Open SQL Editor. New Query."

*[Plak inhoud van schema.sql]*

Vertel: "Dit is mijn schema voor de bands-tabel. Naam, genre, stage, dag, tijd, members, bio. Een paar indexen voor performance. RLS aan en een policy: bands zijn publiek leesbaar. Voor onze chat hebben we read-access nodig, geen schrijfrechten."

*[Klik Run]*

[SCHERM: supabase → Table Editor]

Vertel: "Check — tabel bands bestaat. Leeg. Klaar om gevuld te worden."

Stap 4 — Env vars

[SCHERM: supabase → Settings → API]

Vertel: "Drie dingen pak ik hier op: de Project URL, de anon public key, en de service_role secret key. Die laatste is belangrijk — straks bij het seed-script."

*[Kopieer alle drie]*

[SCHERM: editor → .env.local]

NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENAI_API_KEY=sk-proj-...

Vertel: "Belangrijke nuance — let goed op de namen:

  • NEXT_PUBLIC_SUPABASE_URL en _ANON_KEY — die staan in client-bundle. Mag — anon key heeft alleen leesrechten via RLS.
  • SUPABASE_SERVICE_ROLE_KEY — geen NEXT_PUBLIC_ prefix. Server-only. Deze key bypasst RLS — daarmee kan alles. Lekken = ramp. Gebruiken we alleen lokaal voor het seed-script.
  • OPENAI_API_KEY — geen NEXT_PUBLIC_ prefix. Anders zit-ie in je client-bundle en kan iedereen 'm gebruiken op jouw kosten. Server-only altijd."

Stap 5 — Supabase client

[SCHERM: editor]

*[Nieuwe file: lib/supabase.ts]*

import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

Vertel: "Standaard Supabase client. Voor onze chat — dat is alles wat we nodig hebben. Niets bijzonders. Kennen jullie."

Stap 6 — Dev server check

[SCHERM: terminal]

npm run dev

[SCHERM: browser → localhost:3000]

Vertel: "Standaard Next.js welkomstpagina. Werkt. Supabase staat. Schema staat. Klaar voor data."


BLOK 4 — Live Demo 2: Seed script — 500 records (20 min)

[SLIDE 9 — LIVE DEMO 2] [SCHERM: slides]

Vertel: "Demo 2. We gaan onze bands-tabel vullen. Niet handmatig — met een seed-script. 500 records in ~30 seconden."

[SCHERM: editor]

*[Maak folder: scripts/. Plaats seed-polderfest.ts erin]*

Stap 1 — Het seed-script bekijken

[SCHERM: editor → seed-polderfest.ts]

Vertel: "Dit is mijn seed-script. 200 regels. Laat me even door de structuur lopen — niet alle regels lezen, alleen de aanpak."

*[Scroll naar top]*

Vertel: "Bovenaan: Supabase client. Belangrijk — met de service role key. Niet anon. Want we gaan inserts doen. RLS blokkeert dat anders."

*[Scroll naar de bouwstenen arrays]*

Vertel: "Hier zijn mijn bouwstenen. Adjectives — 'Lost', 'Velvet', 'Iron', 'Neon'. Nouns — 'Tigers', 'Wolves', 'Mirrors'. Cities — Nederlandse steden. Genres — muziekgenres."

*[Scroll naar generateBandName]*

Vertel: "Hier de naam-generator. Vier patronen:

  • 'Lost Tigers' — adjective + noun
  • 'De Wolves' — Dutch prefix + noun
  • 'Sanne Van Dijk' — solo artist
  • 'Sanne & The Wolves' — solo + collectief

Met 30 adjectives × 30 nouns = al 900 unieke combinaties mogelijk. Genoeg voor 500 records."

*[Scroll naar generateBio]*

Vertel: "Bio's. Drie blokken — opening, middle, ending — gecombineerd. 'Begonnen in een garage in [stad]', '[band] experimenteert met analoge synths', 'Debuut-EP eind 2027'. Compositioneel. Geen handmatig getypte bio's — 500× zou krankzinnig zijn."

*[Scroll naar bottom — async function seed]*

Vertel: "De main functie. Genereert 500 bands, dedupe op naam, insert in batches van 100 — Supabase trekt 500 in één keer niet altijd. Done."

Stap 2 — Service role key uitleggen

Vertel: "Even pauze voor één belangrijk ding — de service role key. Die zit boven aan dit script. Drie regels die jullie moeten onthouden:

  1. Alleen lokaal gebruiken. Niet in productie code. Niet in client. Alleen scripts.
  2. Nooit committen naar git. Service role key in .env.local, en .env.local in .gitignore.
  3. Lekt-ie? Direct draaien in Supabase dashboard → Settings → API → Reset service role key.

Vergelijk het met een root password. Behandel 'm zo."

Stap 3 — Dependencies (was al klaar — kort tonen)

[SCHERM: terminal]

# We hebben deze al uit setup, maar voor je eigen project:
npm i @supabase/supabase-js dotenv
npm i tsx --save-dev

Stap 4 — Run het seed-script

[SCHERM: terminal]

npx tsx scripts/seed-polderfest.ts

Vertel terwijl het runt: "Daar gaat 'ie. tsx is een TypeScript-runner die geen build-stap nodig heeft. dotenv leest de .env.local automatisch. 500 bands genereren, vijf batches van 100, klaar."

*[Wacht ~10-30 sec, output verschijnt]*

Genereren van 500 Polderfest bands...
Schrijven naar Supabase in batches van 100...
  ✓ 100/500
  ✓ 200/500
  ✓ 300/500
  ✓ 400/500
  ✓ 500/500
Klaar! 500 Polderfest bands staan in Supabase.

Vertel: "Done."

Stap 5 — Verificatie

[SCHERM: supabase → Table Editor → bands]

*[Klik refresh — 500 records verschijnen]*

Vertel: "500 bands. Allemaal verzonnen. Laten we er even één openklikken."

*[Klik op willekeurige rij, toon bio]*

Vertel: "Kijk — 'Begonnen in een garage in Groningen, De Tigers experimenteert met analoge synths en gefluisterde lyrics. Polderfest is hun grootste festival tot nu toe.' Compleet verzonnen. Geen Wikipedia, geen Spotify — pure fantasy. Maar overtuigend genoeg voor onze AI om mee te werken."

Stap 6 — Quick check met SQL

[SCHERM: supabase → SQL Editor]

select genre, count(*) from bands group by genre order by count desc;

*[Run]*

Vertel: "Genre-verdeling. ~30 per genre. Mooi gespreid. Klaar om mee te chatten."


BLOK 5 — Pauze (15 min)

[SLIDE 10 — Pauze] [SCHERM: slides]

Vertel: "Pauze. 15 minuten. Tot zo."

*[Coffee. Stretch. Check je OpenAI key nog even.]*


BLOK 6 — Live Demo 3: AI SDK + chat-route (30 min)

[SLIDE 11 — LIVE DEMO 3] [SCHERM: slides]

Vertel: "Welkom terug. Nu de echte AI-stap. We bouwen een chat-route in onze API en een chat-pagina in Next.js. Daarmee kunnen we vragen stellen aan onze Polderfest-data."

[SCHERM: terminal]

Stap 1 — Packages

npm i ai @ai-sdk/openai zod

Vertel: "Drie packages. ai is de SDK zelf. @ai-sdk/openai is de provider — we gebruiken OpenAI vandaag. zod is voor schema validatie. Vandaag gebruiken we 'm niet, maar volgende les wel."

Stap 2 — Chat API route

[SCHERM: editor]

*[Maak file: app/api/chat/route.ts. Typ live, niet pasten — geeft studenten tijd om te volgen]*

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

export async function POST(req: Request) {
  const { messages } = await req.json();

Vertel: "API route. POST, want we ontvangen messages van de chat. We destructuren de messages-array."

  // 1. Haal alle bands op uit Supabase
  const { data: bands, error } = await supabase.from("bands").select("*");
  if (error) throw error;

Vertel: "Stap 1 — ALLE bands ophalen uit Supabase. Voor 500 records werkt dit prima. Voor 50.000 niet — volgende les lossen we dat op met Tool Calling. Vandaag: simpele aanpak, alles meesturen."

  // 2. Format bands als context-string
  const context = bands!
    .map((b) =>
      `- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time} ` +
      `op ${b.stage}, uit ${b.origin_city})`
    )
    .join("\n");

Vertel: "Stap 2 — we maken één grote tekst-context. Per band één regel met de belangrijkste velden. AI kan namelijk geen SQL, maar wel tekst lezen."

  // 3. System prompt met context
  const system = `Je bent een festival-assistent voor Polderfest 2027.
Hier zijn alle bands die op het festival spelen:

${context}

Beantwoord vragen van bezoekers over de line-up. Verzin niets — gebruik
alleen bovenstaande data. Antwoord in het Nederlands. Wees beknopt.`;

Vertel: "Stap 3 — de system prompt. Dit is de rol die AI krijgt. Drie belangrijke instructies:

  1. 'Verzin niets' — voorkomt hallucinaties.
  2. 'Gebruik alleen bovenstaande data' — niet uit training-kennis halen.
  3. 'Antwoord in het Nederlands' — anders krijg je Engels.

Een goede system prompt is je hefboom. 50% van de kwaliteit komt hier."

  // 4. Stream naar OpenAI
  const result = streamText({
    model: openai("gpt-4o-mini"),
    system,
    messages,
  });

  return result.toDataStreamResponse();
}

Vertel: "Stap 4 — de AI-call zelf. streamText — onze keuze van vandaag. Model gpt-4o-mini. System message, plus de berichten van de user. En result.toDataStreamResponse() zet 't om naar het juiste streaming-format voor useChat aan de client-kant."

Vertel: "API route klaar."

Stap 3 — Chat pagina

*[Nieuwe file: app/chat/page.tsx]*

"use client";

import { useChat } from "ai/react";

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, status } =
    useChat();

  return (
    <main className="max-w-2xl mx-auto p-6 flex flex-col h-screen">
      <h1 className="text-2xl font-bold mb-4">Polderfest 2027  vraag de AI</h1>

      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={
              m.role === "user"
                ? "bg-blue-50 p-3 rounded-lg ml-12"
                : "bg-gray-50 p-3 rounded-lg mr-12"
            }
          >
            <div className="font-medium text-sm text-gray-500 mb-1">
              {m.role === "user" ? "Jij" : "Festival AI"}
            </div>
            <div className="whitespace-pre-wrap">{m.content}</div>
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Stel een vraag over de line-up..."
          className="flex-1 p-3 border rounded-lg"
          disabled={status !== "ready"}
        />
        <button
          type="submit"
          disabled={status !== "ready"}
          className="px-6 py-3 bg-blue-600 text-white rounded-lg disabled:opacity-50"
        >
          Stuur
        </button>
      </form>
    </main>
  );
}

Vertel: "Client component — 'use client' bovenaan. useChat hook regelt alles: messages-state, input-state, submit-handler, streaming. Vijf properties, geen extra useState nodig.

UI is bewust simpel. Tailwind classes. Berichten van user blauw rechts, AI grijs links. Input + verzenden onderaan. Disabled tijdens streaming via status !== 'ready'."

Stap 4 — Testen

[SCHERM: browser → localhost:3000/chat]

Vertel: "Naar /chat. Eerste vraag."

*[Typ in chat-input]*

Hallo, wie ben jij?

*[Druk Enter. AI antwoordt streamend.]*

Vertel terwijl AI antwoordt: "Daar gaat 'ie. Karakter voor karakter. Streamt. Veel sneller voelend dan wachten op heel antwoord. UseChat regelt het, je hoeft niks zelf te doen voor streaming."

Vertel: "Klaar. Werkt. Nu de leukste vraag: vragen aan onze data."


BLOK 7 — Live Demo 4: Vragen aan onze data (15 min)

[SLIDE 12 — LIVE DEMO 4] [SCHERM: slides]

Vertel: "We gaan vijf vragen stellen. Eén voor één. Bij elke vraag — let op wat de AI doet, en hoe dat anders is dan een SQL query of een gewone chatbot."

[SCHERM: browser → /chat]

Vraag 1 — Filter

*[Type in chat]*

Welke bands spelen zaterdag op de Beach Stage?

*[AI antwoordt — geeft een lijst]*

Vertel: "Filter. AI heeft door de tekst-context gefilterd. Even bevestigen met SQL —"

[SCHERM: supabase → SQL Editor]

select name, start_time from bands
where day = 'Zaterdag' and stage = 'Beach Stage';

*[Run]*

Vertel: "Zelfde resultaat. Maar het verschil — onze chat geeft natuurlijke taal, kan vervolgvragen aan, kan samenvatten. SQL doet alleen filter + select."

Vraag 2 — Sort + Reasoning

[SCHERM: browser → /chat]

Geef me 3 headliners met de meeste popularity, en hun bio's

*[AI antwoordt]*

Vertel: "Sort. AI heeft op popularity gesorteerd en de top 3 gepakt. Plus de bio's erbij."

*[Vraag 2b in zelfde chat:]*

En welke daarvan zou je aanraden voor iemand die houdt van techno?

*[AI antwoordt — beargumenteerd]*

Vertel: "Dit is reasoning. AI redeneert over genre + sub-genre + bio-tekst om een aanbeveling te doen. Een SQL query kan dit niet. Dit is waar AI waarde toevoegt bovenop pure data."

Vraag 3 — Aggregate

Hoeveel jazz fusion acts spelen er totaal op Polderfest?

*[AI antwoordt met een getal]*

Vertel: "Aggregate. AI heeft geteld. Even SQL-bevestigen —"

[SCHERM: supabase → SQL Editor]

select count(*) from bands where genre = 'Jazz Fusion';

Vertel: "Klopt. Goed."

Vraag 4 — Samenvatting (waar AI uitblinkt)

[SCHERM: browser → /chat]

Vat de electronic-scene op Polderfest samen in 3 zinnen

*[AI antwoordt met een echte samenvatting]*

Vertel: "Hier is geen SQL voor. Dit is samenvatten. AI heeft alle electronic acts gelezen, gewogen, en in 3 zinnen samengevat. Dit is de unieke kracht van AI bovenop data."

Vraag 5 — De mist in (bewust)

Wie was de hoofdact van Polderfest 2025?

*[AI antwoordt eerlijk dat hij dit niet weet]*

Vertel: "Goed. Geen verzinsels. AI zegt: 'mijn data is alleen 2027, dat zit er niet in'. Dat is de kracht van onze system prompt. Zonder die prompt zou-ie waarschijnlijk wat hallucineren."

*[Pauze, kijk de klas in]*

Vertel: "Zien jullie wat hier gebeurt? Geen LLM ter wereld kent Polderfest 2027. Geen Wikipedia, geen training-data. Maar onze chat beantwoordt alles — omdat wij de data leveren. AI + data = product."


BLOK 8 — Data + AI = kracht (5 min)

[SLIDE 13 — Data + AI = kracht] [SCHERM: slides]

Vertel: "Reflectie-moment. Drie scenario's:"

*[Wijs naar de grijze box]*

Vertel: "Data alleen. Wat heb je? SQL queries. Filter, sort, select. Geen taal, geen interpretatie. Gebruiker moet zelf SQL kunnen."

*[Wijs naar de roze box]*

Vertel: "AI alleen. ChatGPT zonder context. Generieke kennis uit training. Hallucineert. Geen privé data, geen live data."

*[Wijs naar de blauwe box]*

Vertel: "Data + AI. Wat we vandaag bouwden. Filter via reasoning. Antwoorden in natuurlijke taal. Samenvattingen, vergelijkingen, aanbevelingen. Schaalbaar — voeg data toe en je hebt nieuwe antwoorden mogelijk."

*[Pauze]*

Vertel: "Quote om mee weg te lopen:"

*[Wijs naar de gele callout]*

Vertel: "Een LLM zonder jouw data is een gewone chatbot. Een LLM mét jouw data is een product. Onthoud dit. Dit is de fundering van alle volgende lessen."


BLOK 9 — Lesopdracht + Huiswerk uitleg (20 min)

[SLIDE 14 — Lesopdracht] [SCHERM: slides]

Vertel: "Lesopdracht. Voor thuis — niet vanavond per se, maar vóór volgende les. Je bouwt een eigen versie van wat we vandaag deden. Met je eigen thema."

*[Loop checklist langs op slide]*

Vertel: "Eisen op een rij. Bedenk een eigen thema — moet fictief zijn. Nieuw Next.js project, nieuw Supabase. Eigen seed-script. Minstens 100 records. Chat-route en chat-pagina werkend. Drie vragen stellen die alleen werken dankzij jouw data."

*[Wijs naar pink callout]*

Vertel: "Inspiratie: fictief restaurant-aggregator in een verzonnen stad. Scriptie-archief van NOVI met 1000 nep-titels. Museumcollectie met verzonnen kunstenaars. D&D NPCs. Cryptid-sightings in Nederland.

Belangrijk: moet fictief zijn. Als je echte restaurants in Amsterdam pakt, weet ChatGPT die al — dan zie je niet wat we vandaag demonstreerden. Het hele punt is: data die geen LLM kent."

💬 Verwachte vraag: "Mag ik echt elk thema?" Antwoord: "Ja, zolang het fictief is en minstens 100 records heeft. Twijfel? Stuur 'm op Brightspace, dan kijk ik even mee."

[SLIDE 15 — Huiswerk]

Vertel: "Het huiswerk bouwt voort op de lesopdracht. Drie onderdelen — alle drie verplicht."

*[Loop A, B, C langs op slide]*

Vertel: "Onderdeel A. Pas het Polderfest seed-script aan voor jouw thema. Het script staat klaar als bijlage. Open 't, lees 't, en pas 't aan. AI mag je helpen — letterlijk: open OpenCode, plak m'n script erin, vraag 'pas dit aan voor [mijn thema]'. Klaar in een paar minuten. Daarna jij review. Minstens 200 records.

Onderdeel B. Voeg minstens 1 extra veld toe aan je schema. Iets dat een nieuwe vraag mogelijk maakt. Niet zomaar een extra string-kolom. Concreet voorbeeld: een museumcollectie met acquisition_story veld — dan kun je vragen 'welke kunstwerken zijn op een veiling gekocht?'. Update seed-script, re-seed, test in chat.

Onderdeel C. Schrijf een AI-CHAT.md in je repo-root. Drie secties:

  • Mijn thema — wat is het, waarom kan een gewone LLM dit niet?
  • 3 leuke vragen die werken
  • 1 vraag waar AI moeite mee had + hoe je je prompt aanpaste."

*[Wijs naar gele callout]*

Vertel: "Bonus, geen verplichting: deploy op Vercel, loading skeleton, vergelijk gpt-4o-mini en gpt-4o."

💬 Verwachte vraag: "Hoe lang gaat dit duren?" Antwoord: "Lesopdracht ~2,5 uur. Huiswerk ~1,5 tot 2 uur. Samen een lange middag. Loop je vast — Brightspace of plan een korte 1-op-1 met me."


BLOK 10 — Vragen + Afsluiting (15 min)

[SLIDE 16 — Volgende les: Tool Calling] [SCHERM: slides]

Vertel: "Eén ding voor we afronden — wat komt hierna."

*[Wijs naar pink callout: Het schaal-probleem]*

Vertel: "Wat we vandaag deden: ALLE 500 bands sturen we mee als context bij elke vraag. Dat is ~30.000 tokens per call. Werkt prima voor 500. Werkt niet voor 5.000 records. En al helemaal niet voor 50.000."

*[Wijs naar blauwe callout: De oplossing]*

Vertel: "Volgende les — Tool Calling. In plaats van alle data meesturen, geef je AI functies die hij zelf kan aanroepen. Hij hoort vraag 'welke bands op vrijdag?' en besluit: ik roep searchBands({ day: 'Vrijdag' }) aan. Krijgt 60 bands terug. Antwoordt. Schaalbaar tot duizenden records."

Vertel: "Daarna in deze leerlijn: Agents met maxSteps. RAG met embeddings — semantic search op heel grote datasets. Testing, deployment, performance. En de laatste twee lessen: eindopdracht-werkdagen en je pitch."

[SLIDE 17 — Afsluiting]

Vertel: "Vragen?"

*[Open de vloer. Verwachte vragen + antwoorden:]*

💬 "Wat als ik geen schoolkey heb?" → "Eigen OpenAI account — $5 starter credit zit erin gratis. Of Groq — gratis tier. Of Anthropic — $5 gratis credits."

💬 "Hoe weet ik welk model het beste is?" → "Start met gpt-4o-mini. Upgrade alleen als het écht niet werkt. Premature optimization is een valkuil."

💬 "Kan dit lokaal zonder OpenAI?" → "Ja, via Ollama. Niet vereist voor deze les. Komt eventueel in latere lessen."

💬 "Moet ik de Polderfest demo zelf ook namaken?" → "Nee. Wat we vandaag deden is jullie laten zien. Voor jezelf bouwen = eigen thema, in lesopdracht en huiswerk."

💬 "Hoe duur is dit nou echt?" → "Onze hele les vandaag met 500 bands en 10 vragen — onder de 2 cent. Met gpt-4o (de grote) zou hetzelfde ~30 cent zijn. Met mini blijft het peanuts."

*[Sluit af]*

Vertel: "Zorg dat je vóór volgende les minstens je seed-script werkend hebt voor jouw thema. Dan kunnen we volgende les meteen Tool Calling toepassen. Tot dan!"


Backup-onderwerpen (als tijd over is)

  1. Andere provider tonen — Open route.ts, vervang openai("gpt-4o-mini") door anthropic("claude-sonnet-4"). Werkt direct. Eén regel.
  2. System prompt fine-tuning — Verzwak de prompt ("Help bij vragen"). Vraag iets. Verzin de fout. Versterk weer ("Verzin niets, gebruik alleen onze data"). Toon verschil.
  3. Token-kosten dashboard — Open platform.openai.com/usage. Toon je verbruik van vandaag — letterlijk een paar cent.
  4. Privacy / data retention — Wat gaat naar OpenAI? Zero-data-retention via EU-endpoints. Belangrijk voor productie.
  5. Hallucinatie-test — Probeer met zwakke prompt of de AI iets verzint over Polderfest 2025. Toon dat sterkere prompt dit fixt.