<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Function-Calling on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/function-calling/</link><description>Recent content in Function-Calling on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 04 Jun 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/function-calling/index.xml" rel="self" type="application/rss+xml"/><item><title>Function calling and tool-augmented retrieval: the detective who knows which file to ask for</title><link>https://blog.lo0.es/en/posts/function-calling-tool-augmented-retrieval-detective-knows-which-file/</link><pubDate>Thu, 04 Jun 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/function-calling-tool-augmented-retrieval-detective-knows-which-file/</guid><description>&lt;blockquote>
&lt;p>This post goes deeper into the agentic reasoning mechanism that extends the RAG described in &lt;a href="https://blog.lo0.es/en/posts/reranker-hybrid-retrieval-five-chunks-llm-reads/">RAG with reranker and hybrid retrieval&lt;/a>. The retriever invoked when the LLM chooses &lt;code>vector_search&lt;/code> is exactly the pipeline in that article. The JSON Schema that defines each tool call is &lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">structured output&lt;/a> applied to the tool interface. And the agent&amp;rsquo;s requests pass through the &lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">L7 inference gateway&lt;/a> before reaching the model.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>A naive RAG always queries the same source. Function calling gives the LLM the ability to decide which tool to invoke (vector store, SQL, web search) according to what the query actually needs. The ReAct pattern chains those invocations in a reasoned loop until there is enough evidence. A 3-iteration pipeline with Llama-3.1-70B on on-premise hardware takes ≈ 1.1 s against the ≈ 300 ms of a single-step RAG; the gain is not in speed but in queries that a naive RAG simply cannot answer. The critical eval metric is &lt;strong>tool selection accuracy&lt;/strong>: the percentage of turns in which the model picks the right tool, measured over a synthetic eval set.&lt;/p>
&lt;h2 id="the-analogy-the-detective-who-knows-which-file-to-ask-for">The analogy: the detective who knows which file to ask for&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The detective and his sources of evidence">
&lt;style>
.db{fill:#f8f8f8;stroke:#444;stroke-width:1.4}
.dh{fill:#7aafff;stroke:#444;stroke-width:1.4}
.ds{fill:#ffd76b;stroke:#444;stroke-width:1.4}
.dg{fill:#b2e8b2;stroke:#444;stroke-width:1.4}
.dr{fill:#ffb3b3;stroke:#444;stroke-width:1.4}
.dl{font:600 13px sans-serif;fill:#222}
.dm{font:400 11px sans-serif;fill:#555}
.da{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mda)}
.dq{stroke:#666;stroke-width:1.5;fill:none;stroke-dasharray:5 3;marker-end:url(#mda)}
&lt;/style>
&lt;defs>&lt;marker id="mda" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;!-- Detective (LLM) in the centre -->
&lt;rect x="290" y="130" width="200" height="80" rx="8" class="dh"/>
&lt;text x="390" y="158" text-anchor="middle" class="dl">Detective (LLM)&lt;/text>
&lt;text x="390" y="176" text-anchor="middle" class="dm">reasons what evidence&lt;/text>
&lt;text x="390" y="192" text-anchor="middle" class="dm">it needs and asks for it&lt;/text>
&lt;!-- Case / Query -->
&lt;rect x="310" y="20" width="160" height="50" rx="8" class="db"/>
&lt;text x="390" y="42" text-anchor="middle" class="dl">Case (Query)&lt;/text>
&lt;text x="390" y="60" text-anchor="middle" class="dm">"how many EU contracts &amp;gt; 100k€?"&lt;/text>
&lt;path class="da" d="M390,70 L390,128"/>
&lt;!-- Vector store -->
&lt;rect x="20" y="240" width="160" height="70" rx="8" class="ds"/>
&lt;text x="100" y="263" text-anchor="middle" class="dl">Witness files&lt;/text>
&lt;text x="100" y="281" text-anchor="middle" class="dm">vector_search&lt;/text>
&lt;text x="100" y="297" text-anchor="middle" class="dm">Qdrant · 5-50 ms&lt;/text>
&lt;!-- SQL -->
&lt;rect x="310" y="240" width="160" height="70" rx="8" class="dg"/>
&lt;text x="390" y="263" text-anchor="middle" class="dl">Accounting ledger&lt;/text>
&lt;text x="390" y="281" text-anchor="middle" class="dm">sql_query&lt;/text>
&lt;text x="390" y="297" text-anchor="middle" class="dm">PostgreSQL · 10-200 ms&lt;/text>
&lt;!-- Web search -->
&lt;rect x="600" y="240" width="160" height="70" rx="8" class="dr"/>
&lt;text x="680" y="263" text-anchor="middle" class="dl">Press archive&lt;/text>
&lt;text x="680" y="281" text-anchor="middle" class="dm">web_search&lt;/text>
&lt;text x="680" y="297" text-anchor="middle" class="dm">public · 200-2000 ms&lt;/text>
&lt;!-- Arrows from the detective to the sources -->
&lt;path class="da" d="M310,190 L180,238"/>
&lt;path class="da" d="M390,210 L390,238"/>
&lt;path class="da" d="M470,190 L600,238"/>
&lt;!-- Return arrows (observations) -->
&lt;path class="dq" d="M140,240 Q200,220 300,195"/>
&lt;path class="dq" d="M390,240 L390,212"/>
&lt;path class="dq" d="M640,240 Q560,220 480,195"/>
&lt;!-- Final answer -->
&lt;rect x="310" y="20" width="160" height="50" rx="8" class="db"/>
&lt;text x="390" y="42" text-anchor="middle" class="dl">Case (Query)&lt;/text>
&lt;text x="390" y="60" text-anchor="middle" class="dm">"how many EU contracts &amp;gt; 100k€?"&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>A detective in a novel does not go to the same filing cabinet regardless of the case that lands on his desk. When he takes a case, he reasons first: what kind of evidence do I need? If there are witnesses, he asks for the statements (vector search over unstructured documents). If there are financial transactions, he asks the bank for the accounting records (SQL over the structured database). If the suspect has recent activity that the company cannot possibly have indexed, he goes to the press archive (web search). He does not consult all three sources at once on every case: he picks the one the evidence requires, receives the result, reasons again if he needs more, and only writes the report when he has enough evidence.&lt;/p>
&lt;p>A bad detective always goes to the same filing cabinet. A naive RAG is that bad detective: it vectorises the query, goes to the vector store, and returns whatever it finds even if the question was &amp;ldquo;how many contracts?&amp;rdquo;, something no PDF chunk can answer better than a &lt;code>COUNT(*)&lt;/code> in SQL.&lt;/p>
&lt;p>&lt;strong>Function calling is giving the LLM the ability to reason about which source to ask for, and to invoke it in a structured way.&lt;/strong> The analogy has three edges worth keeping in mind:&lt;/p>
&lt;ol>
&lt;li>The detective does not improvise the file he asks for: there is a catalogue of available sources with a description of what each one contains. The tool description in the system prompt does that job.&lt;/li>
&lt;li>The detective can ask for several pieces of evidence at once if they are independent (parallel tool calling).&lt;/li>
&lt;li>The detective knows when to stop: if after N rounds he reaches no conclusion, he declares that he does not have enough evidence. The agent has an iteration limit for the same reason.&lt;/li>
&lt;/ol>
&lt;h2 id="what-function-calling-is-the-anatomy-of-a-tool-call">What function calling is: the anatomy of a tool call&lt;/h2>
&lt;p>Function calling, also called tool use, is a mechanism by which the LLM, instead of generating free text as its answer, generates a structured JSON object representing a tool invocation. The system intercepts that JSON, executes the real tool, and returns the result as a message with the &lt;code>tool&lt;/code> role in the conversation.&lt;/p>
&lt;h3 id="defining-tools-in-the-system-prompt">Defining tools in the system prompt&lt;/h3>
&lt;p>Each tool is defined through a JSON Schema that specifies name, description and parameters. This JSON Schema is exactly the same mechanism described in &lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">structured output&lt;/a>, applied here to the tool interface:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vector_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Search internal company documents about policies, contracts and procedures. Use when the query requires unstructured text, document context or semantic similarity.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Natural language search query&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;top_k&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;integer&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Query the SQL database for structured metrics, counts, aggregations and financial data. Use when the query requires exact numbers, filters, sums or joins over structured records.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Parameterized SQL query with $1, $2 placeholders&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;array&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;items&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{},&lt;/span> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Parameter values for the placeholders&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;web_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;description&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Search public web for real-time information, recent news or current prices. Use only when data is public and not covered by internal sources.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parameters&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="the-life-cycle-of-a-tool-call">The life cycle of a tool call&lt;/h3>
&lt;p>When the LLM decides to invoke a tool, the message it generates in place of free text has this structure (OpenAI-compatible format, the same one vLLM supports):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool_calls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;query\&amp;#34;: \&amp;#34;SELECT COUNT(*), SUM(amount) FROM contracts WHERE amount &amp;gt; $1 AND year = $2 AND provider_region = $3\&amp;#34;, \&amp;#34;params\&amp;#34;: [100000, 2025, \&amp;#34;EU\&amp;#34;]}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The system executes the tool and returns:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;tool_call_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;count\&amp;#34;: 47, \&amp;#34;total\&amp;#34;: 8300000}&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The LLM receives that message as a continuation of the conversation and decides whether it needs more information or can generate the final answer.&lt;/p>
&lt;h3 id="support-in-oss-models">Support in OSS models&lt;/h3>
&lt;p>In 2026, native function calling support (not emulated via system prompt) is available in:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3.1 / 3.3+&lt;/strong>: native tool call format, supported in vLLM with &lt;code>--enable-auto-tool-choice --tool-call-parser llama3_json&lt;/code>&lt;/li>
&lt;li>&lt;strong>Qwen 2.5+&lt;/strong>: native support with &lt;code>--tool-call-parser hermes&lt;/code>&lt;/li>
&lt;li>&lt;strong>Mistral NeMo / Mistral 7B Instruct v0.3+&lt;/strong>: native format with &lt;code>--tool-call-parser mistral&lt;/code>&lt;/li>
&lt;li>&lt;strong>Qwen3 (2025+)&lt;/strong>: extended native support with parallel tool calling&lt;/li>
&lt;/ul>
&lt;p>Without &lt;code>--enable-auto-tool-choice&lt;/code>, vLLM can force the tool call format via guided decoding (JSON Schema), but the ability to &lt;em>decide when to invoke&lt;/em> a tool versus generating free text requires specific model training.&lt;/p>
&lt;h2 id="the-react-pattern-reasoning--acting">The ReAct pattern: Reasoning + Acting&lt;/h2>
&lt;p>ReAct (Yao et al., 2022) is the loop that turns an LLM with tools into an agent with reasoning. The central idea is that the model interleaves &lt;strong>thoughts&lt;/strong> (explicit reasoning about what to do) with &lt;strong>actions&lt;/strong> (tool invocations) and &lt;strong>observations&lt;/strong> (results received). This loop repeats until the model reckons it has enough information to answer, or until the iteration limit is reached.&lt;/p>
&lt;pre tabindex="0">&lt;code>Thought → Action → Observation → Thought → Action → Observation → ... → Response
&lt;/code>&lt;/pre>&lt;h3 id="a-full-react-trace-a-real-production-query">A full ReAct trace: a real production query&lt;/h3>
&lt;p>Query: &lt;em>&amp;ldquo;How many contracts above 100k€ did we sign in 2025 with EU providers, and which special conditions recur?&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>This query requires two different sources: the exact numbers live in SQL, and the context of special conditions lives in the contract PDFs indexed in the vector store.&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Iteration 1&lt;/strong>&lt;/p>
&lt;p>&lt;em>Thought:&lt;/em> The query asks for aggregated data (count and sum) with exact filters over structured fields (amount, year, region). This is a SQL query, not a semantic search.&lt;/p>
&lt;p>&lt;em>Action:&lt;/em> &lt;code>sql_query&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">num_contratos&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SUM&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">total_eur&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">contracts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">amount&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">year&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">provider_region&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>params: &lt;code>[100000, 2025, &amp;quot;EU&amp;quot;]&lt;/code>&lt;/p>
&lt;p>&lt;em>Observation:&lt;/em> &lt;code>{&amp;quot;num_contratos&amp;quot;: 47, &amp;quot;total_eur&amp;quot;: 8300000}&lt;/code>&lt;/p>
&lt;hr>
&lt;p>&lt;strong>Iteration 2&lt;/strong>&lt;/p>
&lt;p>&lt;em>Thought:&lt;/em> I have the numbers. Now I need the special conditions that recur in these contracts. That kind of information is in the free text of the PDFs, not in the structured table. I go to the vector store.&lt;/p>
&lt;p>&lt;em>Action:&lt;/em> &lt;code>vector_search&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;EU provider contracts 2025 amount above 100k special conditions recurring clauses&amp;#34;
&lt;/code>&lt;/pre>&lt;p>top_k: 5&lt;/p>
&lt;p>&lt;em>Observation:&lt;/em>&lt;/p>
&lt;pre tabindex="0">&lt;code>[chunk_1] Provider A contract — clause 8.3: late-delivery penalty of 2% per month on the invoiced amount...
[chunk_2] Provider C contract — clause 7.1: price revision linked to the euro area CPI capped at 4% per year...
[chunk_3] Provider B contract — clause 8.3: late-delivery penalty of 2% per month...
&lt;/code>&lt;/pre>&lt;hr>
&lt;p>&lt;em>Thought:&lt;/em> I have enough data from both sources. Clause 8.3 on late-delivery penalties appears in at least two of the retrieved contracts. I can synthesise the answer.&lt;/p>
&lt;p>&lt;em>Response:&lt;/em> &amp;ldquo;In 2025 you signed &lt;strong>47 contracts&lt;/strong> with EU providers above 100,000 €, for a cumulative total of &lt;strong>8.3 M€&lt;/strong>. The special condition that appears most often in the higher-value contracts is the late-delivery penalty clause (8.3 in the contract templates of Provider A and B): 2% per month on the invoiced amount. The price revision linked to the euro area CPI, capped at 4% per year (clause 7.1), also recurs.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;p>This trace has &lt;strong>2 iterations&lt;/strong>. A more complex query (comparing with the previous year, cross-referencing with payment status) would add further iterations. Common practice is to set a limit of &lt;strong>5–10 iterations&lt;/strong> maximum to avoid loops.&lt;/p>
&lt;h2 id="the-three-sources-and-when-to-use-each-one">The three sources and when to use each one&lt;/h2>
&lt;p>The choice of source is neither arbitrary nor left solely to the LLM: the architecture defines which tools exist and how they are described. The table below summarises the selection criteria:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Characteristic&lt;/th>
&lt;th>Vector store (Qdrant)&lt;/th>
&lt;th>SQL / structured (PostgreSQL)&lt;/th>
&lt;th>Web search&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Data type&lt;/strong>&lt;/td>
&lt;td>Free text, documents, PDFs&lt;/td>
&lt;td>Tables with a fixed schema&lt;/td>
&lt;td>Public pages, news&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Structure&lt;/strong>&lt;/td>
&lt;td>Unstructured&lt;/td>
&lt;td>Highly structured&lt;/td>
&lt;td>Semi-structured&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Temporality&lt;/strong>&lt;/td>
&lt;td>Static index (periodic refresh)&lt;/td>
&lt;td>Real time (transactional)&lt;/td>
&lt;td>Real time (crawl)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Typical latency&lt;/strong>&lt;/td>
&lt;td>5–50 ms&lt;/td>
&lt;td>10–200 ms&lt;/td>
&lt;td>200–2,000 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Privacy&lt;/strong>&lt;/td>
&lt;td>Internal data, full sovereignty&lt;/td>
&lt;td>Internal data, full sovereignty&lt;/td>
&lt;td>Public data only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Natural query&lt;/strong>&lt;/td>
&lt;td>Yes (natural language → embedding)&lt;/td>
&lt;td>No (parameterised SQL)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Exact aggregations&lt;/strong>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Yes (&lt;code>COUNT&lt;/code>, &lt;code>SUM&lt;/code>, &lt;code>GROUP BY&lt;/code>)&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>When to use&lt;/strong>&lt;/td>
&lt;td>Document context, semantics, PDFs&lt;/td>
&lt;td>Metrics, counts, exact filters, joins&lt;/td>
&lt;td>Data that does not exist internally and is public&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The most important rule of thumb: if the question contains words like &amp;ldquo;how many&amp;rdquo;, &amp;ldquo;sum&amp;rdquo;, &amp;ldquo;total&amp;rdquo;, &amp;ldquo;more than X&amp;rdquo;, &amp;ldquo;in year Y&amp;rdquo; and the data is in a structured table, the right answer is &lt;code>sql_query&lt;/code>. If the question asks for context, explanations, clauses, procedures or examples from documents, the answer is &lt;code>vector_search&lt;/code>. If it asks for the current price of something or recent news about a third party, &lt;code>web_search&lt;/code>, but only if no data sovereignty is involved.&lt;/p>
&lt;h2 id="tool-routing-how-the-llm-picks-the-right-tool">Tool routing: how the LLM picks the right tool&lt;/h2>
&lt;p>The description of each tool in the system prompt is &lt;strong>the most critical factor&lt;/strong> for routing accuracy. An LLM with good function calling capabilities can still choose badly if the descriptions are ambiguous or overlap.&lt;/p>
&lt;h3 id="descriptions-that-work-versus-those-that-do-not">Descriptions that work versus those that do not&lt;/h3>
&lt;p>&lt;strong>Weak description&lt;/strong> (leads the LLM to use the wrong tool):&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;search_docs&amp;#34; — Searches for information in the available sources.
&amp;#34;query_data&amp;#34; — Gets data from the system.
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Strong description&lt;/strong> (delimits precisely when to use each one):&lt;/p>
&lt;pre tabindex="0">&lt;code>&amp;#34;vector_search&amp;#34; — Search internal company documents about policies, contracts and procedures.
Use when the query requires unstructured text, document context or semantic
similarity. NOT for counts, sums or exact filters.
&amp;#34;sql_query&amp;#34; — Query the SQL database for structured metrics, counts, aggregations and
financial data. Use when the query requires exact numbers, filters, sums or
joins over structured records. NOT for finding document context.
&lt;/code>&lt;/pre>&lt;p>The difference lies in two elements: (1) positive use-case examples, and (2) explicit exclusions with &lt;code>NOT for&lt;/code>. Both reduce the semantic overlap between tools and improve tool selection accuracy.&lt;/p>
&lt;h3 id="parallel-tool-calling">Parallel tool calling&lt;/h3>
&lt;p>When two tools are independent of each other, that is, when the result of one does not affect the query of the other, the LLM can invoke them simultaneously in the same turn:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool_calls&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sql_query&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">}},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_02&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;function&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vector_search&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The system executes both in parallel and returns both observations before the LLM&amp;rsquo;s next turn. This cuts total latency when the queries are independent: instead of 2 sequential iterations (2 × tool_latency), you pay 1 × max(sql_latency, vector_latency). For the detective example: if he needs both the accounting records and the witness statements to answer, he can ask for them at the same time.&lt;/p>
&lt;h3 id="tool-selection-accuracy-the-eval-metric">Tool selection accuracy: the eval metric&lt;/h3>
&lt;p>&lt;strong>Tool selection accuracy&lt;/strong> is the percentage of turns in which the LLM picks the right tool given a set of evaluated queries:&lt;/p>
\[
\text{TSA} = \frac{\text{turns with correct tool chosen}}{\text{total turns with expected tool call}}
\]
&lt;p>It is measured over a synthetic eval set built with &lt;code>(query, expected_tool, expected_args)&lt;/code> triples. An example of a minimal eval set:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Query&lt;/th>
&lt;th>Expected tool&lt;/th>
&lt;th>Failure indicator&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;How many orders in March?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>sql_query&lt;/code>&lt;/td>
&lt;td>LLM uses &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What does the returns policy say?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>vector_search&lt;/code>&lt;/td>
&lt;td>LLM uses &lt;code>sql_query&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What is the price of copper today?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>web_search&lt;/code>&lt;/td>
&lt;td>LLM uses &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;Sum the Q3 contracts&amp;rdquo;&lt;/td>
&lt;td>&lt;code>sql_query&lt;/code>&lt;/td>
&lt;td>LLM uses &lt;code>vector_search&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A TSA &amp;lt; 0.85 in a production agent is a sign that the tool descriptions need revising before the model does. For more detail on how to build these evals, see &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLM evals&lt;/a>.&lt;/p>
&lt;h2 id="error-handling-in-tool-calls">Error handling in tool calls&lt;/h2>
&lt;h3 id="sql-injection-via-prompt">SQL injection via prompt&lt;/h3>
&lt;p>The most serious risk in tool-augmented retrieval is that the LLM generates malicious SQL, either because a user induced it via prompt injection, or because the model hallucinated a destructive query. This attack vector is covered in detail in &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>, but the minimum rules on the tool side are:&lt;/p>
&lt;p>&lt;strong>Rule 1: parameterised queries always, never direct interpolation.&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NEVER this:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cursor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;SELECT * FROM contracts WHERE provider = &amp;#39;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">llm_output&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#39;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Always this:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cursor&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;SELECT * FROM contracts WHERE provider = $1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">llm_output&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Rule 2: DB user with minimum permissions.&lt;/strong> The user the agent runs SQL as must have &lt;code>SELECT&lt;/code> on the necessary tables and nothing else. No &lt;code>DROP&lt;/code>, &lt;code>INSERT&lt;/code>, &lt;code>UPDATE&lt;/code> or &lt;code>DELETE&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Rule 3: table allowlist.&lt;/strong> The system validates that the query generated by the LLM only references tables on an allowlist before executing it.&lt;/p>
&lt;p>&lt;strong>Rule 4: per-query timeout.&lt;/strong> Queries that block for too long, potentially induced to DoS the database, are cancelled with a configured timeout.&lt;/p>
&lt;h3 id="rate-limits-timeouts-and-tool-errors">Rate limits, timeouts and tool errors&lt;/h3>
&lt;p>When a tool fails, the error is returned to the LLM as an observation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;tool_call_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;call_01&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;{\&amp;#34;error\&amp;#34;: \&amp;#34;timeout after 5s\&amp;#34;, \&amp;#34;tool\&amp;#34;: \&amp;#34;web_search\&amp;#34;}&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The system prompt must instruct the LLM on what to do in this case:&lt;/p>
&lt;pre tabindex="0">&lt;code>If a tool returns an error or is unavailable, acknowledge the limitation in your response.
Do not retry more than once. If web_search is unavailable, state that real-time data
is not accessible at this moment and answer with available internal sources only.
&lt;/code>&lt;/pre>&lt;p>This stops the agent from entering retry loops and handles graceful degradation: if &lt;code>web_search&lt;/code> is unavailable, it answers with what it has in the internal sources.&lt;/p>
&lt;h2 id="diagram-of-the-react-loop-with-the-three-sources">Diagram of the ReAct loop with the three sources&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 480" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ReAct loop with three data sources">
&lt;style>
.rb{fill:#f8f8f8;stroke:#444;stroke-width:1.4}
.rh{fill:#7aafff;stroke:#444;stroke-width:1.4}
.ry{fill:#ffd76b;stroke:#444;stroke-width:1.4}
.rg{fill:#b2e8b2;stroke:#444;stroke-width:1.4}
.rr{fill:#ffb3b3;stroke:#444;stroke-width:1.4}
.rp{fill:#e0c8ff;stroke:#444;stroke-width:1.4}
.rl{font:600 13px sans-serif;fill:#222}
.rs{font:400 11px sans-serif;fill:#555}
.ri{font:italic 11px sans-serif;fill:#555}
.ra{stroke:#555;stroke-width:1.5;fill:none;marker-end:url(#mra)}
.rloop{stroke:#999;stroke-width:1.2;fill:none;stroke-dasharray:5 3;marker-end:url(#mra)}
&lt;/style>
&lt;defs>&lt;marker id="mra" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#555"/>&lt;/marker>&lt;/defs>
&lt;!-- Query input -->
&lt;rect x="310" y="10" width="200" height="50" rx="8" class="rb"/>
&lt;text x="410" y="32" text-anchor="middle" class="rl">User query&lt;/text>
&lt;text x="410" y="50" text-anchor="middle" class="rs">message with user role&lt;/text>
&lt;!-- LLM: Thought -->
&lt;rect x="290" y="90" width="240" height="60" rx="8" class="rh"/>
&lt;text x="410" y="114" text-anchor="middle" class="rl">LLM — Thought&lt;/text>
&lt;text x="410" y="132" text-anchor="middle" class="rs">reasons what it needs&lt;/text>
&lt;text x="410" y="146" text-anchor="middle" class="rs">and which tool to invoke&lt;/text>
&lt;!-- Action -->
&lt;rect x="290" y="185" width="240" height="50" rx="8" class="ry"/>
&lt;text x="410" y="207" text-anchor="middle" class="rl">Action&lt;/text>
&lt;text x="410" y="223" text-anchor="middle" class="rs">generates tool_call JSON&lt;/text>
&lt;!-- Tool router -->
&lt;rect x="290" y="265" width="240" height="50" rx="8" class="rp"/>
&lt;text x="410" y="287" text-anchor="middle" class="rl">Tool router (system)&lt;/text>
&lt;text x="410" y="303" text-anchor="middle" class="rs">dispatches the call&lt;/text>
&lt;!-- Three sources -->
&lt;rect x="30" y="355" width="160" height="70" rx="8" class="ry"/>
&lt;text x="110" y="378" text-anchor="middle" class="rl">vector_search&lt;/text>
&lt;text x="110" y="396" text-anchor="middle" class="rs">Qdrant&lt;/text>
&lt;text x="110" y="412" text-anchor="middle" class="rs">5–50 ms&lt;/text>
&lt;rect x="330" y="355" width="160" height="70" rx="8" class="rg"/>
&lt;text x="410" y="378" text-anchor="middle" class="rl">sql_query&lt;/text>
&lt;text x="410" y="396" text-anchor="middle" class="rs">PostgreSQL&lt;/text>
&lt;text x="410" y="412" text-anchor="middle" class="rs">10–200 ms&lt;/text>
&lt;rect x="630" y="355" width="160" height="70" rx="8" class="rr"/>
&lt;text x="710" y="378" text-anchor="middle" class="rl">web_search&lt;/text>
&lt;text x="710" y="396" text-anchor="middle" class="rs">external API&lt;/text>
&lt;text x="710" y="412" text-anchor="middle" class="rs">200–2,000 ms&lt;/text>
&lt;!-- Observation -->
&lt;rect x="290" y="355" width="0" height="0" rx="8"/>
&lt;!-- Main arrows -->
&lt;path class="ra" d="M410,60 L410,88"/>
&lt;path class="ra" d="M410,150 L410,183"/>
&lt;path class="ra" d="M410,235 L410,263"/>
&lt;path class="ra" d="M370,315 L190,353"/>
&lt;path class="ra" d="M410,315 L410,353"/>
&lt;path class="ra" d="M450,315 L630,353"/>
&lt;!-- Observation arrows back to the LLM (dashed lines) -->
&lt;path class="rloop" d="M110,355 Q60,260 280,145"/>
&lt;path class="rloop" d="M410,355 Q510,310 530,150"/>
&lt;path class="rloop" d="M710,355 Q780,260 540,145"/>
&lt;!-- Observation labels -->
&lt;text x="60" y="255" class="ri">Observation&lt;/text>
&lt;text x="548" y="255" class="ri">Observation&lt;/text>
&lt;text x="726" y="255" class="ri">Observation&lt;/text>
&lt;!-- Final answer -->
&lt;rect x="600" y="90" width="190" height="60" rx="8" class="rg"/>
&lt;text x="695" y="114" text-anchor="middle" class="rl">Response&lt;/text>
&lt;text x="695" y="132" text-anchor="middle" class="rs">when the LLM has&lt;/text>
&lt;text x="695" y="148" text-anchor="middle" class="rs">enough evidence&lt;/text>
&lt;!-- Arrow to the answer -->
&lt;path class="ra" d="M530,120 L598,120"/>
&lt;!-- Iteration limit -->
&lt;rect x="0" y="185" width="240" height="50" rx="8" class="rb"/>
&lt;text x="120" y="207" text-anchor="middle" class="rl">Iteration limit&lt;/text>
&lt;text x="120" y="223" text-anchor="middle" class="rs">max. 5–10 turns&lt;/text>
&lt;path class="rloop" d="M290,210 L242,210"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="latency-maths-of-the-react-pipeline">Latency maths of the ReAct pipeline&lt;/h2>
&lt;p>Each iteration of the ReAct loop has three latency components:&lt;/p>
\[
T_{\text{iter}} = \text{TTFT}_{\text{LLM}} + T_{\text{tool}} + \Delta_{\text{context}}
\]
&lt;p>where:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT_LLM&lt;/strong>: time to the LLM&amp;rsquo;s first token (dominated by the prefill of the accumulated context)&lt;/li>
&lt;li>&lt;strong>T_tool&lt;/strong>: tool execution time&lt;/li>
&lt;li>&lt;strong>Δ_context&lt;/strong>: overhead of the growing context window (each iteration adds the previous output to the context)&lt;/li>
&lt;/ul>
&lt;h3 id="reference-values-llama-31-70b-on-4h100-sxm-320-gb-nvlink">Reference values: Llama-3.1-70B on 4×H100 SXM (320 GB, NVLink)&lt;/h3>
&lt;p>With Llama-3.1-70B in FP8 on a node with 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s), the typical values in production are:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT with context &amp;lt; 4k tokens&lt;/td>
&lt;td>≈ 150 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT with context 8k tokens&lt;/td>
&lt;td>≈ 220 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sql_query&lt;/code> (simple query, indexed)&lt;/td>
&lt;td>≈ 50 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vector_search&lt;/code> (top-5, Qdrant in RAM)&lt;/td>
&lt;td>≈ 20 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>web_search&lt;/code> (external API)&lt;/td>
&lt;td>≈ 600 ms&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="total-latency-by-number-of-iterations">Total latency by number of iterations&lt;/h3>
&lt;p>&lt;strong>1-iteration pipeline&lt;/strong> (simple query, a single tool):&lt;/p>
\[
T_1 = 150 + 50 = 200 \text{ ms} + \text{final synthesis} \approx 200 + 300 = 500 \text{ ms}
\]
&lt;p>&lt;strong>2-iteration pipeline&lt;/strong> (SQL + vector_search in sequence):&lt;/p>
\[
T_2 = (150 + 50) + (180 + 20) + 400 = 800 \text{ ms}
\]
&lt;p>The context in the second iteration already includes the result of the first, so TTFT rises slightly to ≈ 180 ms.&lt;/p>
&lt;p>&lt;strong>3-iteration pipeline&lt;/strong> (the most common case in complex queries):&lt;/p>
\[
T_3 = (150 + 50) + (180 + 20) + (200 + 50) + 450 \approx 1,100 \text{ ms}
\]
&lt;p>&lt;strong>Parallel tool calling&lt;/strong> (SQL + vector_search in parallel, a single iteration):&lt;/p>
\[
T_{\text{parallel}} = 150 + \max(50, 20) + 400 = 600 \text{ ms}
\]
&lt;p>When the two queries are independent, parallel tool calling trims latency from ≈ 800 ms to ≈ 600 ms: a 25% improvement over the 2 sequential iterations case.&lt;/p>
&lt;h3 id="comparison-with-a-naive-rag">Comparison with a naive RAG&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Configuration&lt;/th>
&lt;th>Latency&lt;/th>
&lt;th>Queries it can answer&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Naive RAG (1 retriever, 1 step)&lt;/td>
&lt;td>≈ 300 ms&lt;/td>
&lt;td>Document context queries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 1 iteration (SQL)&lt;/td>
&lt;td>≈ 500 ms&lt;/td>
&lt;td>Structured aggregation queries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 2 iterations (SQL + vector)&lt;/td>
&lt;td>≈ 800 ms&lt;/td>
&lt;td>Hybrid numeric + context queries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct 3 iterations&lt;/td>
&lt;td>≈ 1,100 ms&lt;/td>
&lt;td>Complex multi-source queries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ReAct with web_search&lt;/td>
&lt;td>≈ 1,500 ms&lt;/td>
&lt;td>Queries requiring real-time data&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The practical conclusion: multi-hop ReAct is &lt;strong>3–5× slower than a single-step naive RAG&lt;/strong>. The gain is not in speed but in &lt;strong>query coverage&lt;/strong>: a naive RAG cannot answer &amp;ldquo;how many contracts?&amp;rdquo; because that answer is in no text chunk at all. For applications with a strict latency SLO (&amp;lt; 500 ms), you have to work out whether the use case really needs ReAct or whether a well-configured RAG with hybrid retrieval covers 90% of the queries.&lt;/p>
&lt;h2 id="on-premise-hardware-for-react-agents">On-premise hardware for ReAct agents&lt;/h2>
&lt;p>A ReAct agent with Llama-3.1-70B in production has different requirements from a naive RAG, because the context grows with each iteration and prefill throughput is more critical.&lt;/p>
&lt;p>&lt;strong>Recommended configuration: 4×H100 SXM (320 GB HBM3, NVLink 900 GB/s)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Llama-3.1-70B in FP8: fits in 2×H100 (70B params × 1 byte/param ≈ 70 GB + KV cache). With 4×H100 you can serve it with tensor parallelism TP=4, cutting prefill TTFT by ≈ 2×.&lt;/li>
&lt;li>Qdrant instance: it can sit on the same node (if the collection fits in RAM) or on a dedicated node. For collections &amp;lt; 50M vectors of 768 dims: ≈ 150 GB, which fits in the RAM of a dual-socket server.&lt;/li>
&lt;li>PostgreSQL: separate node or managed instance. The agent adds no unusual load to SQL; the queries are simple and bounded by timeout.&lt;/li>
&lt;li>vLLM with &lt;code>--enable-auto-tool-choice --tool-call-parser llama3_json --max-model-len 16384&lt;/code>: a 16k token context comfortably covers the 5–10 turns of a ReAct pipeline.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Minimum viable configuration: 2×H100 SXM (160 GB)&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Llama-3.1-70B in FP8 with TP=2. TTFT ≈ 250–300 ms for 4k token contexts (a 60–100% increase over TP=4).&lt;/li>
&lt;li>Good enough for internal workloads with &amp;lt; 20 concurrent requests.&lt;/li>
&lt;li>Not advisable for an SLO &amp;lt; 1 s with more than 5 concurrent users and a long context.&lt;/li>
&lt;/ul>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;p>&lt;strong>Agentic retrieval loops with planning.&lt;/strong> ReAct is the simplest agent pattern. When a query requires decomposition into sub-tasks with dependencies, you need orchestration frameworks such as LangGraph (state graphs), smolagents (Hugging Face, agents with Python code as actions) or llama-index Agents (planning + retrieval pipeline). These introduce a planning step before the execution loop.&lt;/p>
&lt;p>&lt;strong>MCP (Model Context Protocol).&lt;/strong> Anthropic&amp;rsquo;s emerging standard, with OSS implementations, for defining tools portably across frameworks and hosts. Instead of defining each tool&amp;rsquo;s JSON Schema separately in every application, MCP centralises those definitions in an MCP server that any compatible client can discover and invoke. Adoption in 2025–2026 is fast among OSS frameworks (LangChain, smolagents, OpenWebUI).&lt;/p>
&lt;p>&lt;strong>Tool caching.&lt;/strong> If the same tool call (same arguments, same tool) is going to be invoked several times within the same context or in very similar contexts, the result can be cached. The mechanism is analogous to the semantic cache described for RAG: before executing the tool, the hash of the arguments (or its embedding, for semantic matching) is compared against a cache with a TTL. Especially valuable for &lt;code>sql_query&lt;/code> with frequent queries and data that changes little.&lt;/p>
&lt;p>&lt;strong>Multi-agent.&lt;/strong> When an orchestrating agent delegates sub-tasks to specialised agents, one for SQL, one for document retrieval, one for code generation, you enter multi-agent system territory. Each sub-agent can have its own tool set and its own LLM (possibly smaller and more specialised). Coordination between agents introduces additional tracing and observability complexity.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/reranker-hybrid-retrieval-five-chunks-llm-reads/">RAG with reranker and hybrid retrieval&lt;/a> — the retriever invoked when the LLM chooses &lt;code>vector_search&lt;/code> is exactly the pipeline described there: dense + sparse + cross reranker&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">Structured output: fundamentals&lt;/a> — the JSON Schema that defines the contract of each tool call is exactly structured output applied to the tool interface&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">Inference router and L7 LLM gateway&lt;/a> — the L7 gateway that receives the ReAct agent&amp;rsquo;s requests and routes them to the right LLM; it also applies rate limiting per user and tenant&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard: fundamentals&lt;/a> — SQL injection via prompt is a real attack vector in tool-augmented retrieval; LLM Guard covers prompt injection detection before the request reaches the LLM&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">Six-stage LLMOps pipeline&lt;/a> — tool-augmented retrieval lives at the intersection of the pipeline&amp;rsquo;s Deploy and Observe stages: it is deployed as part of the inference system and observed via tracing of each agent turn&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">LLM evals: the layer after tracing&lt;/a> — tool selection accuracy is the critical eval metric for a ReAct agent; the golden eval dataset must include (query, expected tool, expected args) triples&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Yao, S. et al. (2022). &lt;em>ReAct: Synergizing Reasoning and Acting in Language Models&lt;/em>. arXiv:2210.03629. &lt;a href="https://arxiv.org/abs/2210.03629">https://arxiv.org/abs/2210.03629&lt;/a>&lt;/li>
&lt;li>vLLM documentation. &lt;em>Tool calling&lt;/em>. &lt;a href="https://docs.vllm.ai/en/stable/features/tool_calling.html">https://docs.vllm.ai/en/stable/features/tool_calling.html&lt;/a>&lt;/li>
&lt;li>Qdrant documentation. &lt;em>Search&lt;/em>. &lt;a href="https://qdrant.tech/documentation/concepts/search/">https://qdrant.tech/documentation/concepts/search/&lt;/a>&lt;/li>
&lt;li>OpenAI. &lt;em>Function calling&lt;/em>. &lt;a href="https://platform.openai.com/docs/guides/function-calling">https://platform.openai.com/docs/guides/function-calling&lt;/a>&lt;/li>
&lt;li>Meta AI. &lt;em>Llama 3.1 Model Card&lt;/em>. &lt;a href="https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md">https://github.com/meta-llama/llama-models/blob/main/models/llama3_1/MODEL_CARD.md&lt;/a>&lt;/li>
&lt;li>Qwen Team (Alibaba). &lt;em>Qwen2.5 Technical Report&lt;/em>. arXiv:2412.15115. &lt;a href="https://arxiv.org/abs/2412.15115">https://arxiv.org/abs/2412.15115&lt;/a>&lt;/li>
&lt;li>Anthropic. &lt;em>Model Context Protocol&lt;/em>. &lt;a href="https://modelcontextprotocol.io">https://modelcontextprotocol.io&lt;/a>&lt;/li>
&lt;li>OWASP. &lt;em>LLM Top 10 for Large Language Model Applications&lt;/em>. LLM01: Prompt Injection. &lt;a href="https://owasp.org/www-project-top-10-for-large-language-model-applications/">https://owasp.org/www-project-top-10-for-large-language-model-applications/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Structured output: the form with dropdowns that strikes out invalid answers before the model picks one — Outlines, XGrammar, LLGuidance and the maths of the bitmask</title><link>https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/</link><pubDate>Sat, 30 May 2026 16:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/</guid><description>&lt;blockquote>
&lt;p>This post complements those on &lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> (where the scheduler lives) and &lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding&lt;/a> (another technique operating on the last mile of the sampler). Structured output is the LLM&amp;rsquo;s output contract towards the code that consumes it; without it, the integration between an LLM and applications is fragile by default.&lt;/p>
&lt;/blockquote>
&lt;h2 id="you-are-here-deploy">You are here: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#som)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#som)}&lt;/style>
&lt;defs>&lt;marker id="som" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · constraint on the logits, the sampler's last layer&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An LLM produces free text, but many applications (function calling, entity extraction, routing, text-to-SQL, config generation) need to parse it as JSON, as a tool call with typed args, as a valid SQL statement, or as one option from an enum. The naive solutions fail: prompt engineering (&amp;ldquo;answer in JSON&amp;rdquo;) leaves &lt;strong>25 %&lt;/strong> of outputs unparseable in many models; post-hoc validation plus retry costs latency and does not guarantee termination; json-repair libraries are heuristic patches. &lt;strong>Constrained decoding&lt;/strong> guarantees conformance 100 %, by construction: at every generation step, before sampling from the softmax over the &lt;code>V&lt;/code> vocabulary tokens, the tokens that would break the target structure are masked to -∞. The output is valid by mathematical contract, not by luck. The four dominant families in May 2026 are &lt;strong>Outlines&lt;/strong> (Willard &amp;amp; Louf, 2023; FSM + token trie precomputed from regex/JSON Schema/CFG), &lt;strong>XGrammar&lt;/strong> (Dong et al., 2024-25, CMU+MLC; byte-level pushdown automaton with an adaptive cache of &lt;em>context-independent&lt;/em> tokens, the default in vLLM v1, SGLang, TensorRT-LLM, NIM and MLC-LLM), &lt;strong>LLGuidance&lt;/strong> (Microsoft Research; Earley parser + regex derivatives, ~50 µs of CPU per token, underneath OpenAI Structured Outputs) and &lt;strong>LM Format Enforcer&lt;/strong> (noamgat; aimed at JSON Schema, integrated in many engines as a fallback). &lt;strong>XGrammar-2&lt;/strong> (May 2026) introduces Structural Tag and a cross-grammar cache for dynamic agentic tool calling. The real cost when well integrated: &lt;strong>&amp;lt;5 %&lt;/strong> in TPOT and ~40 µs of CPU per token of mask computation, partially overlappable with the model&amp;rsquo;s forward pass. The open operational question: &lt;em>does it degrade reasoning?&lt;/em> The paper &lt;em>Let me Speak Freely?&lt;/em> (Tam et al., EMNLP 2024) reported significant degradation in reasoning under format constraints; the dottxt rebuttal showed that the effect came from prompts that were not equivalent between experiments. The emerging consensus in May 2026: use &lt;strong>two-pass&lt;/strong> (free reasoning with CoT in text plus structured output in a second call) for tasks that require multi-step reasoning; use single-pass constrained for extraction, classification and function calling, where forced structure improves exactness and reduces hallucinations. This post takes apart the mechanism, the maths (bitmask size = V/8 bytes, per-step latency), the comparison table of backends, the pitfalls (compile time, tokenizer-specific FSM, streaming SSE) and the production deployment pattern.&lt;/p>
&lt;h2 id="the-analogy-the-form-with-dropdowns-instead-of-free-text-fields">The analogy: the form with dropdowns instead of free-text fields&lt;/h2>
&lt;p>Imagine two ways of asking someone to fill in a form.&lt;/p>
&lt;p>The &lt;strong>first way&lt;/strong> is to hand them the paper with blank fields and say &amp;ldquo;fill it in with this exact format: the name goes here, then the ID number without spaces, then the date as YYYY-MM-DD, then mark one of these five possible reasons separated by semicolons&amp;rdquo;. You explain the format in exhaustive detail, but the person is still free to write whatever they like in each field. If they are in a hurry they may drop a zero from the ID number, put the date in the American format, tick two reasons when only one was asked for. When you receive the form, you often have to send it back: field X badly formatted, reason Y invalid, date Z impossible. This is exactly what an LLM does when you ask it to &amp;ldquo;answer in JSON with this schema&amp;rdquo;: it works most of the time, it fails a non-negligible percentage, and you have no formal guarantee of anything.&lt;/p>
&lt;p>The &lt;strong>second way&lt;/strong> is to give them an electronic form where the fields &lt;strong>are not free text&lt;/strong>. The name accepts any text, but the ID number has a mask that only lets you type eight digits followed by a letter; the date is a picker that only lets you choose valid dates; the reasons are a dropdown with five closed options. The person can type whatever they want, but the form does not accept invalid characters at any point. The result is parseable by construction: when you receive the completed form, the ID number has exactly the expected format, the date is a real date, the reason is one of the five. This is &lt;strong>constrained decoding&lt;/strong>.&lt;/p>
&lt;p>The analogy holds on four mappings:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The person filling it in&lt;/strong> = the LLM producing logits over the vocabulary.&lt;/li>
&lt;li>&lt;strong>The characters the form lets you type in each field&lt;/strong> = the bitmask applied to the logits before sampling.&lt;/li>
&lt;li>&lt;strong>How the form knows which characters to allow depending on which field you are in&lt;/strong> = the automaton (FSM or PDA) that holds the current state of the grammar.&lt;/li>
&lt;li>&lt;strong>The fact that the dropdown is precomputed when the page loads rather than recomputed each time&lt;/strong> = the precomputed table of valid tokens per FSM state, which makes the per-step cost amortised O(1).&lt;/li>
&lt;/ul>
&lt;h2 id="the-problem-structured-output-solves">The problem structured output solves&lt;/h2>
&lt;p>The operational problem is the contract between the LLM and the code that consumes its output. There are three naive approaches, all with documented failures:&lt;/p>
&lt;p>&lt;strong>Pure prompt engineering.&lt;/strong> &amp;ldquo;Answer only with valid JSON, no comments or prose&amp;rdquo;. It works most of the time for good models; it fails between 5 % and 25 % of the time depending on the model, the temperature, the complexity of the schema and the length of the output. The model adds a trailing comma, escapes a quote badly, wraps the JSON in a &lt;code>```json&lt;/code> markdown block, hallucinates a field that was not in the schema, ignores a required field. SqueezeBits measures a ≤72 % correct rate without constraining for some models on JSON Schemas of moderate complexity.&lt;/p>
&lt;p>&lt;strong>Post-hoc validation plus retry.&lt;/strong> The server receives the output, tries to parse it, and if that fails returns the error to the model and asks it to try again. Cost: 2-3× latency in the worst case (typically 2-3 retries before giving up), no guarantee of termination, noise in the logs, hard to test deterministically.&lt;/p>
&lt;p>&lt;strong>JSON repair libraries&lt;/strong> (&lt;code>json_repair&lt;/code>, the &lt;code>fast-json-repair&lt;/code> Rust port). Heuristic patches for common errors: trailing commas, missing quotes, prose interleaved with JSON. Useful as a &lt;strong>fallback&lt;/strong> for approach 2; they are not a contract.&lt;/p>
&lt;p>The accumulated operational cost: latency inflated by retries, noise in production, painful debugging of intermittent parse errors, broken contracts with downstream clients that assumed clean parsing.&lt;/p>
&lt;h2 id="constrained-decoding-the-principle">Constrained decoding: the principle&lt;/h2>
&lt;p>At each decode step, the model produces a vector &lt;code>logits ∈ R^V&lt;/code> where &lt;code>V&lt;/code> is the vocabulary size (Llama 3: 128,256, GPT-4o: ~200,000, Qwen 3: 152,064). The conventional sampler applies softmax plus a sampling strategy (greedy, top-k, top-p, temperature) over the &lt;code>V&lt;/code> tokens.&lt;/p>
&lt;p>Constrained decoding interposes one operation before the softmax: it applies a bitmask that sets to -∞ the logits of the tokens that would violate the target grammar at this step. The result: the softmax only assigns probability mass to admissible tokens; sampling, whatever its strategy, can only pick a valid one.&lt;/p>
&lt;p>The two operational questions are always the same:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>How do you know which tokens are valid at each step?&lt;/strong> → hold the current state of an automaton (FSM/PDA) built from the grammar; look up the table &lt;code>(state → set of valid tokens)&lt;/code>.&lt;/li>
&lt;li>&lt;strong>How much does it cost to compute and apply the mask?&lt;/strong> → this is where Outlines, XGrammar, LLGuidance and LM Format Enforcer compete.&lt;/li>
&lt;/ol>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Constrained decoding flow">
&lt;style>
.box{fill:#fff;stroke:#444;stroke-width:1.4;rx:6}
.logits{fill:#fff4d6;stroke:#a48000;stroke-width:1.4;rx:6}
.mask{fill:#f6caca;stroke:#a52a2a;stroke-width:1.4;rx:6}
.fsm{fill:#e6d0ff;stroke:#5a2db0;stroke-width:1.4;rx:6}
.smpl{fill:#cdebd0;stroke:#2a7a40;stroke-width:1.4;rx:6}
.tok{fill:#d4ecff;stroke:#1f5fa8;stroke-width:1.4;rx:6}
.lbl{font:600 12px sans-serif;fill:#222}
.sub{font:400 10px sans-serif;fill:#555}
.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#som1)}
.dotted{stroke:#999;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#som1)}
&lt;/style>
&lt;defs>&lt;marker id="som1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>&lt;/marker>&lt;/defs>
&lt;text x="20" y="22" class="lbl">1. The model produces logits over the V vocabulary tokens&lt;/text>
&lt;rect x="20" y="30" width="200" height="50" class="logits"/>
&lt;text x="120" y="50" text-anchor="middle" class="lbl">logits ∈ R^V&lt;/text>
&lt;text x="120" y="68" text-anchor="middle" class="sub">[2.1, 5.3, -1.2, 8.7, 0.4, ..., 3.1]&lt;/text>
&lt;text x="260" y="22" class="lbl">2. The automaton knows what state we are in (in this JSON)&lt;/text>
&lt;rect x="260" y="30" width="240" height="50" class="fsm"/>
&lt;text x="380" y="46" text-anchor="middle" class="lbl">FSM / PDA state&lt;/text>
&lt;text x="380" y="62" text-anchor="middle" class="sub">waiting: opening quote of a string&lt;/text>
&lt;text x="380" y="76" text-anchor="middle" class="sub">(after the "name": field)&lt;/text>
&lt;path class="arr" d="M500,55 L540,55"/>
&lt;text x="540" y="22" class="lbl">3. Precomputed table&lt;/text>
&lt;rect x="540" y="30" width="220" height="50" class="box"/>
&lt;text x="650" y="46" text-anchor="middle" class="lbl">cache: state → valid tokens&lt;/text>
&lt;text x="650" y="62" text-anchor="middle" class="sub">{ '"', ' ', '\t', '\n' }&lt;/text>
&lt;text x="650" y="76" text-anchor="middle" class="sub">rest of vocab → -∞&lt;/text>
&lt;path class="arr" d="M650,80 L650,120 L380,120"/>
&lt;text x="260" y="140" class="lbl">4. Resulting bitmask (16 KB for Llama 3, V=128256)&lt;/text>
&lt;rect x="260" y="148" width="240" height="40" class="mask"/>
&lt;text x="380" y="170" text-anchor="middle" class="lbl">bitmask: 0..010..010..0..0&lt;/text>
&lt;text x="380" y="184" text-anchor="middle" class="sub">1 = allowed, 0 = set to -∞&lt;/text>
&lt;path class="arr" d="M120,80 L120,200"/>
&lt;path class="arr" d="M380,188 L380,200"/>
&lt;text x="20" y="222" class="lbl">5. logits + bitmask → constrained logits → softmax → sampling&lt;/text>
&lt;rect x="20" y="230" width="500" height="50" class="smpl"/>
&lt;text x="270" y="252" text-anchor="middle" class="lbl">apply_token_bitmask_inplace(logits, bitmask)&lt;/text>
&lt;text x="270" y="268" text-anchor="middle" class="sub">softmax → top-k / top-p / greedy → sampled token&lt;/text>
&lt;path class="arr" d="M520,255 L560,255"/>
&lt;rect x="560" y="230" width="180" height="50" class="tok"/>
&lt;text x="650" y="252" text-anchor="middle" class="lbl">token = '"'&lt;/text>
&lt;text x="650" y="268" text-anchor="middle" class="sub">(valid by construction)&lt;/text>
&lt;path class="dotted" d="M650,280 L650,300 L380,300 L380,82"/>
&lt;text x="395" y="297" class="sub">6. update FSM/PDA state with the chosen token → next step&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-four-families-of-backends">The four families of backends&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Backend&lt;/th>
&lt;th>Origin&lt;/th>
&lt;th>Algorithm&lt;/th>
&lt;th>Grammar formats&lt;/th>
&lt;th>Default in&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Outlines&lt;/td>
&lt;td>dottxt (2023)&lt;/td>
&lt;td>FSM + precomputed token trie&lt;/td>
&lt;td>regex, JSON Schema, Lark CFG&lt;/td>
&lt;td>HF TGI&lt;/td>
&lt;td>The Rust version (outlines-core) closes part of the gap with XGrammar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>XGrammar&lt;/td>
&lt;td>CMU + MLC (2024-25)&lt;/td>
&lt;td>Byte-level PDA + adaptive ctx-indep/dep cache&lt;/td>
&lt;td>regex, JSON Schema, EBNF/CFG&lt;/td>
&lt;td>vLLM v1, SGLang, TensorRT-LLM, NIM, MLC-LLM&lt;/td>
&lt;td>Speedup vs naive: up to 100× CFG, 3× JSON; &amp;lt;40 µs/token JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLGuidance&lt;/td>
&lt;td>Microsoft Research&lt;/td>
&lt;td>Earley parser + regex derivatives&lt;/td>
&lt;td>regex, JSON Schema, Lark&lt;/td>
&lt;td>OpenAI Structured Outputs (internally), Chromium&lt;/td>
&lt;td>~50 µs CPU/token; underneath Guidance, llama.cpp, mistral.rs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LM Format Enforcer&lt;/td>
&lt;td>noamgat&lt;/td>
&lt;td>char-level parser + tokenizer prefix tree&lt;/td>
&lt;td>JSON Schema, regex&lt;/td>
&lt;td>(deprecated default in many engines, fallback in NIM)&lt;/td>
&lt;td>Slower than XGrammar (3.5× worse JSON, 10× worse CFG)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Three operational observations:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>XGrammar is the de facto default in May 2026&lt;/strong> across the open-source ecosystem (vLLM v1, SGLang, TensorRT-LLM, NIM, MLC-LLM). Its byte-level PDA with an adaptive cache gives it almost zero overhead when well integrated.&lt;/li>
&lt;li>&lt;strong>LLGuidance is the least known but most used piece on the market&lt;/strong>, because it sits underneath OpenAI Structured Outputs (confirmed in the README of the repo itself). 50 µs of CPU per token for 128k tokenizers.&lt;/li>
&lt;li>&lt;strong>Outlines was the first&lt;/strong>, it still holds the conceptual mindshare (papers, canonical blog posts), but it lost operational ground to XGrammar. The Rust version (outlines-core) closes part of the gap.&lt;/li>
&lt;/ol>
&lt;h2 id="xgrammar-in-detail-what-sits-under-vllm-and-sglang">XGrammar in detail (what sits under vLLM and SGLang)&lt;/h2>
&lt;p>The paper by Dong, Yin, Ruan and Chen (arXiv:2411.15100, November 2024) introduces a partitioning technique that is key to understanding why XGrammar is 3-100× faster than the alternatives.&lt;/p>
&lt;p>&lt;strong>Partitioning the vocabulary at each PDA state&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;em>Context-independent&lt;/em> tokens (~99 % of the vocab in a typical JSON Schema): their validity is decided &lt;strong>only&lt;/strong> by the current PDA position, without needing to inspect the full stack. These are precomputable and stored as bitmasks in a cache.&lt;/li>
&lt;li>&lt;em>Context-dependent&lt;/em> tokens (~1 %): they require inspecting the PDA stack at runtime. They are handled case by case at a higher cost.&lt;/li>
&lt;/ul>
&lt;p>That partition is the fundamental reason why XGrammar works in production at low TPOT. 99 % of the lookups go to a precomputed table in O(1); only the residual 1 % pays the real cost.&lt;/p>
&lt;p>&lt;strong>Other combined techniques&lt;/strong>: a pushdown automaton for full CFGs (not just regex), a persistent stack for fast branching/rollback, JIT compilation plus an Earley parser in XGrammar-2.&lt;/p>
&lt;p>&lt;strong>Reported speedups (paper)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Up to &lt;strong>100×&lt;/strong> over previous solutions on CFG.&lt;/li>
&lt;li>&lt;strong>3×&lt;/strong> on JSON Schema versus Outlines.&lt;/li>
&lt;li>Latency &amp;lt;40 µs per token on JSON Schema; &amp;lt;200 µs on XML/Python DSL.&lt;/li>
&lt;li>When well integrated in vLLM/SGLang/TRT-LLM: near-zero end-to-end overhead.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>XGrammar-2&lt;/strong> (May 2026, arXiv:2601.04426) adds two pieces that matter for agents:&lt;/p>
&lt;ul>
&lt;li>Structural Tag: a composable JSON protocol that unifies OpenAI harmony, tool calling and reasoning channels.&lt;/li>
&lt;li>Cross-Grammar Cache for reuse at sub-structure level → it allows dynamic switching between sub-grammars in agentic loops without recompiling.&lt;/li>
&lt;li>6× faster compile time than XGrammar-1.&lt;/li>
&lt;/ul>
&lt;h2 id="the-maths-of-the-bitmask">The maths of the bitmask&lt;/h2>
&lt;p>Three numbers drive the operational decision.&lt;/p>
&lt;p>&lt;strong>Bitmask size per step.&lt;/strong> If the vocabulary has &lt;code>V&lt;/code> tokens, the bit-packed bitmask takes &lt;code>V/8&lt;/code> bytes:&lt;/p>
$$\text{bitmask size} = \frac{V}{8} \text{ bytes}$$
&lt;ul>
&lt;li>Llama 3 (V=128,256): 16 KB per bitmask per request per step.&lt;/li>
&lt;li>GPT-4o tokenizer o200k (V≈200,000): 25 KB.&lt;/li>
&lt;li>Llama 2 (V=32,000): 4 KB.&lt;/li>
&lt;/ul>
&lt;p>For a batch of 32 requests with structured output active on an H100, we are talking about ~512 KB of bitmasks per step, trivial against the GBs the forward pass moves.&lt;/p>
&lt;p>&lt;strong>Cost of applying the bitmask to the logits.&lt;/strong> A simple CUDA kernel, &lt;code>O(V)&lt;/code> complexity, ~5-10 µs of latency on an H100. Negligible.&lt;/p>
&lt;p>&lt;strong>Cost of computing the bitmask (CPU-side).&lt;/strong> This is where backends differ:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Backend&lt;/th>
&lt;th>CPU latency per token (Llama 3, V=128k)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>LLGuidance&lt;/td>
&lt;td>~50 µs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>XGrammar&lt;/td>
&lt;td>~40 µs (JSON Schema), ~200 µs (XML/DSL)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>outlines-core&lt;/td>
&lt;td>comparable to XGrammar after 2024&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Outlines Python (legacy)&lt;/td>
&lt;td>200-1000 µs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LM Format Enforcer&lt;/td>
&lt;td>intermediate, degrades with a large vocab&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>With a model forward pass on the order of 10-50 ms per token in decode, a 40-50 µs mask is &lt;strong>&amp;lt;0.5 % overhead&lt;/strong>, invisible. The operational key is that the CPU-side mask computation can be overlapped with the GPU-side forward pass: while the GPU computes the logits for token &lt;em>t&lt;/em>, the CPU precomputes the mask for token &lt;em>t+1&lt;/em> based on the FSM state after token &lt;em>t-1&lt;/em>. SGLang does this explicitly; vLLM v1 improved substantially over v0.&lt;/p>
&lt;p>&lt;strong>Compile-time cost.&lt;/strong> Precomputing the FSM/PDA and the cache:&lt;/p>
&lt;ul>
&lt;li>Simple schemas (1-5 fields): &amp;lt;100 ms.&lt;/li>
&lt;li>Deep JSON Schemas with many &lt;code>$defs&lt;/code>: seconds.&lt;/li>
&lt;li>OpenAI structured outputs: &amp;ldquo;10s typical, up to 1 minute for complex schemas&amp;rdquo;, cached after the first call.&lt;/li>
&lt;li>XGrammar-2: 6× faster than XGrammar-1.&lt;/li>
&lt;li>LLGuidance: ~2 ms startup.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Operational best practice&lt;/strong>: pre-cache the known schemas when the server starts, to avoid latency spikes on the first request for each new schema.&lt;/p>
&lt;h2 id="does-it-degrade-the-models-reasoning">Does it degrade the model&amp;rsquo;s reasoning?&lt;/h2>
&lt;p>It is the most interesting question in the field and the answer is not obvious.&lt;/p>
&lt;p>&lt;strong>The theoretical argument for degradation&lt;/strong>: forcing the structure changes the model&amp;rsquo;s probability distribution; if the structured path pushes towards low-probability paths, the model gets &amp;ldquo;stuck&amp;rdquo; in a suboptimal branch with no room to explore.&lt;/p>
&lt;p>&lt;strong>The paper &lt;em>Let me Speak Freely?&lt;/em>&lt;/strong> (Tam et al., arXiv:2408.02442, EMNLP 2024 Industry Track) reported significant degradation on reasoning tasks under format constraints (JSON/XML/YAML). The stricter the format, the greater the degradation. The paper itself, however, acknowledged an improvement on classification tasks with forced structure.&lt;/p>
&lt;p>&lt;strong>The dottxt rebuttal, &lt;em>Say What You Mean&lt;/em>&lt;/strong> (official blog post): a methodological critique. The prompts in the original paper were different between structured and unstructured (not apples to apples). The JSON prompts in the original experiment gave less information than the unstructured ones. Re-running with equivalent prompts (Llama-3-8B-Instruct), dottxt does not reproduce the degradation. The conclusion: the paper confused the &lt;em>format constraint&lt;/em> with the &lt;em>prompt engineering&lt;/em> of that constraint.&lt;/p>
&lt;p>&lt;strong>Emerging consensus in May 2026&lt;/strong> (gathered from technical blogs and community empiricism):&lt;/p>
&lt;ul>
&lt;li>For extraction, classification, function calling and routing: constrained decoding improves exactness and reduces hallucinations. It is the right tool.&lt;/li>
&lt;li>For multi-step reasoning (maths, logic, code review): use &lt;strong>two-pass&lt;/strong>:
&lt;ol>
&lt;li>First call: free reasoning with Chain-of-Thought in natural text (generous &lt;code>max_tokens&lt;/code>, no constraint).&lt;/li>
&lt;li>Second call: pass it the reasoning plus the schema, and ask it to produce structured output with constrained decoding.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>The &amp;ldquo;reason then structure&amp;rdquo; pattern gives the best of both worlds: reasoning without a straitjacket plus an output guaranteed to be parseable.&lt;/p>
&lt;h2 id="real-implementations-in-may-2026">Real implementations in May 2026&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Engine&lt;/th>
&lt;th>Default backend&lt;/th>
&lt;th>Others available&lt;/th>
&lt;th>API&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>vLLM v1&lt;/td>
&lt;td>XGrammar (auto)&lt;/td>
&lt;td>Outlines, Guidance (llguidance), LM Format Enforcer&lt;/td>
&lt;td>&lt;code>guided_json&lt;/code>, &lt;code>guided_regex&lt;/code>, &lt;code>guided_choice&lt;/code>, &lt;code>guided_grammar&lt;/code> in the request&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SGLang&lt;/td>
&lt;td>XGrammar&lt;/td>
&lt;td>Outlines, LLGuidance&lt;/td>
&lt;td>&lt;code>response_format.json_schema&lt;/code>, &lt;code>extra_body.regex&lt;/code>, &lt;code>extra_body.ebnf&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TensorRT-LLM&lt;/td>
&lt;td>XGrammar&lt;/td>
&lt;td>LLGTRT (Rust llguidance)&lt;/td>
&lt;td>Official integration since Jan 2025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVIDIA NIM&lt;/td>
&lt;td>XGrammar (switched from Outlines in 2025)&lt;/td>
&lt;td>LM Format Enforcer (requires &lt;code>NIM_ENABLE_KV_CACHE_REUSE=0&lt;/code>)&lt;/td>
&lt;td>Pluggable multi-backend&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>llama.cpp&lt;/td>
&lt;td>Native GBNF&lt;/td>
&lt;td>LLGuidance&lt;/td>
&lt;td>Each candidate token is tested against the parse state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HF TGI&lt;/td>
&lt;td>Outlines&lt;/td>
&lt;td>XGrammar (experimental)&lt;/td>
&lt;td>&amp;ldquo;Guidance&amp;rdquo; feature with &lt;code>/generate&lt;/code> and &lt;code>/chat/completion&lt;/code> with &lt;code>tools&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MLC-LLM&lt;/td>
&lt;td>XGrammar (native, same team)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Its own API&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>vLLM example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpenAI&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://localhost:8000/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;x&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;meta-llama/Llama-3.1-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Extract name and age from: María is 34 years old&amp;#34;&lt;/span>&lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">extra_body&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;guided_json&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;properties&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;string&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;integer&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;required&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;age&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>SGLang equivalent:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;meta-llama/Llama-3.1-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response_format&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;json_schema&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;json_schema&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;person&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;strict&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;schema&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="usage-patterns-in-production">Usage patterns in production&lt;/h2>
&lt;p>&lt;strong>Function calling / tool use.&lt;/strong> The LLM produces &lt;code>{tool_name: enum, arguments: object}&lt;/code> according to a schema. The dominant case today (OpenAI, Anthropic, open models). The schema guarantees that &lt;code>tool_name&lt;/code> is in the set of available tools and that &lt;code>arguments&lt;/code> has the right types.&lt;/p>
&lt;p>&lt;strong>Entity extraction.&lt;/strong> A schema with &lt;code>{name, address, phone, ...}&lt;/code> from unstructured text. It moves schema adherence from 76% to 98% in vendor benchmarks.&lt;/p>
&lt;p>&lt;strong>Routing / classification.&lt;/strong> The LLM picks between N options (&lt;code>enum&lt;/code> in the schema). The bitmask shrinks to a handful of valid tokens → almost zero overhead, maximum reliability.&lt;/p>
&lt;p>&lt;strong>Constrained SQL.&lt;/strong> An SQL grammar as GBNF or Lark → it avoids injection and syntax errors. Useful in text-to-SQL agents.&lt;/p>
&lt;p>&lt;strong>Code generation with a valid AST.&lt;/strong> A grammar for the target language (Python, Rust, a custom DSL). It guarantees that the output is compilable or parseable code.&lt;/p>
&lt;p>&lt;strong>Agentic loops.&lt;/strong> XGrammar-2 Structural Tag for dynamic switching between formats (tool call → reasoning channel → tool result) without recompiling the grammar.&lt;/p>
&lt;h2 id="operational-pitfalls">Operational pitfalls&lt;/h2>
&lt;p>&lt;strong>Compile time for large schemas.&lt;/strong> JSON Schemas with many &lt;code>$defs&lt;/code> can take seconds to compile. This causes slow startup if they are loaded at boot, or latency spikes on the first request if they are loaded on demand. Mitigation: warm the cache at boot with the fleet&amp;rsquo;s known schemas.&lt;/p>
&lt;p>&lt;strong>Tokenizer-specific FSM/PDA.&lt;/strong> A schema precompiled for Llama 3 (128k tokenizer) does not work for Qwen (152k). The schema cache must be keyed by &lt;code>(tokenizer_hash, schema_hash)&lt;/code>. Changing model = invalidating the cache.&lt;/p>
&lt;p>&lt;strong>Schema changes.&lt;/strong> Version schemas explicitly. Breaking changes → rebuild plus warm cache. Do not silence compile errors in production.&lt;/p>
&lt;p>&lt;strong>Streaming SSE.&lt;/strong> Structured output with streaming requires parsers tolerant of partial output (&lt;code>partial-json-parser&lt;/code>, &lt;code>json-stream&lt;/code>). Strict Pydantic v1 fails; v2 with &lt;code>partial validation&lt;/code> works. Older clients may not handle incremental validation, so test with the real client.&lt;/p>
&lt;p>&lt;strong>Token healing.&lt;/strong> Tokens that cross boundaries (&lt;code>://&lt;/code> as a single token versus &lt;code>:&lt;/code> + &lt;code>//&lt;/code>) can break pure constraints. Outlines and XGrammar mitigate this internally; llama.cpp and others require explicit token healing. If the model sneaks a &amp;ldquo;strange&amp;rdquo; character in after the structure, this is probably why.&lt;/p>
&lt;p>&lt;strong>Real known bugs in vLLM 2025-26&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>vLLM 0.8.4: XGrammar rejects &lt;code>minItems&lt;/code> in JSON Schema (issue #16880).&lt;/li>
&lt;li>vLLM with Qwen 2.5 VL: Outlines/XGrammar does not respect the schema in some cases (issue #13038).&lt;/li>
&lt;li>The vLLM &lt;code>apply_token_bitmask_inplace&lt;/code> bitmask backend is always &lt;code>auto&lt;/code>, not exposed to the user.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>OpenAI subset limitations&lt;/strong>: if you are going to port a schema validated on OpenAI structured outputs to another backend, check that you are not using constructs that OpenAI rejects but others allow (deep &lt;code>$ref&lt;/code>, &lt;code>pattern&lt;/code>, &lt;code>default&lt;/code>, depth &amp;gt;5, &lt;code>anyOf&lt;/code> as root).&lt;/p>
&lt;h2 id="implications-on-on-premise-hardware">Implications on on-premise hardware&lt;/h2>
&lt;p>&lt;strong>On an RTX 4090 (24 GB).&lt;/strong> Any model you serve with vLLM or llama.cpp can carry structured output at practically negligible cost. The CPU-side mask latency (~40 µs) is trivial compared with typical TPOT (30-100 ms on consumer hardware). The interesting case: function calling over Llama 3 8B or Qwen 3 14B INT4 → reliable tool use without retries, with no need for a hosted API.&lt;/p>
&lt;p>&lt;strong>On a generic 4×H100 SXM cluster (320 GB, NVLink, native FP8).&lt;/strong> Here XGrammar as the vLLM v1 / SGLang default is the way:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3 70B FP8 + XGrammar JSON Schema&lt;/strong>: TPOT P95 stable under 60 ms even with structured output active across the whole batch. It supports hundreds of distinct cached schemas.&lt;/li>
&lt;li>&lt;strong>DeepSeek-V3 + XGrammar Structural Tag&lt;/strong>: agentic tool calling with native MTP and constrained decoding combined; the mask cost overlaps with the MoE forward pass.&lt;/li>
&lt;li>&lt;strong>Multi-tenant function calling&lt;/strong>: each client can have its own set of tools (that is, its own schemas); compile time is amortised through caching, runtime is invariant.&lt;/li>
&lt;/ul>
&lt;p>The rule of thumb for May 2026: XGrammar by default, pre-cache the fleet&amp;rsquo;s schemas at boot, two-pass for reasoning tasks.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Dynamic tool routing&lt;/strong> with XGrammar-2 Structural Tag: the detail of how TagDispatch picks sub-grammars at runtime.&lt;/li>
&lt;li>&lt;strong>Constrained beam search&lt;/strong> and its interaction with grammar: theoretical quality degradation versus greedy.&lt;/li>
&lt;li>&lt;strong>Grammars for code generation with a full AST&lt;/strong>: production-grade Python/Rust grammars, performance.&lt;/li>
&lt;li>&lt;strong>JSON Schema → Pydantic → grammar pipelines&lt;/strong>: tooling to reduce human error.&lt;/li>
&lt;li>&lt;strong>Inhibition decoding&lt;/strong> (the Inhibition Decoding paper, 2025): a variant that penalises but does not forbid certain tokens, useful for soft safety constraints.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching: the hairdresser with 8 chairs&lt;/a> — the scheduler where structured output is applied request by request; the CPU-side mask computation can overlap with the GPU-side forward pass.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding&lt;/a> — another technique operating on the sampler; speculative plus structured can be combined but requires care (the acceptance rule has to respect the bitmask).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">Multi-LoRA serving&lt;/a> — an adapter may be trained specifically for function calling, complementing the structured output guarantee with the model&amp;rsquo;s affinity for the task.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge: the exam marker&lt;/a> — the judge produces a structured verdict (&lt;code>{score, reasoning, decision}&lt;/code>) that can be secured with structured output to avoid manual parsing.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs&lt;/a> — evals with LLM-as-judge benefit enormously from guaranteed structured output.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where Deploy is stage 4.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ontologies-knowledge-graphs-llmops-linnaean-nomenclature/">Ontologies and knowledge graphs in LLMOps&lt;/a> — the JSON Schemas imposed on the sampler here usually derive from a SHACL shape of the corporate ontology; structured output is the mechanism by which the LLM populates the KG&amp;rsquo;s ABox in line with the declared TBox.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/function-calling-tool-augmented-retrieval-detective-knows-which-file/">Function calling and tool-augmented retrieval: the detective who knows which file to ask for&lt;/a> — the JSON Schema defining each tool call is structured output applied to the tool interface; the constrained decoding guarantee from this post is what makes the LLM generate calls that parse 100 % of the time.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Willard, B., Louf, R. &lt;em>Efficient Guided Generation for Large Language Models&lt;/em> (Outlines). 2023. &lt;a href="https://arxiv.org/abs/2307.09702">https://arxiv.org/abs/2307.09702&lt;/a>&lt;/li>
&lt;li>Dong, Y., Yin, X., Ruan, F., Chen, T. &lt;em>XGrammar: Flexible and Efficient Structured Generation Engine for Large Language Models&lt;/em>. 2024. &lt;a href="https://arxiv.org/abs/2411.15100">https://arxiv.org/abs/2411.15100&lt;/a>&lt;/li>
&lt;li>&lt;em>XGrammar-2: Dynamic Structured Generation for Agentic LLMs&lt;/em>. 2026. &lt;a href="https://arxiv.org/abs/2601.04426">https://arxiv.org/abs/2601.04426&lt;/a>&lt;/li>
&lt;li>Tam, Z. et al. &lt;em>Let Me Speak Freely? A Study on the Impact of Format Restrictions on Performance of Large Language Models&lt;/em>. EMNLP 2024 Industry. &lt;a href="https://arxiv.org/abs/2408.02442">https://arxiv.org/abs/2408.02442&lt;/a>&lt;/li>
&lt;li>dottxt blog, &lt;em>Say What You Mean&lt;/em>: &lt;a href="https://blog.dottxt.ai/say-what-you-mean.html">https://blog.dottxt.ai/say-what-you-mean.html&lt;/a>&lt;/li>
&lt;li>OpenAI, &lt;em>Introducing Structured Outputs in the API&lt;/em>: &lt;a href="https://openai.com/index/introducing-structured-outputs-in-the-api/">https://openai.com/index/introducing-structured-outputs-in-the-api/&lt;/a>&lt;/li>
&lt;li>Outlines repo: &lt;a href="https://github.com/dottxt-ai/outlines">https://github.com/dottxt-ai/outlines&lt;/a>&lt;/li>
&lt;li>outlines-core (Rust): &lt;a href="https://github.com/dottxt-ai/outlines-core">https://github.com/dottxt-ai/outlines-core&lt;/a>&lt;/li>
&lt;li>XGrammar repo: &lt;a href="https://github.com/mlc-ai/xgrammar">https://github.com/mlc-ai/xgrammar&lt;/a>&lt;/li>
&lt;li>LLGuidance (Microsoft Research) repo: &lt;a href="https://github.com/guidance-ai/llguidance">https://github.com/guidance-ai/llguidance&lt;/a>&lt;/li>
&lt;li>LM Format Enforcer repo: &lt;a href="https://github.com/noamgat/lm-format-enforcer">https://github.com/noamgat/lm-format-enforcer&lt;/a>&lt;/li>
&lt;li>llama.cpp grammars: &lt;a href="https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md">https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md&lt;/a>&lt;/li>
&lt;li>vLLM Structured Outputs docs: &lt;a href="https://docs.vllm.ai/en/stable/features/structured_outputs/">https://docs.vllm.ai/en/stable/features/structured_outputs/&lt;/a>&lt;/li>
&lt;li>SGLang Structured Outputs docs: &lt;a href="https://docs.sglang.io/advanced_features/structured_outputs.html">https://docs.sglang.io/advanced_features/structured_outputs.html&lt;/a>&lt;/li>
&lt;li>TensorRT-LLM guided decoding (Triton): &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/tensorrtllm_backend/docs/guided_decoding.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/tensorrtllm_backend/docs/guided_decoding.html&lt;/a>&lt;/li>
&lt;li>NIM Structured Generation: &lt;a href="https://docs.nvidia.com/nim/large-language-models/1.12.0/structured-generation.html">https://docs.nvidia.com/nim/large-language-models/1.12.0/structured-generation.html&lt;/a>&lt;/li>
&lt;li>MLC blog, &lt;em>XGrammar&lt;/em>: &lt;a href="https://blog.mlc.ai/2024/11/22/achieving-efficient-flexible-portable-structured-generation-with-xgrammar">https://blog.mlc.ai/2024/11/22/achieving-efficient-flexible-portable-structured-generation-with-xgrammar&lt;/a>&lt;/li>
&lt;li>MLC blog, &lt;em>XGrammar-2&lt;/em>: &lt;a href="https://blog.mlc.ai/2026/05/04/xgrammar-2-fast-customizable-structured-generation">https://blog.mlc.ai/2026/05/04/xgrammar-2-fast-customizable-structured-generation&lt;/a>&lt;/li>
&lt;li>SqueezeBits, &lt;em>Guided decoding performance: vLLM vs SGLang&lt;/em>: &lt;a href="https://blog.squeezebits.com/guided-decoding-performance-vllm-sglang">https://blog.squeezebits.com/guided-decoding-performance-vllm-sglang&lt;/a>&lt;/li>
&lt;li>Red Hat, &lt;em>Structured outputs in vLLM&lt;/em>: &lt;a href="https://developers.redhat.com/articles/2025/06/03/structured-outputs-vllm-guiding-ai-responses">https://developers.redhat.com/articles/2025/06/03/structured-outputs-vllm-guiding-ai-responses&lt;/a>&lt;/li>
&lt;li>Aidan Cooper, &lt;em>Constrained Decoding&lt;/em> guide: &lt;a href="https://www.aidancooper.co.uk/constrained-decoding/">https://www.aidancooper.co.uk/constrained-decoding/&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>