final lessons

This commit is contained in:
2026-06-07 10:44:05 +02:00
parent ceea2f206a
commit 39ec1bac72
105 changed files with 7789 additions and 1515 deletions

View File

@@ -1,474 +0,0 @@
# Les 13 — Lesstof
## Agents — LLMs die tools gebruiken in een loop
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 12 — Tool Calling
**Volgende les:** Les 14 — RAG + embeddings
---
## Inhoud
1. [Wat is een agent](#1-wat-is-een-agent)
2. [De agent-loop in detail](#2-de-agent-loop-in-detail)
3. [ToolLoopAgent — de v6 abstractie](#3-toolloopagent--de-v6-abstractie)
4. [Stop-condities](#4-stop-condities)
5. [prepareStep — control per stap](#5-preparestep--control-per-stap)
6. [Done-tool pattern](#6-done-tool-pattern)
7. [Planning + reflectie patronen](#7-planning--reflectie-patronen)
8. [Agent vs tool-call vs workflow](#8-agent-vs-tool-call-vs-workflow)
9. [Productie-overwegingen](#9-productie-overwegingen)
---
## 1. Wat is een agent
De definitie van Vercel AI SDK is kort en goed bruikbaar:
> Agents are LLMs that use tools in a loop to accomplish tasks.
Drie ingrediënten:
1. **LLM** — beslist iedere stap wat de volgende actie is. Tool aanroepen of antwoorden in text.
2. **Tools** — uitbreidingen van wat het model kan: zoeken, lezen, schrijven, rekenen, anything.
3. **Loop** — de runtime die LLM en tools afwisselt. Beheert berichten (context) en stop-condities.
Het verschil met Les 12 zit niet in *of* je een loop hebt, maar in **hoe lang die loop draait** en **hoeveel autonomie** het model krijgt. In Les 12 deden we maximaal 5 stappen — kort en gecontroleerd. In Les 13 gaan we naar 20-50 stappen, en het model bepaalt zelf welke tools het in welke volgorde gebruikt.
### Voorbeelden van agent-taken
- **Research agent** — gegeven een vraag, zoek op het web, lees relevante pagina's, schrijf een rapport
- **Coding agent** — gegeven een bugreport, lees code, draai tests, voer fix door
- **Planner** — gegeven beschikbaarheid + voorkeuren, plan een vakantie
- **Customer support triage** — gegeven een ticket, classify, gather context, draft response
In al deze gevallen is de *volgorde* van stappen niet vooraf bekend. Daarom heeft een agent meer ruimte nodig dan een simpele tool-call.
---
## 2. De agent-loop in detail
Wat gebeurt er per iteratie?
```
┌────────────────────────────────────────┐
│ STAP N: │
│ │
│ 1. Stuur huidige messages naar LLM │
│ 2. LLM antwoordt met: │
│ a) text-only? → loop stopt │
│ b) tool-call(s)? → ga door │
│ 3. Execute(s) de tools (parallel) │
│ 4. Voeg tool-result(s) toe aan msgs │
│ 5. Check stopWhen-conditie │
│ 6. Indien niet gestopt → STAP N+1 │
└────────────────────────────────────────┘
```
De loop stopt in vier gevallen:
- **Natural finish** — model genereert text in plaats van tool-call
- **stopWhen voldaan** — bijv. `stepCountIs(20)`
- **Tool zonder execute aangeroepen** — done-pattern
- **Tool needs approval** — agent vraagt user-confirmation (advanced)
> **Default:** `stepCountIs(20)`. Dit is een veiligheidsgrens om runaway loops te voorkomen.
---
## 3. ToolLoopAgent — de v6 abstractie
In AI SDK v3-v5 schreef je de loop vaak zelf met `streamText` + `stopWhen`. In v6 is er een dedicated class: `ToolLoopAgent`.
### Het basis-pattern
```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";
import { z } from "zod";
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
system: "Je bent een research-assistent. Plan je werk, zoek, lees, vat samen.",
tools: {
webSearch,
readPage,
saveReport,
},
stopWhen: stepCountIs(30),
});
const result = await researchAgent.generate({
prompt: "Schrijf een rapport over AI in de Nederlandse bouwsector",
});
console.log(result.text); // finale antwoord
console.log(result.steps); // array van alle stappen
console.log(result.steps[0].toolCalls); // welke tools werden aangeroepen
console.log(result.steps[0].toolResults); // resultaten daarvan
```
### Waarom ToolLoopAgent?
- **Minder boilerplate** — geen handmatige while-loop, geen message-array management
- **Herbruikbaar** — definieer agent 1x, gebruik in API-routes / scripts / queues
- **Single source of config** — system prompt, tools, model, stop-conditie op één plek
### streaming vs generate
```typescript
// Eenmalig antwoord
const result = await agent.generate({ prompt });
// Voor chat-UI: streaming
const result = agent.stream({ prompt });
return result.toUIMessageStreamResponse(); // streamt steps naar client
```
---
## 4. Stop-condities
De `stopWhen` parameter bepaalt wanneer de agent klaar is. Vier opties:
### 4.1 `stepCountIs(N)` — maximum aantal stappen
```typescript
stopWhen: stepCountIs(50) // stop na 50 stappen
```
Bruikbaar als veiligheidsgrens. Default is `stepCountIs(20)`.
### 4.2 `hasToolCall(toolName)` — stop bij specifieke tool
```typescript
stopWhen: hasToolCall("saveReport")
```
Stop zodra een bepaalde tool is aangeroepen. Handig voor "doe het werk en sla op, dan klaar".
### 4.3 `isLoopFinished()` — onbeperkt
```typescript
stopWhen: isLoopFinished() // GEEN limit — agent stopt alleen natuurlijk
```
> **Waarschuwing:** zonder limiet kan agent oneindig doorgaan. Alleen gebruiken met andere safeguards (cost limit, timeout).
### 4.4 Combineren — array van condities
```typescript
stopWhen: [
stepCountIs(50), // max 50 stappen, of
hasToolCall("submit"), // submit aangeroepen, of
]
```
Stop zodra één conditie waar is.
### 4.5 Custom stop-condition
```typescript
import { StopCondition, ToolSet } from "ai";
const tools = { webSearch, readPage, saveReport } satisfies ToolSet;
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const tokens = steps.reduce(
(sum, s) => sum + (s.usage?.totalTokens ?? 0),
0
);
return tokens > 50_000; // stop bij 50k tokens
};
new ToolLoopAgent({
tools,
stopWhen: [stepCountIs(30), budgetExceeded],
});
```
De callback krijgt alle `steps` tot nu toe. Je kunt op alles stoppen wat je kunt afleiden uit de step-historie.
---
## 5. prepareStep — control per stap
`prepareStep` is een async callback die VOOR elke stap draait. Je kunt dynamisch het model, de tools, de messages of de toolChoice aanpassen.
### 5.1 Signature
```typescript
prepareStep: async ({
model, // huidig model
stepNumber, // 0-indexed
steps, // alle vorige steps
messages, // berichten die naar model gaan
}) => {
// return iets — of {} voor "geen wijziging"
return {
model?: ...,
messages?: ...,
activeTools?: [...],
toolChoice?: ...,
};
}
```
### 5.2 Dynamic model selection
Begin goedkoop, switch naar duur model voor complex reasoning:
```typescript
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 2 && messages.length > 10) {
return { model: "anthropic/claude-sonnet-4.5" };
}
return {};
}
```
### 5.3 Tool-filtering per fase
```typescript
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 3) {
return { activeTools: ["webSearch"], toolChoice: "required" };
}
if (stepNumber <= 6) {
return { activeTools: ["readPage"] };
}
return { activeTools: ["saveReport", "done"], toolChoice: "required" };
}
```
Forceer een research → read → save flow zonder dat agent kan 'cheaten'.
### 5.4 Context-trimming
Lange agents genereren veel berichten. Token-budget bewaken:
```typescript
prepareStep: async ({ messages }) => {
if (messages.length > 20) {
return {
messages: [
messages[0], // system
...messages.slice(-10),// laatste 10
],
};
}
return {};
}
```
---
## 6. Done-tool pattern
Soms wil je dat agent ALTIJD via een tool stopt — bijvoorbeeld omdat je het uiteindelijke antwoord gestructureerd wilt.
```typescript
const tools = {
webSearch,
readPage,
saveReport,
done: tool({
description: "Roep aan wanneer onderzoek klaar is en rapport is opgeslagen.",
inputSchema: z.object({
finalReportId: z.number(),
summary: z.string(),
}),
// GEEN execute — dit signaleert: stop de loop
}),
};
const agent = new ToolLoopAgent({
model: "openai/gpt-4o",
tools,
toolChoice: "required", // model MOET altijd een tool aanroepen
});
const result = await agent.generate({ prompt });
// Het finale antwoord zit in de done-tool call:
const doneCall = result.staticToolCalls[0];
if (doneCall?.toolName === "done") {
console.log(doneCall.input.summary);
}
```
**Wanneer dit pattern gebruiken:**
- Je wilt een gestructureerd eindantwoord (geen vrije text)
- Je wilt voorkomen dat agent tussendoor stopt zonder iets op te slaan
- Combineren met `toolChoice: "required"` om text-generatie uit te sluiten
---
## 7. Planning + reflectie patronen
### 7.1 Plan-Act-Reflect (ReAct)
Een populair pattern uit de literatuur:
1. **Plan** — agent schrijft eerst een plan in text (stap 0)
2. **Act** — agent voert plan uit via tools (stap 1-N)
3. **Reflect** — agent kijkt terug, vat samen, evalueert (laatste stap)
In de praktijk komt dit door simpelweg in de system prompt te zetten:
```
1. Begin met een plan in 3-5 bullets.
2. Voer het plan uit met tools.
3. Aan het eind: schrijf een korte reflectie — wat ging goed, wat zou anders?
```
Het model doet dit dan vanzelf binnen de loop — geen aparte code nodig.
### 7.2 Sub-agents
Een agent kan een andere agent aanroepen als tool:
```typescript
const summarizer = new ToolLoopAgent({
model: "openai/gpt-4o-mini",
system: "Vat één webpagina samen in 5 bullets.",
tools: { readPage },
stopWhen: stepCountIs(3),
});
const summarizePageTool = tool({
description: "Vat een URL samen in bullets",
inputSchema: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const result = await summarizer.generate({
prompt: `Vat samen: ${url}`,
});
return { summary: result.text };
},
});
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
tools: { webSearch, summarizePage: summarizePageTool, saveReport },
});
```
Voordelen:
- Token-budget: sub-agent ziet kleinere context
- Specialisatie: sub-agent kan kleiner/goedkoper model gebruiken
- Modulair: sub-agent te testen los van hoofdagent
### 7.3 Self-correction
Geef de agent een tool om eigen tussenresultaat te valideren:
```typescript
const validate = tool({
description: "Check of een rapport feitelijk juist is. Returnt issues.",
inputSchema: z.object({ reportText: z.string() }),
execute: async ({ reportText }) => {
// bv. tweede LLM-call als fact-checker
return { issues: [...] };
},
});
```
Combineer met system prompt: "Als validate issues teruggeeft, herschrijf en check opnieuw."
---
## 8. Agent vs tool-call vs workflow
Niet elke taak hoort bij een agent. Drie niveaus:
| Niveau | Wanneer | Voorbeeld |
|--------|---------|-----------|
| **Plain tool-call** | 1 tool nodig, 1 antwoord | "Wat is het weer in Utrecht?" |
| **Multi-step (Les 12)** | 2-5 tools, vraag heeft 1 doel | "Hoeveel jazz acts vs rock?" |
| **Agent (Les 13)** | 10-50 stappen, open-ended | "Onderzoek X en schrijf rapport" |
| **Workflow (code)** | Reproduceerbaar, audit-trail | Order intake pipeline |
### Wanneer GEEN agent
- **Determinisme nodig** — finance, juridisch, medische data
- **Latency** — agent gebruikt al snel 30s+
- **Voorspelbare kosten** — agent met 50 stappen kan duur uitlopen
- **Eenvoudige flow** — een if-statement is genoeg
### Wanneer WEL agent
- **Open-ended** — research, planning, debugging
- **Volgorde onbekend** — model bepaalt zelf
- **Lange ketens** — meerdere bronnen, meerdere stappen
- **Latency + kosten acceptabel** — async use cases vooral
Quote uit AI SDK docs:
> Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes with explicit control flow, use core functions with structured workflow patterns.
---
## 9. Productie-overwegingen
### 9.1 Kosten bewaken
Eén onbedoelde loop kan tientallen euro's kosten. Altijd:
- `stepCountIs(N)` als safety cap
- Custom `StopCondition` op tokens of dollar-amount
- Logging per step (welke tool, hoeveel tokens)
```typescript
const costCap: StopCondition<typeof tools> = ({ steps }) => {
const usd = steps.reduce((s, step) =>
s + estimateCost(step.usage), 0);
return usd > 0.50;
};
```
### 9.2 Latency
Een 20-step agent op gpt-4o duurt 30-60 seconden. Voor user-facing:
- Stream steps naar client (`agent.stream()` + `toUIMessageStreamResponse()`)
- Toon progress (welke stap, welke tool)
- Loading state met geschatte tijd
### 9.3 Observability
Log per step:
- Step number
- Tool calls + input
- Tool result (truncated)
- Token usage
- Latency
Dit is essentieel voor debugging — agents zijn niet-deterministisch, dus een specifieke run is vaak niet exact reproduceerbaar.
### 9.4 Veiligheid
- Write-tools (DB inserts, mails, API calls) altijd met explicit user-intent
- Confirmation UI voor irreversible acties
- Read-only sandbox voor development (Supabase RLS policies open)
- Production: tighter RLS + write-tools authenticated
### 9.5 Testing
Agents zijn lastig te testen — non-determinisme. Strategieën:
- **Mock LLM** — test je tools los met fake LLM
- **Snapshot tests** — log volledige step-trace, vergelijk met goedgekeurde versie
- **Eval suites** — set van vragen + verwachte tool-sequences
---
## Bronnen
- **AI SDK Agents — Overview:** https://ai-sdk.dev/docs/agents/overview
- **AI SDK Agents — Loop Control:** https://ai-sdk.dev/docs/agents/loop-control
- **AI SDK Agents — Building Agents:** https://ai-sdk.dev/docs/agents/building-agents
- **AI SDK Agents — Subagents:** https://ai-sdk.dev/docs/agents/subagents
- **AI SDK Workflow patterns:** https://ai-sdk.dev/docs/agents/workflows
- **Tavily Web Search API:** https://tavily.com/
- **Anthropic — Building effective agents:** https://www.anthropic.com/research/building-effective-agents
- **ReAct paper:** https://arxiv.org/abs/2210.03629