Files
novi-lessons/Les15-Agents/Les15-Huiswerk.md
2026-06-07 10:44:05 +02:00

8.4 KiB

Les 15 — Huiswerk

Agent uitbreiden + prepareStep + custom stop + AGENT.md

Vak: AI-Assisted Development Opleiding: NOVI Hogeschool Utrecht Deadline: Voor Les 16 — RAG + embeddings Inleveren: GitHub repo + AGENT.md in root


Doel

Bouwt voort op de lesopdracht (research-agent met 3 tools + stepCountIs(10)). In dit huiswerk maak je de agent slimmer: extra tools, dynamic step-control, een eigen stop-conditie, en documenteer je het geheel.

Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt nodig.


Onderdeel A — Extra tool (verplicht)

Voeg een 4e tool toe: listReports om opgeslagen rapporten op te halen.

export const listReports = tool({
  description:
    "Toon eerder opgeslagen onderzoeksrapporten. " +
    "Gebruik dit als de gebruiker vraagt naar bestaand onderzoek " +
    "voordat je nieuw onderzoek doet.",
  inputSchema: z.object({
    limit: z.number().int().min(1).max(20).default(5),
  }),
  execute: async ({ limit }) => {
    const { data, error } = await supabase
      .from("research_reports")
      .select("id, query, created_at")
      .order("created_at", { ascending: false })
      .limit(limit);
    if (error) return { error: error.message };
    return data;
  },
});

Voeg toe aan agent.tools en test:

"Heb ik eerder iets over AI in de bouw onderzocht? Zo nee, doe het nu."

Verwacht: agent roept eerst listReports, dan eventueel webSearchreadPagesaveReport.

Eisen

  • listReports werkt
  • System prompt aangepast — agent weet wanneer te checken
  • Minstens 1 test-query gedraaid die listReports triggert

Onderdeel B — prepareStep toevoegen (verplicht)

Voeg een prepareStep callback toe aan je ToolLoopAgent. Kies minimaal één use case:

Optie B1 — Dynamic model

Begin met gpt-4o-mini, switch naar betere model bij complexe stappen.

import { openai } from "@ai-sdk/openai";

new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  tools: { webSearch, readPage, saveReport, listReports },
  prepareStep: async ({ stepNumber, messages }) => {
    if (stepNumber > 4 || messages.length > 12) {
      return { model: openai("gpt-4o") };
    }
    return {};
  },
});

Optie B2 — Fase-gebaseerde tools

prepareStep: async ({ stepNumber }) => {
  if (stepNumber <= 2) {
    return { activeTools: ["listReports", "webSearch"] };
  }
  if (stepNumber <= 5) {
    return { activeTools: ["readPage"] };
  }
  return { activeTools: ["saveReport"], toolChoice: "required" };
}

Optie B3 — Context-trimming

prepareStep: async ({ messages }) => {
  if (messages.length > 15) {
    return {
      messages: [messages[0], ...messages.slice(-10)],
    };
  }
  return {};
}

Eisen

  • Minimaal 1 prepareStep use-case geïmplementeerd
  • Effect zichtbaar in console-logs (welk model / welke tools per stap)
  • In AGENT.md uitleggen welke optie gekozen + waarom

Onderdeel C — Custom stop-conditie (verplicht)

Schrijf een eigen StopCondition. Twee voorbeelden — kies één of bedenk eigen:

Optie C1 — Token-budget

import type { StopCondition } from "ai";

const tokenBudget: StopCondition<typeof tools> = ({ steps }) => {
  const total = steps.reduce(
    (sum, s) => sum + (s.usage?.totalTokens ?? 0),
    0
  );
  return total > 30_000;
};

Optie C2 — Genoeg sources verzameld

const enoughSources: StopCondition<typeof tools> = ({ steps }) => {
  const readPages = steps.flatMap((s) =>
    s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
  );
  return readPages.length >= 5; // 5 pagina's gelezen = genoeg
};

Combineren met andere condities:

stopWhen: [
  stepCountIs(30),
  tokenBudget,
  hasToolCall("saveReport"),
]

Eisen

  • Custom StopCondition werkt
  • Gecombineerd met stepCountIs(N) voor veiligheid
  • In AGENT.md uitleggen wat de conditie doet

Onderdeel D — AGENT.md documentatie (verplicht)

Schrijf in repo-root.

Sectie 1 — Tools

Tool Wat doet 't Read / Write
webSearch Tavily query → 5 resultaten Read
readPage Tavily extract → fulltext Read
saveReport Insert in research_reports Write
listReports Bestaande rapporten ophalen Read
(eigen tool?) ... ...

Sectie 2 — Stop-strategie

  • stepCountIs(N) — safety cap, N = ...
  • Custom: ... (uitleg)
  • Combinatie waarom: ...

Sectie 3 — prepareStep keuze

  • Welke optie gekozen (B1/B2/B3)
  • Waarom
  • Wat zie je in de console-logs verschillen tussen "met" en "zonder" prepareStep

Sectie 4 — Voorbeeld-run

Pak een echte query, draai 'm, en log alle steps. Plak in AGENT.md:

Query: "Wat zijn de belangrijkste AI-trends in 2026?"

Step 0: webSearch({ query: "AI trends 2026" })
  → 5 resultaten
Step 1: readPage({ url: "..." })
  → 4500 char tekst
Step 2: readPage({ url: "..." })
Step 3: webSearch({ query: "AI regulation EU 2026" })
Step 4: readPage({ url: "..." })
Step 5: saveReport({ ... })
  → reportId: 42

Finale rapport: (200-300 woorden)

Sectie 5 — Eén observatie

Iets wat opviel:

  • Onverwacht goed gedrag?
  • Loop die te lang duurde?
  • Tool die niet werd gekozen?
  • Effect van prepareStep zichtbaar?

Vorm

  • Max 700 woorden totaal
  • Concrete logs, geen abstracte tekst
  • Mag wat informeel

Bonus (optioneel)

Bonus 1 — UI met live steps

app/page.tsx — een form + lijst die elke step toont terwijl agent draait.

Gebruik agent.stream() ipv agent.generate():

const result = researchAgent.stream({ prompt: query });
return result.toUIMessageStreamResponse();

Client: tonen welke tool aangeroepen wordt + resultaat collapsed.

Bonus 2 — Sub-agent voor samenvatten

Maak een 2e ToolLoopAgent (kleiner model) die per URL samenvat. Hoofdagent gebruikt deze als tool:

const summarizer = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: "Vat één pagina samen in 5 bullets.",
  tools: { readPage },
  stopWhen: stepCountIs(3),
});

const summarizePage = tool({
  description: "Vat een URL samen",
  inputSchema: z.object({ url: z.string().url() }),
  execute: async ({ url }) => {
    const r = await summarizer.generate({ prompt: `Samenvat: ${url}` });
    return { summary: r.text };
  },
});

Bonus 3 — Done-tool pattern

Vervang hasToolCall("saveReport") door een expliciete done-tool zonder execute, met toolChoice: "required". Documenteer in AGENT.md wat het verschil was.


Inleveren

  1. GitHub repo URL in Brightspace
  2. AGENT.md in repo-root
  3. Updated lib/tools.ts met listReports (+ optionele extra tool)
  4. Updated lib/agent.ts met prepareStep + custom stop
  5. Schema-update in schema.sql (mocht je nieuwe tabel hebben)

Beoordeling

Criterium Punten
A — listReports werkt + agent gebruikt 'm correct 2
B — prepareStep geïmplementeerd + zichtbaar effect 2
C — Custom StopCondition werkt 2
D — AGENT.md compleet met 5 secties 3
Agent draait end-to-end (geen broken runs) 1
Totaal 10

Voldoende = 6+. Bonus telt mee bij twijfelgevallen.


Tijd-indicatie

Onderdeel Tijd
A — listReports + test 20 min
B — prepareStep 30 min
C — Custom StopCondition + tests 30 min
D — AGENT.md schrijven met logs 30 min
Totaal ~2 uur

Veelvoorkomende valkuilen

  • prepareStep returnt niks → je moet {} returnen voor "geen wijziging", niet undefined
  • Custom stop returnt nooit true → log steps.length en je conditie-waarde per stap
  • Agent stopt te vroeg → check of je stopWhen array niet te restrictief is
  • Sub-agent recursie → een sub-agent gebruikt zelf weer een sub-agent? Stack-overflow risico — limit op depth
  • Token-cost out of control → log per stap usage en zet kostencap

Tips

  • Log per stapconsole.log({ step, toolCalls, usage }) is de beste debug-tool
  • Test isolated — test je StopCondition met fake steps eerst
  • Houd AGENT.md bij tijdens werken — niet aan eind
  • Eindrapport kwaliteit — als agent rapport een 4 is van 10, ligt het meestal aan system prompt — niet aan tools

Volgende les: RAG + embeddings. Combo met agents: een RAG-tool in een agent. Tot dan!