Batchverwerking via de LLM API

Duizenden prompts verwerken zonder je rate limits of budget op te blazen

Het verwerken van een enkele prompt via een Language Model API is eenvoudig. Maar zodra je duizenden documenten moet analyseren, klantrecensies moet classificeren of complete databases wilt migreren, verandert de dynamiek fundamenteel. Realtime API-calls schieten dan al snel tekort. Je botst tegen harde limieten aan, applicaties crashen door geheugenproblemen en een onverwachte netwerkfout kan je hele workflow ruïneren.

Om grote hoeveelheden data stabiel te verwerken, is een doordachte architectuur nodig. Dit artikel bespreekt hoe je batchverwerking opzet, hoe je omgaat met rate limits, waarom idempotentie cruciaal is en hoe je kosten beheersbaar houdt. Voor inzicht in de basiskosten en limieten kun je ook terecht bij rate limits en kosten.

Realtime versus Batchverwerking

Bij traditionele webtoepassingen worden API-verzoeken synchroon afgehandeld: de gebruiker wacht op het antwoord. Voor grootschalige achtergrondtaken is dit patroon ongeschikt. De tabel hieronder illustreert de belangrijkste verschillen:

Eigenschap Realtime (Synchroon) Batchverwerking (Asynchroon)
Latentie Direct (milliseconden tot seconden) Variabel (uren tot dagen afhankelijk van volume)
Foutafhandeling Directe feedback naar client Automatische herhaling en isolatie van fouten
Resource-gebruik Hoge piekbelasting, gevoelig voor spikes Gelijkmatig verdeeld over de tijd
Kosten Standaard tarieven Vaak korting bij native batch-endpoints van providers

Wanneer je kiest voor asynchrone verwerking, verschuift het ontwerpprincipe van "direct antwoord" naar "betrouwbare statusadministratie". Dit sluit nauw aan bij het opzetten van robuuste systemen, vergelijkbaar met de principes die je leest bij rag voor beginners voor het indexeren van grote documentcollecties.

Het Verwerkingspatroon: Statusadministratie per Item

Een robuuste batch-engine steunt op een database die de status van elk individueel verzoek nauwkeurig bijhoudt. Zonder zo'n administratie ben je bij een crash volledig blind en kun je geen onderscheid maken tussen succesvolle items en mislukte taken.

Elk item in je wachtrij doorloopt een vaste levenscyclus:

  1. Pending: Het item is aangemaakt, maar nog niet opgepakt.
  2. Processing: Een worker heeft het item gelocked en verzonden naar de API.
  3. Completed: Het LLM-antwoord is succesvol ontvangen en opgeslagen.
  4. Failed: Het verzoek is definitief mislukt na alle pogingen.

Hieronder zie je een schematische weergave van een database-schema in SQL dat deze statussen ondersteunt:

CREATE TABLE batch_jobs (
    id SERIAL PRIMARY KEY,
    payload JSONB NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    attempts INT DEFAULT 0,
    response TEXT,
    error_message TEXT,
    updated_at TIMESTAMP DEFAULT NOW()
);

Parallellisme Begrenzen en Rate Limits Ontwijken

Het blind afvuren van duizenden parallelle HTTP-requests leidt onmiddellijk tot 429 Too Many Requests errors. LLM-providers hanteren strikte grenzen op basis van *Requests Per Minute* (RPM) en *Tokens Per Minute* (TPM).

Om binnen deze grenzen te blijven, gebruik je een worker pool met een rate limiter (bijvoorbeeld een Token Bucket-algoritme). Dit zorgt ervoor dat het aantal gelijktijdige verzoeken dynamisch wordt afgekapt zodra je de limiet nadert. Mocht je toch tegen een limiet aanlopen, dan is een correcte strategie voor hertentendement essentieel. Lees meer over hoe je hiermee omgaat via retries en backoff.

Concurrency Control in code

In plaats van oneindige async threads te starten, beperk je het aantal actieve workers. Dit voorkomt dat je geheugen volloopt en houdt je netwerkverkeer voorspelbaar.

import asyncio

async def worker(queue, semaphore, api_client):
    while True:
        item_id, payload = await queue.get()
        async with semaphore:
            try:
                # Verwerk LLM call
                result = await api_client.generate(payload)
                await save_success(item_id, result)
            except Exception as e:
                await handle_failure(item_id, str(e))
            finally:
                queue.task_done()

Idempotentie: Veilig Herstarten Zonder Dubbelwerk

Wat gebeurt er als je batch-script vastloopt op 80% van de dataset en je herstart de applicatie? Zonder idempotentie loop je het risico dat je alle eerdere items opnieuw verstuurt. Dat kost dubbele tokens en dus dubbel geld.

Idempotentie betekent dat een actie meerdere keren kan worden uitgevoerd met exact hetzelfde resultaat als eenmalige uitvoering. Dit bereik je door:

Deelresultaten Tussentijds Wegschrijven

Het opslaan van resultaten pas aan het very einde van een batch van 10.000 items is een recept voor frustratie. Als het proces na 9.000 items crasht door een onverwachte geheugenlek, ben je alles kwijt.

Schrijf daarom resultaten in kleine batches (chunks van bijvoorbeeld 50 of 100 items) direct weg naar je database of een object store. Dit minimaliseert het risico op dataverlies en geeft je direct inzicht in de voortgang via je dashboard.

Omgaan met Gedeeltelijke Mislukking (Partial Failure)

Bij grootschalige verwerking is de kans honderd procent dat een deel van de items faalt. Misschien bevat een specifieke prompt illegale karakters, weigert het model te antwoorden vanwege veiligheidsfilters of treedt er een tijdelijke netwerkstoring op.

Een professionele batch-pipeline stopt nooit bij een fout in een enkel item. In plaats daarvan:

  1. Vang de specifieke uitzondering op binnen de worker loop.
  2. Log de foutmelding en het item-ID in de database.
  3. Verhoog de teller voor het aantal pogingen (attempts).
  4. Laat de batch doorgaan met de resterende items.
  5. Exporteer na afloop alle gefaalde items naar een aparte "dead-letter queue" voor handmatige analyse.

Kosten vooraf schatten met een steekproef

Voordat je een gigantische batch loslaat op een duur model, wil je weten wat de kosten gaan worden. Token-aantallen laten zich vooraf moeilijk exact voorspellen omdat de lengte van zowel de input als de output varieert.

De oplossing is een representatieve steekproef:

Op deze manier voorkom je financiële verrassingen achteraf en kun je op basis van de uitkomst eventueel kiezen voor een goedkoper model of kortere prompts.

Conclusie

Batchverwerking van LLM-calls vraagt om een verschuiving van ad-hoc scripts naar robuuste achtergrondarchitectuur. Door te werken met een strikte statusadministratie, parallellisme te begrenzen, idempotentie toe te passen en tussentijds resultaten weg te schrijven, bouw je systemen die autonoom duizenden verzoeken veilig verwerken. Combineer dit met een goede kosteninschatting vooraf en je houdt zowel de techniek als je budget volledig onder controle.