fix: add les 11

This commit is contained in:
2026-05-19 18:50:11 +02:00
parent b053fc7206
commit 634789e615
37 changed files with 7587 additions and 209 deletions

View File

@@ -0,0 +1,527 @@
# Les 11 — Vercel AI SDK
## Lesstof
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Onderwerp:** Vercel AI SDK — AI features bouwen, gekoppeld aan eigen dataset
**Demo:** Polderfest 2027 — fictief festival, 500 records
---
## Inhoudsopgave
1. [Wat is de Vercel AI SDK?](#1-wat-is-de-vercel-ai-sdk)
2. [Het modellen-landschap](#2-het-modellen-landschap)
3. [De vier kern-functies](#3-de-vier-kern-functies)
4. [Project setup van A tot Z](#4-project-setup-van-a-tot-z)
5. [Seed script: dummy data in Supabase](#5-seed-script-dummy-data-in-supabase)
6. [Chat-route + chat-UI implementeren](#6-chat-route--chat-ui-implementeren)
7. [Waarom data + AI samen krachtig zijn](#7-waarom-data--ai-samen-krachtig-zijn)
8. [Best practices & valkuilen](#8-best-practices--valkuilen)
9. [Wat komt hierna? Tool Calling teaser](#9-wat-komt-hierna)
10. [Bronnen](#10-bronnen)
---
## 1. Wat is de Vercel AI SDK?
De Vercel AI SDK is een open-source TypeScript library waarmee je AI-features in je webapplicatie bouwt. Gemaakt door Vercel — de makers van Next.js — en daarom perfect geïntegreerd met React, Server Components en Server Actions.
### Wat krijg je?
- **Unified API** — Zelfde code voor elk model (40+ providers)
- **Streaming out-of-the-box** — Geen WebSocket-setup nodig
- **React hooks** — `useChat`, `useCompletion` voor instant chat UI
- **Tool Calling** — AI kan functies aanroepen die jij definieert (volgende les)
- **Structured output** — Type-safe data via `generateObject` + Zod
- **Multi-step agents** — Via `maxSteps`
### Eerste indruk
```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Vat de Polderfest 2027 line-up samen",
});
```
Wil je naar Anthropic? Eén regel veranderen:
```typescript
import { anthropic } from "@ai-sdk/anthropic";
const { text } = await generateText({
model: anthropic("claude-sonnet-4"),
prompt: "Vat de Polderfest 2027 line-up samen",
});
```
Dat is de waarde.
---
## 2. Het modellen-landschap
| Provider | Model | Sterke punten | Prijs (in/out per 1M tokens) |
|----------|-------|---------------|------------------------------|
| OpenAI | `gpt-4o-mini` | Snel, goedkoop, default | $0.15 / $0.60 |
| OpenAI | `gpt-4o` | Multimodal (vision), krachtig | $2.50 / $10 |
| OpenAI | `gpt-4.1` | Reasoning, voor agents | $2 / $8 |
| Anthropic | `claude-sonnet-4` | Coding, lange context (200k) | $3 / $15 |
| Google | `gemini-2.5-flash` | Multimodal, ultra goedkoop | $0.075 / $0.30 |
| Groq | `llama-3.3-70b` | Ultra-fast inference | $0.59 / $0.79 |
**Vuistregel:** start met `gpt-4o-mini`. Werkt het niet goed? Upgrade naar `gpt-4o`. Dan pas exotisch.
**Wat kost onze Polderfest demo?**
- Context = 500 bands → ~30.000 tokens per chat-request
- 1 vraag = ~$0.005 (een halve cent)
- 50 vragen = $0.25 (kwart euro)
Met gpt-4o (15× duurder) zou dezelfde demo ~$4 kosten. Daarom: start mini.
---
## 3. De vier kern-functies
### `generateText` — Antwoord ophalen
Wacht tot het AI-antwoord compleet is, returnt dan een string.
```typescript
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Geef 3 koffie-poll opties",
});
```
**Wanneer:** Korte antwoorden, server-only, niet-interactief.
### `streamText` — Streaming antwoord
Streamt karakter voor karakter. Goed voor chat UI.
```typescript
const result = streamText({
model: openai("gpt-4o-mini"),
messages,
});
return result.toDataStreamResponse();
```
**Wanneer:** Chat UI, lange antwoorden, "ChatGPT-gevoel". Vandaag gebruiken we dit.
### `useChat` — React hook
Aan client-kant: complete chat UI in 10 regels.
```tsx
"use client";
import { useChat } from "ai/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<form onSubmit={handleSubmit}>
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
<input value={input} onChange={handleInputChange} />
</form>
);
}
```
**Belangrijk:** werkt alleen met `streamText` als API endpoint. Vandaag gebruiken we dit ook.
### `generateObject` — Gestructureerde data
In plaats van een string krijg je type-safe data terug — gevalideerd met Zod.
```typescript
const { object } = await generateObject({
model: openai("gpt-4o-mini"),
schema: z.object({
question: z.string(),
options: z.array(z.string()).length(4),
}),
prompt: "Maak een poll over koffie",
});
```
**Wanneer:** Database inserts, formulieren vullen, classificatie. **Niet** vandaag — komt terug in latere lessen.
---
## 4. Project setup van A tot Z
Dit is wat Tim in de les live deed. Voor je eigen project (lesopdracht/huiswerk): zelfde stappen.
### Stap 1 — Next.js scaffolden
```bash
npx create-next-app@latest mijn-thema \
--typescript --tailwind --app --eslint --no-src-dir --turbopack
cd mijn-thema
```
### Stap 2 — Supabase project aanmaken
- Ga naar https://supabase.com/dashboard
- **New Project** → kies naam
- Wacht ~2 min op deploy
- Settings → API → kopieer:
- Project URL
- anon public key
- service_role secret key
### Stap 3 — Schema definiëren
Pas `schema.sql` aan voor jouw thema. Bv:
```sql
create table items (
id bigserial primary key,
name text not null,
category text,
rating int,
description text,
created_at timestamp default now()
);
```
Run in Supabase → SQL Editor.
### Stap 4 — Env variables
`.env.local`:
```
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENAI_API_KEY=sk-proj-...
```
Belangrijke regels:
- `NEXT_PUBLIC_*` = client-leesbaar
- `SUPABASE_SERVICE_ROLE_KEY` = server-only, voor seed script (geen `NEXT_PUBLIC_`)
- `OPENAI_API_KEY` = server-only (geen `NEXT_PUBLIC_`)
### Stap 5 — Packages installeren
```bash
npm install @supabase/supabase-js ai @ai-sdk/openai zod
npm install --save-dev tsx dotenv
```
### Stap 6 — Supabase client
`lib/supabase.ts`:
```typescript
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
```
---
## 5. Seed script: dummy data in Supabase
Een seed-script is een TypeScript bestand dat **éénmalig** je tabel vult met dummy data. Geen handmatige inserts — procedureel gegenereerd.
### Het Polderfest script (voorbeeld)
Het volledige `seed-polderfest.ts` zit als bijlage bij deze les. Kernidee:
```typescript
import { createClient } from "@supabase/supabase-js";
import "dotenv/config";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
);
const adjectives = ["Lost", "Velvet", "Iron", "Neon", "Silent", ...];
const nouns = ["Tigers", "Wolves", "Mirrors", "Clouds", ...];
const genres = ["Indie Rock", "Electronic", ...];
function generateBand(i: number) {
return {
name: `${pick(adjectives)} ${pick(nouns)}`,
genre: pick(genres),
// ...
};
}
async function seed() {
const bands = [];
for (let i = 0; i < 500; i++) bands.push(generateBand(i));
for (let i = 0; i < bands.length; i += 100) {
await supabase.from("bands").insert(bands.slice(i, i + 100));
}
}
seed();
```
### Runnen
```bash
npx tsx scripts/seed-polderfest.ts
```
### Waarom procedureel?
- **Met 500 hard-coded records** = 500 regels handmatige data → mind-numbing
- **Met combinaties** van 30 adjectives × 30 nouns = 900 unieke namen mogelijk
- **Met seed-random** = reproduceerbaar (zelfde data bij re-run)
### Voor jouw eigen thema
Open `seed-polderfest.ts`, kopieer de structuur, en vervang de **bouwstenen**:
- Domein-specifieke arrays (in plaats van bands: restaurants, scripties, kunstwerken…)
- Domein-specifieke velden
- Domein-specifieke bio/beschrijving-fragmenten
**Pro tip:** vraag een AI om dit te doen! "Pas het Polderfest seed-script aan voor [thema]." OpenCode of Cursor doet dit in 30 seconden.
---
## 6. Chat-route + chat-UI implementeren
Dit is wat **alleen nieuw** is — Next.js + Supabase kennen jullie al.
### De chat-route
`app/api/chat/route.ts`:
```typescript
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
export async function POST(req: Request) {
const { messages } = await req.json();
// 1. Haal data op
const { data: bands } = await supabase.from("bands").select("*");
// 2. Format als context
const context = bands!
.map((b) =>
`- ${b.name} (${b.genre}, ${b.tier}, ${b.day} ${b.start_time})`,
)
.join("\n");
// 3. System prompt met context
const system = `Je bent een festival-assistent voor Polderfest 2027.
Hier zijn alle bands:
${context}
Beantwoord vragen op basis van bovenstaande data. Verzin niets.
Antwoord in het Nederlands.`;
// 4. Stream
const result = streamText({
model: openai("gpt-4o-mini"),
system,
messages,
});
return result.toDataStreamResponse();
}
```
### De chat-pagina
`app/chat/page.tsx`:
```tsx
"use client";
import { useChat } from "ai/react";
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, status } =
useChat();
return (
<main className="max-w-2xl mx-auto p-6 flex flex-col h-screen">
<h1 className="text-2xl font-bold mb-4">Polderfest 2027 vraag de AI</h1>
<div className="flex-1 overflow-y-auto space-y-4 mb-4">
{messages.map((m) => (
<div
key={m.id}
className={m.role === "user" ? "bg-blue-50 p-3 rounded-lg ml-12"
: "bg-gray-50 p-3 rounded-lg mr-12"}
>
<div className="font-medium text-sm text-gray-500 mb-1">
{m.role === "user" ? "Jij" : "Festival AI"}
</div>
<div className="whitespace-pre-wrap">{m.content}</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Stel een vraag over de line-up..."
className="flex-1 p-3 border rounded-lg"
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-lg"
>
Stuur
</button>
</form>
</main>
);
}
```
### Voorbeeld-vragen die we live stelden
| Vraag | Wat AI doet |
|-------|-------------|
| "Welke bands spelen zaterdag op de Beach Stage?" | Filtert door context |
| "3 headliners met meeste populariteit" | Sorteert + select top |
| "Hoeveel jazz fusion acts totaal?" | Telt |
| "Vat de electronic-scene samen" | **Samenvatting** — alleen AI kan dit |
| "Wie was hoofdact van Polderfest 2025?" | Eerlijk: "weet ik niet" — perfect |
---
## 7. Waarom data + AI samen krachtig zijn
### Data alleen
- SQL queries: filter, sort, select
- Geen interpretatie, geen taal, geen samenvatting
- Gebruiker moet zelf SQL kunnen
### AI alleen
- Kennis is generiek (training data)
- Verzint vaak (hallucinatie)
- Geen toegang tot live data of private data
### Data + AI
- AI filtert via reasoning op tekst-context
- Antwoorden in natuurlijke taal
- Samenvattingen en interpretatie
- Domein-kennis = jouw data, AI redeneert erover
> "Een LLM zonder jouw data is een gewone chatbot.
> Een LLM mét jouw data is een product."
---
## 8. Best practices & valkuilen
### Doen
- **Begin met `gpt-4o-mini`** — upgrade pas als nodig
- **System prompt is essentieel** — "gebruik alleen onze data, verzin niets"
- **Stream alles** — `streamText` voelt 5× sneller dan `generateText`
- **AI-calls altijd server-side** — keys blijven veilig
- **Loading state** — AI duurt 1-5 sec, zonder feedback voelt het stuk
- **Foutafhandeling** — `try/catch` rond elke AI-call
### Niet doen
- **Geen `NEXT_PUBLIC_OPENAI_API_KEY`** — wordt zichtbaar in client
- **Niet de output blind vertrouwen** — AI hallucineert
- **Niet alle data altijd meesturen** — werkt voor 500 records, niet voor 50.000 (volgende les)
- **Niet `gpt-4o` als default** — 15× duurder dan mini, vaak onnodig
### Veelvoorkomende fouten
| Fout | Oorzaak | Oplossing |
|------|---------|-----------|
| `OPENAI_API_KEY is not defined` | `.env.local` niet geladen | Dev server herstarten |
| `Cannot find module 'ai'` | npm install vergeten | `npm i ai @ai-sdk/openai` |
| Seed: `permission denied` | Anon key i.p.v. service role | Gebruik `SUPABASE_SERVICE_ROLE_KEY` |
| AI antwoordt in Engels | Niet expliciet om NL gevraagd | System prompt aanpassen |
| AI verzint feiten | System prompt te zwak | Voeg toe: "verzin niets, gebruik alleen onze data" |
| Chat laadt niet | `useChat` zonder `streamText` API | Endpoint moet `result.toDataStreamResponse()` returnen |
---
## 9. Wat komt hierna?
### Het schaalprobleem
Vandaag sturen we **alle 500 bands** mee als context bij elke request. Dat is ~30k tokens. Werkt prima voor 500. Werkt **niet** voor:
- 5.000 records → te duur, te traag
- 50.000 records → past niet in context window
- Real-time data → context wordt steeds opnieuw gebouwd
### Volgende les: Tool Calling (Les 12)
In plaats van **alle data te sturen**, geef je de AI **tools** (functies). De AI besluit zelf welke te gebruiken:
```typescript
const { text } = await generateText({
model: openai("gpt-4o-mini"),
messages,
tools: {
searchBands: tool({
description: "Zoek bands op dag, stage, of genre",
parameters: z.object({
day: z.string().optional(),
stage: z.string().optional(),
genre: z.string().optional(),
}),
execute: async ({ day, stage, genre }) => {
// Supabase query
const { data } = await supabase.from("bands").select("*")
.eq("day", day || undefined)
.eq("stage", stage || undefined);
return data;
},
}),
},
maxSteps: 5,
});
```
Workflow:
1. User: "Welke bands op vrijdag?"
2. AI: "Ik roep `searchBands({ day: 'Vrijdag' })` aan"
3. Supabase: 60 bands terug
4. AI: "Op vrijdag spelen 60 bands. De headliners zijn..."
Schaalbaar. Slim. Multi-step (combineer meerdere tools).
### Daarna in deze leerlijn
- **Les 13:** Agents + `maxSteps` (autonome multi-step taken)
- **Les 14:** RAG + embeddings (semantic search op heel grote datasets)
- **Les 15-16:** Testing + Deployment + Performance
- **Les 17-18:** Eindopdracht-werkdagen + Pitch
---
## 10. Bronnen
### Vercel AI SDK
- Hoofdpagina: https://ai-sdk.dev/docs/introduction
- Voorbeelden: https://ai-sdk.dev/examples
- `streamText`: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text
- `useChat`: https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat
- Tool Calling (volgende les): https://ai-sdk.dev/docs/foundations/tools
### Supabase
- JS Client: https://supabase.com/docs/reference/javascript
- Row Level Security: https://supabase.com/docs/guides/auth/row-level-security
- Server-side usage: https://supabase.com/docs/guides/auth/server-side
### Inspiratie
- v0.dev — Generative UI in actie
- chat.vercel.ai — Officiële demo van AI SDK
- Vercel templates met AI: https://vercel.com/templates?type=ai
### Tokens & kosten
- OpenAI pricing: https://openai.com/api/pricing
- Tokenizer: https://platform.openai.com/tokenizer
- Usage dashboard: https://platform.openai.com/usage