Files
novi-lessons/Les13-Agents/Les13-Lesstof.md
2026-06-09 18:02:04 +02:00

359 lines
14 KiB
Markdown

# Les 13 — Lesstof
## Agents — een Polderfest-app die zelf nadenkt
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 12 — Tool Calling
**Volgende les:** Les 14 — Externe APIs + Cursor + Vercel deploy
---
## Inhoud
1. [Wat is een agent?](#1-wat-is-een-agent)
2. [Tool calling vs Agent — een continuüm](#2-tool-calling-vs-agent--een-continuüm)
3. [Wanneer voegt een agent écht waarde toe?](#3-wanneer-voegt-een-agent-écht-waarde-toe)
4. [De drie AI SDK functies die we gebruiken](#4-de-drie-ai-sdk-functies-die-we-gebruiken)
5. [Externe APIs als tool](#5-externe-apis-als-tool)
6. [De agent-loop](#6-de-agent-loop)
7. [Stappenplan: Polderfest → Polderfest-met-agent](#7-stappenplan-polderfest--polderfest-met-agent)
8. [Veelvoorkomende fouten en debug-tips](#8-veelvoorkomende-fouten-en-debug-tips)
---
## 1. Wat is een agent?
Een **agent** is een Large Language Model dat in een loop draait, tools gebruikt, en zelfstandig kiest welke volgende stap nodig is om een doel te bereiken.
Een voorbeeld maakt het concreet. Stel een gebruiker stelt deze vraag aan onze Polderfest-app:
> "Plan een zaterdagavond met indie en techno, geen tijdoverlap, alleen onder een dak als het regent."
Hierin zitten meerdere vragen verstopt:
- Welke indie-bands spelen er zaterdag?
- Welke techno-bands spelen er zaterdag?
- Wat is het schema (om overlap te checken)?
- Hoe is het weer op zaterdag?
- Kunnen we al die info combineren tot één avondprogramma?
Een gewone chatbot beantwoordt één vraag tegelijk. Een agent pakt alle vijf de sub-vragen autonoom op, in de juiste volgorde, en levert een eindantwoord.
---
## 2. Tool calling vs Agent — een continuüm
Belangrijk om vooraf te zeggen: **er is geen harde lijn tussen tool calling en agents.** In de Vercel AI SDK v6 gebruik je voor beide dezelfde syntax: `streamText`, `tool()`, `stopWhen`.
Het verschil zit in **hoe je het ontwerpt en gebruikt**.
| Aspect | Tool calling (Les 12) | Agent (Les 13) |
|--------|----------------------|----------------|
| Vraag-type | "Voer deze functie uit" | "Bereik dit doel — kies zelf hoe" |
| Aantal stappen | Meestal 1-3 (ook met `stopWhen`) | Vaak 4-10+, variabel |
| Wie kiest de volgorde | Jouw prompt stuurt sterk | AI plant zelf, autonomer |
| Bronnen combineren | Per tool één bron | DB + externe API + ... door elkaar |
| Reflectie / bijsturen | Nee — direct antwoord | AI checkt resultaat, beslist vervolg |
Belangrijk: tool calling kón in Les 12 al externe APIs aanroepen. De `execute()` van een tool is gewoon JavaScript — een `fetch` is prima. Het is niet alsof tool calling daar beperkt in is.
Wat agents onderscheidt is dat ze **meerdere bronnen door elkaar combineren** in één antwoord, omdat ze meer stappen mogen zetten en zelf de volgorde bepalen.
---
## 3. Wanneer voegt een agent écht waarde toe?
**Tool calling is genoeg als:**
- Je weet welke functie nodig is voor de vraag (een DB-zoekopdracht, een API-call)
- Het antwoord ligt in één bron
- Het pad is voorspelbaar — je kunt in je eigen prompt al beschrijven "doe eerst X, dan Y"
**Een agent is écht zinvol als:**
- De vraag is open: "plan", "vergelijk", "vind het beste", "concludeer"
- De AI moet meerdere bronnen combineren in één antwoord
- Het pad is onvoorspelbaar — soms drie stappen, soms zeven
- Het eerste resultaat triggert vervolgvragen ("die band is uitverkocht — zoek alternatief")
**Waarom niet altijd een agent?**
Agents lopen langer. Meer stappen = meer tokens = hogere kosten en hogere latency. Ook lastiger te debuggen door non-determinisme — twee dezelfde vragen kunnen verschillende paden bewandelen.
**Pragmatisch advies:** start altijd met tool calling. Schaal pas naar agent-niveau als je merkt dat je system prompt steeds langer wordt om "doe eerst dit, dan dat" voor te schrijven — dan kan de AI dat beter zelf bepalen.
---
## 4. De drie AI SDK functies die we gebruiken
### 4.1 `tool({ description, inputSchema, execute })`
Dezelfde functie als in Les 12. Drie velden:
```typescript
import { tool } from "ai";
import { z } from "zod";
searchBands: tool({
description: "Zoek bands op genre en/of dag.",
inputSchema: z.object({
genre: z.string().optional(),
day: z.enum(["vrijdag", "zaterdag", "zondag"]).optional(),
}),
execute: async ({ genre, day }) => {
// Supabase query
return data;
},
}),
```
- `description` — wat de AI hierover leest om te beslissen of-ie de tool wil gebruiken
- `inputSchema` — Zod-schema, AI vult de parameters automatisch in
- `execute` — wat draait als de tool wordt aangeroepen. Gewoon JavaScript: DB-query, fetch, computatie
### 4.2 `stopWhen` — de "knop" om de loop te bouwen
```typescript
import { stepCountIs } from "ai";
streamText({
model: openai("gpt-5.2"),
messages,
tools: { ... },
stopWhen: stepCountIs(8),
});
```
`stopWhen` is een conditie waarop de loop stopt. `stepCountIs(N)` is de meest gebruikte: stop na maximaal N stappen.
In Les 12 hebben we dit ook gebruikt, vaak met `stepCountIs(3)`. Voor agents zetten we 'm typisch hoger — 8 of meer — omdat we de AI ruimte willen geven om zelf het juiste pad te kiezen.
Andere stop-condities (combineerbaar):
```typescript
stopWhen: [
stepCountIs(8),
hasToolCall("finishPlan"), // stop als specifieke tool is aangeroepen
],
```
`hasToolCall` is handig als je de AI dwingt een "klaar"-tool aan te roepen voor-ie stopt.
### 4.3 `hasToolCall` — stop op specifieke tool (NIEUW)
Combineer `stepCountIs` met een **stop-zodra-tool-aangeroepen** conditie:
```typescript
import { stepCountIs, hasToolCall } from "ai";
stopWhen: [stepCountIs(8), hasToolCall("finishPlan")],
```
Effect: zodra de agent een tool genaamd `finishPlan` heeft aangeroepen,
stopt de loop direct — ongeacht hoeveel stappen er nog over zijn. Dit
forceert een nette afsluiting.
Combineer dit met een tool die **structured output** vraagt:
```typescript
finishPlan: tool({
description: "Roep dit ALS LAATSTE aan met een gestructureerd plan.",
inputSchema: z.object({
summary: z.string(),
items: z.array(z.object({
time: z.string(),
band: z.string(),
stage: z.string(),
reason: z.string(),
})),
}),
execute: async ({ summary, items }) => ({ summary, items, ok: true }),
}),
```
De UI kan dit object renderen als een mooie timeline of kaartenset.
Geen risico op vrije-vorm-tekst die je moet parsen.
### 4.4 `prepareStep` — dynamisch model per stap (NIEUW)
Geef per stap ander gedrag:
```typescript
streamText({
model: openai("gpt-5.2"), // default
prepareStep: ({ stepNumber }) => {
if (stepNumber === 0) {
return { model: openai("gpt-5.2") }; // krachtig — planning
}
return { model: openai("gpt-5-mini") }; // goedkoper — uitvoering
},
// ...tools, stopWhen, etc.
});
```
Eerste stap (begrip + planning) = duur, krachtig model. Latere stappen
(simpele tool-calls uitvoeren) = goedkoop, snel model. Zelfde resultaat,
significant lagere kosten en latency.
`prepareStep` kan ook andere dingen aanpassen per stap: `activeTools`
(welke tools beschikbaar zijn), `system` prompt, etc. Voor vandaag
gebruiken we het alleen voor model-switching.
---
## 5. Externe APIs als tool
Tools hoeven niet alleen je eigen Supabase-database te zijn. Externe APIs werken net zo. Een tool is gewoon een functie — wat erin gebeurt is aan jou.
Voor onze Polderfest-agent gebruiken we **Open-Meteo** — een gratis weer-API die geen account of API key nodig heeft. Perfect voor demos en onderwijs.
```typescript
getWeather: tool({
description: "Weer-forecast voor een festivaldag.",
inputSchema: z.object({ date: z.string() }),
execute: async ({ date }) => {
const url = new URL("https://api.open-meteo.com/v1/forecast");
url.searchParams.set("latitude", "52.0907");
url.searchParams.set("longitude", "5.1214");
url.searchParams.set("daily", "temperature_2m_max,precipitation_probability_max");
url.searchParams.set("start_date", date);
url.searchParams.set("end_date", date);
url.searchParams.set("timezone", "Europe/Amsterdam");
const res = await fetch(url);
const data = await res.json();
return {
temperatureMaxC: data.daily.temperature_2m_max[0],
rainChancePercent: data.daily.precipitation_probability_max[0],
};
},
}),
```
**Vuistregel:** wat een fetch kan, kan een tool. Stripe-saldo opvragen. GitHub-issues lezen. Wikipedia. Spotify. Alles via HTTP — alles kan een tool zijn.
> Dit is een mogelijkheid die ook in tool calling van Les 12 al bestond. Het verschil bij agents: omdat de loop meerdere stappen mag duren, kan de agent het *resultaat* van een externe API gebruiken om vervolgens een tweede tool aan te roepen op basis daarvan. Dat is de échte meerwaarde.
### 5.1 Write-tools — de wereld veranderen, niet alleen lezen
Tot nu toe lazen al onze tools data uit Supabase of externe APIs. Tools
kunnen óók schrijven. Dat opent een hele nieuwe categorie: agents die
**iets doen** ipv. alleen iets opzoeken.
```typescript
addToFavorites: tool({
description: "Voeg een band toe aan de favorieten van de gebruiker.",
inputSchema: z.object({
bandName: z.string(),
}),
execute: async ({ bandName }) => {
// 1. Zoek band op naam (we hebben band_id nodig — FK in user_favorites)
const { data: band } = await supabase
.from("bands").select("id, name")
.ilike("name", `%${bandName}%`).limit(1).single();
if (!band) return { error: `Band niet gevonden` };
// 2. Insert in bestaande user_favorites tabel
const { data, error } = await supabase
.from("user_favorites")
.insert({ user_email: "demo@polderwave.app", band_id: band.id })
.select().single();
if (error) return { error: error.message };
return { added: true, band: band.name, favoriteId: data.id };
},
}),
```
De gebruiker zegt *"Zet Band 5 in mijn favorieten."* — de agent roept
`addToFavorites` aan — er staat een rij in `user_favorites` met
`band_id` als foreign-key naar `bands`. We gebruiken de bestaande
tabel uit Les 11/12, geen nieuwe tabel nodig. Voor demos hardcoderen
we `user_email` op `"demo@polderwave.app"`; in productie gebruik je
echte auth + RLS (zie Les 10).
Belangrijke veiligheidsoverweging: write-tools zijn machtig én riskant.
Een agent die kan schrijven kan ook fout schrijven. In productie:
- Beperk wat een tool mag (alleen INSERT op specifieke tabellen)
- Beperk per user (Supabase RLS — Les 10)
- Log alle writes voor auditability
---
## 6. De agent-loop
Visueel:
```
┌───────────────────────────────────────┐
│ Gebruiker stelt een vraag │
└─────────────┬─────────────────────────┘
┌──────────────┐
┌──▶│ LLM │ → kies een tool of geef antwoord
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Voer tool │ → bv. searchBands of getWeather
│ │ uit │
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Resultaat │ → terug naar LLM
│ └──────┬───────┘
└──────────┘
Stopt wanneer: stopWhen-conditie waar wordt
```
Soms duurt het twee stappen. Soms zes. Soms acht — dan kicked `stepCountIs(8)` in. De agent beslist.
---
## 7. Stappenplan: Polderfest → Polderfest-met-agent
Compleet stap-voor-stap met code-blokken: zie `lesbestanden/Les13-Stap-voor-stap.md`.
Samenvatting van de 11 stappen:
1. Polderfest-original uitpakken (Les 12 versie) of eigen versie gebruiken
2. Zod check — al geïnstalleerd
3. Maak nieuwe route `app/api/agent/route.ts`
4. Voeg eerste tool toe: `searchBands` (Supabase)
5. Voeg `stopWhen: stepCountIs(8)` toe
6. Voeg tweede tool toe: `getStageSchedule`
7. Voeg derde tool toe: `getWeather` (externe API, Open-Meteo)
8. Schrijf system prompt
9. Maak UI-pagina `app/agent/page.tsx`
10. Testen in browser met drie voorbeeld-prompts
11. Deploy naar Vercel
Twee referentie-zips staan in `lesbestanden/`:
- `polderfest-original.zip` — startpunt
- `polderfest-with-agent.zip` — referentie als je vastloopt
---
## 8. Veelvoorkomende fouten en debug-tips
| Probleem | Oplossing |
|----------|-----------|
| Agent stopt na 1 tool-call | Heb je `stopWhen: stepCountIs(8)` toegevoegd? |
| Tool wordt nooit aangeroepen | Description is te vaag — schrijf in heldere taal wat de tool doet |
| Tool wordt te vaak aangeroepen | System prompt is te open — wees specifieker |
| Loop blijft hangen / oneindig | Check `stopWhen` — zonder maximum kan hij hard doorgaan |
| Open-Meteo geeft niets terug | Check ISO datum-format: `YYYY-MM-DD` |
| UI toont geen tool-calls | Loop door `m.parts` heen, herken `p.type.startsWith("tool-")` |
| Supabase error in agent-route | Check `SUPABASE_SERVICE_ROLE_KEY` in `.env.local` |
| Vercel deploy faalt | Env vars in Vercel toegevoegd? Build logs lezen |
**Debug-tip:** zet `console.log` in elke `execute` om te zien welke tools de agent aanroept en in welke volgorde. Heel leerzaam om dit één keer te doen.
---
## Verder lezen
- Vercel AI SDK docs over tools en agents: <https://ai-sdk.dev/docs/foundations/agents>
- Anthropic over "Workflows vs Agents": <https://www.anthropic.com/engineering/building-effective-agents>
- Open-Meteo API: <https://open-meteo.com/en/docs>