add les 12
This commit is contained in:
537
Les12-Tool-Calling/Les12-Lesstof.md
Normal file
537
Les12-Tool-Calling/Les12-Lesstof.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# Les 12 — Tool Calling
|
||||
## Lesstof
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Onderwerp:** Tool Calling — AI besluit zelf welke functie aan te roepen
|
||||
**Demo:** Polderfest 2027 (vervolg op Les 11)
|
||||
|
||||
---
|
||||
|
||||
## Inhoudsopgave
|
||||
|
||||
1. [Het schaalprobleem dat we oplossen](#1-het-schaalprobleem)
|
||||
2. [Wat is Tool Calling?](#2-wat-is-tool-calling)
|
||||
3. [Anatomie van een tool](#3-anatomie-van-een-tool)
|
||||
4. [Multi-step met stopWhen](#4-multi-step-met-stopwhen)
|
||||
5. [Refactor: chat-route met tools](#5-refactor-chat-route)
|
||||
6. [Tool-invocations in de UI](#6-tool-invocations-in-de-ui)
|
||||
7. [Edge cases & error handling](#7-edge-cases--error-handling)
|
||||
8. [Tool Calling vs context-all — vergelijking](#8-tool-calling-vs-context-all)
|
||||
9. [Best practices](#9-best-practices)
|
||||
10. [Wat komt hierna? Agents teaser](#10-wat-komt-hierna)
|
||||
11. [Bronnen](#11-bronnen)
|
||||
|
||||
---
|
||||
|
||||
## 1. Het schaalprobleem
|
||||
|
||||
In Les 11 stuurden we **alle 500 bands** mee als tekst-context bij elke chat-request:
|
||||
|
||||
```typescript
|
||||
const { data: bands } = await supabase.from("bands").select("*");
|
||||
const context = bands.map((b) => `- ${b.name} (${b.genre}, ...)`).join("\n");
|
||||
const system = `Hier zijn alle bands:\n${context}\nBeantwoord vragen...`;
|
||||
```
|
||||
|
||||
**Werkt voor 500 records. Werkt niet voor 50.000.**
|
||||
|
||||
| Records | Tokens per call | Cost (gpt-4o-mini) | Probleem |
|
||||
|---------|----------------|---------------------|----------|
|
||||
| 500 | ~30.000 | $0.005 | OK |
|
||||
| 5.000 | ~300.000 | $0.05 | Te traag, context-window grenst aan limit |
|
||||
| 50.000 | ~3 miljoen | n.v.t. | **Past niet in context** |
|
||||
|
||||
Daarnaast:
|
||||
- Context is een **snapshot** — als data verandert tijdens chat, weet AI 't niet
|
||||
- **Geen write-acties** mogelijk — alleen lezen
|
||||
- AI moet zelf "denken" om de juiste records te vinden in een lap tekst
|
||||
|
||||
Tool Calling lost dit op.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wat is Tool Calling?
|
||||
|
||||
Het idee: in plaats van **alle data** mee te sturen, geef je AI **tools** (functies). AI ziet je vraag, kiest welke tool relevant is, roept 'm aan met de juiste parameters, en gebruikt het resultaat om te antwoorden.
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
User: "Welke bands spelen vrijdag op de Main Stage?"
|
||||
↓
|
||||
AI: kiest tool → searchBands({ day: "Vrijdag", stage: "Main Stage" })
|
||||
↓
|
||||
Supabase: 12 bands terug
|
||||
↓
|
||||
AI: formuleert antwoord op basis van resultaat
|
||||
↓
|
||||
User: "Op vrijdag op de Main Stage spelen: ..."
|
||||
```
|
||||
|
||||
### Wat win je
|
||||
|
||||
- **Schaalbaar** — werkt voor 100 of 10 miljoen records
|
||||
- **Real-time** — tool draait elke keer opnieuw, geen snapshot
|
||||
- **Type-safe** — parameters via Zod schema, gevalideerd
|
||||
- **Multi-step** — meerdere tools combineren voor complexe vragen
|
||||
- **Write-acties** — AI kan ook iets in je database zetten (favorieten, votes, notes)
|
||||
|
||||
---
|
||||
|
||||
## 3. Anatomie van een tool
|
||||
|
||||
Drie verplichte delen.
|
||||
|
||||
```typescript
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
const searchBands = tool({
|
||||
description:
|
||||
"Zoek bands in de Polderfest line-up. Filter op dag, stage, genre, of tier.",
|
||||
inputSchema: z.object({
|
||||
day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
|
||||
stage: z.string().optional(),
|
||||
genre: z.string().optional(),
|
||||
tier: z.enum(["headliner", "mid", "opener"]).optional(),
|
||||
}),
|
||||
execute: async ({ day, stage, genre, tier }) => {
|
||||
let q = supabase.from("bands").select("*");
|
||||
if (day) q = q.eq("day", day);
|
||||
if (stage) q = q.eq("stage", stage);
|
||||
if (genre) q = q.eq("genre", genre);
|
||||
if (tier) q = q.eq("tier", tier);
|
||||
const { data, error } = await q.limit(20);
|
||||
if (error) return { error: error.message };
|
||||
return data;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `description`
|
||||
Wat doet deze tool en wanneer gebruik je 'm? **AI kiest tools op basis van descriptions.**
|
||||
|
||||
- ❌ Vaag: `"Doe iets met bands"`
|
||||
- ✅ Goed: `"Zoek bands op dag, stage, genre of tier. Gebruik dit voor filtervragen."`
|
||||
|
||||
Schrijf alsof je 't aan een collega uitlegt. Twee zinnen vaak genoeg.
|
||||
|
||||
### `inputSchema`
|
||||
Zod schema. Type-safe. AI weet welke parameters mogen. (Heette `parameters` in AI SDK v3 — sinds v5/v6 `inputSchema`.)
|
||||
|
||||
- `z.string()` — vrije tekst
|
||||
- `z.enum([...])` — vaste keuze (AI mag alleen een van deze)
|
||||
- `z.number().min(1).max(100)` — getal met bounds
|
||||
- `z.boolean()`
|
||||
- `.optional()` — parameter mag weg
|
||||
- `.describe("...")` — extra context voor AI
|
||||
|
||||
### `execute`
|
||||
Async functie. Returnt wat AI moet zien. **Belangrijk:** errors als data terug returnen, niet als exception throwen:
|
||||
|
||||
```typescript
|
||||
// ❌ Niet doen — AI ziet de error niet
|
||||
execute: async (...) => {
|
||||
const data = await supabase.from(...).select();
|
||||
if (data.error) throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
// ✅ Wel doen — AI kan dit zelf afhandelen
|
||||
execute: async (...) => {
|
||||
const { data, error } = await supabase.from(...).select();
|
||||
if (error) return { error: error.message };
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Multi-step met stopWhen
|
||||
|
||||
Met `stopWhen: stepCountIs(5)` geef je AI toestemming om tot 5 keer een tool aan te roepen voordat hij definitief antwoordt. (In AI SDK v3 heette dit `maxSteps: 5` — sinds v5/v6 gebruik je `stopWhen` met een conditie zoals `stepCountIs(N)`.)
|
||||
|
||||
```typescript
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
tools: { searchBands, getStats, getBandByName },
|
||||
stopWhen: stepCountIs(5),
|
||||
messages,
|
||||
});
|
||||
```
|
||||
|
||||
### Voorbeeld
|
||||
|
||||
User: *"Vergelijk de top headliner met de drukst geplande opener."*
|
||||
|
||||
```
|
||||
Stap 1: AI roept searchBands({ tier: "headliner" }) aan
|
||||
→ 50 bands terug, sorteer op popularity, top 1 gevonden
|
||||
Stap 2: AI roept searchBands({ tier: "opener" }) aan
|
||||
→ 100 bands terug, top 1 gevonden
|
||||
Stap 3: AI vergelijkt + antwoordt
|
||||
```
|
||||
|
||||
Drie stappen, één request. AI plant zelf de volgorde.
|
||||
|
||||
### Wanneer welk step-limit?
|
||||
|
||||
| Use case | `stepCountIs(N)` |
|
||||
|----------|------------------|
|
||||
| Simpele query ("welke bands op vrijdag?") | 1-2 |
|
||||
| Vergelijking ("X vs Y") | 3-5 |
|
||||
| Onderzoek ("vat alle X samen") | 5-10 |
|
||||
| Agentic ("plan mijn weekend") | 15-30+ (volgende les) |
|
||||
|
||||
**Default in AI SDK:** 1 stap. Dus expliciet `stopWhen` zetten als je multi-step wilt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Refactor: chat-route met tools
|
||||
|
||||
De volledige `app/api/chat/route.ts` na refactor:
|
||||
|
||||
```typescript
|
||||
import { streamText, tool } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { z } from "zod";
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
);
|
||||
|
||||
const searchBands = tool({
|
||||
description: "Zoek bands op dag, stage, genre, of tier.",
|
||||
inputSchema: z.object({
|
||||
day: z.enum(["Vrijdag", "Zaterdag", "Zondag"]).optional(),
|
||||
stage: z.string().optional(),
|
||||
genre: z.string().optional(),
|
||||
tier: z.enum(["headliner", "mid", "opener"]).optional(),
|
||||
}),
|
||||
execute: async ({ day, stage, genre, tier }) => {
|
||||
let q = supabase.from("bands").select("*");
|
||||
if (day) q = q.eq("day", day);
|
||||
if (stage) q = q.eq("stage", stage);
|
||||
if (genre) q = q.eq("genre", genre);
|
||||
if (tier) q = q.eq("tier", tier);
|
||||
const { data, error } = await q.limit(20);
|
||||
if (error) return { error: error.message };
|
||||
return { count: data.length, bands: data };
|
||||
},
|
||||
});
|
||||
|
||||
const getStats = tool({
|
||||
description: "Geef verdeling van bands per groep (genre, day, stage, tier).",
|
||||
inputSchema: z.object({
|
||||
groupBy: z.enum(["genre", "day", "stage", "tier"]),
|
||||
}),
|
||||
execute: async ({ groupBy }) => {
|
||||
const { data, error } = await supabase.from("bands").select(groupBy);
|
||||
if (error) return { error: error.message };
|
||||
const counts: Record<string, number> = {};
|
||||
for (const row of data) {
|
||||
const key = row[groupBy as keyof typeof row] as string;
|
||||
counts[key] = (counts[key] ?? 0) + 1;
|
||||
}
|
||||
return { total: data.length, counts };
|
||||
},
|
||||
});
|
||||
|
||||
const getBandByName = tool({
|
||||
description: "Haal alle details op van één band bij naam (inclusief bio).",
|
||||
inputSchema: z.object({ name: z.string() }),
|
||||
execute: async ({ name }) => {
|
||||
const { data, error } = await supabase
|
||||
.from("bands").select("*").ilike("name", name).single();
|
||||
if (error) return { error: `Band '${name}' niet gevonden.` };
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages } = await req.json();
|
||||
|
||||
const system = `Je bent een festival-assistent voor Polderfest 2027.
|
||||
Gebruik de beschikbare tools om vragen te beantwoorden.
|
||||
Verzin nooit data. Antwoord beknopt en in het Nederlands.`;
|
||||
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
system,
|
||||
messages,
|
||||
tools: { searchBands, getStats, getBandByName },
|
||||
stopWhen: stepCountIs(5),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
```
|
||||
|
||||
**Wat is veranderd t.o.v. Les 11:**
|
||||
- Geen `select("*")` op start meer
|
||||
- Geen grote context-string in system prompt
|
||||
- Tools-object toegevoegd
|
||||
- `stopWhen: stepCountIs(5)` voor multi-step
|
||||
- System prompt veel korter — alleen rol + regels
|
||||
|
||||
---
|
||||
|
||||
## 6. Tool-invocations in de UI
|
||||
|
||||
`useChat` returnt `messages` met **parts** — een array van delen per bericht. Tekst-parts én tool-invocation-parts.
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ChatPage() {
|
||||
const { messages, sendMessage, status } = useChat();
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
return (
|
||||
<main>
|
||||
{messages.map((m) => (
|
||||
<div key={m.id}>
|
||||
<strong>{m.role}:</strong>
|
||||
{m.parts?.map((part, i) => {
|
||||
if (part.type === "text") {
|
||||
return <div key={i}>{part.text}</div>;
|
||||
}
|
||||
// In AI SDK v6 zijn tool-parts genaamd `tool-<toolName>`
|
||||
if (part.type?.startsWith("tool-")) {
|
||||
const toolName = part.type.replace("tool-", "");
|
||||
return (
|
||||
<div key={i} className="bg-yellow-50 p-2 rounded">
|
||||
🔧 {toolName}({JSON.stringify(part.input)})
|
||||
{part.state === "output-available" && (
|
||||
<details>
|
||||
<summary>Toon resultaat</summary>
|
||||
<pre>{JSON.stringify(part.output, null, 2)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim()) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
}}
|
||||
>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Waarom tool-invocations tonen?
|
||||
|
||||
- **Debug** — zien wat AI aanroept met welke args
|
||||
- **Vertrouwen** — gebruiker ziet "ja, hij heeft echt iets opgezocht"
|
||||
- **Demo** — voor presentaties, hackathons, onboarding
|
||||
|
||||
In productie kun je dit optioneel verstoppen, voor jullie eindopdracht: **wel tonen**.
|
||||
|
||||
### Part states (v6)
|
||||
|
||||
| state | Wat |
|
||||
|-------|-----|
|
||||
| `"input-streaming"` | Tool args worden nog gestreamed |
|
||||
| `"input-available"` | Args compleet, tool draait |
|
||||
| `"output-available"` | Tool is gerund, resultaat beschikbaar |
|
||||
| `"output-error"` | Tool gaf een error |
|
||||
|
||||
---
|
||||
|
||||
## 7. Edge cases & error handling
|
||||
|
||||
### Ongeldige input (enum-restrictie)
|
||||
|
||||
User: *"Welke bands spelen op Donderdag?"*
|
||||
AI ziet dat `day` enum is — `["Vrijdag", "Zaterdag", "Zondag"]`. Donderdag past niet. AI weigert tool aan te roepen en legt uit.
|
||||
|
||||
**Les:** gebruik enums voor restricted values. AI respecteert ze.
|
||||
|
||||
### Lege resultaten
|
||||
|
||||
User: *"Death metal bands?"*
|
||||
Tool returnt `{ count: 0, bands: [] }`. AI legt uit: "Geen death metal op Polderfest 2027."
|
||||
|
||||
**Les:** lege array is OK, AI handelt 't af.
|
||||
|
||||
### Database errors
|
||||
|
||||
```typescript
|
||||
execute: async (...) => {
|
||||
const { data, error } = await supabase.from(...).select();
|
||||
if (error) return { error: error.message };
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
AI ziet `{ error: "..." }`, communiceert dit netjes. **Niet** een exception throwen — dan crasht de hele chat.
|
||||
|
||||
### Write-tools en user-intent
|
||||
|
||||
Write-tools (insert/update/delete) zijn gevaarlijker — ze veranderen echt iets. Bescherm via:
|
||||
|
||||
1. **Descriptions**: "Alleen gebruiken als gebruiker expliciet vraagt om X toe te voegen"
|
||||
2. **Confirmation UI**: laat user nog een keer bevestigen voor de write definitief gaat
|
||||
3. **Permissions**: write-tool checked of user ingelogd is via auth.uid()
|
||||
|
||||
Voor demo open laten kan. Voor productie: streng.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tool Calling vs context-all
|
||||
|
||||
| Aspect | Les 11 (context-all) | Les 12 (tool calling) |
|
||||
|--------|---------------------|----------------------|
|
||||
| Tokens per call | ~30.000 (500 bands) | ~2.000 (tools + 1 result) |
|
||||
| Schaal | Tot ~1000 records | Tot duizenden makkelijk |
|
||||
| Live data | Snapshot bij chat-start | Actueel per call |
|
||||
| Write operaties | Niet mogelijk | Wel (addFavorite etc.) |
|
||||
| Multi-step | Beperkt — alleen reasoning | Native (`stopWhen` + `stepCountIs`) |
|
||||
| Cost | Hoger | Lager (kleinere context) |
|
||||
| Complexiteit | Lager | Iets hoger (tools definiëren) |
|
||||
|
||||
**Wanneer toch context-all?**
|
||||
- Hele kleine dataset (<100 records)
|
||||
- Snel prototype
|
||||
- Geen schaal nodig
|
||||
|
||||
**Voor productie: bijna altijd Tool Calling.**
|
||||
|
||||
---
|
||||
|
||||
## 9. Best practices
|
||||
|
||||
### Descriptions schrijven
|
||||
|
||||
```typescript
|
||||
// ❌
|
||||
description: "zoek dingen"
|
||||
|
||||
// ❌ Te vaag
|
||||
description: "zoek bands"
|
||||
|
||||
// ✅
|
||||
description:
|
||||
"Zoek bands in de Polderfest line-up. Filter op dag, stage, genre, " +
|
||||
"of tier. Gebruik dit voor filtervragen zoals 'welke X op vrijdag?'"
|
||||
```
|
||||
|
||||
### Parameter design
|
||||
|
||||
- Gebruik `enum` voor categorische waarden (niet string)
|
||||
- `.optional()` voor filter-parameters
|
||||
- `.describe()` op elke parameter — extra context
|
||||
- Houd parameter-sets klein (3-5 max)
|
||||
|
||||
### Returns
|
||||
|
||||
- Returnt **JSON-serializable** waarden (geen Date-objecten direct, gebruik ISO strings)
|
||||
- Errors als `{ error: "..." }` terug — niet throwen
|
||||
- Beperk grootte van responses (limit 20 records, niet 500)
|
||||
|
||||
### System prompts
|
||||
|
||||
```typescript
|
||||
const system = `Je bent een festival-assistent voor Polderfest 2027.
|
||||
Gebruik de beschikbare tools om vragen te beantwoorden.
|
||||
|
||||
Tips:
|
||||
- Voor "welke bands op X?" → searchBands
|
||||
- Voor "hoeveel" → getStats
|
||||
- Voor specifieke band → getBandByName
|
||||
|
||||
Verzin nooit data. Als tools een error returnen, leg dat uit aan de gebruiker.`;
|
||||
```
|
||||
|
||||
Schrijf het system prompt als handleiding voor AI — wanneer welke tool.
|
||||
|
||||
### Tools registeren
|
||||
|
||||
Houd tools in een aparte module als ze veel worden:
|
||||
|
||||
```typescript
|
||||
// lib/tools.ts
|
||||
export const searchBands = tool({ ... });
|
||||
export const getStats = tool({ ... });
|
||||
export const getBandByName = tool({ ... });
|
||||
|
||||
// app/api/chat/route.ts
|
||||
import * as tools from "@/lib/tools";
|
||||
|
||||
streamText({ ..., tools, stopWhen: stepCountIs(5) });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Wat komt hierna?
|
||||
|
||||
### Volgende les: Agents (Les 13)
|
||||
|
||||
Tool Calling met `stopWhen: stepCountIs(5)` is een eerste stap richting agents. Volgende les: **AI Agents** met `stepCountIs(20)` en hoger, of zelfs custom stop-condities.
|
||||
|
||||
```typescript
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
tools: { ... },
|
||||
stopWhen: stepCountIs(30),
|
||||
experimental_telemetry: { isEnabled: true },
|
||||
messages,
|
||||
});
|
||||
```
|
||||
|
||||
**Voorbeeld autonome workflow:**
|
||||
*"Plan mijn volledige Polderfest weekend op basis van mijn smaak."*
|
||||
|
||||
Agent doet:
|
||||
1. Vraag user smaakprofiel (Hip-Hop + Indie)
|
||||
2. searchBands per dag + genre
|
||||
3. Filtert op overlap-vermijding
|
||||
4. addFavorite per geselecteerde band
|
||||
5. listFavorites om finaal schema te tonen
|
||||
6. Wijst conflicten aan in tijdsloten
|
||||
7. Optimaliseert en herhaalt
|
||||
|
||||
30+ tool-calls in één user-request.
|
||||
|
||||
### Daarna in deze leerlijn
|
||||
|
||||
- **Les 14:** RAG + embeddings — semantic search op grote tekst-corpora
|
||||
- **Les 15-16:** Testing + Deployment + Performance
|
||||
- **Les 17-18:** Eindopdracht-werkdagen + Pitch
|
||||
|
||||
---
|
||||
|
||||
## 11. Bronnen
|
||||
|
||||
### Vercel AI SDK
|
||||
- Tools-documentatie: https://ai-sdk.dev/docs/foundations/tools
|
||||
- Agents + stopWhen: https://ai-sdk.dev/docs/foundations/agents
|
||||
- streamText reference: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text
|
||||
- useChat reference: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
|
||||
|
||||
### Underlying providers
|
||||
- OpenAI Function Calling: https://platform.openai.com/docs/guides/function-calling
|
||||
- Anthropic Tool Use: https://docs.anthropic.com/en/docs/build-with-claude/tool-use
|
||||
|
||||
### Zod
|
||||
- Docs: https://zod.dev
|
||||
- Schema reference: https://zod.dev/?id=primitives
|
||||
|
||||
### Supabase JS
|
||||
- Query builder: https://supabase.com/docs/reference/javascript/select
|
||||
- Filters: https://supabase.com/docs/reference/javascript/using-filters
|
||||
Reference in New Issue
Block a user