Deel:𝕏LinkedInRedditFacebookKopieer link

Idempotentie bij LLM-API-calls

In gedistribueerde systemen is netwerkfoutafhandeling een constante uitdaging. Wanneer je applicatie communiceert met externe Large Language Model (LLM) API's, versterken de trage responstijden en hoge rekenkosten deze problematiek. Het willekeurig opnieuw proberen van een mislukte HTTP-aanroep kan leiden tot dubbele facturatie, inconsistente systeemtoestanden en ongewenste acties in externe systemen.

Idempotentie is het ontwerpprincipe dat garandeert dat het meerdere keren uitvoeren van dezelfde operatie exact hetzelfde resultaat oplevert als een enkele uitvoering. Dit artikel behandelt waarom LLM-calls van nature niet idempotent zijn, hoe je een robuust idempotentiepatroon bouwt, en hoe je omgaat met neveneffecten en gedeeltelijk mislukte streaming-antwoorden.

Waarom dubbele verwerking een reëel probleem is

Dubbele verwerking bij API-interacties ontstaat zelden doordat een ontwikkelaar per ongeluk dezelfde functie twee keer aanroept. Het is vrijwel altijd het resultaat van de onderliggende infrastructuur en menselijk gedrag:

Waarom een LLM-call van nature NIET idempotent is

In tegenstelling tot een HTTP GET-verzoek of een SQL UPDATE die een vaste waarde instelt, is een standaard HTTP POST naar een /v1/chat/completions endpoint inherent niet-idempotent op drie vlakken:

1. Directe financiële schade

Elke verwerkte prompt kost tokens. Wanneer jouw systeem een prompt van 4.000 tokens door een netwerkstoring drie keer verstuurt, betaal je drie keer voor dezelfde invoer. Op schaal leidt dit tot een aanzienlijke lek in je API-budget.

2. Niet-deterministische output

Tenzij de temperature parameter strikt op 0.0 staat (en zelfs dan garanderen niet alle providers determinisme vanwege parallelle GPU-berekeningen), levert elke uitvoering van dezelfde prompt een andere tekst op. Als twee parallelle workers dezelfde taak uitvoeren, genereren ze twee afwijkende antwoorden. Welke van de twee sla je op in de database? Dit veroorzaakt race conditions en datainconsistentie.

3. Neveneffecten (Function Calling en Agentic Workflows)

Wanneer een LLM wordt ingezet als agent met 'function calling' mogelijkheden, kan een dubbele call verregaande gevolgen hebben. Een model dat de opdracht krijgt om "de klant een bevestigingsmail te sturen en een ticket aan te maken", zal bij een dubbele verwerking twee tickets aanmaken en twee e-mails versturen.

Kernregel: Beschouw elke LLM-API-call als een operatie met neveneffecten. Vertrouw er nooit op dat de provider de call automatisch voor je ontdubbelt, tenzij de provider expliciet een idempotentie-header ondersteunt.

Het patroon van de Idempotentiesleutel (Idempotency Key)

Om een niet-idempotente LLM-call idempotent te maken, introduceren we een unieke Idempotency Key. Dit patroon werkt volgens het principe dat de vragende partij een uniek kenmerk meestuurt met het verzoek. De ontvangende of tussenliggende laag controleert dit kenmerk voordat de LLM wordt aangeroepen.

Sommige LLM-providers ondersteunen een custom HTTP-header voor idempotentie (zoals Idempotency-Key: <key>). De exacte headernaam en ondersteuning verschillen per provider. Indien de provider dit niet ondersteunt, moet je deze laag in je eigen applicatie-architectuur of API-gateway implementeren.

Hoe kies of leid je de sleutel af?

De sleutel moet de unieke intentie van de operatie vertegenwoordigen:

Bewaartermijn en opslag

Sla de status van idempotentiesleutels op in een snelle, centrale key-value store zoals Redis. De levensduur (TTL) van de sleutel hangt af van de use case, maar 24 tot 48 uur is voor de meeste LLM-toepassingen voldoende om netwerk-retries en queue-herhalingen op te vangen.

Implementatie: Ontdubbeling in je eigen applicatielaag

Onderstaande pseudocode illustreert hoe je met behulp van Redis een idempotentiestructuur bouwt rondom een LLM-API-call. Dit voorkomt dat twee gelijktijdige verzoeken dezelfde LLM-call uitvoeren en zorgt ervoor dat een latere retry het reeds gegenereerde antwoord terugkrijgt.

import redis
import time
import json

db = redis.Redis(host='localhost', port=6379, db=0)

def execute_idempotent_llm_call(idempotency_key, prompt_data):
    lock_key = f"lock:{idempotency_key}"
    response_key = f"response:{idempotency_key}"
    
    # 1. Controleer of het resultaat al bestaat
    cached_response = db.get(response_key)
    if cached_response:
        return json.loads(cached_response), "CACHE_HIT"

    # 2. Probeer een distributed lock te verkrijgen (atomic SETNX)
    # TTL van 60 seconden voorkomt dat een gecrashte worker de key voor altijd blokkeert
    acquired = db.set(lock_key, "PROCESSING", nx=True, ex=60)
    
    if not acquired:
        # Een andere worker verwerkt dit verzoek momenteel. Wacht en poll.
        return wait_for_concurrent_request(response_key)

    try:
        # 3. Voer de werkelijke LLM API call uit
        llm_result = call_external_llm_api(prompt_data)
        
        # 4. Sla het resultaat op met een TTL van 24 uur (86400 sec)
        db.set(response_key, json.dumps(llm_result), ex=86400)
        
        return llm_result, "EXECUTED"

    finally:
        # Verwijder de lock zodra het werk gedaan (of mislukt) is
        db.delete(lock_key)

def wait_for_concurrent_request(response_key, timeout=30):
    start = time.time()
    while time.time() - start < timeout:
        cached_response = db.get(response_key)
        if cached_response:
            return json.loads(cached_response), "CACHE_HIT_WAIT"
        time.sleep(0.5)
    raise TimeoutError("LLM verwerking duurde te lang bij concurrent verzoek.")

Merk op dat dit patroon verschilt van algemene antwoord-caching. Terwijl caching bedoeld is om prestaties te verbeteren en kosten te drukken voor identieke vragen, is dit patroon specifiek ontworpen voor correctheid en het voorkomen van dubbele verwerking bij foutsituaties. Raadpleeg ons artikel over caching van LLM-antwoorden voor de verschillen in implementatie.

API-call ontdubbelen vs. Neveneffecten (Side Effects) ontdubbelen

Het beveiligen van de HTTP-call naar de LLM-provider is pas de helft van de oplossing. In moderne softwarearchitecturen verwerkt een LLM niet alleen tekst, maar genereert het gestructureerde data waarmee vervolgacties worden aangestuurd.

Er is een essentieel verschil tussen twee niveaus van ontdubbeling:

  1. Transport-niveau (API-call): Het voorkomen dat de HTTP-aanroep naar de LLM-provider twee keer wordt uitgevoerd.
  2. Domein-niveau (Neveneffecten): Het voorkomen dat de actie die voortvloeit uit de LLM-output (bijvoorbeeld het bijwerken van een database of het versturen van een pushnotificatie) twee keer wordt uitgevoerd.

Stel dat de LLM-call succesvol is en het antwoord opslaat in de cache, maar de worker crasht nét voordat de vervolginformatie naar de CRM-database geschreven wordt. Bij de volgende retry van de queue leest de worker het antwoord direct uit de cache (transport-idempotentie geslaagd), maar voert de CRM-update alsnog voor de eerste keer uit.

Om domein-idempotentie te bereiken, moet je database-operaties uitvoeren met behulp van upserts (INSERT ... ON CONFLICT DO UPDATE) en externe systemen aanroepen met unieke transactie-id's die zijn afgeleid van de oorspronkelijke idempotency_key.

Omgaan met gedeeltelijke fouten en streaming-antwoorden

Streaming-responses (via Server-Sent Events of WebSockets) vormen een bijzondere uitdaging voor idempotentie. Omdat de data in brokstukken (chunks) binnenkomt, kan een verbinding halverwege verbreken nadat er al 500 van de 1000 tokens zijn ontvangen.

Als een streaming call halverwege afbreekt en je voert een eenvoudige retry uit, leidt dit tot problemen:

Strategieën voor streaming-idempotentie

Voor streaming-scenario's zijn er twee gangbare benaderingen:

1. Bufferen tot voltooiing: Sla streaming-chunks op in een tijdelijke geheugenbuffer (of Redis). Pas wanneer de stream het expliciete afsluitteken (zoals [DONE] of finish_reason: "stop") bereikt, markeer je de idempotency_key als voltooid en schrijf je het resultaat naar de permanente opslag. Mislukt de stream halverwege? Dan gooi je de buffer leeg en voert de retry een schone nieuwe call uit.

2. Append-only met sessie-tracking: Sla elke chunk op in de database gekoppeld aan een uniek chunk_index en de idempotency_key. Bij een hervatting vraagt de client de huidige max(chunk_index) op en stuurt de LLM-prompt opnieuw met een instructie om verder te gaan vanaf dat punt. Dit vereist echter specifieke ondersteuning van het model en de provider.

Samenspel met retries en exponential backoff

Idempotentie en retries zijn twee kanten van dezelfde medaille. Retries zonder idempotentie zijn gevaarlijk; idempotentie zonder retries is nutteloos bij netwerkstoringen.

Wanneer je een retry-mechanisme met exponential backoff en jitter implementeert, moet de idempotency_key constant blijven over alle opeenvolgende pogingen van dezelfde logische actie.

Checklist voor idempotentie bij LLM-integraties

Gebruik deze checklist bij het ontwerpen van je LLM-integratielaag:

  1. Unieke identificatie: Genereer je aan de bron (frontend/client) een unieke sleutel per gebruikersactie of taak?
  2. Provider-ondersteuning: Controleer je of de specifieke LLM-provider een Idempotency-Key header ondersteunt en wat de exacte voorwaarden zijn?
  3. Eigen opslaglaag: Is er een snelle key-value store (zoals Redis) aanwezig met gedistribueerde locks (SETNX) om concurrent verzoeken te blokkeren?
  4. TTL beheer: Hebben de opgeslagen idempotentie-records en locks een gepaste automatische vervaldatum (TTL)?
  5. Domein-ontdubbeling: Zijn de vervolgacties (database-writes, e-mails, webhooks) eveneens beschermd tegen dubbele uitvoering middels unieke identifiers?
  6. Streaming afhandeling: Worden afgebroken streaming-responses correct opgeruimd of gebufferd voordat ze als 'afgerond' worden gemarkeerd?

Door idempotentie vanaf de start op te nemen in je API-architectuur, voorkom je niet alleen onvoorziene kosten bij LLM-providers, maar bouw je ook aan een systeem dat bestand is tegen netwerkstoringen en onverwachte achtergrondherstarts.

Wil je dieper ingaan op het testen van de robuustheid van je LLM-infrastructuur of zoek je advies voor jouw specifieke architectuur? Bekijk de mogelijkheden op LLMNet Consultancy of neem deel aan de discussies op onze LLMNet Community.