Case

Top2000.ai: een kleine studie in AI die alles op je eigen machine houdt

Top2000.ai is een chatapplicatie die vragen beantwoordt over de Nederlandse Radio 2 Top 2000, de jaarlijkse lijst van 2.000 nummers die luisteraars kiezen. Ik heb hem gebouwd om voor mezelf één vraag te beantwoorden: hoe ver kom je met AI als er niets van de machine af mag?

Bekijk de code
externe calls tijdens gebruik
0
testcases met gouden antwoorden
31
recall@5 op beschrijvende vragen
32%

Wat het is

Wat is Top2000.ai?

De Top 2000 is het vehikel, niet het onderwerp. De data is openbaar, in Nederland bekend en groot genoeg om interessant te zijn: 54.000 noteringen, 4.925 nummers en 27 edities van 1999 tot 2025.

Je kunt twee soorten vragen stellen. Vragen met een exact antwoord, zoals hoe vaak een nummer in de lijst stond of welke artiest in 2024 de meeste noteringen had. En vragen over betekenis, zoals welk nummer gaat over een bokser die ten onrechte werd veroordeeld.

Het echte onderwerp is de infrastructuur eronder. Elke keuze staat opgeschreven met de reden erbij, en waar een keuze uit een meting kwam, staat die meting er ook bij. Voor mij was het ook een studie in het toepassen van retrieval-augmented generation (RAG) met uitsluitend lokale modellen.

Architectuur

Het model vertaalt, de database rekent

Build time · Claude mag meehelpen
Publieke bronnen
Wikipedia-jaarlijsten, MusicBrainz, Wikidata
Scripts
Parsen, ophalen, verrijken, in chunks knippen, embedden
Twee lokale bestanden
Een database met nummers en lijstposities. Een index met tekstchunks en vectoren.
Onder deze lijn geen enkele API-call
Run time · alleen lokaal
Vraag
Router
Vaste regels, geen model. Bij twijfel: SQL.
Exacte vragen
SQL-pad (text-to-SQL)
qwen2.5-coder:32b
  1. Het model schrijft één query
  2. De query wordt gecontroleerd: alleen lezen, twee tabellen
  3. De database doet het telwerk
  4. Het model verwoordt de rijen die het terugkrijgt
Vragen over betekenis
RAG-pad
bge-m3 voor de zoekopdracht, qwen2.5:7b-instruct voor het antwoord
  1. Keyword- en vectorzoekopdracht in de lokale index
  2. Beste match te zwak? Dan wordt er geen antwoord gegenereerd
  3. Het model antwoordt op basis van de gevonden passages
Antwoord
Met het gekozen pad, de query of bronnen, en het energieverbruik
De grens tussen build time en run time. Boven die lijn mag Claude meehelpen. Eronder draait alles op één machine.

Bij exacte vragen doet het taalmodel maar één ding: het zet de vraag om in één databasequery. Dat heet text-to-SQL. De database telt en sorteert. Het model ziet daarna alleen de rijen die terugkomen en schrijft daar een zin omheen. Een getal dat niet in die rijen staat, mag niet in het antwoord.

Een router bepaalt welk pad een vraag neemt. Die gebruikt vastgelegde regels en geen model, dus dezelfde vraag neemt altijd hetzelfde pad en elke beslissing is met de hand te controleren. Bij twijfel kiest hij de database. Een fout databaseantwoord is een lege tabel, en dat is zichtbaar fout. Een fout gegenereerd antwoord leest prettig en is alsnog fout.

Vragen over betekenis gaan via het RAG-pad. De applicatie doorzoekt een lokale index van Wikipedia-tekst en het model antwoordt op basis van de passages die het vindt. Is de beste match te zwak, dan wordt er helemaal geen antwoord gegenereerd. Die controle zit buiten het model, want een klein model moet niet zelf oordelen over zijn eigen context.

Privacy

Wat op de machine blijft

Terwijl de applicatie draait, gaat er geen enkel verzoek naar een extern model of een externe dienst. Het taalmodel en het embeddingmodel draaien allebei lokaal via Ollama. De data is één bestand op schijf, read-only geopend. De interface laadt geen externe scripts, fonts of analytics, en vragen worden niet opgeslagen.

Je hoeft me niet op mijn woord te geloven. De code is open en in het pad dat draait staat geen enkel extern adres.

Ik heb Claude wél gebruikt om het te bouwen. Claude schreef samen met mij scripts en pipelines, en zit nooit in het pad dat een vraag beantwoordt. Die grens staat in de repository en geldt als werkafspraak: Claude bouwt, Ollama draait. Het betekent ook dat elke evaluatie tegen het lokale model draait, nooit tegen het sterkere model dat meehielp met bouwen.

Energie

Energieverbruik, per vraag

Bij elk antwoord zie je hoeveel stroom het kostte, gemeten op de machine terwijl het model draaide en omgerekend naar CO2 met de Nederlandse stroommix. De interface vergelijkt het totaal met één wasbeurt op 40 graden in de wasmachine. De machine trekt ongeveer 3 watt in rust en 35 tot 36 watt tijdens het genereren.

Beperkingen

Waar het tekortschiet

Het RAG-pad is de zwakke plek. Vragen waarin een nummer of artiest genoemd wordt, vinden in mijn testset elke keer het juiste nummer. Vragen die een nummer alleen omschrijven, vinden het in 32% van de gevallen terug in de top vijf. Ik had de lat vooraf op 60% gelegd. De tekst in kleinere stukken knippen kostte twee uur rekentijd en veranderde niets aan dat getal.

Er zit geen persoonsdata in dit project, waardoor de privacybelofte hier makkelijker te houden is dan in een klantsysteem. De architectuur is dezelfde die ik zou gebruiken waar de data wél gevoelig is.

De modellen zijn open weights, getraind door een groot technologiebedrijf, en de hardware komt van een ander. Onafhankelijkheid is een schaal, geen schakelaar.

De demo draait op mijn eigen machine. Zodra hij publiek gehost wordt, wordt de keuze van de host onderdeel van het privacyverhaal.

Lagen

Laag voor laag

Router

  • Geschreven regels, geen model
  • Kiest bij twijfel de database
  • 31 van de 31 routeringsvragen correct

Gegenereerde queries

  • Eén read-only statement, twee bekende tabellen
  • Maximaal twee retries, daarna gewoon een weigering
  • Na vijf seconden gestopt

Database

  • Read-only geopend, zodat de querycheck niet het enige slot is
  • Eén SQLite-bestand, geen databaseserver

Retrieval (RAG)

  • Keyword- en vectorzoeken over een lokale index
  • Geen antwoord onder de similarity-drempel
  • Elk antwoord noemt de passages die het heeft gebruikt

Modellen

  • qwen2.5-coder:32b voor text-to-SQL
  • qwen2.5:7b-instruct voor RAG-antwoorden
  • bge-m3 voor embeddings
  • Alle drie draaien lokaal via Ollama
  • 0 externe calls tijdens gebruik

Meten

  • 31 testcases met antwoorden uit de database
  • Evaluaties draaien alleen tegen het lokale model
  • Wattuur en CO2 per antwoord getoond

python · fastapi · sqlite · ollama · react