Files
novi-lessons/Les14-RAG-Embeddings/Les14-Huiswerk.md
2026-06-03 16:58:25 +02:00

9.0 KiB

Les 14 — Huiswerk

Eigen PDF + UI + RAG-tool in agent + analyse

Vak: AI-Assisted Development Opleiding: NOVI Hogeschool Utrecht Deadline: Voor Les 15 — Cursor + Vercel deploy Inleveren: GitHub repo + RAG.md in root


Doel

Bouwt voort op de lesopdracht (werkende PDF Q&A app). In dit huiswerk:

  • Index een echte PDF van minimaal 20 pagina's
  • Maak een nette chat-UI
  • Bouw een RAG-tool in een ToolLoopAgent
  • Documenteer 5 vragen + 1 fail-case in RAG.md

Onderdeel A — Echte PDF + UI (verplicht)

Stap 1 — Kies een PDF

Kies een PDF van minimaal 20 pagina's, iets dat jou interesseert:

  • Studieboek-hoofdstuk
  • Open-source whitepaper (LLaMA, RAG-papers)
  • Bedrijfsjaarverslag
  • Wikipedia-artikel als PDF geprint
  • Game manual
  • Wat dan ook — moet text-based zijn (geen scan)

Stap 2 — Upload-UI

app/page.tsx — file upload + lijst van geïndexeerde docs:

"use client";
import { useState } from "react";

export default function Page() {
  const [file, setFile] = useState<File | null>(null);
  const [indexing, setIndexing] = useState(false);
  const [result, setResult] = useState<string>("");

  async function handleUpload() {
    if (!file) return;
    setIndexing(true);
    const fd = new FormData();
    fd.append("file", file);
    const res = await fetch("/api/index", { method: "POST", body: fd });
    const data = await res.json();
    setResult(`${data.chunks} chunks geïndexeerd`);
    setIndexing(false);
  }

  return (
    <main className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-6">PDF Q&A</h1>
      <input type="file" accept=".pdf"
        onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
      <button onClick={handleUpload} disabled={indexing}
        className="ml-3 bg-blue-600 text-white px-4 py-2 rounded">
        {indexing ? "..." : "Index"}
      </button>
      {result && <p className="mt-4 text-green-600">{result}</p>}

      <ChatBox />
    </main>
  );
}

Stap 3 — Chat-interface

Gebruik useChat van AI SDK (zelfde als Les 12):

// In een aparte component
"use client";
import { useChat } from "@ai-sdk/react";

export function ChatBox() {
  const { messages, sendMessage, status } = useChat({
    api: "/api/chat", // nieuwe route — zie hieronder
  });
  const [input, setInput] = useState("");
  // ... form + render messages ...
}

Pas app/api/ask/route.ts aan naar app/api/chat/route.ts die streamText returnt voor mooie streaming.

Eisen

  • Upload werkt — file input + index endpoint call
  • Geïndexeerde docs zichtbaar (count of lijst)
  • Chat-interface met streaming antwoorden
  • Antwoorden zijn duidelijk gebaseerd op PDF-inhoud

Onderdeel B — RAG-tool in een agent (verplicht)

Combineer Les 13 (Agents) + Les 14 (RAG):

// lib/agent.ts
import { ToolLoopAgent, tool, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { embedOne } from "./embeddings";
import { supabase } from "./supabase";
import { z } from "zod";

const ragSearch = tool({
  description:
    "Zoek in geüploade documenten op basis van semantic similarity. " +
    "Gebruik voor inhoudelijke vragen over de documenten.",
  inputSchema: z.object({
    query: z.string().describe("Wat je wilt vinden"),
  }),
  execute: async ({ query }) => {
    const embedding = await embedOne(query);
    const { data } = await supabase.rpc("match_chunks", {
      query_embedding: embedding,
      match_count: 5,
    });
    return data;
  },
});

export const docAgent = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: `Je beantwoordt vragen over geüploade documenten. Werkwijze:
1. Zoek met ragSearch voor relevante info
2. Lees de top chunks
3. Eventueel: tweede ragSearch met andere query voor meer context
4. Antwoord met bronvermeldingen (source filenames).
Verzin niets — alleen wat in de chunks staat.`,
  tools: { ragSearch },
  stopWhen: stepCountIs(10),
});

Test in chat-route:

// app/api/chat/route.ts
import { docAgent } from "@/lib/agent";
import { convertToModelMessages } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = docAgent.stream({
    messages: convertToModelMessages(messages),
  });
  return result.toUIMessageStreamResponse();
}

Eisen

  • ragSearch tool gedefinieerd + werkend
  • ToolLoopAgent gebruikt deze tool
  • In UI: tool-invocations zichtbaar (zoals Les 12)
  • Voor complexe vragen: agent doet 2-3 searches

Onderdeel C — RAG.md documentatie (verplicht)

Schrijf in repo-root.

Sectie 1 — Document info

  • Bestandsnaam + onderwerp
  • Aantal pagina's
  • Aantal chunks na indexing
  • Chunk-size + overlap

Sectie 2 — 5 succesvolle vragen

Voor elk:

**Vraag 1:** "Wat zijn de drie hoofdkenmerken van X?"

**Antwoord (samenvatting):** ...

**Top chunks (top 3 met similarity score):**
- Source page 12 (sim: 0.87) — "X heeft drie kenmerken..."
- Source page 14 (sim: 0.81) — "Het derde kenmerk..."
- Source page 11 (sim: 0.74) — "Overzicht van X..."

Sectie 3 — 1 fail-case

Een vraag waar RAG het niet goed deed:

**Vraag:** "Wat staat er over Y?"

**Antwoord (incorrect):** "Y wordt niet besproken in het document."

**Wat ging mis:** Y wordt wel besproken op pagina 8, maar onder een ander woord (Z). Embedding van "Y" was te ver van embedding van paragraaf over "Z" voor cosine similarity threshold.

**Wat ik probeerde:** ...
**Wat zou helpen:** Hybrid search (keyword + semantic), of synoniem-expansion in de query.

Sectie 4 — Chunking experiment

Probeer 1 andere chunking-strategie naast je default:

  • Andere chunk-size (200 of 1000)
  • Andere overlap (0 of 100)
  • Of: split per pagina i.p.v. fixed chars

Documenteer:

**Default:** chunk-size 500, overlap 50 → 30 chunks, avg quality 8/10
**Experiment:** chunk-size 200, overlap 30 → 75 chunks, avg quality 6/10
**Conclusie:** ...

Sectie 5 — Eén observatie

Iets wat opviel:

  • Welke vragen werken het best?
  • Hoe lang duurt index van een PDF van X pagina's?
  • Effect van top-k (3 vs 5 vs 10)?
  • Verschil tussen simple RAG en RAG-in-agent?

Vorm

  • Max 800 woorden totaal
  • Concrete cijfers + voorbeelden
  • Mag wat informeel

Bonus (optioneel)

Bonus 1 — Streaming antwoorden + sources

Gebruik streamText met source-citations in UI. Toon naam + page-number naast elke antwoord-claim.

Voeg keyword filter toe naast semantic. Postgres full-text search of simpel ilike:

-- in match_chunks
where to_tsvector(content) @@ plainto_tsquery($keyword)
   or embedding <=> $query_embedding < 0.5

Bonus 3 — Per-document filter

UI dropdown: selecteer welk doc te bevragen. Backend: filter op source in match function.

Bonus 4 — Re-ranking

Top-20 ophalen via vector search, dan re-rank met Cohere of LLM-as-judge naar top-5.

Bonus 5 — Multi-document

Index 3-5 verschillende PDFs. Vraag over meerdere docs tegelijk. Documenteer hoe agent omgaat met conflicting sources.


Inleveren

  1. GitHub repo URL in Brightspace
  2. RAG.md in repo-root (5 secties)
  3. Updated lib/agent.ts met RAG-tool
  4. Updated app/api/chat/route.ts met agent integration
  5. Working app lokaal demonstreerbaar

Beoordeling

Criterium Punten
A — UI + indexing + chat werkend 3
B — RAG-tool in ToolLoopAgent werkend 2
C — RAG.md compleet met 5 secties 3
Fail-case analyse is concreet en doordacht 1
Chunking-experiment uitgevoerd + reflectie 1
Totaal 10

Voldoende = 6+. Bonus telt mee bij twijfelgevallen.


Tijd-indicatie

Onderdeel Tijd
A — UI + chat + streaming 45 min
B — RAG-tool in agent 25 min
C — RAG.md schrijven (incl. testen) 50 min
Totaal ~2 uur

Veelvoorkomende valkuilen

Probleem Oplossing
Scan-PDF — geen text unpdf werkt niet op image-PDFs. Gebruik OCR (tesseract) of kies andere PDF
Vector dimension mismatch Index én query moeten zelfde model gebruiken
Top-1 is altijd irrelevant Chunks te klein, of vraag te abstract — experimenteer
Agent doet 1 search en stopt System prompt expliciet: "doe meerdere searches indien nodig"
Cost-pieken bij grote PDF Indexing per chunk-batch van 100 ipv all-at-once
RAG hallucineert Prompt strikter: "Als info niet in context staat, zeg dat"

Tips

  • Test met simpele vragen eerst — zorg dat basis werkt voor complexer wordt
  • Log similarity scores — onder 0.4 is meestal niet relevant
  • Source-citations zijn goud — gebruiker kan zelf verifiëren
  • Chunk-experiment is verplicht — het verschil is groter dan je denkt
  • Eén goede fail-case is meer waard dan 10 succesvolle

Volgende les: Cursor + Vercel deploy. Voice transcriptie met Whisper, foto-analyse met GPT-4o vision, image generation. We gaan letterlijk de zintuigen toevoegen aan onze apps. Tot dan!