# 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 = ({ 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 = ({ 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