Files
novi-lessons/Les12-Tool-Calling/Les12-Docenttekst.md
2026-05-21 08:52:47 +02:00

27 KiB
Raw Blame History

Les 12 — Tool Calling

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

Les: 12 van 18 Onderwerp: Tool Calling — AI besluit zelf welke functie aan te roepen Duur: 180 minuten Format: Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = thuis. Demo-app: Polderfest 2027 (verder bouwen op Les 11)


Hoe deze tekst werkt

Dit document is een lopend script. Lees mee tijdens de les op je laptop.

  • [SLIDE X] — Klik naar slide X
  • [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 (45 min)

1. Voortbouwen op Les 11 demo

  • Open polderfest-demo repo uit Les 11
  • Check Supabase dashboard: nog 500 bands aanwezig?
  • Zo niet: npx tsx scripts/seed-polderfest.ts om opnieuw te seeden
  • Open polderfest-demo in editor

2. Schema check — user_favorites tabel

In Supabase SQL Editor:

create table if not exists user_favorites (
  id          bigserial primary key,
  user_email  text not null,
  band_id     bigint not null references bands(id) on delete cascade,
  created_at  timestamp default now(),
  unique(user_email, band_id)
);

alter table user_favorites enable row level security;

create policy "Favorites zijn publiek leesbaar (demo)"
  on user_favorites for select using (true);

create policy "Anyone kan favorites toevoegen (demo)"
  on user_favorites for insert with check (true);

*[In productie zou je dit aan auth.uid() koppelen. Voor demo: open.]*

3. Tools-demo bestand klaarzetten

  • Plaats tools-demo.ts ergens als referentie (open in editor tab)
  • Niet kopiëren — naslag tijdens demo

4. Reset chat-route uit Les 11

  • Open app/api/chat/route.ts
  • Wijs naar de oude versie (const { data: bands } = ... + grote context-string)
  • Klaar om straks live te refactoren

5. Browser tabs

6. Backup

  • Werkende eindstaat (route.ts met alle tools) op USB
  • Verwacht: 1 of 2 typos tijdens live coding — geen ramp, fix klassikaal

HET SCRIPT — Lees mee tijdens de les

BLOK 1 — Welkom + Terugblik + Probleem (10 min)

[SLIDE 1] [SCHERM: slides]

Vertel: "Welkom bij les 12. Vandaag gaan we het probleem oplossen dat we vorige les introduceerden — onze chat schaalt niet. We doen dat met Tool Calling: AI besluit zelf welke functie hij aanroept om aan informatie te komen."

[SLIDE 2 — Terugblik + schaalprobleem]

Vertel: "Vorige les bouwden we Polderfest 2027. Vercel AI SDK, 500 bands in Supabase, chat-pagina die vragen beantwoordt. Goed werk.

Maar er was een probleem. Bij elke vraag stuurden we alle 500 bands mee als tekst in de context. Ongeveer 30.000 tokens per call. Werkt prima voor 500 — werkt niet voor 50.000 records, voor 5.000 al moeilijk. Te duur, te traag, past niet in context-window."

*[Wijs naar de pijl op slide]*

Vertel: "Vandaag draaien we het om. Niet de hele database mee, maar functies waar AI uit kan kiezen. AI ziet een vraag, denkt 'oh, dan roep ik functie X aan', krijgt het resultaat, antwoordt. Schaalbaar, slim, en — bonus — ook write-acties mogelijk: 'voeg toe aan mijn favorieten'."

[SLIDE 3 — Planning]

Vertel: "Planning. Eerst 30 minuten theorie. Dan vier live demo's, verspreid voor en na pauze. Lesopdracht en huiswerk leg ik aan het eind uit.

Dit is opnieuw een kijk-les. Jullie typen niet mee. Notitieboek wel."


BLOK 2 — Theorie: wat is Tool Calling? (30 min)

[SLIDE 4 — Wat is Tool Calling] [SCHERM: slides]

Vertel: "Tool Calling. Het idee is simpel. In plaats van alle data meesturen, geef je AI tools — functies die hij mag aanroepen. AI leest jouw vraag, kijkt naar de beschikbare tools, kiest welke relevant is, roept 'm aan met de juiste parameters, krijgt resultaat terug, en formuleert dan een antwoord."

*[Wijs naar het flow-blok]*

Vertel: "Voorbeeld. User vraagt: 'Welke bands spelen vrijdag op de Main Stage?' AI herkent: 'Aha, dat is een zoekvraag, ik roep searchBands aan met day: 'Vrijdag' en stage: 'Main Stage''. Supabase returnt 12 bands. AI formuleert: 'Op vrijdag op de Main Stage spelen: ...'."

Vertel: "Wat win je hiermee?

  • Schaalbaar. 10 records of 10 miljoen — voor de AI maakt het niks uit, hij krijgt alleen de relevante set terug.
  • Real-time. Geen verouderde snapshot van data. Tool draait elke keer opnieuw.
  • Type-safe. Via Zod schema's weet AI exact welke parameters mogen.
  • Multi-step. Hij mag meerdere tools achter elkaar gebruiken voor complexe vragen."

[SLIDE 5 — Anatomie van een tool]

Vertel: "Hoe ziet een tool eruit in code? Drie delen, alle drie verplicht."

*[Wijs naar code-blok]*

Vertel: "description — wat doet deze tool? Dit leest AI om te beslissen welke tool relevant is. Vaag beschreven = verkeerde tool gekozen. Cruciaal om dit goed te schrijven.

parameters — wat heeft de tool nodig? Zod schema. Type-safe, gevalideerd, geforceerd door AI. Hier kan ik enums geven — 'day' is alleen Vrijdag/Zaterdag/Zondag. Probeert AI iets anders? Krijgt-ie een error.

execute — wat gebeurt er als de tool wordt aangeroepen? Async functie. Hier zit jouw Supabase query, je API-call, je business logic."

*[Pauze]*

Vertel: "Eén tip die later gaat schelen: schrijf je descriptions duidelijk. AI kiest de tool op basis daarvan. Schrijf je 'searchBands' met description 'iets met bands' — dan kiest hij verkeerd. Schrijf je 'zoek bands op dag, stage, genre of tier; gebruik voor filtervragen' — dan klopt het."

[SLIDE 6 — Multi-step met stopWhen]

Vertel: "En dan multi-step. Met stopWhen: stepCountIs(5) geef je AI toestemming om tot 5 keer een tool aan te roepen voordat hij definitief antwoordt."

*[Wijs naar voorbeeld]*

Vertel: "Stel: 'Vergelijk de top headliner met de drukst geplande opener'. Eén tool-call is niet genoeg — AI moet twee queries doen, daarna vergelijken. Met stopWhen werkt dat in één request:

  • Stap 1: searchBands({ tier: 'headliner' }) — 50 bands terug
  • Stap 2: searchBands({ tier: 'opener' }) — 100 bands terug
  • Stap 3: AI verwerkt + vergelijkt + antwoordt"

Vertel: "Default is meestal 1 stap — geen multi-step. Je moet expliciet stopWhen zetten om hem multi-step te laten doen."

*[Pauze]*

[SLIDE 7 — Vandaag bouwen we]

Vertel: "Vandaag refactoren we Polderfest. Stap voor stap. Eerst de oude chat-route slopen — geen alle-bands-meesturen meer. Eén tool toevoegen. Dan meer tools. Dan in de UI tonen welke tools AI aanriep. Tot slot: edge cases en errors."

*[Wijs naar tools-tabel]*

Vertel: "Zes tools gaan we bouwen — vijf read en één write. searchBands met filters, getBandByName voor exacte lookup, getStats voor aggregaties, getScheduleByDay voor tijdschema's, addFavorite om favorieten op te slaan, listFavorites om ze op te halen. De write-tool is interessant — eerste keer dat AI iets in onze database wijzigt, niet alleen leest."

Vertel: "Klaar voor de demo? Daar gaan we."


BLOK 3 — Live Demo 1: Eerste tool (20 min)

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

Vertel: "Demo 1. We refactoren de chat-route en zetten één tool op."

[SCHERM: editor → app/api/chat/route.ts]

Stap 1 — Oude code wegsmijten

Vertel: "Hier is de chat-route van vorige les. Bovenaan: Supabase client. Dan: alle 500 bands ophalen, formatteren als string, system prompt met die hele lap erin. Weg ermee."

*[Selecteer de hele functie body — alles tussen { en } van POST — verwijder]*

Vertel: "Wat blijft staan: de imports, de Supabase client, de POST-handler-shell. Rest gaan we anders bouwen."

Stap 2 — Tool importeren

*[Boven in file, naast streamText/openai import:]*

import { streamText, tool } from "ai";
import { z } from "zod";

Vertel: "tool helper uit ai package. Zod voor het parameter-schema. Beide gebruiken we straks."

Stap 3 — searchBands tool definiëren

*[Boven de POST-functie:]*

const searchBands = tool({
  description:
    "Zoek bands in de Polderfest line-up. Filter op dag, stage, genre, " +
    "of tier. Gebruik dit als de gebruiker iets zoekt op één of meerdere criteria.",
  inputSchema: z.object({
    day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional()
      .describe("Festival-dag"),
    stage: z.string().optional()
      .describe("Bv. Main Stage, Tent Stage, Beach Stage"),
    genre: z.string().optional()
      .describe("Bv. Indie Rock, Electronic, Hip-Hop"),
    tier: z.enum(["headliner", "mid", "opener"]).optional(),
  }),
  execute: async ({ day, stage, genre, tier }) => {
    let q = supabase.from("bands").select(
      "name, genre, stage, day, start_time, tier, popularity"
    );
    if (day) q = q.eq("day", day);
    if (stage) q = q.eq("stage", stage);
    if (genre) q = q.eq("genre", genre);
    if (tier) q = q.eq("tier", tier);
    const { data, error } = await q.limit(20);
    if (error) return { error: error.message };
    return { count: data.length, bands: data };
  },
});

Vertel terwijl je typt:

  • Bij description: "Schrijf alsof je 't aan een collega uitlegt. Wat doet 't, en wanneer gebruikt AI 'm. Twee zinnen meestal genoeg."
  • Bij inputSchema: "Zod schema. day is enum — alleen Vrijdag, Zaterdag, Zondag. .optional() want misschien wil de gebruiker geen day-filter. .describe() op elke parameter — AI gebruikt dit ook."
  • Bij execute: "Standaard Supabase query met chained filters. Returnt { error } of { count, bands }. Limit 20 zodat we niet teveel terugkrijgen."

Stap 4 — POST-functie aanpassen

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

  const system = `Je bent een festival-assistent voor Polderfest 2027.
Gebruik de beschikbare tools om vragen te beantwoorden over de bands.
Verzin nooit data — als je 't niet weet, zeg dat. Antwoord in het Nederlands.`;

  const result = streamText({
    model: openai("gpt-4o-mini"),
    system,
    messages,
    tools: { searchBands },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}

Vertel:

  • "System prompt is veel korter — geen bands-context meer. Alleen instructies. 'Gebruik tools, verzin niets.'"
  • "tools: { searchBands } — voor nu één tool. Later breiden we uit."
  • "stopWhen: stepCountIs(5) — geef AI ruimte voor multi-step. Voor één query genoeg, voor complexe vragen straks ook."

*[Save]*

Stap 5 — Testen

[SCHERM: browser → localhost:3000/chat]

*[Refresh pagina]*

Welke bands spelen zaterdag op de Beach Stage?

*[Druk Enter, wacht op antwoord. AI moet searchBands aanroepen.]*

Vertel: "Daar gaat 'ie. AI denkt na, roept searchBands aan met day: 'Zaterdag' en stage: 'Beach Stage', krijgt resultaten, formuleert antwoord."

*[Wijs naar het antwoord]*

Vertel: "Werkt. Maar op dit moment zien we niet welke tool aangeroepen werd — dat gaan we straks in de UI fixen. Voor nu: het werkt, en — als ik in mijn terminal kijk —"

[SCHERM: terminal — dev server logs]

Vertel: "Daar zie ik de query gerund. Veel sneller dan vorige les met 500 bands meesturen. En als ik nu duizenden records zou hebben? Maakt voor de chat geen verschil."


BLOK 4 — Live Demo 2: Multi-step + meer tools (20 min)

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

Vertel: "Eén tool werkt. Nu meer tools, en multi-step in actie."

[SCHERM: editor → app/api/chat/route.ts]

Stap 1 — getStats tool

*[Boven POST, na searchBands:]*

const getStats = tool({
  description:
    "Geef statistieken over de festival-line-up — totaal aantal bands, " +
    "verdeling per genre, per dag, of per stage. Geen filters — overzicht.",
  inputSchema: z.object({
    groupBy: z.enum(["genre", "day", "stage", "tier"])
      .describe("Hoe te groeperen"),
  }),
  execute: async ({ groupBy }) => {
    const { data, error } = await supabase
      .from("bands")
      .select(groupBy);
    if (error) return { error: error.message };

    const counts: Record<string, number> = {};
    for (const row of data) {
      const key = row[groupBy as keyof typeof row] as string;
      counts[key] = (counts[key] ?? 0) + 1;
    }
    return { total: data.length, counts };
  },
});

Vertel: "getStats. Eén verplichte parameter — groupBy, een enum. Execute: kolom ophalen, in JavaScript tellen, terugsturen. Voor 'hoeveel jazz acts' of 'verdeling over dagen'."

Stap 2 — getBandByName tool

const getBandByName = tool({
  description:
    "Haal alle details op van één specifieke band, inclusief members en bio. " +
    "Gebruik dit als de gebruiker naar een specifieke band vraagt bij naam.",
  inputSchema: z.object({
    name: z.string().describe("Exacte band-naam"),
  }),
  execute: async ({ name }) => {
    const { data, error } = await supabase
      .from("bands").select("*").ilike("name", name).single();
    if (error) return { error: `Band '${name}' niet gevonden.` };
    return data;
  },
});

Vertel: "Voor 'vertel me over Lost Tigers'. ilike voor case-insensitive match. .single() — verwacht één resultaat."

Stap 3 — Tools registreren

*[Pas streamText aan:]*

tools: { searchBands, getStats, getBandByName },
stopWhen: stepCountIs(5),

*[Save]*

Stap 4 — Multi-step in actie

[SCHERM: browser → /chat]

*[Nieuwe chat — refresh]*

Hoeveel jazz fusion acts spelen er totaal? En geef me daarvan de top 3 qua populariteit.

*[AI roept eerst getStats, dan searchBands aan. Beide tools in één request.]*

Vertel: "Hier zie je multi-step. AI dacht: 'eerst telling — getStats. Dan top 3 — searchBands met tier of popularity filter.' Twee tool-calls, één antwoord. Dat is stopWhen in actie."

*[Volgende vraag:]*

Vertel me over een specifieke band die je interessant vindt. Kies er één.

*[AI roept searchBands aan om opties te zien, kiest er één, roept getBandByName aan voor details]*

Vertel: "Twee stappen weer. Zonder stopWhen had-ie maar één tool kunnen aanroepen — moeilijk om eerst opties te zien en dan detail te kiezen."

Stap 5 — Hidden complexity

*[Open Vercel-style network tab, of dev console]*

Vertel: "Onder de motorkap: per gebruikersvraag stuurt AI z'n plan terug. Hij kan na stap 1 zien wat het resultaat was, en op basis daarvan beslissen voor stap 2. Dat is wat 'autonoom' AI doet zonder dat jij elke stap voorprogrammeert. Volgende les zien we hoe ver dit gaat — Agents."


BLOK 5 — Pauze (15 min)

[SLIDE 10 — Pauze] [SCHERM: slides]

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


BLOK 6 — Live Demo 3: Tool-calls in UI (25 min)

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

Vertel: "We hebben tools werkend. Maar in de chat zien we nog niet welke tool aangeroepen werd. Dat fix ik nu. Twee redenen: debugging tijdens dev, en gebruikersvertrouwen — 'ja hij heeft echt de DB geraadpleegd'."

[SCHERM: editor → app/chat/page.tsx]

Stap 1 — Messages.parts uitleggen

Vertel: "Tot nu toe gebruikten we message.content — één string. Maar als AI tools aanroept, krijg je parts — een array van delen. Tekst-parts en tool-parts (in v6 zijn die genaamd tool-<toolName>). We gaan die parts mappen."

Stap 2 — Refactor de rendering

*[Selecteer de hele messages.map(...) — vervang door:]*

{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>

    {m.parts?.map((part, i) => {
      if (part.type === "text") {
        return (
          <div key={i} className="whitespace-pre-wrap">
            {part.text}
          </div>
        );
      }

      // In AI SDK v6 zijn tool-parts genaamd `tool-<toolName>`
      if (part.type?.startsWith("tool-")) {
        const toolName = part.type.replace("tool-", "");
        return (
          <div
            key={i}
            className="my-2 p-2 bg-yellow-50 border border-yellow-300 rounded text-sm font-mono"
          >
            🔧 <span className="font-semibold">{toolName}</span>
            ({JSON.stringify(part.input)})
            {part.state === "output-available" && (
              <details className="mt-1">
                <summary className="cursor-pointer text-xs text-gray-500">
                  Toon resultaat
                </summary>
                <pre className="text-xs mt-1 overflow-auto max-h-40">
                  {JSON.stringify(part.output, null, 2)}
                </pre>
              </details>
            )}
          </div>
        );
      }

      return null;
    })}
  </div>
))}

Vertel terwijl je typt:

  • "m.parts.map — loop door alle parts."
  • "Text-parts — gewoon de tekst tonen."
  • "Tool-parts (type begint met tool-) — geel chip met tool-naam en args. Detail-element met collapsible result."
  • "state === 'output-available' — alleen als tool al gerund heeft, tonen we resultaat. Tijdens streaming kan dit kort input-streaming of input-available zijn."

Stap 3 — Test

[SCHERM: browser → /chat]

*[Refresh, nieuwe chat]*

Welke bands uit Groningen?

*[Antwoord komt — nu zie je gele chip: 🔧 searchBands({...})]*

Vertel: "Daar — chip met tool-naam en argumenten. Klik op 'Toon resultaat'..."

*[Klik details open — JSON resultaat verschijnt]*

Vertel: "...JSON-output van Supabase. Volledig transparant. Studenten — als jullie thuis vastlopen, dit is je debug tool. Zie je wat AI aanroept met welke args, en wat 't terugkrijgt."

Stap 4 — Multi-step visualiseren

*[Volgende vraag:]*

Hoeveel hip-hop acts zijn er? En geef me daarvan de populairste.

*[Twee chips verschijnen — getStats én searchBands]*

Vertel: "Twee chips. Twee tool-calls. Multi-step zichtbaar gemaakt."


BLOK 7 — Live Demo 4: Edge cases + errors (15 min)

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

Vertel: "Tools werken. UI toont alles. Maar wat als dingen mis gaan? Vier edge cases."

[SCHERM: browser → /chat]

Edge case 1 — Ongeldige input

Welke bands op Donderdag?

Vertel: "Donderdag is geen Polderfest-dag. Onze enum is Vrijdag/Zaterdag/Zondag."

*[AI's reactie — meestal antwoordt-ie 'er is geen donderdag, alleen vrij-za-zo' zonder tool-call. Of probeert iets anders.]*

Vertel: "Mooi. AI kreeg de enum-restrictie mee uit het schema, weet dat Donderdag niet kan. Geen verspilde tool-call."

Edge case 2 — Lege resultaten

Death metal bands?

*[AI roept searchBands aan met genre: 'Death Metal'. Krijgt lege array.]*

Vertel: "Tool returnt 0 bands. AI legt dat netjes uit — 'geen death metal op Polderfest 2027'. Geen verzinsels, geen hallucinatie. Goed."

Edge case 3 — Database error (simuleer)

*[Open route.ts, voeg tijdelijk error toe in searchBands execute:]*

execute: async ({ day, stage, genre, tier }) => {
  // Tijdelijke testlijn:
  return { error: "Database timeout — probeer opnieuw" };
  // (rest van execute hieronder)
}

*[Save, vraag opnieuw:]*

Welke bands spelen vrijdag?

*[AI krijgt { error: "..." } terug. Antwoordt netjes.]*

Vertel: "AI vertaalt onze error naar een gebruikersvriendelijke melding. Geen stack-trace, geen tech-jargon. Dat is precies wat je wilt voor productie."

*[Verwijder de testlijn, save]*

Edge case 4 — Write tool met confirmation

Vertel: "Laatste edge case — een write-tool. We voegen addFavorite toe."

*[Plak addFavorite tool uit tools-demo.ts in route.ts:]*

const addFavorite = tool({
  description:
    "Voeg een band toe aan favorieten. Alleen gebruiken als gebruiker " +
    "expliciet vraagt 'voeg X toe aan mijn favorieten'.",
  inputSchema: z.object({
    userEmail: z.string().email(),
    bandName: z.string(),
  }),
  execute: async ({ userEmail, bandName }) => {
    const { data: band } = await supabase
      .from("bands").select("id").ilike("name", bandName).single();
    if (!band) return { error: `Band '${bandName}' niet gevonden.` };
    const { error } = await supabase
      .from("user_favorites")
      .insert({ user_email: userEmail, band_id: band.id });
    if (error) return { error: error.message };
    return { success: true, bandName };
  },
});

*[Registreer in tools object:]*

tools: { searchBands, getStats, getBandByName, addFavorite },

*[Save]*

[SCHERM: browser → /chat]

Voeg de eerste hip-hop band toe aan mijn favorieten, mijn email is tim@novi.nl

*[AI roept searchBands aan (om eerste hip-hop band te vinden), dan addFavorite. Returnt success.]*

Vertel: "Multi-step + write. Eerst zocht-ie de band, daarna voegde-ie 'm toe. Belangrijk om hier op te wijzen: de description zegt 'alleen gebruiken als expliciet gevraagd'. Hierdoor doet AI niet random favorieten toevoegen. Voor productie: zet write-tools achter expliciete user confirmation."

[SCHERM: supabase → user_favorites]

*[Refresh tabel — favoriete is toegevoegd]*

Vertel: "En in de database staat het ook. AI heeft echt iets geschreven."


BLOK 8 — Tool Calling vs context-all (5 min)

[SLIDE 13 — Vergelijking] [SCHERM: slides]

Vertel: "Reflectie. Hoe verhoudt vandaag zich tot vorige les?"

*[Loop tabel langs]*

  • "Tokens — vorige les 30k per call, nu ~2k. 15× minder. 15× goedkoper."
  • "Schaal — vorige les max ~1000 records. Nu duizenden, makkelijk."
  • "Live data — vorige les snapshot bij chat-start. Nu actueel per call."
  • "Write — vorige les niet mogelijk. Nu wel — addFavorite, addNote, anything."
  • "Multi-step — vorige les beperkt. Nu native."

Vertel: "Wanneer toch context-all? Snel prototype, hele kleine dataset (<100 records), of als je écht zeker weet dat je nooit gaat schalen. Voor alles wat richting productie gaat: Tool Calling."


BLOK 9 — Lesopdracht + Huiswerk uitleg (20 min)

[SLIDE 14 — Lesopdracht] [SCHERM: slides]

Vertel: "Lesopdracht. Bouw op je eigen thema-app uit Les 11. Hetzelfde stappenplan als vandaag, maar dan voor jouw dataset."

*[Loop punten langs]*

Vertel: "Refactor je chat-route — weg met alle data meesturen. Definieer minstens 3 tools voor je dataset. stopWhen: stepCountIs(5). Pas system prompt aan. Test 3 vragen die meerdere tools triggeren.

Voor jouw thema betekenen die tools iets anders. Stel je hebt een restaurant-aggregator: searchRestaurants, getMenu, getStats per cuisine. Stel je hebt scriptie-archief: searchTheses, getThesisById, listSupervisors. Bedenk wat voor jouw thema natural fits zijn."

[SLIDE 15 — Huiswerk]

Vertel: "Huiswerk. Drie onderdelen, alle drie verplicht."

*[Loop A, B, C langs]*

  • "A — Write-tool. Voeg een tabel toe voor user-acties — favorieten, notes, votes, watch-later, wat past bij jouw thema. Schrijf een write-tool. Test dat AI 'm gebruikt op de juiste momenten."
  • "B — Tool-calls in UI. Refactor je chat UI om tool-invocations te tonen. Wat we vandaag bouwden — chip met tool-naam, collapsible result. Verplicht."
  • "C — TOOLS.md. Documenteer in je repo wat je gebouwd hebt. Lijst van tools + descriptions. 3 vragen die 1 tool triggeren, 1 vraag die 2+ tools triggeren. 1 edge case die AI goed afhandelde."

Vertel: "Bonus: loading indicator per tool-call, mooie kaartjes voor results in plaats van JSON-dump."

💬 "Wat als ik vorige week geen eigen thema-app heb gebouwd?" → "Dan eerst die afmaken. Dit huiswerk bouwt erop voort."

💬 "Hoeveel tools is genoeg?" → "Drie voor de lesopdracht. Voor huiswerk drie + één write-tool. Voor eindopdracht? Zo veel als nuttig, niet meer."


BLOK 10 — Vragen + Afsluiting (15 min)

[SLIDE 16 — Volgende les: Agents] [SCHERM: slides]

Vertel: "Eén ding voor we eindigen — wat komt na vandaag."

Vertel: "Vandaag deden we Tool Calling. Maximaal 5 stappen, dan moet AI antwoorden. Werkt voor zoekvragen, vergelijkingen, simpele acties.

Volgende les — Agents. Daar geven we AI veel meer autonomie. stopWhen: stepCountIs(20) of meer, of zelfs custom stop-condities. AI plant, voert uit, evalueert resultaat, beslist of-ie verder gaat. Eén user-request kan 30+ tool-calls triggeren. Voorbeeld: 'plan mijn volledige Polderfest weekend' — AI kijkt alle bands, ranked op jouw favorieten, maakt schema per dag, voegt toe aan favorieten, optimaliseert voor overlap. Multi-step in 't kwadraat."

Vertel: "Daarna in deze leerlijn:

  • Les 14: RAG + embeddings — semantic search op heel grote datasets (denk 100.000+ records, of vrije tekst-corpora)
  • Les 15-16: Testing + Deployment + Performance
  • Les 17-18: Eindopdracht-werkdagen + Pitch"

[SLIDE 17 — Afsluiting]

Vertel: "Vragen?"

*[Open de vloer. Verwachte vragen:]*

💬 "Hoe weet AI welke tool te kiezen?" → "Op basis van de description en de parameter-schema's. Daarom: schrijf descriptions zoals je 't aan een collega zou uitleggen. Vaag = verkeerde tool. Specifiek = juiste."

💬 "Kan AI tools combineren die ik niet voorzag?" → "Ja, dat is multi-step. Hij kan creatief zijn — eerst searchBands, dan getBandByName op het resultaat. Soms verrast 't je. Dat is goed, betekent dat tools generiek genoeg zijn."

💬 "Wat als AI verkeerde tool kiest?" → "Twee opties: description verbeteren, of system prompt aanvullen met regels. 'Voor X gebruik tool Y.' Iteratief proces."

💬 "Is dit duurder?" → "Per call ongeveer hetzelfde (kleinere context). Maar omdat AI vaker antwoordt zonder veel context, juist goedkoper. En je betaalt alleen voor wat je nodig hebt — geen heel grote context erbij stoppen."

💬 "Werkt dit met andere modellen?" → "Ja. OpenAI, Anthropic, Google — allemaal ondersteunen tool calling via Vercel AI SDK. Code blijft hetzelfde, alleen model: regel verandert."

*[Sluit af]*

Vertel: "Zorg dat je vóór les 13 je eigen tools werkend hebt — dan kunnen we Agents direct toepassen. Tot dan!"


Backup-onderwerpen (als tijd over)

  1. Parallel tool calls — Sommige modellen kunnen meerdere tools tegelijk aanroepen. experimental_continueSteps parameter, niet vandaag.
  2. Tool result transformations — Bewerk tool output vóór AI 'm ziet (bv. velden weglaten voor privacy).
  3. Streaming tool-args — AI begint args te streamen voor execute aangeroepen wordt. Te zien in dev-console.
  4. Anthropic vs OpenAI tool calling — Subtiele verschillen in hoe AI tools kiest. Code blijft gelijk, gedrag iets anders.
  5. MCP servers — Tools as a service. Externe MCP-servers leveren tools aan AI. Concept van 2025, snel groeiend.