Files
novi-lessons/Les14-Agents/Les14-Huiswerk.md
2026-06-07 13:35:02 +02:00

323 lines
8.4 KiB
Markdown

# Les 14 — Huiswerk
## Agent uitbreiden + prepareStep + custom stop + AGENT.md
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Deadline:** Voor Les 15 — 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.
```typescript
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 `webSearch``readPage``saveReport`.
### 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.
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```typescript
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()`:
```typescript
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:
```typescript
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 stap** — `console.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!