475 lines
14 KiB
Markdown
475 lines
14 KiB
Markdown
# Les 15 — 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 16 — MCP servers
|
|
|
|
---
|
|
|
|
## 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 15 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 15)** | 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
|