Le Mans is vier dagen lang het mooiste wat er is in de autosport. Vrije trainingen, Hyperpole, de race zelf, van donderdag tot zondagmiddag. Ik volg het al jaren. Maar dit jaar wilde ik iets anders: niet zelf het weekend bijhouden, maar kijken of een AI-agent dat kon. Van eerste training tot eindvlag, autonoom.
Dat werd RaceDesk. Laravel 12, Prism PHP als Claude-wrapper, Redis, Blade/Livewire. Wat er daarna mee gebeurde is het eigenlijke verhaal.
Hoe het werkte
Elke 10 tot 120 minuten startte de agent een cycle, afhankelijk van of er een sessie bezig was. Hij kreeg 20 tools: live timing ophalen, het incident-feed raadplegen, artikelen schrijven, publiceren, de standings checken. Welke tools hij gebruikte en in welke volgorde bepaalde hij zelf, op basis van wat er op dat moment speelde.
Verplicht aan het begin van elke cycle: sessiestand opvragen, laatste artikelen ophalen, schedule checken. Pas daarna mocht hij handelen. Als hij iets wilde publiceren, ging dat door vijf lagen: een dry-run flag die alles stilvlegt, een sessie-check (geen roundup tijdens een lopende race), deterministische checks op wagennummers en links, een fact-check op het event profiel, en een tweede AI-call die het naleest als onafhankelijk reviewer.
Die reviewer is hetzelfde model als de schrijver, maar met een compleet andere systeemprompt. Hij weet welke bronnen intern geldig zijn, wanneer live timing primeert boven een bericht van een uur geleden, en dat hij alleen afkeurt bij aantoonbare fabricatie. Twee instanties van hetzelfde model, tegengestelde rollen. In de praktijk werkte dat beter dan verwacht.


Het meeste ontwikkelwerk zat niet in code maar in de systeemprompt. PromptBuilder::systemPrompt() genereert elke cycle zo’n 3000 woorden, dynamisch samengesteld op basis van de huidige modus (pre-race, live, post-race), het sessie-schedule, geverifieerde feiten, standings, verhaalhoeken voor rustige periodes, en de lichtomstandigheid op het circuit zodat cover images altijd kloppen met wat er buiten gebeurt.
Daarboven groeide tijdens de week een lijst redactionele regels, elke regel een gecodificeerde observatie. Dag 1 publiceerde de agent standings op willekeurige tijdstippen. “Hour 1.7 Standings.” Daarna stond er:
Never write a specific lap time, gap in seconds, or finishing position unless the exact number appears verbatim in a tool result from this cycle.
Do not use em-dashes. Use commas, colons, or split sentences instead.
STANDINGS CADENCE: Publish standings articles ONLY at exact half-hour or full-hour race milestones.
Zo werkt bijsturen van een LLM in de praktijk: preciezere instructies, geen hertraining.
De verrassingen
Buddy negeerde buddy.yml. Pipeline-configuratie wordt enkel gelezen bij de initiële import, daarna is de live pipeline leidend. Ik had storage:link en images:localize in het bestand staan, maar die draaiden nooit. De fix is een API-patch, niet het bestand aanpassen. Dit kost je een uur als je het niet weet.
fal.media URLs verlopen. AI-gegenereerde cover images werden extern geserveerd. Na een paar uur: 404. ImageLocalizer lost dat op: elke afbeelding direct na generatie downloaden naar lokale opslag, externe URL nooit bewaren in de database. Had er van dag één in gemoeten.
php artisan tinker --execute="..." in een geautomatiseerde SSH-sessie start PsySH en sluit niet proper af. Zombie-processen op productie, dagenlang. Voor eenmalige DB-checks schrijf je gewoon een dedicated artisan command.
Griiip, de live timing API van FIA WEC, faalt bij meer dan de helft van de polls tijdens Hyperpole. Redis cache bewaarde de laatste goede snapshot, dus de agent tools bleven werken, maar echt real-time was het niet.
Na een week
Na een volledige week: 378 cycles, 225 gepubliceerde artikelen, $107,25 in API-kosten. En dan de eerlijke beoordeling:
| Type | Feiten | Toon | Bronnen |
|---|---|---|---|
| updates (65) | 5/10 | 7/10 | 8/10 |
| standings (15) | 4/10 | 7/10 | 7/10 |
| incidents (16) | 5/10 | 7/10 | 6/10 |
| storylines (14) | 6/10 | 7/10 | 7/10 |
Toon zit goed. Feiten zijn het probleem.

Wat er structureel misging
BMW #20 staat in 18 artikelen als Frijns/Rast/Van der Linde. Correct is Dumas/Müller/Van der Linde. De agent haalt rijdersnamen uit Griiip live timing, niet uit de officiële entry list, en Griiip toont soms de rijder van de vorige stint. Één foute databron, tientallen besmette artikelen.
Gaptabellen met onmogelijke verhoudingen: P5 dichter bij de leider dan P4. Het model genereert ze zonder te controleren of ze wiskundig kloppen. Veertig minuten voor de finish stond er “insurmountable lead” voor Cadillac #12. Toyota #7 won. Een model dat een toestand naar de toekomst projecteert zonder onzekerheid te markeren: in de finale uren van Le Mans gaat dat bijna altijd fout.
Peugeot #93 en #94 doken op in de FCY-data. Peugeot was in 2026 niet aanwezig in Le Mans. De agent gebruikte een bron met foute data en had geen manier om dat te detecteren.
Al deze fouten zijn voorspelbaar als je weet hoe taalmodellen werken. Databron-fouten propageren ongecontroleerd als er geen kruisreferentie is. Wiskundige output wordt niet intern gecheckt. Temporele redenering is zwak zonder expliciete grounding op de klok. Geen van die dingen vereist een andere architectuur, wel een zwaardere validatielaag: rijdersnamen valideren tegen de officiële entry list, gaptabellen parsen op monotonie, elapsed time claims vergelijken met race_start + now(), en de Griiip tool splitsen in één voor posities en een aparte voor rijders en teams.
Le Mans heeft uitzonderlijk rijke data-infrastructuur: Griiip met 5-secondenupdates, Al Kamel, Radio Le Mans, real-time press releases. De meeste evenementen hebben dat niet, wat de architectuur generiek maakt maar de databronnen niet. Toon en editorieel oordeel zijn overdraagbaar; feitelijke grounding hangt af van wat er beschikbaar is.
Voor Spa 2027 bouw ik het opnieuw, met die fixes ingebouwd van dag één. De architectuur blijft, de validatielaag wordt zwaarder.
RaceDesk draait op Laravel 12, Prism PHP en Redis. Eén productie-omgeving, geen staging.







