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

15 KiB
Raw Blame History

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?
  2. Het modellen-landschap
  3. De vier kern-functies
  4. Project setup van A tot Z
  5. Seed script: dummy data in Supabase
  6. Chat-route + chat-UI implementeren
  7. Waarom data + AI samen krachtig zijn
  8. Best practices & valkuilen
  9. Wat komt hierna? Tool Calling teaser
  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 hooksuseChat, 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

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:

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.

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.

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.

"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.

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

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:

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

npm install @supabase/supabase-js ai @ai-sdk/openai zod
npm install --save-dev tsx dotenv

Stap 6 — Supabase client

lib/supabase.ts:

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:

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

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:

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:

"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 allesstreamText 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
  • Foutafhandelingtry/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:

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

Supabase

Inspiratie

Tokens & kosten