fix: update lessons
This commit is contained in:
330
Les14-RAG-Embeddings/Les14-Huiswerk.md
Normal file
330
Les14-RAG-Embeddings/Les14-Huiswerk.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 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:
|
||||
|
||||
```tsx
|
||||
"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):
|
||||
|
||||
```tsx
|
||||
// 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):
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```typescript
|
||||
// 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:
|
||||
|
||||
```markdown
|
||||
**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**:
|
||||
|
||||
```markdown
|
||||
**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:
|
||||
|
||||
```markdown
|
||||
**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.
|
||||
|
||||
### Bonus 2 — Hybrid search
|
||||
|
||||
Voeg keyword filter toe naast semantic. Postgres full-text search of simpel `ilike`:
|
||||
|
||||
```sql
|
||||
-- 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!
|
||||
Reference in New Issue
Block a user