Files
novi-lessons/Les11-AI-SDK/Les11-Lesstof.md
2026-05-19 18:50:11 +02:00

528 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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