Streaming met tool calls: gedeeltelijke output en afgebroken calls
De dualiteit van streaming en function calling in productie
Het gelijktijdig toepassen van Server-Sent Events (SSE) en functie-aanroepen introduceert een uniek architecturaal vraagstuk in productie-omgevingen. Los van elkaar zijn beide patronen transparant: streaming levert direct verwerkbare tekst-tokens voor gebruikersinteractie, terwijl function calling een gestructureerde JSON-data-envelop genereert voor backend-systemen. Raadpleeg de handleiding over streaming responses bij LLM-API's voor de basisprincipes van SSE-verbindingen en token-buffering. Wanneer u deze twee technieken combineert, verandert het gedrag van de API-stream fundamenteel. Een Large Language Model (LLM) streamt de argumenten van een functie-aanroep namelijk niet als één valide JSON-object, maar als een reeks gefragmenteerde tekstschijven (delta's) gedurende de tijdsduur van het verzoek. Zie voor de fundamentele werking van functie-aanroepen en schema-definities het overzicht van function calling en tool-gebruik.
De combinatie van streaming en tool calls wijkt af van reguliere text-streaming omdat de tussentijdse gegevensstroom onbruikbaar is voor directe verwerking door externe applicatielogica. Zolang het laatste token van een argumenten-array niet door de server is verzonden, bevindt het geserialiseerde JSON-fragment zich in een syntactisch onvolledige staat. Het te vroeg parsen van een geaccumuleerde buffer leidt onherroepelijk tot runtime-uitzonderingen. Bovendien kan een stream op elk willekeurig punt worden onderbroken door een netwerkfout, client-disconnectie, of provider-timeout. In een standaard tekststream resulteert een onderbreking slechts in een afgekapte zin op het scherm van de gebruiker. Bij streaming tool calls kan een onderbreking leiden tot een half ontvangen payload, waardoor backend-systemen onzeker zijn over de integriteit en de intentie van het verzoek.
In dit artikel analyseren we de specifieke faalmodi van streaming tool calls binnen productie-omgevingen. Voor elke faalmodus behandelen we de systematische detectie, concrete mitigatiestrategieën en de bijbehorende operationele kosten op het gebied van latency, financieel budget en systeemcomplexiteit.
Faalmodus 1: Fragmentatie en accumulatiefouten bij argumentdelta's
Faalmodus: Het model verdeelt de JSON-argumenten van een tool call over tientallen losse SSE-chunks. Wanneer meerdere tool calls in parallel worden aangeroepen, verzendt de API de argumenten-delta's dooreengeweven, gecorreleerd via een index-veld. Indien de verwerkende applicatie deze chunks simpelweg achter elkaar plakt zonder rekening te houden met de specifieke tool-index, ontstaat een verminkte string die niet kan worden geanalyseerd.
Detectie: De detectie vindt plaats op het niveau van het stream-parsing mechanisme. Zodra JSON.parse() een SyntaxError genereert bij het verwerken van een geaccumuleerde buffer, of wanneer verplichte velden ontbreken na schema-validatie, is er sprake van een accumulatiefout of een ontbrekende delta.
Mitigatie: Implementeer een expliciete buffering-laag die inkomende chunks opdeelt in twee gescheiden kanalen: directe tekst-output voor de gebruikersinterface en geïndexeerde argument-buffers voor tool calls. Elke inkomende tool-delta moet worden toegewezen aan een interne staat-tabel op basis van de meegeleverde index en id. Pas wanneer de stream het expliciete eindsignaal verzendt, mag de buffer per index worden samengevoegd en aan een JSON-parser en schema-validator worden aangeboden.
Onderstaande provider-onafhankelijke pseudocode illustreert hoe u een SSE-stream verwerkt met strikte scheiding van tekst en tool-delta's, inclusief een time-out op de stroom en een expliciet foutpad.
async function processLLMStream(streamReader, timeoutMs) {
const toolCallBuffers = new Map();
let textBuffer = "";
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("STREAM_TIMEOUT")), timeoutMs)
);
try {
await Promise.race([
(async () => {
for await (const chunk of streamReader) {
if (chunk.type === "text_delta") {
textBuffer += chunk.content;
emitUIUpdate(chunk.content);
} else if (chunk.type === "tool_delta") {
const index = chunk.index;
if (!toolCallBuffers.has(index)) {
toolCallBuffers.set(index, { id: chunk.id, name: chunk.name, argsText: "" });
}
toolCallBuffers.get(index).argsText += chunk.args_delta;
}
}
})(),
timeoutPromise
]);
} catch (error) {
logError("Streamverwerking mislukt of getime-out", error);
throw new StreamProcessingException("Stream onderbroken voor accumulatie", error);
}
const validatedToolCalls = [];
for (const [index, call] of toolCallBuffers.entries()) {
try {
const parsedArgs = JSON.parse(call.argsText);
validatedToolCalls.push({ id: call.id, name: call.name, args: parsedArgs });
} catch (parseError) {
throw new InvalidToolPayloadException(`Ongeldige JSON in tool call index ${index}`, parseError);
}
}
return { text: textBuffer, toolCalls: validatedToolCalls };
}
Kosten van de mitigatie:
- Latency: Geen extra netwerk-latency, maar de verwerking van de tool call wordt uitgesteld tot de volledige stroom is afgerond. Tussentijdse uitvoering van de functie is niet mogelijk.
- Geld: Verwaarloosbaar. De extra CPU-overhead voor het beheren van de Map-structuur in het geheugen heeft geen impact op de API-facturering.
- Complexiteit: Gemiddeld. De applicatie moet een stateful stream-parser onderhouden die bestand is tegen ongeordende of vertraagde SSE-pakketten.
Faalmodus 2: Onvolledige streams en afgebroken verbindingen
Faalmodus: De SSE-verbinding wordt voortijdig verbroken door een wegvallende netwerkverbinding, het overschrijden van de maximale responstijd, of een actieve annulering door de client. De ontvangen buffer bevat wel een gedeeltelijke JSON-structuur, maar het eindteken van de stream ontbreekt.
Detectie: Inspecteer de metadata van de stream bij het sluiten van het transportkanaal. Een correct afgesloten stream bevat een expliciete status-indicator, zoals finish_reason. Indien de stroom sluit zonder dat finish_reason gelijk is aan tool_calls of stop (bijvoorbeeld bij length of wanneer het veld volledig ontbreekt), is het signaal onvolledig. Tevens zal een poging om de geaccumuleerde string te parsen falen met een onverwacht einde van de invoer.
Mitigatie: Pas een strikt "alles-of-niets"-beginsel toe op de verwerking van tool calls. Gedeeltelijk ontvangen argument-strings mogen nooit geforceerd worden gerepareerd of gedeeltelijk worden geëxecuteerd. Indien de stream onderbroken wordt voordat het sluitings-event is ontvangen, dient de gehele payload van de betreffende tool call te worden goedgegooid. Het systeem moet de status van de sessie terugrollen naar de laatst bekende valide staat. Lees voor het opzetten van robuuste controlemechanismen en geautomatiseerde validaties ook de gids over het testen van LLM-integraties.
Het onderstaande fragment toont hoe een applicatie de finale status van een stream evalueert en bij een afgebroken verbinding een gecontroleerde uitzondering opwerpt in plaats van de onvolledige data door te sturen naar bedrijfskritische systemen.
function finalizeStreamPayload(streamState) {
if (!streamState.isCompleted) {
throw new AbortedStreamException("Stream verbroken voordat de server het eindsignaal stuurde.");
}
if (streamState.finishReason === "length") {
throw new TokenLimitExceededException("Model heeft de max_tokens limiet bereikt tijdens tool call.");
}
if (streamState.finishReason !== "tool_calls" && streamState.finishReason !== "stop") {
throw new InvalidFinishReasonException(`Onverwachte finish_reason: ${streamState.finishReason}`);
}
return streamState.toolCalls.map(call => {
const parsed = safelyParseJSON(call.rawArgs);
if (!parsed.success) {
throw new MalformedPayloadException(`Onvolledige JSON voor tool ${call.name}`);
}
return { id: call.id, name: call.name, args: parsed.data };
});
}
Kosten van de mitigatie:
- Latency: Indien een stream halverwege afbreekt, moet het verzoek volledig opnieuw worden ingediend. Dit verdubbelt de totale verwerkingstijd voor de uiteindelijke actie.
- Geld: Hoge kostenpost. Alle tokens die tot aan het moment van onderbreking zijn gegenereerd door de provider worden volledig gefactureerd, terwijl de gegenereerde output onbruikbaar is geresulteerd.
- Complexiteit: Hoog. De applicatie moet over transactie-achtige rollback-mechanismen beschikken om reeds verwerkte tekst-delta's in de gebruikersinterface af te vangen of te markeren als ongeldig.
Faalmodus 3: Dubbele uitvoering bij onzekere execution-states
Faalmodus: Het model verstuurt de volledige argumenten van een tool call via de stream. De client ontvangt de argumenten, de stream sluit, en de client start de uitvoering van de externe functie (bijvoorbeeld een betalingsopdracht of een database-mutatie). Indien de netwerkverbinding direct na de uitvoering wegvalt — maar voordat de client het resultaat van de functie kan terugkoppelen aan het LLM — kan een automatische retry-handler het verzoek opnieuw aanbieden. Hierdoor bestaat het risico dat de externe actie twee keer wordt uitgevoerd.
Detectie: Dit probleem manifesteer zich niet als een directe API-fout, maar als inconsistentie in externe systemen (zoals dubbele boekingen of dubbel aangemaakte resources). In de applicatielogging is dit zichtbaar doordat een verzoek opnieuw wordt geïnitieerd met dezelfde business-context maar een nieuw stream-ID.
Mitigatie: Voer tool calls nooit blindelings opnieuw uit bij een netwerkfout of afgebroken vervolgstream. Gebruik verplicht een unieke idempotency key voor elke externe actie die mutaties veroorzaakt. Deze sleutel moet worden afgeleid van het unieke tool call ID dat door de LLM-provider in de stream wordt meegeleverd, gecombineerd met de hash van de gevalideerde argumenten. Raadpleeg voor het opzetten van gedistribueerde unieke sleutels en de verwerking daarvan in backend-systemen het artikel over idempotentie bij LLM-calls.
Daarnaast is het noodzakelijk om automatische retry-mechanismen op de stream zelf af te kaderen. Voor de precieze configuratie van retry-strategieën bij wegvallende HTTP-verbindingen verwijzen we naar het overzicht over retries en exponentiële backoff.
async function executeToolCallWithIdempotency(toolCall, executionContext) {
const idempotencyKey = `exec_${toolCall.id}_${hashPayload(toolCall.args)}`;
const lockAcquired = await executionContext.idempotencyStore.lock(idempotencyKey);
if (!lockAcquired) {
return await executionContext.idempotencyStore.getResult(idempotencyKey);
}
try {
const result = await Promise.race([
executeExternalService(toolCall.name, toolCall.args),
new Promise((_, reject) => setTimeout(() => reject(new Error("EXECUTION_TIMEOUT")), 5000))
]);
await executionContext.idempotencyStore.save(idempotencyKey, result);
return result;
} catch (error) {
await executionContext.idempotencyStore.clearLock(idempotencyKey);
throw new ToolExecutionException(`Fout tijdens uitvoering van ${toolCall.name}`, error);
}
}
Kosten van de mitigatie:
- Latency: Lichte toename (5-20 ms) vanwege het raadplegen en opslaan van idempotency-states in een snelle caching-laag zoals Redis.
- Geld: Verwaarloosbare infrastructurele kosten voor de state-store. Voorkomt echter potentieel zeer hoge operationele schade door dubbele transacties.
- Complexiteit: Hoog. Vereist een gecentraliseerde state-store en gedisciplineerde sleutel-generatie over het gehele gedistribueerde systeem.
Faalmodus 4: Provider-side annulering, AbortControllers en tokenkosten
Faalmodus: De client breekt de HTTP-stream af met behulp van een AbortController omdat een ingestelde client-timeout verstrijkt of de gebruiker op "annuleren" drukt. De veronderstelling dat het stopzetten van de stroom direct de kosten en verwerking aan de providerzijde stopzet, is in veel gevallen onjuist.
Detectie: Monitoring op de API-gateway toont een verschil tussen het aantal door de client ontvangen tokens en het aantal gefactureerde tokens in de gebruiks-rapportages van de provider. Voor een diepgaande analyse van responstijden, actieve annuleringen en de inrichting van uiterste verwerkingstijden verwijzen we naar het artikel over timeouts, annuleringen en deadline-budgetten.
Mitigatie: Wanneer een client een AbortController activeert, sluit de onderliggende TCP-verbinding. LLM-providers verwerken het stopzetten van een stream echter asynchroon. Het model kan intern doorgenereren totdat de eerstvolgende netwerk-write faalt. Voor een nauwkeurige cost-control dient het applicatiesysteem het verbroken verzoek te registreren in een observatie-module. Zie voor de opzet van dergelijke bewaking het document over observability en logging bij LLM-integraties.
Om onverwachte kosten te vermijden bij grote volumes, dienen systeemarchitecten daarnaast rekening te houden met de tarieven voor afgebroken gegenereerde tokens. Informatie over tariefstructuren en limieten is beschikbaar in de gids over rate limits en kostenbeheer.
Het onderstaande pseudocode-voorbeeld demonstreert de correcte hantering van een AbortController in combinatie met een timeout-budget en het opvangen van de annuleringsevent.
async function fetchStreamWithCancellation(apiEndpoint, payload, clientSignal) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
if (clientSignal) {
clientSignal.addEventListener("abort", () => controller.abort());
}
try {
const response = await fetch(apiEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal
});
clearTimeout(timeoutId);
return response.body;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === "AbortError") {
logMetric("stream_cancelled_by_client_or_timeout");
throw new RequestCancelledException("API-verzoek geannuleerd via AbortSignal.");
}
throw new NetworkException("Transportfout tijdens API-verzoek", error);
}
}
Kosten van de mitigatie:
- Latency: Geen. Het tijdig afbreken van onnodige streams verlaagt juist de bezetting van de client-threads.
- Geld: Gegenereerde tokens tot het moment waarop de provider het afbreken registreert, blijven gefactureerd. Illustratieve metingen bij grote providers (stand: augustus 2026, gebaseerd op officiële providerdocumentatie voor streaming API-endpoints) tonen aan dat er een vertraging van 100 tot 500 milliseconden kan zitten tussen de client-abort en de daadwerkelijke stopzetting van de GPU-inference. Dit kan resulteren in tientallen "spooktokens" per geannuleerde stream.
- Complexiteit: Laag. Het correct koppelen van AbortSignals vereist minimale code, maar wel een consistente toepassing over alle API-lagen.
Faalmodus 5: Spanningsveld tussen streaming en strict structured output
Faalmodus: Veel LLM-providers bieden de mogelijkheid om via gepatenteerde of open standaarden strikte JSON-schema's af te dwingen (structured output). Wanneer dit wordt gecombineerd met streaming, ontstaat een architecturaal spanningsveld. Om te garanderen dat de output 100% voldoet aan het meegegeven schema, moet de provider-engine het generatieproces op token-niveau beperken met een grammatica-masker (constrained sampling). Tussentijdse tekst-delta's voldoen per definitie nooit aan het volledige schema zolang het sluitteken niet is verwerkt.
Detectie: De applicatie ontvangt streaming chunks die op zichzelf geen geldige JSON vormen, of de provider retourneert een API-foutmelding (zoals HTTP status 400) omdat de combinatie van stream: true en bepaalde strikte schema-opties door de betreffende engine niet wordt ondersteund.
Mitigatie: Accepteer dat bij het streamen van tool-argumenten de syntactische validatie pas aan het einde van de stroom kan plaatsvinden. Gebruik het strikte schema als hulpmiddel voor de provider om het model te dwingen valide JSON te genereren, maar voer de definitieve schema-validatie (bijvoorbeeld via Zod of JSON Schema validators) pas uit in uw eigen applicatiecode nadat de stroom volledig is geaccumuleerd. Lees voor een diepgaande beschouwing over schema-afdwinging het overzicht van structured output en JSON-schema's.
Indien het voor uw bedrijfsvoering van kritiek belang is dat uitvallen door ongeldige JSON tot een absoluut minimum beperkt wordt, kan het noodzakelijk zijn om terug te vallen op robuuste verwerkingsstrategieën. Raadpleeg voor verdere continuïteitsoplossingen de gids over graceful degradation bij LLM-uitval.
Kosten van de mitigatie:
- Latency: Constrained sampling aan de providerzijde kan de Time To First Token (TTFT) met 10% tot 30% verhogen (illustratieve schatting gebaseerd op benchmarkdata van open-source inference engines, stand: augustus 2026). De totale verwerkingstijd blijft nagenoeg gelijk.
- Geld: Geen directe extra kosten per token, behalve wanneer extra pre-processing of grotere system-prompts noodzakelijk zijn.
- Complexiteit: Gemiddeld. Vereist dubbele validatie-logica: een strikt schema in de API-call en een defensieve controle in de ontvangende applicatie.
Beslisboom en integratiechecklist voor streaming tool calls
Het combineren van streaming en tool calls is een krachtig patroon, maar het introduceert aanzienlijke overhead. Gebruik onderstaande beslisboom en overwegingen om te bepalen of streaming voor uw specifieke tool-call scenario de juiste keuze is.
Wanneer wel streaming met tool calls gebruiken?
- Simultane tekst- en tool-generatie: Wanneer de LLM eerst een toelichtende tekst voor de gebruiker genereert ("Ik ga nu de wisselkoers ophalen...") en direct daarna een tool call verricht binnen dezelfde respons. Streaming zorgt hier voor een lage perceptuele latency (TTFT) bij de gebruiker.
- Lange argumenten-payloads: Indien de tool call zeer grote tekstblokken als argument verwacht (zoals het overbrengen van een gegenereerd document naar een opslag-tool), waardoor het wachten op de volledige respons zonder streaming leidt tot client-timeouts.
- Directe feedback in de gebruikersinterface: Wanneer de applicatie visuele indicatoren wil tonen (zoals een draaiend icoon of een voortgangsbalk van een specifieke tool) zodra de naam van de tool in de stream verschijnt, nog voordat de argumenten compleet zijn.
Wanneer géén streaming met tool calls gebruiken?
- Systeem-naar-systeem integraties (Headless execution): Wanneer de verzoeker een achtergrond-worker, webhook of CRON-job is zonder menselijke gebruiker. Streaming voegt hier enkel netwerk-overhead en parsing-complexiteit toe zonder enig voordeel op het gebied van gebruikerservaring. Gebruik in dit geval een standaard synchrone of asynchrone JSON-call. Lees hiervoor het artikel over webhooks en asynchrone taken.
- Batch-verwerking: Bij het in bulk verwerken van duizenden gegevensronden is streaming onefficiënt en verhoogt het de kans op afgebroken TCP-verbindingen.
- Strikte transacties: Wanneer de uitkomst van de tool call direct een onomkeerbare financiële of fysieke actie aanroept waarbij elke vorm van netwerk-onzekerheid geëlimineerd moet worden.
Productie-checklist voor uitrol
- Buffers opzetten: Is er een gescheiden buffer aanwezig per
tool_call.indexdie pas wordt vrijgegeven bij het sluitselement van de stream? - Eindsignaal-controle: Controleert de parser expliciet of de stream is gesloten met
finish_reason: "tool_calls"offinish_reason: "stop"? - Idempotentie geborgd: Wordt het door de provider meegeleverde tool call ID doorgegeven aan de uitvoerende backend-service als idempotency key?
- Timeout-budgetten: Is er een harde tijdsgrens ingesteld voor de gehele duur van de stream, gekoppeld aan een
AbortController? - Foutpad-afhandeling: Wordt bij een gefragmenteerde of mislukte JSON-parse de actie volledig geannuleerd zonder gedeeltelijke uitvoering?
Dit artikel behandelt het verwerken van retries en backoff, de inrichting van een centrale proxy-architectuur en de selectie van geschikte modellen voor tool-gebruik niet; zie hiervoor de artikelen over retries en exponentiële backoff bij LLM-calls, het zelf hosten van een LLM-gateway, en het kiezen van het juiste model voor function calling.


