29 KiB
Les 14 — Agents
Docenttekst (Klas A — 3 uur, fysiek, demo-driven)
Les: 13 van 18 Onderwerp: Agents — LLM in een loop met tools Duur: 180 minuten Format: Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk. Demo-app: Nieuwe research-agent (from scratch, los van Polderfest)
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. Tavily account
- Maak account op https://tavily.com/
- Kopieer API key (begint met
tvly-...) - Gratis tier = 1000 calls/maand, ruim genoeg voor deze les
2. Nieuwe lege Next.js demo-folder
*[Doe dit thuis vooraf, niet in de klas. Klas kijkt naar de setup-stap als je 'm doet.]*
cd ~/novi/novi-lessons/Les14-Agents
pnpm create next-app@latest research-agent \
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd research-agent
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
3. Environment vars
.env.local:
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
4. Supabase tabel
create table research_reports (
id bigserial primary key,
query text not null,
summary text not null,
sources jsonb default '[]'::jsonb,
created_at timestamp default now()
);
alter table research_reports enable row level security;
create policy "demo open" on research_reports
for all to anon using (true) with check (true);
5. Backup eindstaat
- Werkende
lib/tools.ts,lib/agent.ts,app/api/research/route.tsergens als backup - Verwacht: 1-2 typos tijdens live coding — geen ramp
6. Browser tabs
localhost:3000(dev server, maar gaan we voornamelijk via terminal testen)- Supabase dashboard
- https://ai-sdk.dev/docs/agents/overview (referentie)
- https://tavily.com/ (dashboard voor usage)
HET SCRIPT — Lees mee tijdens de les
BLOK 1 — Welkom + Terugblik (10 min)
[SLIDE 1 — Title] [SCHERM: slides]
Vertel: "Welkom bij les 13. Vandaag stappen we van tool-calling naar agents. Eén stap verder qua autonomie van AI."
[SLIDE 2 — Terugblik]
Vertel: "Vorige les bouwden we Tool Calling in onze Polderfest-app. Zes tools. stopWhen: stepCountIs(5). AI roept zelf functies aan om aan data te komen. Goed werk.
Maar er waren dingen die een tool-call bot nog niet kon. Lange ketens — 30, 40, 50 stappen — daar is stepCountIs(5) te krap voor. Een eigen plan maken en bijstellen. Tools die andere tools triggeren. Per stap een ander model kiezen.
Daarvoor heb je een agent nodig. Een LLM die tools gebruikt in een loop, en zelf bepaalt hoe lang die loop duurt en welke kant het op gaat."
[SLIDE 3 — Planning]
Vertel: "Vandaag drie uur. Eerst een half uur theorie — wat is een agent precies, hoe ziet de loop eruit, wat is een ToolLoopAgent. Daarna vier live demos waarin we from scratch een research-agent bouwen. Geen polderfest vandaag — losse repo, omdat agents een nieuwe context verdienen.
Aan het eind: wanneer wel/niet een agent, en wat doe je voor huiswerk."
BLOK 2 — Theorie (30 min)
[SLIDE 4 — Wat is een agent]
Vertel: "De definitie uit de AI SDK docs is kort en bruikbaar:
Agents are LLMs that use tools in a loop to accomplish tasks.
Drie ingrediënten. Eén — een LLM die besluit wat de volgende actie is. Twee — tools die het LLM uitbreiden. Drie — een loop die deze twee orchestreert.
In Les 12 hadden we deze drie ook al — stopWhen: stepCountIs(5) is in feite een mini-agent. Het verschil zit in schaal. Vandaag praten we over 20-50 stappen, en het model bepaalt zelf welke tools het wanneer gebruikt."
💬 Mogelijke vraag: "Wat is dan het verschil met Les 12 echt?"
Antwoord: "Schaal en autonomie. Les 12 was: vraag → 2-5 tool-calls → antwoord. Vandaag: vraag → agent maakt plan → voert plan uit (10-50 stappen) → schrijft rapport. Het model beslist veel meer zelf."
[SLIDE 5 — Anatomie van de loop]
Vertel: "Wat gebeurt er per loop-iteratie, één stap? Vijf dingen.
Eén: we sturen de huidige messages naar het LLM. Twee: LLM antwoordt — óf met text, óf met een tool-call. Als het text is, stopt de loop. Dat is een natural finish — model heeft niks meer te doen. Als het een tool-call is, gaan we door. Drie: we voeren de tool execute uit. Vier: we voegen het resultaat toe aan de berichten. Vijf: we checken stop-conditie. Niet voldaan? Volgende iteratie.
De loop stopt in vier gevallen: text-only antwoord, stopWhen geactiveerd, tool zonder execute aangeroepen — dat is het 'done-pattern', komen we straks op terug, en tool die approval nodig heeft. Die laatste skippen we vandaag.
Default in v6: stepCountIs(20). Dat is een veiligheidsgrens — voorkomt dat een rare bug in je systeem prompt resulteert in 200 API-calls en een rekening van 30 euro."
[SLIDE 6 — ToolLoopAgent]
Vertel: "In oudere AI SDK versies moest je de loop zelf schrijven — een while-loop rond streamText en generateText. In v6 is er een dedicated class: ToolLoopAgent.
*[Wijs naar code op slide]*
Je geeft model, system prompt, tools, en een stop-conditie. Eén keer definiëren. Dan agent.generate({ prompt }) aanroepen, of agent.stream voor streaming.
Wat je terugkrijgt: result.text is het eindantwoord, result.steps is een array met alle stappen die de agent zette — welke tool aangeroepen, met welke input, welk resultaat. Dat is je debug-goud."
💬 Vraag: "Kan dat nog steeds met streamText?"
Antwoord: "Ja, voor backwards compatibility. Maar voor nieuwe code is ToolLoopAgent de aanrader. Minder boilerplate, herbruikbaar."
[SLIDE 7 — Stop-condities]
Vertel: "Vier soorten stop-condities.
Eén: stepCountIs(N) — stop na N stappen. Standaard veiligheidsmaatregel.
Twee: hasToolCall('saveReport') — stop zodra een specifieke tool is aangeroepen. Logisch voor 'doe het werk en sla op, dan klaar'.
Drie: isLoopFinished() — onbeperkt. Agent stopt alleen natuurlijk wanneer model klaar is. Dit is gevaarlijk zonder andere safeguards — agent kan in theorie oneindig doorgaan. Alleen gebruiken met cost limit erbij.
Vier: combineer ze in een array. Loop stopt zodra één conditie waar is.
En vijf — niet built-in maar zelf te schrijven: StopCondition callback. Daarmee kun je stoppen op kosten, op aantal tools, op wat dan ook dat je uit de step-historie kunt halen."
[SLIDE 8 — prepareStep]
Vertel: "prepareStep is misschien de krachtigste feature van ToolLoopAgent. Het is een async callback die VOOR elke stap draait. Je kunt het model dynamisch wisselen, tools beperken, messages trimmen.
Voorbeeld: begin met gpt-4o-mini — snel en goedkoop. Maar als de stap-historie complex wordt, switch naar Claude Sonnet voor beter reasoning. Dat scheelt geld zonder kwaliteit te verliezen.
Of: dwing een research-fase af. Stap 0-3 mag alleen webSearch. Stap 4-6 alleen readPage. Stap 7+ alleen saveReport. Agent kan niet 'cheaten' door te vroeg op te slaan.
Of: trim oude tool-results uit de berichten. Een lange agent kan 30k tokens aan tool-output verzamelen. Niet meer relevant voor de huidige stap — eruit, scheelt budget."
💬 Vraag: "Hoe weet ik wanneer ik prepareStep nodig heb?"
Antwoord: "Niet bij elke agent. Begin zonder. Als je merkt dat je agent veel geld kost, of te vaak in een verkeerde fase blijft hangen, dan kom je terug om prepareStep toe te voegen. Premature optimization is ook in agents reëel."
BLOK 3 — Live Demo 1: Setup research-agent (25 min)
[SLIDE 9 — Wat we bouwen]
Vertel: "Genoeg theorie. We gaan een research-agent bouwen. Doel: gebruiker stelt een vraag — bijvoorbeeld 'wat zijn AI-trends in 2026?' — en de agent zoekt zelfstandig op het web, leest de relevante pagina's, schrijft een rapport, slaat op in Supabase.
Vier tools: webSearch met Tavily, readPage om URLs te lezen, saveReport om in DB te zetten, en eventueel een done-tool.
Stack: Next.js 16, AI SDK v6, Tavily, Supabase, gpt-4o-mini. Losse repo — niet de Polderfest-demo. Agents verdienen een schone context."
[SLIDE 10 — LIVE DEMO 1] [SCHERM: terminal + editor]
Vertel: "Klas, leun achterover. Vragen tussendoor mogen. Ik laat zien hoe je een agent from-scratch bouwt."
*[Open terminal in /Les14-Agents]*
pnpm create next-app@latest research-agent \
--typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd research-agent
*[Wacht op next-app, ondertussen praat door over Tavily]*
Vertel: "Voor web search heb je een API nodig. OpenAI heeft sinds kort een eigen web-search tool, maar die werkt alleen met gpt-4o en is duur. Voor onafhankelijke search gebruiken we Tavily — gemaakt voor AI agents, gratis tier van 1000 calls per maand. Gewoon registreren met email, API key gepakt, klaar.
Andere opties: Brave Search API, Serper, Google Custom Search. Tavily is het simpelst voor onze case omdat het naast search ook content-extractie biedt."
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
*[Open editor]*
Vertel: "Eerst environment vars."
*[Maak .env.local]*
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
Vertel: "Belangrijk: OPENAI_API_KEY is GEEN NEXT_PUBLIC_*. Anders zit-ie in je client-bundle en kan iedereen 'm zien in DevTools. Server-only. Hetzelfde voor TAVILY_API_KEY."
*[Check .gitignore — moet .env.local in staan]*
Vertel: "Check je .gitignore. .env.local moet erin. Standaard create-next-app doet dit goed maar — altijd checken."
*[Supabase Dashboard openen]* [SCHERM: supabase]
create table research_reports (
id bigserial primary key,
query text not null,
summary text not null,
sources jsonb default '[]'::jsonb,
created_at timestamp default now()
);
Vertel: "Tabel voor onze rapporten. query is de vraag, summary is het rapport zelf, sources is een JSON array van URLs die werden gebruikt."
alter table research_reports enable row level security;
create policy "demo open" on research_reports
for all to anon using (true) with check (true);
Vertel: "RLS aan, en een open policy voor demo. Productie: koppelen aan auth.uid()."
*[Terug naar editor]* [SCHERM: editor]
Vertel: "Nu de tools. Ik maak een lib/tools.ts met onze drie tools."
*[Maak lib/tools.ts]*
import { tool } from "ai";
import { z } from "zod";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export const webSearch = tool({
description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
inputSchema: z.object({
query: z.string().describe("Zoekterm"),
}),
execute: async ({ query }) => {
const res = await fetch("https://api.tavily.com/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.TAVILY_API_KEY,
query,
max_results: 5,
}),
});
const data = await res.json();
return data.results?.map((r: any) => ({
url: r.url,
title: r.title,
snippet: r.content,
})) ?? [];
},
});
Vertel: "Beschrijving van webSearch is kort en specifiek. AI leest description om te beslissen welke tool nodig is — vaag = verkeerde keuze. max_results: 5 houdt het beheersbaar — Tavily kan ook 10 of 20, maar 5 is goed startpunt."
*[readPage tool toevoegen]*
export const readPage = tool({
description: "Haal de volledige tekst van een URL op.",
inputSchema: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const res = await fetch("https://api.tavily.com/extract", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: process.env.TAVILY_API_KEY,
urls: [url],
}),
});
const data = await res.json();
const content = data.results?.[0]?.raw_content ?? "";
return { url, content: content.slice(0, 5000) };
},
});
Vertel: "readPage haalt de fulltext van een URL — Tavily heeft een extract-endpoint dat ruis verwijdert. Ik knip op 5000 chars om token-budget niet te exploderen. Voor productie: chunken + samenvatten."
*[saveReport tool]*
export const saveReport = tool({
description:
"Sla een onderzoeksrapport op in de database. " +
"Alleen aanroepen als het rapport compleet is.",
inputSchema: z.object({
query: z.string(),
summary: z.string().min(100),
sources: z.array(z.object({
url: z.string().url(),
title: z.string(),
})),
}),
execute: async ({ query, summary, sources }) => {
const { data, error } = await supabase
.from("research_reports")
.insert({ query, summary, sources })
.select()
.single();
if (error) return { error: error.message };
return { reportId: data.id, success: true };
},
});
Vertel: "saveReport is onze enige write-tool. Description benadrukt: alleen aanroepen als compleet. summary.min(100) — zod schema dwingt minimum lengte af, AI mag geen 'TODO' insturen."
*[lib/agent.ts maken]*
Vertel: "Nu de agent zelf."
import { ToolLoopAgent, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { webSearch, readPage, saveReport } from "./tools";
export const researchAgent = new ToolLoopAgent({
model: openai("gpt-4o-mini"),
system: `Je bent een research-assistent. Werk als volgt:
1. Begin met een kort plan (3-5 bullets).
2. Gebruik webSearch voor oriëntatie.
3. Gebruik readPage voor de 2-3 meest relevante URLs.
4. Schrijf een samenvatting van min 200 woorden.
5. Sla op met saveReport. Inclusief alle gebruikte sources.
Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
tools: { webSearch, readPage, saveReport },
stopWhen: stepCountIs(10),
});
Vertel: "System prompt is het kritieke onderdeel. Ik geef expliciete fasen — plan, search, read, samenvat, save. Hoe duidelijker, hoe minder de agent drifft. 'Verzin niets' is belangrijk — anders maakt AI bronnen op."
*[app/api/research/route.ts]*
import { researchAgent } from "@/lib/agent";
export async function POST(req: Request) {
const { query } = await req.json();
const result = await researchAgent.generate({ prompt: query });
return Response.json({
text: result.text,
steps: result.steps.map((s, i) => ({
step: i,
toolCalls: s.toolCalls?.map((tc) => ({
tool: tc.toolName,
input: tc.input,
})),
hasText: !!s.text,
})),
});
}
Vertel: "Endpoint stuurt query naar de agent, returnt text + step-samenvatting. Voor productie zou je dit streamen, voor demo houden we het simpel — <20><>én POST, JSON response."
*[Run]* [SCHERM: terminal]
pnpm dev
*[In tweede terminal:]*
curl -X POST http://localhost:3000/api/research \
-H "Content-Type: application/json" \
-d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \
| jq
Vertel: "We zien... *[wachten 20-40s]* ...de agent draait. JSON komt terug. Kijk — steps array. webSearch, dan readPage, readPage, saveReport. Vier stappen. text veld is een samenvatting."
*[Open Supabase, check tabel]* [SCHERM: supabase]
Vertel: "Rapport staat in Supabase. Werkend agent. From scratch in 20 minuten."
💬 Vraag: "Hoeveel tokens kost zo'n call?"
Antwoord: "Goeie vraag. result.usage heeft inputTokens en outputTokens. Voor zo'n research van 4 stappen: ongeveer 8.000 input + 1.500 output tokens. Op gpt-4o-mini: ~0.001 dollar. Bijna gratis. Op gpt-4o: ~0.05 dollar. Houdbaar."
BLOK 4 — Live Demo 2: stopWhen variants (25 min)
[SLIDE 11 — DEMO 2] [SCHERM: editor]
Vertel: "We hebben nu een agent met stepCountIs(10). Laten we de andere stop-condities uitproberen en zien wat het verschil maakt."
*[Wijzig stopWhen]*
import { ToolLoopAgent, stepCountIs, hasToolCall } from "ai";
// ...
stopWhen: hasToolCall("saveReport"),
Vertel: "Nu stopt agent zodra saveReport is aangeroepen. Geen veiligheidscap. Laten we draaien."
*[Curl draaien]*
Vertel: "Werkt. Agent doet zijn werk, roept saveReport, stopt. Voordeel: geen onnodige extra stappen. Nadeel: als saveReport faalt en agent retried, kan-ie alsnog lang doorgaan."
*[Beter: combineren]*
stopWhen: [
stepCountIs(20),
hasToolCall("saveReport"),
],
Vertel: "Best practice: array. Veiligheidscap én task-completion. Loop stopt op de eerste van de twee."
*[Probeer isLoopFinished]*
import { isLoopFinished } from "ai";
// ...
stopWhen: isLoopFinished(),
Vertel: "isLoopFinished is letterlijk: geen limit. Alleen natuurlijke finish stopt het. Dit is gevaarlijk zonder andere safeguards. Een misverstand in je system prompt en agent draait 50 stappen door."
*[Curl draaien — agent doet nu 8-10 stappen]*
Vertel: "Zie je? Agent gaat door tot het zelf besluit te stoppen. Eindigt met text — geen tool-call. Werkt, maar — wat als prompt slecht is, of als API een rare fout gooit en de agent retried?"
*[Custom StopCondition]*
Vertel: "Eigen stop. Bijvoorbeeld stoppen als we genoeg pagina's hebben gelezen."
import type { StopCondition } from "ai";
import type { researchAgent } from "./agent";
type Tools = typeof researchAgent extends ToolLoopAgent<infer T> ? T : never;
const enoughReading: StopCondition<any> = ({ steps }) => {
const readCount = steps.flatMap((s) =>
s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
).length;
return readCount >= 3;
};
stopWhen: [
stepCountIs(20),
enoughReading,
],
Vertel: "Drie pagina's lezen is genoeg, stoppen. Custom logic die uit step-historie wordt gehaald."
*[Demo run]*
Vertel: "Zie: agent stopt op stap waar readPage voor de derde keer wordt aangeroepen. Conditie werkt. Voor productie gebruik je dit vaak voor kostenlimieten — daar komen we morgen op terug, eh, in het huiswerk."
💬 Vraag: "Waarom : StopCondition<any> en niet typed?"
Antwoord: "Excuus, dat is een shortcut. In productie zou je een ToolSet type extracten zodat je StopCondition<typeof tools> krijgt — dan kun je tool-names typesafe checken. Voor demo houd ik 't simpel."
BLOK 5 — Pauze (15 min)
[SLIDE 12 — Pauze]
Vertel: "Pauze. Vijftien minuten. Loop een ronde."
*[Reset agent.ts naar stepCountIs(15) + hasToolCall("saveReport") combo voor volgende demo]*
BLOK 6 — Live Demo 3: prepareStep (25 min)
[SLIDE 13 — DEMO 3] [SCHERM: editor]
Vertel: "Nu het krachtigste stuk: prepareStep. Voor elke stap kunnen we model, tools, messages aanpassen. Drie use cases vandaag."
Use case 1 — Dynamic model
prepareStep: async ({ stepNumber, messages }) => {
console.log(`Stap ${stepNumber}, ${messages.length} messages`);
if (stepNumber > 3) {
console.log("→ switch naar gpt-4o");
return { model: openai("gpt-4o") };
}
return {};
},
Vertel: "Stap 0-3 doen we met gpt-4o-mini — goedkoop en snel. Vanaf stap 4 switchen we naar de grote gpt-4o. Reden: vroege stappen zijn 'zoek even', late stappen zijn 'vat samen en redeneer'. Dat laatste verdient een sterker model."
*[Curl draaien — kijk in console naar logs]*
Vertel: "Zien jullie: 'Stap 0 — mini'. 'Stap 1 — mini'. 'Stap 4 — switch naar gpt-4o'. Dynamic. Effect: lagere kosten, betere eindkwaliteit."
Use case 2 — Fase-gebaseerde tools
*[Pas aan]*
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 2) {
return { activeTools: ["webSearch"] };
}
if (stepNumber <= 5) {
return { activeTools: ["readPage"] };
}
return {
activeTools: ["saveReport"],
toolChoice: "required",
};
},
Vertel: "Nu dwingen we een fase af. Stap 0-2 mag alleen searchen. Stap 3-5 alleen lezen. Stap 6+ alleen opslaan, en moet die ook gebruiken want toolChoice: required."
*[Curl draaien]*
Vertel: "Agent kan niet 'cheaten'. Geen saveReport op stap 2 met een vage samenvatting. Eerst werken, dan opslaan."
💬 Vraag: "Is dat niet een beetje vechten tegen het model?"
Antwoord: "Het is een trade-off. Meer controle = minder flexibel. Voor research is een gefaseerde aanpak vaak prima. Voor open-ended problem solving — bijv. een coding agent — zou ik 't niet doen. Daar moet het model vrij kunnen kiezen."
Use case 3 — Context-trimming
prepareStep: async ({ messages, stepNumber }) => {
if (messages.length > 12) {
return {
messages: [
messages[0], // system
...messages.slice(-8) // laatste 8
],
};
}
return {};
},
Vertel: "Bij lange agents groeit messages snel — 30+ items, elk met tool-results van duizenden tokens. Hier zeggen we: na 12 messages, gooi de oudste weg, hou alleen system + laatste 8.
Voor research specifiek niet zo nodig — meestal eindigt agent na 8-10 stappen. Maar voor coding agents of debugging-agents die 30+ stappen doen — essentieel."
*[Test]*
Vertel: "Voor onze research-agent niet zichtbaar effect. Maar nu je het weet: bij langere agents — gebruik je dit."
BLOK 7 — Live Demo 4: Done-tool + custom stop (20 min)
[SLIDE 14 — DEMO 4] [SCHERM: editor]
Vertel: "Nog twee patronen. Eerst de done-tool. Een tool zonder execute."
export const done = tool({
description:
"Roep aan wanneer onderzoek klaar is en rapport is opgeslagen. " +
"Geef het reportId en een korte samenvatting.",
inputSchema: z.object({
reportId: z.number(),
summary: z.string(),
}),
// GEEN execute — dat stopt de loop
});
Vertel: "Geen execute betekent: als agent deze tool aanroept, stopt de loop automatisch. Geen tool-result toegevoegd, geen volgende stap.
Combineren met toolChoice: 'required' — dwingen dat agent altijd via een tool stopt, nooit met losse text."
export const researchAgent = new ToolLoopAgent({
model: openai("gpt-4o-mini"),
system: `... [zelfde] ...
6. Wanneer alles opgeslagen is, roep dan 'done' aan met het reportId.`,
tools: { webSearch, readPage, saveReport, done },
toolChoice: "required",
stopWhen: stepCountIs(15),
});
*[Run]*
Vertel: "Agent doet zijn werk, eindigt met done({ reportId: 42, summary: '...' }). We pakken het eindresultaat uit result.staticToolCalls:"
const result = await researchAgent.generate({ prompt: query });
const doneCall = result.staticToolCalls[0];
if (doneCall?.toolName === "done") {
console.log("Final:", doneCall.input.summary);
}
Vertel: "Voordeel: gestructureerd eindantwoord. Geen vage text. Voor pipelines waar je het resultaat ergens anders moet injecten — heel handig."
Custom stop op kosten
*[Voeg toe]*
const costLimit: StopCondition<any> = ({ steps }) => {
const tokens = steps.reduce(
(sum, s) => sum + (s.usage?.totalTokens ?? 0),
0
);
const usd = (tokens * 0.00015) / 1000; // gpt-4o-mini rate
console.log(`Total tokens: ${tokens}, ~$${usd.toFixed(4)}`);
return usd > 0.05;
};
stopWhen: [stepCountIs(30), costLimit],
Vertel: "Stop als kosten boven 5 dollarcent uitkomen. Voor productie: kritiek. Eén bug in je prompt en je hebt 100 calls — zonder cost cap is dat 20 euro. Mét cap: maximaal 5 cent per run, klaar."
Plan-Act-Reflect
[SLIDE — terug naar slide 14 visual]
Vertel: "Laatste pattern — alleen kort toelichten. ReAct: Reason + Act. Idee: in de system prompt vraag je expliciet om eerst een plan, dan acties, dan reflectie.
System prompt:
1. Begin met een plan in 3-5 bullets.
2. Voer plan uit met tools.
3. Eindig met korte reflectie — wat zou je anders doen?
Geen extra code nodig — het model doet dit vanzelf binnen de loop. Effect: tracebaarder gedrag, en zelf-correctie in latere stappen.
Voor sub-agents — een agent die een andere agent als tool gebruikt — schuiven we naar Les 14, want we hebben tijdsdruk."
BLOK 8 — Wanneer wel, wanneer niet (10 min)
[SLIDE 15 — Agent vs tool-call vs workflow]
Vertel: "Drie niveaus van AI-autonomie. Plain tool-call: 1 tool, 1 antwoord. Multi-step uit Les 12: 2-5 tools, 1 doel. Agent uit Les 14: 10-50 stappen, open-ended. En het hoogste niveau van controle — een explicit workflow. Code die exact bepaalt welke tool wanneer.
*[Wijs tabel op slide]*
Wanneer GEEN agent. Determinisme nodig — finance, juridisch, medische data. Geen agent. Doe het in code. Latency belangrijk — agent kost al snel 30 seconden. Voor user-facing chat: te traag. Voorspelbare kosten — agent met 50 stappen kan duur zijn.
Wanneer WEL agent. Open-ended — research, planning, debugging. Volgorde onbekend — model moet zelf bepalen. Async gebruik — backend job, niet realtime UI.
Quote uit de docs: Agents are flexible and powerful, but non-deterministic."
💬 Vraag: "Wat is een workflow dan precies in code?"
Antwoord: "Gewone JavaScript. If-statements, switches, expliciete functies. Bijvoorbeeld:
async function processOrder(order) {
const validated = await validate(order);
if (!validated.ok) return reject(validated.errors);
const enriched = await enrichWithAI(validated.data);
await save(enriched);
await notify(enriched);
}
Dat is een workflow. Reproduceerbaar, debuggable, testbaar. Een agent zou dit ook kunnen, maar je weet niet exact wat-ie doet — non-deterministisch."
BLOK 9 — Lesopdracht + Huiswerk (15 min)
[SLIDE 16 — Lesopdracht + Huiswerk]
Vertel: "Lesopdracht. Vandaag in de les: setup van de research-agent zoals ik 'm vandaag liet zien. Tavily account, Supabase tabel, 3 tools, agent, één test. Half uur — kun je halen voor we klaar zijn.
Voor huiswerk: uitbreiden. Vier dingen.
Eén: 4e tool listReports — om eerder opgeslagen rapporten op te halen. System prompt aanpassen zodat agent eerst checkt of er al onderzoek is.
Twee: prepareStep toevoegen. Kies één use case — dynamic model, fase-tools, of context-trimming. Documenteer wat je koos en waarom.
Drie: custom StopCondition. Token-budget, of aantal pagina's gelezen, of iets eigens.
Vier: schrijf een AGENT.md in repo-root. Tool-lijst, stop-strategie, prepareStep-keuze, één voorbeeld-run met logs, één observatie.
Bonus voor wie wil: UI met live step-rendering, of een sub-agent voor samenvatting.
Beoordeling: 10 punten totaal, voldoende is 6+. Inleveren via Brightspace met repo URL voor Les 14."
BLOK 10 — Afsluiting (5 min)
[SLIDE 17 — Afsluiting]
Vertel: "Wat hebben we vandaag gedaan. Een agent is: LLM in een loop met tools. We gebruikten ToolLoopAgent om de loop te managen. Vier stop-condities — stepCountIs, hasToolCall, isLoopFinished, custom. prepareStep voor dynamic model, tools, context. Done-tool pattern voor gestructureerde eindantwoorden. En we wisten wanneer een agent wel/niet de juiste tool is.
Volgende les: RAG. Embeddings. Vector search in pgvector. Combo met agents wordt heel sterk — een 'searchKnowledgeBase' tool die semantic search doet, in een research-agent.
Daarna: testing, deployment, performance, eindopdracht.
Vragen?"
*[Vragenronde — minstens 5 minuten over laten]*
NA DE LES — Wrap-up
- Push working
research-agentrepo naar GitHub als referentie - Brightspace: link naar deze repo + huiswerk-instructie
- Verzamel vragen die niet beantwoord — neem mee in Les 14 opening
- Note voor Klas B (later): Klas B doet dit waarschijnlijk in week na RAG, niet ervoor — orde aanpassen indien nodig
Veelvoorkomende fouten tijdens live coding
| Fout | Oplossing |
|---|---|
TAVILY_API_KEY undefined |
pnpm dev opnieuw starten — env wordt bij start ingelezen |
| Tavily 401 | Verkeerde key — check dashboard tavily.com |
result.steps is undefined |
Je gebruikt oude v5 API — check je ai versie is ^6 |
ToolLoopAgent import faalt |
pnpm update ai — moet 6.0+ zijn |
| Agent stopt na 1 stap | stopWhen te restrictief, of system prompt zegt niet duidelijk wat te doen |
Tool returnt [Object] |
JSON.stringify voor logs — Tavily resultaat is genest |
| Supabase RLS error | Open policy nog niet aangemaakt, of enable rls ontbreekt |
| Cost-cap stop werkt niet | Check of step.usage echt populated is — sommige providers geven het niet altijd terug |
Mentale model voor de klas
Als studenten verward zijn over "wanneer is iets een agent vs tool-call":
Tool-calling (Les 12) = "AI mag tot 5 keer een tool aanroepen om je vraag te beantwoorden."
Agent (Les 14) = "AI krijgt een DOEL, mag autonoom plannen en 20-50 stappen doen, en levert pas op als-ie zelf vindt dat het klaar is."
Het verschil is dus doelgericht autonoom werken, niet zomaar 'meer stappen'.