fix: update lessons
This commit is contained in:
785
Les13-Agents/Les13-Docenttekst.md
Normal file
785
Les13-Agents/Les13-Docenttekst.md
Normal file
@@ -0,0 +1,785 @@
|
||||
# Les 13 — 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.]*`
|
||||
|
||||
```bash
|
||||
cd ~/novi/novi-lessons/Les13-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
|
||||
|
||||
```sql
|
||||
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.ts` ergens 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 /Les13-Agents]*`
|
||||
|
||||
```bash
|
||||
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."
|
||||
|
||||
```bash
|
||||
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]`
|
||||
|
||||
```sql
|
||||
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."
|
||||
|
||||
```sql
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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."
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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]`
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
`*[In tweede terminal:]*`
|
||||
|
||||
```bash
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
stopWhen: [
|
||||
stepCountIs(20),
|
||||
hasToolCall("saveReport"),
|
||||
],
|
||||
```
|
||||
|
||||
**Vertel:** "Best practice: array. Veiligheidscap én task-completion. Loop stopt op de eerste van de twee."
|
||||
|
||||
`*[Probeer isLoopFinished]*`
|
||||
|
||||
```typescript
|
||||
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."
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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**."
|
||||
|
||||
```typescript
|
||||
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."
|
||||
|
||||
```typescript
|
||||
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`:"
|
||||
|
||||
```typescript
|
||||
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]*`
|
||||
|
||||
```typescript
|
||||
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 13: 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:
|
||||
|
||||
```typescript
|
||||
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-agent` repo 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 13) = "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'.
|
||||
Reference in New Issue
Block a user