Over scrapers die zichzelf repareren: AI-eigenaarschap inbouwen in de data die Andri voedt

Over scrapers die zichzelf repareren: AI-eigenaarschap inbouwen in de data die Andri voedt

17 april 2026

Over scrapers die zichzelf repareren

Twee maanden in Andri kwam ik erachter dat het niet het model was dat me 's nachts wakker hield.

Het was een publieke rechtbankuitgever die ergens tussen vrijdagavond en zondagochtend had besloten de structuur van zijn responses aan te passen. Onze scraper draaide op zijn gebruikelijke cron, kreeg netjes een 200 terug, parste nul documenten, schreef niets weg en meldde succes. Tegen de tijd dat een advocaat ons vroeg waarom een zaak van maandag niet in de index stond, hadden we stilletjes drie publicatiecycli overgeslagen.

Er was niets stuk. Dat was het erge. De site werkte. Onze code werkte. Ze waren het alleen niet meer eens over hoe de wereld eruitzag.

Ik denk nog vaak aan dat weekend.


Wat niemand hardop zegt over juridische AI

Elk bedrijf in deze hoek zal je over hun model vertellen. Over hun retrieval-pipeline. Over hoe ze chunken en embedden en herrangschikken. Niemand heeft het over de scrapers.

Maar de scrapers zíjn het. Als de data er niet is, doet al dat slimme ophalen er niet toe. Als de data verkeerd is, heb je een zelfverzekerde maar foute zoekmachine gebouwd. Als de data verouderd is, laat je advocaten recht citeren dat allang gewijzigd is.

De scrapers zijn ook waar al het sloopwerk zit. Elke juridische uitgever heeft zijn eigen eigenaardigheden. Sommige draaien op infrastructuur uit het begin van deze eeuw en zetten af en toe proof-of-work-bescherming tegen bots in. Anderen publiceren XML die technisch de specificatie schendt. Weer anderen zijn SharePoint-omgevingen van wisselende leeftijd, met zoek-API's die vaker een 503 teruggeven dan niet.

Dat is niemands schuld. Dit zijn publieke diensten, gerund met bescheiden budgetten door mensen die, over het algemeen, niet bouwen met downstream AI-verbruik in gedachten. De enige reden dat wij deze data überhaupt mogen gebruiken is dat ze publiek is en de regels om erbij te komen netjes in een robots.txt staan of op een terms-of-use-pagina, en wij behandelen beide als bindend.

Tientallen scrapers dus. Tientallen manieren om stilletjes fout te gaan.


Wat we eerst probeerden

We deden wat iedereen doet. CloudWatch-alarmen. Een Slack-kanaal #scraper-alerts. De ochtend erna wakker worden, fixen, door.

Dat werkte. Het schaalde alleen niet. Drie storingen per week over tientallen bronnen is gemiddeld één Slack-brandje per werkdag, en het onderzoek volgde elke keer dezelfde stappen:

  1. Haal de laatste orchestration-run op.
  2. Lees de input van de laatste stap.
  3. Scroll door de logs tot ergens ERROR verschijnt.
  4. Kruislings checken met object storage of er documenten zijn weggeschreven.
  5. Kruislings checken met de zoekindex of ze geïndexeerd zijn.
  6. Theorie vormen. Bevestigen of verwerpen met een tweede query. Patchen.

Het probleem met deze workflow is niet dat hij moeilijk is. Het probleem is dat hij elke keer identiek is. De vorm van het onderzoek hangt niet af van welke scraper stuk is. Wat afhangt van de scraper is de specifieke fout, het specifieke bestandspad, de specifieke selector die aangepast moet worden.

Een engineer die twaalf keer per maand hetzelfde onderzoek doet, is een engineer die uit verveling een subtiele regressie gaat introduceren. We zijn allemaal die engineer geweest. Na drie uur gebeurt er niks goeds meer.

Dus stelde ik de enige nuttige vraag: wat als het onderzoek zichzelf zou uitvoeren?


Een kanttekening: wat we niet bouwen

Er is een groeiend corpus aan werk over zelfherstellende software. Het grootste deel daarvan gaat over services die zichzelf tijdens runtime repareren: een pod crasht, de orchestrator herstart hem; een node wordt traag, de load balancer routeert eromheen; een anomalie-detector slaat aan, een rollback treedt in. Gesloten lus, intern, zonder mens. Dat is een echte en nuttige discipline. Het is niet wat wij doen.

Ons probleem zit een laag hoger. We hebben geen service nodig die zichzelf herstart. We hebben nodig dat de repetitieve menselijke onderdelen van incident response (logs lezen, storage en indexen kruislings checken, een ticket schrijven, een PR openen) stoppen met engineering-uren op te slurpen. De "healing" landt uiteindelijk als een normale codewijziging, gereviewd door een mens, gemerged via normale CI. Er herstart geen pod zichzelf. Een agent doet het on-call-sloopwerk dat een vermoeide engineer vroeger om 2 uur 's nachts deed, en doet het in plaats daarvan tijdens kantooruren.

Dit "zelfherstel" noemen is royaal. Het zit dichter bij agentic incident response met een mens die op de merge-knop drukt. We gebruiken de term omdat dat is waar mensen naar grijpen, niet omdat we denken dat we bouwen wat de literatuur beschrijft.


Wat de runtime-literatuur mist

Hier wijkt ons probleem af van het probleem dat het meeste self-healing-schrijfwerk adresseert.

Runtime self-healing gaat ervan uit dat het systeem dat heelt hetzelfde systeem is dat je bezit. Je service ligt plat, je AI diagnosticeert, je agent herstart de pod. Netjes. Intern. Niemand buiten je VPC doet mee.

Ons probleem is niet zo. De helft van onze faalmodi leeft op andermans infrastructuur. Een uitgever zet een toegangspagina aan die er vorige week niet was. Een andere schakelt in een weekend over op een ander documentformaat. Een derde haalt stilletjes een archief offline. Niets daarvan is een bug in onze code. Strikt genomen is niets ervan stuk. De wereld is gewoon opgeschoven.

Een systeem dat alleen naar binnen kijkt, zal elk van die gevallen diagnosticeren als een storing aan onze kant, rekenkracht verbranden om iets te herstarten dat niet gecrasht is, en de lus nooit sluiten. De fix moet tot in de code reiken: een nieuwe parser, een aangepaste selector, een ander extractiepad. Dat schrijven is een cognitieve taak, geen herstart.

Het andere wat de runtime-framing mist is ongemakkelijker, dus ik schrijf het maar gewoon op: niemand heeft het over toestemming. De aanname is dat het systeem dat je heelt er een is dat je zelf bezit. Scraping is anders. De data die we verzamelen is gemaakt door andere mensen, gepubliceerd onder regels die wij niet hebben geschreven, en we kunnen hier alleen mee door blijven gaan omdat we ons aan die regels houden.

Een systeem dat zichzelf "repareert" door een geblokkeerd endpoint te bestormen, is erger dan een stukke.

Daar kom ik op terug.


Wat we eigenlijk gebouwd hebben

Elke scraper krijgt een state machine. Elke state machine krijgt een dokter.

De dokter is een agent. Eén per scraper, draaiend als nachtelijke job, verantwoordelijk voor de gezondheid van zijn eigen state machine. Hij raakt de code van de scraper niet aan. Hij observeert.

Elke ochtend om 07:00 UTC wordt hij wakker, trekt het register van al onze scrapers op, en waaiert uit over de vloot. Voor elke scraper doet hij de vijfstappenonderzoek hierboven, maar parallel, met een kleine toolkit:

  • Een health check die de laatste N orchestration-runs ophaalt, bestanden telt die zijn weggeschreven over 24u/7d/altijd, en geïndexeerde documenten telt voor dezelfde periodes. Drie signalen in één request.
  • Een log-query-tool beperkt tot de logstreams van één scraper, gericht op onze gestructureerde agent-logregels voordat hij terugvalt op ruwe foutmeldingen.
  • Infrastructuur-sanity-checks: staat de planning nog aan, stapelt er iets op in een dead-letter queue, draait er een state machine die niet in ons register staat.

De agent classificeert elke scraper als OK, WARNING of CRITICAL, en voor elke bevinding met een aanpakbare root cause is hij verplicht een remediation-ticket te sturen naar een tweede agent.

De twee agents zijn bewust gescheiden. Diagnose en reparatie zijn verschillende cognitieve taken en horen geen context te delen. In de praktijk gaat het ook over verantwoordelijkheid. De health-agent is eigenaar van "er is iets mis en hier is het bewijs". De fix-it-agent is eigenaar van "hier is de codewijziging die dat oplost". Als je ze samensmelt, is geen van beide goed in zijn werk.


De regel die alles veranderde

De eerste versie van onze health-agent produceerde zelfverzekerd klinkende tickets met dun bewijs. We hebben een tijd zitten discussiëren of hij hallucineerde. Dat deed hij niet. Hij rapporteerde gewoon een eerste gok als diagnose, omdat we hem niet hadden verteld dat niet te doen.

De oplossing was structureel, geen prompt-aanpassing.

De agent is nu verplicht bewijs te verzamelen uit minstens twee onafhankelijke bronnen voordat hij een ticket stuurt. Orchestration-runs plus logs. Of object storage plus zoekindex. Of logs plus dead-letter queue. Als hij maar één signaal heeft, ticket hij niet. Als de zoekindex nul documenten toont maar de object store heeft bestanden, is de scraper prima: de indexer is stuk. Dat is een ander ticket, naar een ander component, met andere remediation. De regel vangt dat onderscheid automatisch op.

De inhoud van een ticket is een strikt schema: scrapernaam, state-machine-identifier, faalcategorie, root cause in één zin, bewijs direct geciteerd uit de logs (niet geparafraseerd, niet samengevat), remediation-stappen, betrokken componenten en een diagnostisch blok afgestemd op de faalcategorie.

We hebben negen categorieën gedefinieerd. Ze dekken elk scraper-incident dat we in achttien maanden hebben gezien:

  • SOURCE_CHANGED: websitestructuur of API is gewijzigd. Scraper geeft 200 terug, extraheert nul documenten.
  • BOT_PROTECTION: site heeft een WAF, toegangs-challenge, captcha of vergelijkbare controle toegevoegd. Gaat altijd naar menselijke review, nooit automatisch omzeilen.
  • IAM_PERMISSION: een job probeerde iets te doen waar de cloudprovider geen toestemming voor gaf.
  • CONFIG_ERROR: ontbrekende environment variable, verkeerde identifier, verouderde bucketnaam.
  • BATCH_FAILURE: LLM-batchjob niet voltooid.
  • INDEXING_BREAK: documenten in storage, afwezig in de zoekindex.
  • CODE_BUG: runtime gooit een exceptie. We willen de traceback.
  • SCHEDULE_DISABLED: de scheduler-regel staat uit. Dat doen we onszelf aan tijdens deploys.
  • INFRA_FAILURE: OOM, timeout, ophoping in dead-letter queue, history-limieten van de orchestrator.

Elke categorie vertelt de fix-it-agent welk playbook te volgen en welk deel van het systeem aan te raken. SOURCE_CHANGED betekent de scraper-module voor die jurisdictie. IAM_PERMISSION betekent infrastructure-as-code. INDEXING_BREAK betekent de indexer, niet de scraper. De categorie is eerst een routeringsbeslissing voordat het een inhoudelijke beslissing is, en de routering goed krijgen is zestig procent van de fix goed krijgen.

Het verschil zie je terug in de tickets. Onze eerste versie zei dingen als "scraper faalt, onderzoek". Dat is detectie met een snufje diagnose. Onze huidige versie leest meer als: "scraper X krijgt nu een redirect naar een toegangspagina die er vorige week niet was (zie bewijsblok). Het eerdere ophaalpad geldt niet meer voor deze bron. Fix: markeer voor menselijke review, ga niets automatisch omzeilen, neem contact op met de uitgever om het nieuwe bedoelde toegangspatroon te bevestigen."

Dat is een diagnose. Daar kan de fix-it-agent mee aan de slag. Een mens kan de resulterende PR in negentig seconden reviewen.


De grens die we getrokken hebben

De fix-it-agent schrijft een codewijziging. Hij opent een pull request. Een mens reviewt hem. De merge loopt via onze normale deployment-pipeline.

Dat is het hele ontwerp. Geen runtime auto-remediation, geen self-configuration, geen agent-geïnitieerde deploys. Als het systeem zichzelf tijdens runtime zou aanpassen, omzeilt het code review. Als een mens elke wijziging reviewt, zelfs een die door een agent is geschreven, behouden we de eigenschap dat onze infrastructure-as-code en onze applicatiecode de bron van waarheid zijn voor wat productie doet. Dat is het saaie maar juiste antwoord op "hoeveel autonomie geef je een agent". Onze versie van toezicht is de review-knop van GitHub.

De fix-it-agent mag een nieuwe parser toevoegen. Een selector fixen. Een nieuw documenttype afhandelen. Een infrastructuur-permissie aanpassen. Een environment variable bijwerken. Wat hij onder geen enkele omstandigheid mag, is onze rate limits wijzigen.

Dat brengt me terug bij het ongemakkelijke deel.


De regel die in de literatuur niet voorkomt

Onze scrapers raken servers van anderen. Dat is geen detail. Het is de hele operationele randvoorwaarde.

Een zelfherstellend systeem dat geen respect heeft voor de regels rond publieke data is, in legal tech, een zelfvernietigend systeem. Onze agents hebben dus een extra regel die in geen van de drie bronnen voorkomt en die we zelf moesten uitwerken:

De agent heelt nooit door harder te duwen op een bron die ons heeft gevraagd gas terug te nemen.

In de praktijk betekent dat een paar concrete dingen.

De health-agent beveelt nooit aan de scrape-frequentie te verhogen. Als een dagelijkse scraper documenten mist, is de oplossing beter parsen, niet vaker pollen. Een scraper die twee dagen lang nul nieuwe documenten vindt op een bron die normaal tientallen produceert, wordt als CRITICAL gemarkeerd en als stuk beschouwd, niet als achterlopend.

De fix-it-agent is expliciet geblokkeerd van het wijzigen van rate-limit-constanten. Onze limieten worden door het engineeringteam gezet met de operator van de bron in gedachten, met conservatieve gaten tussen verzoeken voor de meeste bronnen, ruimere gaten voor backfill, en strakkere alleen voor lichte probes. Als de agent denkt dat hij ze moet aanscherpen, gaat het ticket in plaats daarvan naar een menselijk kanaal.

Backfill-bewustzijn is expliciet. Als een state machine nul historische uitvoeringen heeft, wordt hij geclassificeerd als "nog niet geactiveerd", niet als stuk. Geen ticket. Deze regel kwam uit een vroege false positive waarbij de agent ons piepte voor elke scraper in een net uitgerolde jurisdictie, omdat we de backfills nog niet gedraaid hadden.

Elk SOURCE_CHANGED-remediation-ticket voor een site met een restrictieve robots.txt wordt gemarkeerd voor menselijke review in plaats van een automatische PR. We willen niet dat een agent "behulpzaam" onze dekking uitbreidt naar paden waarvan de site-eigenaar heeft gezegd dat we ze niet mogen crawlen. Die beslissingen worden door mensen gemaakt, per mail, met de bron.

Niets hiervan is moeilijk te formuleren. Alles is triviaal om te vergeten zodra je jezelf afrekent op mean-time-to-resolution. Kosten-baten op een datacenter is één ding; kosten-baten op een relatie met een openbaar archief is iets anders. Zelfde vorm. Andere inzet.


Wat dit ons concreet oplevert

De scraper-health-agent draait elke ochtend. Tientallen state machines, parallel onderzocht over vijf of zes iteraties. Produceert een Slack-gezondheidsrapport dat leest als een radioloog-notitie: "Vijftien van achttien actieve scrapers gezond, drie behoeven aandacht, negen nog niet geactiveerd." Stuurt tussen de nul en zes remediation-tickets. Logt een volledig audit-spoor.

De volledige run kost een paar cent aan model-tokens, dankzij prompt caching. Dat verdient zichzelf vele malen terug in de iteraties na de eerste.

De fix-it-agent pakt tickets op, opent PR's tegen de juiste componenten, draait tests en wijst reviewers toe. Een mens reviewt, merget, en de volgende geplande run verifieert de fix. De rondgang die vroeger een piep om 2 uur 's nachts was gevolgd door drie uur loggraven, is nu een GitHub-notificatie bij het ontbijt.

We zitten niet op nul menselijke interventies. Dat willen we ook niet. Een scraper waarvan de bron echt veranderd is, is een stuk institutionele kennis dat we bewust moeten opnemen. Het doel was nooit om mensen uit de lus te halen. Het doel was om de repetitieve menselijke stappen eruit te halen, de stappen die scraper na scraper identiek zijn, en menselijke aandacht te reserveren voor de dingen die oordeel vragen.


Wat ik anders zou doen

Als ik opnieuw zou beginnen, zou ik de faaltaxonomie eerst schrijven en de code daarna. De negen categorieën waar we op uit zijn gekomen, zijn niet willekeurig. Ze koppelen aan concrete playbooks. Maar we hebben ze ontdekt door zes maanden naar incidenten te kijken voordat we ze benoemden. Dat was te langzaam. Een team dat vandaag begint, kan onze negen lezen en als bodem gebruiken.

Ik zou me op dag één committeren aan de regel "bewijs uit twee bronnen". Het meeste van wat leek op "de agent hallucineert" bleek de agent te zijn die een eerste gok rapporteerde omdat we hem niet hadden verteld dat niet te doen. Structurele randvoorwaarden verslaan prompt engineering elke keer.

En ik zou de toestemmingsgrens eerst trekken, voordat ik ook maar één regel remediation-logica schrijf. Niet als beleidsdocument. Als code. Als een faalcategorie mogelijk kan leiden tot veranderen hoe we met een bron omgaan, gaat die categorie standaard naar een menselijk kanaal, en die default moet in het tool-schema staan, niet in een system prompt waarvan je hoopt dat het model hem onthoudt.


Hoe dan ook

De biologische metafoor is verleidelijk, en deels klopt hij ook, alleen niet op de "één slim immuunsysteem"-manier. Menselijke lichamen worden niet geheeld door één brein. Ze worden geheeld door vele gespecialiseerde systemen, elk smal competent, elk parallel opererend, elk rapporterend aan een centraal zenuwstelsel dat weet hoe te triëren.

Dat is de architectuur waar we naartoe bewegen. Niet één reusachtig zelfherstellend brein. Een ziekenhuis.

De scraper-health-agent is de eerste dokter. De fix-it-agent is de eerste chirurg. Daarna komt een zoekkwaliteit-agent die de relevantie beoordeelt van wat we indexeren. Een metadata-agent die verrijkingsregressies vangt. Een kostenagent die opmerkt wanneer een batch-provider anders begint te rekenen dan we verwachten. Kleine, nauw afgebakende dokters. Elk eigenaar van één dimensie van systeemgezondheid, rapporterend aan hetzelfde reviewkanaal, allemaal uiteindelijk verantwoording schuldig aan mensen die pull requests lezen.

Ik weet oprecht niet of dit over een jaar nog de juiste vorm is. De technische en ethische grond blijft bewegen. Wat ik wel kan zeggen, is dat we een bewuste keuze hebben gemaakt om heal-infrastructuur te bouwen waar de mens altijd het laatste woord heeft, waar het systeem de bronnen respecteert waarvan het afhankelijk is, en waar "de agent heeft het gefikst" altijd betekent "de agent heeft een fix voorgesteld, een mens heeft hem goedgekeurd, en we hebben het audit-spoor om het te bewijzen".

Of dit allemaal klopt, zal de tijd leren. De mensen die Andri dagelijks gebruiken, en de mensen die de sites runnen die we scrapen, zijn de echte toets.

Als het advocaten helpt beter werk te leveren, dataproblemen vangt voordat ze ertoe doen, en de goodwill bewaart van de publieke uitgevers waarvan we afhankelijk zijn, dan heeft het gewerkt.

Zo niet, dan leren we er iets van en proberen we het anders. Dat is ook hoe zelfherstel eruitziet, vermoed ik.


Noot over framing: het meeste schrijfwerk over "zelfherstellende software" gaat over runtime-systemen die zichzelf zonder mens herstellen. Wat wij gebouwd hebben is iets anders: agents die de repetitieve menselijke onderdelen van incident response doen en codewijzigingen voorstellen die een mens daarna merget. We gebruiken de vocabulaire omdat dat is waar mensen naar grijpen, maar de belangrijke eigenschap van ons systeem is dat een mens altijd het laatste woord heeft, en dat de bronsystemen waarvan we afhankelijk zijn publieke archieven zijn, gepubliceerd door mensen die niet gevraagd hebben om gescraped te worden.

Lees ook: waarom juridische AI moet nadenken, niet alleen reageren, waarom agentisch redeneren de enige weg is naar productierijpe juridische AI, over langzaam denken: waarom we Andri bouwden om te beraadslagen, en waarom we Andri laten pentesten door Fox-IT. Voor meer over hoe wij omgaan met documentmetadata, zie forensische AI-metadata. Bekijk alle Andri-features.