Het verbeteren van de nauwkeurigheid van tool-calls in vLLM

"Wanneer ik een open-weight model gebruik, zie ik resultaten die totaal niet overeenkomen met wat de kwaliteitsbenchmarks aangeven." — Softwareontwikkelaar die werkt met AI-modellen

De nauwkeurigheid van tool-calls is een essentieel onderdeel bij het serveren van open-weight modellen. Wanneer tool-calls niet nauwkeurig zijn, bouwt het model voort op onzinnige context. Dit leidt tot resultaten van lage kwaliteit, vooral bij agentische workloads waar multi-turn sessies gebruikelijk zijn.

Helaas voldoen veel providers en veelgebruikte inference-engines niet volledig aan de tests voor tool-call nauwkeurigheid. Wanneer we bijvoorbeeld Kimi K2.6 serveerden met vLLM versie 0.28, kregen we slechts in 92% van de gevallen het verwachte tool-call schema. Met andere woorden: 2 van de 25 gepoogde tool-calls mislukten.

Gelukkig heeft vLLM onlangs ondersteuning toegevoegd voor constrained decoding (beperkte decodering), een techniek om schema-naleving te garanderen. Deze functie is in vLLM 0.31 uitgebracht als een server-side flag. Door dit in te schakelen, kan een nauwkeurigheid van 100% worden behaald met de nieuwere Kimi- en DeepSeek-modellen.

In dit artikel beschrijven we eerst wat tool-call schema-nauwkeurigheid is en waarom dit belangrijk is voor agentische workloads. Daarna bespreken we constrained decoding en hoe dit het probleem van schema-nauwkeurigheid oplost.

Tool-call Schema's

Je kunt een LLM in twee stappen in staat stellen om een externe tool aan te roepen:

  1. Het AI-model informeren over het bestaan van extern beschikbare tool-calls.
  2. Het model instructies geven over hoe gestructureerde tool-calls moeten worden uitgevoerd.

Wanneer OpenCode bijvoorbeeld wordt verbonden met Kimi, stuurt OpenCode een JSON-array naar Kimi waarin de beschikbare tools worden beschreven. Een van de tools in deze lijst is webfetch, die webpagina-inhoud ophaalt:

[{
"type": "function",
"function": {
"name": "webfetch",
"description": "- Fetches content from a specified URL\n- Takes a URL and optional format as input\n- Fetches the URL content, converts to requested format (markdown by default)\n- Returns the content in the specified format\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n  [content redacted for brevity]\n",
"parameters": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to fetch content from"
},
"format": {
"type": "string",
"enum": [
"text",
"markdown",
"html"
],
"description": "The format to return the content in (text, markdown, or html). Defaults to markdown.",
"default": "markdown"
},
"timeout": {
"type": "number",
"description": "Optional timeout in seconds (max 120)"
}
},
"required": [
"url"
]
}
}
}, ...]

De inference-engine wrapt deze JSON-beschrijving vervolgens in LLM-specifieke tokens voordat deze naar het LLM wordt gestuurd, zodat dit overeenkomt met hoe het LLM is getraind om tool-declaraties te herkennen. Bij het serveren van Kimi ziet dit er ongeveer zo uit:

<|imsystem|>tooldeclare<|immiddle|>[{"type":"function","function":{...}, ...]<|imend|>

Het JSON-object dat de tool (of functie) beschrijft, bevat de parameters van de tool, hun typen, welke parameters verplicht zijn, enzovoort. Dit staat bekend als het schema van de tool. Om een tool-call die door het LLM is gegenereerd succesvol uit te voeren, moet de call voldoen aan de regels die in dit schema zijn gespecificeerd.

Wanneer een gebruiker via hun agent-harness vraagt om een webpagina op te halen, kan het LLM de volgende tokens genereren:

I'll fetch that page. <|toolcallssectionbegin|><|toolcallbegin|>functions.webfetch:0<|toolcallargumentbegin|>{"url": "https://ubicloud.com", "format": "markdown"}<|toolcallend|><|toolcallssection_end|>

Deze tokens worden door de parser van de inference-server verwerkt, waarbij de LLM-specifieke tokens worden geëxtraheerd en de call wordt omgezet in een JSON-object zoals:

{
"role": "assistant",
"content": "I'll fetch that page.",
"tool_calls": [
{
"id": "functions.webfetch:0",
"type": "function",
"function": {
"name": "webfetch",
"arguments": "{\"url\": \"https://example.com\", \"format\": \"markdown\"}"
}
}
]
}

Dit wordt teruggestuurd naar de harness (in dit voorbeeld OpenCode), die de webfetch-tool uitvoert en de output terugstuurt naar het model.

Het belang van schema-nauwkeurigheid

Voor een succesvolle tool-call moet de call strikt voldoen aan het schema van de tool.

De eenvoudige aanpak is om het model tool-calls vrij te laten genereren en erop te vertrouwen dat de training van het model voldoende is om correcte calls te produceren. Dit is vergelijkbaar met hoe we erop vertrouwen dat het model syntactisch correct proza genereert.

Deze aanpak werkt het grootste deel van de tijd, omdat moderne modellen goed zijn in het volgen van gestructureerde patronen. Maar "het grootste deel van de tijd" betekent dat er nog steeds veel tool-calls kunnen mislukken. Dit resulteert in vervuiling van het contextvenster door foutmeldingen, lagere modelprestaties en hogere kosten omdat de agent de call één of meerdere keren opnieuw moet proberen.

Om dit te voorkomen bij het gebruik van Kimi, heeft Moonshot AI de K2VV ToolCall benchmark gepubliceerd. Hiermee kunnen providers verifiëren of ze Kimi serveren met dezelfde kwaliteit als het origineel, inclusief het standaardgedrag om alleen geldige tool-calls te genereren.

De workload van de K2VV ToolCall bestaat uit 2000 requests om te evalueren wanneer het LLM tool-calls genereert en of deze syntactisch correct zijn (dus voldoen aan het verstrekte schema). De benchmark analyseert de kwaliteit via twee metrieken:

  • Trigger similarity: Bekijkt wanneer de service besluit een tool-call te triggeren en hoe vergelijkbaar deze beslissingen zijn met die van de eigen service van Moonshot AI.
  • Schema accuracy: Kwantificeert het aandeel tool-calls dat voldoet aan het schema en dus succesvol kan worden aangeroepen. Bij een schema-nauwkeurigheid van 50% zijn de helft van de gegenereerde tool-calls malformed.

Schema-nauwkeurigheid schiet vaak tekort door de probabilistische aard van taalmodellen. Een model genereert tokens op basis van waarschijnlijkheid; het draait geen compiler of type-checker. Zelfs met een duidelijke prompt kan het model malformed tool-calls produceren. Dit kan worden veroorzaakt door een mismatch tussen de trainingsdata van het model en de huidige inference-harness. Modellen zijn getraind op diverse datasets met elk hun eigen tools, schema's en conventies. Hierdoor kan een model een tool- of veldnaam genereren die het tijdens de training heeft gezien, zelfs als deze niet bestaat in de huidige productieomgeving. Bijvoorbeeld: het genereren van searchweb terwijl de beschikbare tool websearch is, of het gebruik van city_name terwijl het actieve schema location verwacht.

Constrained Decoding

Een LLM berekent voor elk token de waarschijnlijkheid dat dit het volgende token in de sequentie is, en selecteert vervolgens een token via een gewogen steekproef (weighted sample). Constrained decoding voorkomt malformed tool-calls door het model te verbieden tokens te samplen die zouden leiden tot een syntactisch ongeldige tool-call. In plaats van het model vrij te laten samplen en het resultaat achteraf te valideren, begeleidt de inference-engine de generatie stap voor stap. In elke stap zijn alleen geldige tokens toegestaan.

Welke tokens zijn toegestaan, wordt bepaald door het verstrekte tool-schema. vLLM implementeert dit met structurele tags uit de XGrammar-bibliotheek. Het onderstaande fragment laat zien hoe XGrammar constrained decoding implementeert in Kimi K2.6. Het definieert de geldige structuur van een tool-call, inclusief waar de call begint, waar de index verschijnt, waar de argumenten beginnen en waar schema-gebonden JSON moet worden gegenereerd.

# Kimi K2.6 tool call syntax:
# <|tool_call_begin|>{function.name}:{index}
# <|tool_call_argument_begin|>{arguments}
# <|tool_call_end|>
TagFormat(
begin=f"{TOOL_CALL_BEGIN_PREFIX}{name}{TOOL_CALL_SUFFIX}",
content=SequenceFormat(
elements=[
RegexFormat(pattern=r"\d+"),
ConstStringFormat(value=TOOL_CALL_ARGUMENT_BEGIN),
JSONSchemaFormat(
json_schema=parameters,
any_order=any_order,
max_whitespace_cnt=max_whitespace_cnt,
),
]
),
end=TOOL_CALL_END,
)

Met deze definitie moet de generatie een geldig pad volgen: Begin tool call → Tool naam → Tool call index → Begin argumenten → Schema-gebonden JSON → Einde tool call

Ongeldige paden worden tijdens de generatie geblokkeerd. Wanneer de generatie bijvoorbeeld de fase van de tool-call index bereikt, beperkt de engine het samplen tot integer-tokens en maskeert deze alle andere opties.

Dit verandert schema-nauwkeurigheid van een "best-effort" benadering naar een runtime-garantie. Het model beslist nog steeds welk token wordt gegenereerd, maar het token wordt nu gekozen uit een beperkte en syntactisch correcte set.

Opvallend is dat deze runtime-handhaving een verwaarloosbare invloed heeft op de prestaties. Door token-masks voor elke fase vooraf te berekenen en deze efficiënt op de GPU toe te passen tijdens de generatie, garandeert XGrammar correcte tool-calls zonder significante prestatieverliezen.

Verschillen in Constrained-Decoding Gedrag

Moonshot AI adviseert om constrained decoding in te schakelen en hun eigen API werkt ook zo. Daarom hadden we deze functionaliteit aanvankelijk toegevoegd in onze eigen fork van vLLM. Nadat vLLM deze functie in v0.25 upstream had toegevoegd, stapten we over naar de officiële versie voor Kimi en DeepSeek. Kort daarna ontdekten we echter dat onze score voor tool-call nauwkeurigheid terugviel naar 91%.

Na analyse bevestigden we dat de implementatie van constrained decoding in vLLM correct was. In plaats daarvan ontdekten we dat vLLM het standaardgedrag had gewijzigd voor requests waarbij constrained decoding niet expliciet was ingeschakeld. Dit komt doordat vLLM is ontworpen om een breed scala aan modellen te ondersteunen, en ondersteuning voor constrained decoding per model verschilt. Problemen vallen meestal in drie categorieën:

  1. Algemene modelproblemen: Bij sommige modellen wijkt de ongeconstreineerde output significant af van het schema of het tool-calling formaat. Dit dwingt het model om tokens te selecteren die het normaal niet zou doen, waardoor het "uit koers" raakt. Gebruikers van gpt-oss-20b, Qwen 3.6 en GLM 5.1 meldden problemen nadat deze functie standaard was ingeschakeld.
  2. Bugs in grammatica-vertaling: vLLM kan bugs hebben bij het vertalen van een tool-schema of tool-calling formaat naar de specifieke grammatica van het model. Dit kan leiden tot ongeldige tool-calls, mislukte requests, het stilzwijgend negeren van constrained decoding of een lagere nauwkeurigheid.
  3. Bugs in feature-interactie: Er zijn bugs in de interactie tussen constrained decoding en andere functies van de serving-engine. Zo kan een bug in de samenwerking met speculative decoding ertoe leiden dat speculatieve tokens onterecht door de grammatica worden afgewezen, wat resulteert in falende requests.

Vanwege deze problemen meldden de maintainers van vLLM problemen bij diverse modellen wanneer constrained decoding standaard aanstond. Het uitschakelen ervan als standaardoptie was daarom de veiligere keuze voor een model-agnostische engine. Later voegde vLLM de flag --tool-strict-level toe, zodat gebruikers de stritheid van tool-calls zelf kunnen configureren.

De focus van Ubicloud is echter anders dan die van vLLM. Wij richten ons op het serveren van Kimi- en DeepSeek-modellen met de kwaliteit die gebruikers van first-party providers verwachten. Om die reden hebben we constrained decoding ingeschakeld en de K2VV ToolCall benchmark opnieuw uitgevoerd. Nadat we 100% schema-nauwkeurigheid hadden geverifieerd voor alle geserveerde modelversies, hebben we de wijziging in productie genomen.

Conclusie

Bij het gebruik van LLM's wordt de kwaliteit niet alleen bepaald door het model zelf. De inference-engine speelt ook een belangrijke rol. Door de engine of de instellingen te wijzigen, verandert de outputkwaliteit van het model.

Bij het serveren van Kimi K2.6 en DeepSeek V4 Flash observeerden we zichtbare kwaliteitsvermindering door onnauwkeurigheden in het tool-call schema. We hebben dit opgelost door eerst een eigen vLLM-fork te gebruiken en vervolgens de nieuwe constrained decoding feature in vLLM in te schakelen. Dit hielp om onze tool-call nauwkeurigheid in de K2VV ToolCall benchmark te verbeteren van 92% naar 100%.