<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Structured-Output on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/structured-output/</link><description>Recent content in Structured-Output on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sat, 30 May 2026 16:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/structured-output/index.xml" rel="self" type="application/rss+xml"/><item><title>Structured output: el formulario con desplegables que tacha respuestas inválidas antes de que el modelo elija — Outlines, XGrammar, LLGuidance y la matemática del bitmask</title><link>https://blog.lo0.es/posts/structured-output-fundamentos/</link><pubDate>Sat, 30 May 2026 16:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/structured-output-fundamentos/</guid><description>&lt;blockquote>
&lt;p>Este post complementa los de &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> (donde el scheduler vive) y &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding&lt;/a> (otra técnica que opera en el último kilómetro del sampler). Structured output es el contrato de salida del LLM hacia el código que lo consume; sin él, la integración entre LLM y aplicaciones es frágil por defecto.&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-deploy">Estás aquí: 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="estás aquí: 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">Estás aquí: DEPLOY · constraint sobre los logits, capa última del sampler&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>Un LLM produce texto libre, pero muchas aplicaciones —function calling, extracción de entidades, routing, text-to-SQL, generación de configs— necesitan parsearlo como JSON, una llamada a herramienta con args tipados, una sentencia SQL válida, o una opción de un enum. Las soluciones naïve fallan: prompt engineering (&amp;ldquo;responde en JSON&amp;rdquo;) deja &lt;strong>25 %&lt;/strong> de outputs no parseables en muchos modelos; validación post-hoc + retry cuesta latencia y no garantiza terminación; json-repair libraries son parches heurísticos. &lt;strong>Constrained decoding&lt;/strong> garantiza la conformidad al 100 %, por construcción: a cada paso de generación, antes de samplear del softmax sobre los &lt;code>V&lt;/code> tokens del vocabulario, se enmascara a -∞ los tokens que romperían la estructura objetivo. El output es válido por contrato matemático, no por suerte. Las cuatro familias dominantes en mayo de 2026 son &lt;strong>Outlines&lt;/strong> (Willard &amp;amp; Louf, 2023; FSM + token trie precomputado a partir de regex/JSON Schema/CFG), &lt;strong>XGrammar&lt;/strong> (Dong et al., 2024-25, CMU+MLC; pushdown automaton byte-level con cache adaptativo de tokens &lt;em>context-independent&lt;/em>, &lt;strong>default en vLLM v1, SGLang, TensorRT-LLM, NIM y MLC-LLM&lt;/strong>), &lt;strong>LLGuidance&lt;/strong> (Microsoft Research; Earley parser + derivadas de regex, ~50 µs CPU por token, &lt;strong>debajo de OpenAI Structured Outputs&lt;/strong>) y &lt;strong>LM Format Enforcer&lt;/strong> (noamgat; orientado a JSON Schema, integrado en muchos engines como fallback). &lt;strong>XGrammar-2&lt;/strong> (mayo 2026) introduce &lt;strong>Structural Tag&lt;/strong> y cross-grammar cache para tool calling agentic dinámico. El coste real bien integrado: &lt;strong>&amp;lt;5 %&lt;/strong> en TPOT y ~40 µs CPU por token de cómputo de máscara, parcialmente solapable con el forward pass del modelo. La pregunta operacional abierta: &lt;em>¿degrada el reasoning?&lt;/em> El paper &lt;em>Let me Speak Freely?&lt;/em> (Tam et al., EMNLP 2024) reportó degradación significativa en reasoning bajo format constraints; el rebuttal de dottxt mostró que el efecto se debía a prompts no equivalentes entre experimentos. El consenso emergente mayo 2026: usar &lt;strong>two-pass&lt;/strong> (reasoning libre con CoT en texto + structured output en segundo call) para tareas que requieren razonamiento multi-step; usar &lt;strong>single-pass constrained&lt;/strong> para extracción, classification y function calling donde estructura forzada &lt;strong>mejora&lt;/strong> la exactness y reduce alucinaciones. Este post desmonta el mecanismo, las matemáticas (tamaño del bitmask = V/8 bytes, latencia por step), la tabla comparativa de backends, los pitfalls (compile time, tokenizer-specific FSM, streaming SSE) y el patrón de despliegue en producción.&lt;/p>
&lt;h2 id="la-analogía-el-formulario-con-desplegables-en-lugar-de-campos-libres">La analogía: el formulario con desplegables en lugar de campos libres&lt;/h2>
&lt;p>Imagina dos formas de pedirle a alguien que rellene un formulario.&lt;/p>
&lt;p>La &lt;strong>primera forma&lt;/strong> es darle el papel con campos en blanco y decirle &amp;ldquo;rellénalo en este formato exacto: el nombre va aquí, después el DNI sin espacios, después la fecha como YYYY-MM-DD, después marca uno de estos cinco motivos posibles separados por punto y coma&amp;rdquo;. Le explicas el formato con todo el detalle del mundo, pero la persona sigue siendo libre de escribir lo que quiera en cada campo. Si tiene prisa puede saltarse un cero del DNI, poner la fecha en formato americano, marcar dos motivos cuando solo se pedía uno. Cuando recibes el formulario, mucha veces tienes que devolverlo: campo X mal formateado, motivo Y inválido, fecha Z imposible. Es exactamente lo que un LLM hace cuando le pides &amp;ldquo;responde en JSON con este schema&amp;rdquo;: funciona la mayoría de veces, falla un porcentaje no despreciable, y no tienes garantía formal de nada.&lt;/p>
&lt;p>La &lt;strong>segunda forma&lt;/strong> es darle un formulario electrónico donde los campos &lt;strong>no son texto libre&lt;/strong>. El nombre acepta cualquier texto, pero el DNI tiene una máscara que solo permite teclear ocho dígitos seguidos de letra; la fecha es un selector que solo deja elegir fechas válidas; los motivos son un dropdown con cinco opciones cerradas. La persona puede teclear lo que quiera, pero &lt;strong>el formulario no acepta caracteres inválidos en cada momento&lt;/strong>. El resultado es parseable por construcción: cuando recibes el formulario relleno, el DNI tiene exactamente el formato esperado, la fecha es una fecha real, el motivo es uno de los cinco. Esto es &lt;strong>constrained decoding&lt;/strong>.&lt;/p>
&lt;p>La analogía se sostiene en cuatro mapeos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>La persona rellenando&lt;/strong> = el LLM produciendo logits sobre el vocabulario.&lt;/li>
&lt;li>&lt;strong>Los caracteres que el formulario permite teclear en cada campo&lt;/strong> = el bitmask aplicado a los logits antes del sampling.&lt;/li>
&lt;li>&lt;strong>Cómo el formulario sabe qué caracteres permitir según en qué campo estás&lt;/strong> = el autómata (FSM o PDA) que mantiene el estado actual de la grammar.&lt;/li>
&lt;li>&lt;strong>Que el dropdown se pre-compute al cargar la página, no se calcule cada vez&lt;/strong> = la tabla precomputada de tokens válidos por estado del FSM, lo que hace que el coste por step sea O(1) amortizado.&lt;/li>
&lt;/ul>
&lt;h2 id="el-problema-que-structured-output-resuelve">El problema que structured output resuelve&lt;/h2>
&lt;p>El problema operacional es &lt;strong>el contrato entre el LLM y el código que consume su output&lt;/strong>. Hay tres approaches naïve, todos con fallos documentados:&lt;/p>
&lt;p>&lt;strong>Prompt engineering puro.&lt;/strong> &amp;ldquo;Responde solo con un JSON válido, sin comentarios ni prosa&amp;rdquo;. Funciona la mayoría de las veces para modelos buenos; falla entre el 5 % y el 25 % de las veces dependiendo del modelo, la temperatura, la complejidad del schema y la longitud del output. El modelo mete una coma extra, escapa mal una cita, encierra el JSON en bloque markdown &lt;code>```json&lt;/code>, alucina un campo que no estaba en el schema, ignora un campo obligatorio. SqueezeBits mide ≤72 % correct rate sin constraining para algunos modelos en JSON Schema de complejidad moderada.&lt;/p>
&lt;p>&lt;strong>Validación post-hoc + retry.&lt;/strong> El servidor recibe el output, intenta parsearlo, si falla devuelve el error al modelo y le pide que reintente. Coste: latencia 2-3× en peor caso (típicamente 2-3 retries antes de rendirse), sin garantía de terminación, ruido en logs, dificil de testear deterministamente.&lt;/p>
&lt;p>&lt;strong>JSON repair libraries&lt;/strong> (&lt;code>json_repair&lt;/code>, &lt;code>fast-json-repair&lt;/code> Rust port). Parches heurísticos para errores comunes: comas extras, comillas faltantes, prosa intercalada con JSON. Útiles como &lt;strong>fallback&lt;/strong> del approach 2; no son un contrato.&lt;/p>
&lt;p>El coste operacional acumulado: latencia inflada por retries, ruido en producción, debugging penoso de parse errors intermitentes, contratos rotos con clientes downstream que asumían parseo limpio.&lt;/p>
&lt;h2 id="constrained-decoding-el-principio">Constrained decoding: el principio&lt;/h2>
&lt;p>En cada paso de decode, el modelo produce un vector &lt;code>logits ∈ R^V&lt;/code> donde &lt;code>V&lt;/code> es el tamaño del vocabulario (Llama 3: 128 256, GPT-4o: ~200 000, Qwen 3: 152 064). El sampler convencional aplica softmax + estrategia de sampling (greedy, top-k, top-p, temperature) sobre los &lt;code>V&lt;/code> tokens.&lt;/p>
&lt;p>&lt;strong>Constrained decoding&lt;/strong> intercala una operación antes del softmax: aplica un bitmask que pone a -∞ los logits de los tokens que &lt;strong>violarían la grammar objetivo en este paso&lt;/strong>. Resultado: el softmax solo asigna masa de probabilidad a tokens admisibles; el sampling, cualquiera que sea su estrategia, solo puede elegir uno válido.&lt;/p>
&lt;p>Las dos preguntas operacionales son siempre las mismas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>¿Cómo saber qué tokens son válidos en cada step?&lt;/strong> → mantener el estado actual de un autómata (FSM/PDA) construido a partir de la grammar; consultar la tabla &lt;code>(estado → set de tokens válidos)&lt;/code>.&lt;/li>
&lt;li>&lt;strong>¿Cuánto cuesta calcular y aplicar la máscara?&lt;/strong> → ahí compiten Outlines, XGrammar, LLGuidance y LM Format Enforcer.&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;p>&lt;text x="20" y="22" class="lbl">1. El modelo produce logits sobre los V tokens del vocabulario&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, &amp;hellip;, 3.1]&lt;/text>&lt;/p>
&lt;p>&lt;text x="260" y="22" class="lbl">2. El autómata sabe en qué estado estamos (en este 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">esperando: comilla apertura de string&lt;/text>
&lt;text x="380" y="76" text-anchor="middle" class="sub">(después del campo &amp;ldquo;name&amp;rdquo;:)&lt;/text>&lt;/p>
&lt;path class="arr" d="M500,55 L540,55"/>
&lt;p>&lt;text x="540" y="22" class="lbl">3. Tabla precomputada&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 → tokens válidos&lt;/text>
&lt;text x="650" y="62" text-anchor="middle" class="sub">{ &amp;lsquo;&amp;quot;&amp;rsquo;, &amp;rsquo; &amp;lsquo;, &amp;lsquo;\t&amp;rsquo;, &amp;lsquo;\n&amp;rsquo; }&lt;/text>
&lt;text x="650" y="76" text-anchor="middle" class="sub">resto del vocab → -∞&lt;/text>&lt;/p>
&lt;path class="arr" d="M650,80 L650,120 L380,120"/>
&lt;p>&lt;text x="260" y="140" class="lbl">4. Bitmask resultante (16 KB para 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 = permitido, 0 = a -∞&lt;/text>&lt;/p>
&lt;path class="arr" d="M120,80 L120,200"/>
&lt;path class="arr" d="M380,188 L380,200"/>
&lt;p>&lt;text x="20" y="222" class="lbl">5. logits + bitmask → logits restringidos → 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 → token muestreado&lt;/text>&lt;/p>
&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">(válido por construcción)&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 con el token elegido → siguiente step&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="las-cuatro-familias-de-backends">Las cuatro familias de backends&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Backend&lt;/th>
&lt;th>Origen&lt;/th>
&lt;th>Algoritmo&lt;/th>
&lt;th>Grammar formats&lt;/th>
&lt;th>Default en&lt;/th>
&lt;th>Notas&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Outlines&lt;/strong>&lt;/td>
&lt;td>dottxt (2023)&lt;/td>
&lt;td>FSM + token trie precomputado&lt;/td>
&lt;td>regex, JSON Schema, Lark CFG&lt;/td>
&lt;td>HF TGI&lt;/td>
&lt;td>Versión Rust (outlines-core) cierra parte del gap con XGrammar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>XGrammar&lt;/strong>&lt;/td>
&lt;td>CMU + MLC (2024-25)&lt;/td>
&lt;td>PDA byte-level + adaptive cache ctx-indep/dep&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: hasta 100× CFG, 3× JSON; &amp;lt;40 µs/token JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLGuidance&lt;/strong>&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 (interno), Chromium&lt;/td>
&lt;td>~50 µs CPU/token; debajo de Guidance, llama.cpp, mistral.rs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LM Format Enforcer&lt;/strong>&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 en muchos engines, fallback en NIM)&lt;/td>
&lt;td>Más lento que XGrammar (3.5× peor JSON, 10× peor CFG)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tres observaciones operacionales:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>XGrammar es el default de facto en mayo 2026&lt;/strong> en el ecosistema open-source (vLLM v1, SGLang, TensorRT-LLM, NIM, MLC-LLM). Su PDA byte-level con cache adaptativo le da overhead casi cero cuando bien integrado.&lt;/li>
&lt;li>&lt;strong>LLGuidance es la pieza menos conocida pero más usada del mercado&lt;/strong> porque está debajo de OpenAI Structured Outputs (confirmado en el README del propio repo). 50 µs CPU por token para tokenizers de 128 k.&lt;/li>
&lt;li>&lt;strong>Outlines fue el primero&lt;/strong>, sigue manteniendo el mindshare conceptual (papers, blog posts canónicos), pero perdió terreno operacional vs XGrammar. La versión Rust (outlines-core) cierra parte del gap.&lt;/li>
&lt;/ol>
&lt;h2 id="xgrammar-en-detalle-lo-que-hay-debajo-de-vllm-y-sglang">XGrammar en detalle (lo que hay debajo de vLLM y SGLang)&lt;/h2>
&lt;p>El paper de Dong, Yin, Ruan y Chen (arXiv:2411.15100, noviembre 2024) introduce una técnica de particionado que es clave para entender por qué XGrammar es 3-100× más rápido que las alternativas.&lt;/p>
&lt;p>&lt;strong>Particionado del vocabulario en cada estado del PDA&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tokens &lt;em>context-independent&lt;/em> (~99 % del vocab en JSON Schema típico)&lt;/strong>: su validez se decide &lt;strong>solo&lt;/strong> por la posición actual del PDA, sin necesitar inspeccionar la stack completa. Estos son &lt;strong>precomputables&lt;/strong> y se almacenan como bitmasks en cache.&lt;/li>
&lt;li>&lt;strong>Tokens &lt;em>context-dependent&lt;/em> (~1 %)&lt;/strong>: requieren inspeccionar la stack del PDA en runtime. Se manejan caso a caso con coste mayor.&lt;/li>
&lt;/ul>
&lt;p>Esta partición es la razón fundamental por la que XGrammar funciona en producción a TPOT bajo. El 99 % de los lookups van a una tabla precomputada en O(1); solo el 1 % residual paga el coste real.&lt;/p>
&lt;p>&lt;strong>Otras técnicas combinadas&lt;/strong>: pushdown automaton para CFG completos (no solo regex), persistent stack para branching/rollback rápido, JIT compilation + Earley parser en XGrammar-2.&lt;/p>
&lt;p>&lt;strong>Speedups reportados (paper)&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Hasta &lt;strong>100×&lt;/strong> sobre soluciones previas en CFG.&lt;/li>
&lt;li>&lt;strong>3×&lt;/strong> en JSON Schema vs Outlines.&lt;/li>
&lt;li>Latencia &amp;lt;40 µs por token JSON Schema; &amp;lt;200 µs XML/Python DSL.&lt;/li>
&lt;li>Cuando bien integrado en vLLM/SGLang/TRT-LLM: &lt;strong>near-zero end-to-end overhead&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>XGrammar-2&lt;/strong> (mayo 2026, arXiv:2601.04426) añade dos piezas relevantes para agentes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Structural Tag&lt;/strong>: protocolo JSON componible que unifica OpenAI harmony, tool calling, reasoning channels.&lt;/li>
&lt;li>&lt;strong>Cross-Grammar Cache&lt;/strong> para reuso a nivel sub-estructura → permite switching dinámico entre sub-grammars en agentic loops sin recompilar.&lt;/li>
&lt;li>&lt;strong>6× faster compile time&lt;/strong> que XGrammar-1.&lt;/li>
&lt;/ul>
&lt;h2 id="la-matemática-del-bitmask">La matemática del bitmask&lt;/h2>
&lt;p>Tres números mueven la decisión operacional.&lt;/p>
&lt;p>&lt;strong>Tamaño del bitmask por step.&lt;/strong> Si el vocabulario tiene &lt;code>V&lt;/code> tokens, el bitmask packed en bits ocupa &lt;code>V/8&lt;/code> bytes:&lt;/p>
&lt;p>$$\text{tamaño bitmask} = \frac{V}{8} \text{ bytes}$$&lt;/p>
&lt;ul>
&lt;li>Llama 3 (V=128 256): &lt;strong>16 KB&lt;/strong> por bitmask por request por step.&lt;/li>
&lt;li>GPT-4o tokenizer o200k (V≈200 000): &lt;strong>25 KB&lt;/strong>.&lt;/li>
&lt;li>Llama 2 (V=32 000): &lt;strong>4 KB&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Para un batch de 32 requests con structured output activo en H100, hablamos de ~512 KB de bitmasks por step — trivial vs los GBs que mueve el forward pass.&lt;/p>
&lt;p>&lt;strong>Coste de aplicar el bitmask a los logits.&lt;/strong> Un kernel CUDA simple, complejidad &lt;code>O(V)&lt;/code>, latencia ~5-10 µs en H100. Despreciable.&lt;/p>
&lt;p>&lt;strong>Coste de calcular el bitmask (CPU-side).&lt;/strong> Aquí está la diferencia entre backends:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Backend&lt;/th>
&lt;th>Latencia CPU por 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 a XGrammar tras 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>intermedio, degrada con vocab grande&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Con un forward pass del modelo en orden de 10-50 ms por token en decode, &lt;strong>una máscara de 40-50 µs es &amp;lt;0.5 % de overhead&lt;/strong> — invisible. La clave operacional es que el cómputo de la máscara CPU-side se puede &lt;strong>solapar con el forward pass GPU-side&lt;/strong>: mientras la GPU calcula los logits del token &lt;em>t&lt;/em>, la CPU pre-computa la máscara para el token &lt;em>t+1&lt;/em> basándose en el estado del FSM tras el token &lt;em>t-1&lt;/em>. SGLang lo hace explícitamente; vLLM v1 mejoró notablemente sobre v0.&lt;/p>
&lt;p>&lt;strong>Coste de compile-time.&lt;/strong> Pre-cómputo del FSM/PDA y de la cache:&lt;/p>
&lt;ul>
&lt;li>Schemas simples (1-5 campos): &amp;lt;100 ms.&lt;/li>
&lt;li>JSON Schemas profundos con muchos &lt;code>$defs&lt;/code>: segundos.&lt;/li>
&lt;li>OpenAI structured outputs: &amp;ldquo;10s típico, hasta 1 minuto para schemas complejos&amp;rdquo; — cacheado tras el primer call.&lt;/li>
&lt;li>XGrammar-2: 6× faster que XGrammar-1.&lt;/li>
&lt;li>LLGuidance: ~2 ms startup.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Best practice operacional&lt;/strong>: pre-cachear los schemas conocidos al arrancar el servidor para evitar latency spikes en la primera request de cada schema nuevo.&lt;/p>
&lt;h2 id="degrada-el-reasoning-del-modelo">¿Degrada el reasoning del modelo?&lt;/h2>
&lt;p>Es la pregunta más interesante del campo y la respuesta no es obvia.&lt;/p>
&lt;p>&lt;strong>El argumento teórico de la degradación&lt;/strong>: forzar la estructura cambia la distribución de probabilidad del modelo; si el camino estructurado empuja a paths bajos en probabilidad, el modelo se queda &amp;ldquo;atascado&amp;rdquo; en una rama subóptima sin poder explorar.&lt;/p>
&lt;p>&lt;strong>Paper &lt;em>Let me Speak Freely?&lt;/em>&lt;/strong> (Tam et al., arXiv:2408.02442, EMNLP 2024 Industry Track): reportó &lt;strong>degradación significativa en reasoning tasks&lt;/strong> bajo format constraints (JSON/XML/YAML). Cuanto más estricto el formato, mayor la degradación. Sin embargo, el propio paper reconocía &lt;strong>mejora en classification tasks&lt;/strong> con estructura forzada.&lt;/p>
&lt;p>&lt;strong>Rebuttal dottxt — &lt;em>Say What You Mean&lt;/em>&lt;/strong> (blog post oficial): crítica metodológica. Los prompts del paper original eran &lt;strong>diferentes&lt;/strong> entre estructurado y no-estructurado (no apples-to-apples). Los prompts JSON del experimento original daban menos información que los unstructured. Re-corriendo con prompts equivalentes (Llama-3-8B-Instruct), dottxt no reproduce la degradación. La conclusión: el paper confundió &lt;em>format constraint&lt;/em> con &lt;em>prompt engineering&lt;/em> del mismo.&lt;/p>
&lt;p>&lt;strong>Consenso emergente mayo 2026&lt;/strong> (recogido por blogs técnicos y empirismo de la comunidad):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Para extracción, classification, function calling, routing&lt;/strong>: constrained decoding &lt;strong>mejora exactness y reduce alucinaciones&lt;/strong>. Es la herramienta correcta.&lt;/li>
&lt;li>&lt;strong>Para reasoning multi-step (matemáticas, lógica, code review)&lt;/strong>: usar &lt;strong>two-pass&lt;/strong>:
&lt;ol>
&lt;li>Primera llamada: reasoning libre con Chain-of-Thought en texto natural (&lt;code>max_tokens&lt;/code> generoso, sin constraint).&lt;/li>
&lt;li>Segunda llamada: pasarle el reasoning + el schema, pedirle que produzca structured output con constrained decoding.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ul>
&lt;p>El patrón &amp;ldquo;&lt;strong>reason then structure&lt;/strong>&amp;rdquo; da el mejor de los dos mundos: razonamiento sin coartar + output garantizado parseable.&lt;/p>
&lt;h2 id="implementaciones-reales-en-mayo-2026">Implementaciones reales en mayo 2026&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Engine&lt;/th>
&lt;th>Default backend&lt;/th>
&lt;th>Otros disponibles&lt;/th>
&lt;th>API&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM v1&lt;/strong>&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> en request&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang&lt;/strong>&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>&lt;strong>TensorRT-LLM&lt;/strong>&lt;/td>
&lt;td>XGrammar&lt;/td>
&lt;td>LLGTRT (Rust llguidance)&lt;/td>
&lt;td>Integración oficial desde ene 2025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NVIDIA NIM&lt;/strong>&lt;/td>
&lt;td>XGrammar (cambió de Outlines en 2025)&lt;/td>
&lt;td>LM Format Enforcer (requiere &lt;code>NIM_ENABLE_KV_CACHE_REUSE=0&lt;/code>)&lt;/td>
&lt;td>Multi-backend pluggable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>llama.cpp&lt;/strong>&lt;/td>
&lt;td>GBNF nativo&lt;/td>
&lt;td>LLGuidance&lt;/td>
&lt;td>Cada candidate token se testea contra parse state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HF TGI&lt;/strong>&lt;/td>
&lt;td>Outlines&lt;/td>
&lt;td>XGrammar (experimental)&lt;/td>
&lt;td>Feature &amp;ldquo;Guidance&amp;rdquo; con &lt;code>/generate&lt;/code> y &lt;code>/chat/completion&lt;/code> con &lt;code>tools&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLC-LLM&lt;/strong>&lt;/td>
&lt;td>XGrammar (nativo, mismo equipo)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>API propia&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Ejemplo vLLM:&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;Extrae nombre y edad de: María tiene 34 años&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>Equivalente SGLang:&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="patrones-de-uso-en-producción">Patrones de uso en producción&lt;/h2>
&lt;p>&lt;strong>Function calling / tool use.&lt;/strong> El LLM produce &lt;code>{tool_name: enum, arguments: object}&lt;/code> según schema. Caso dominante hoy (OpenAI, Anthropic, modelos open). El schema garantiza que &lt;code>tool_name&lt;/code> está en el set de tools disponibles y que &lt;code>arguments&lt;/code> tiene los tipos correctos.&lt;/p>
&lt;p>&lt;strong>Extracción de entidades.&lt;/strong> Schema con &lt;code>{name, address, phone, ...}&lt;/code> desde texto no estructurado. Reduce 76% → 98% schema adherence en benchmarks vendor.&lt;/p>
&lt;p>&lt;strong>Routing / classification.&lt;/strong> El LLM elige entre N opciones (&lt;code>enum&lt;/code> en schema). El bitmask se reduce a unos pocos tokens válidos → overhead casi nulo, máxima fiabilidad.&lt;/p>
&lt;p>&lt;strong>SQL constrained.&lt;/strong> Grammar SQL como GBNF o Lark → evita inyección y errores sintácticos. Útil en text-to-SQL agents.&lt;/p>
&lt;p>&lt;strong>Code generation con AST válido.&lt;/strong> Grammar del lenguaje target (Python, Rust, DSL propio). Garantiza que el output es código compilable / parseable.&lt;/p>
&lt;p>&lt;strong>Agentic loops.&lt;/strong> XGrammar-2 Structural Tag para switching dinámico entre formatos (tool call → reasoning channel → tool result) sin recompilar la grammar.&lt;/p>
&lt;h2 id="pitfalls-operacionales">Pitfalls operacionales&lt;/h2>
&lt;p>&lt;strong>Compile time de schemas grandes.&lt;/strong> JSON Schemas con muchos &lt;code>$defs&lt;/code> pueden tardar segundos en compilar. Causa &lt;strong>startup lento&lt;/strong> si se cargan al arranque, o latency spikes en la primera request si se cargan on-demand. Mitigación: warm cache al boot con los schemas conocidos del fleet.&lt;/p>
&lt;p>&lt;strong>Tokenizer-specific FSM/PDA.&lt;/strong> Un schema pre-compilado para Llama 3 (tokenizer de 128 k) &lt;strong>no sirve para Qwen&lt;/strong> (152 k). La cache de schemas debe estar tipada por &lt;code>(tokenizer_hash, schema_hash)&lt;/code>. Cambio de modelo = invalidar cache.&lt;/p>
&lt;p>&lt;strong>Cambios de schema.&lt;/strong> Versionar schemas explícitamente. Cambios breaking → rebuild + warm cache. No silenciar errors de compile en producción.&lt;/p>
&lt;p>&lt;strong>Streaming SSE.&lt;/strong> Structured output con streaming requiere parsers tolerantes a output parcial (&lt;code>partial-json-parser&lt;/code>, &lt;code>json-stream&lt;/code>). Pydantic v1 estricto &lt;strong>falla&lt;/strong>; v2 con &lt;code>partial validation&lt;/code> funciona. Los clients antiguos pueden no manejar la validación incremental — testear con el client real.&lt;/p>
&lt;p>&lt;strong>Token healing.&lt;/strong> Tokens que cruzan boundaries (&lt;code>://&lt;/code> como single token vs &lt;code>:&lt;/code> + &lt;code>//&lt;/code>) pueden romper constraint puro. Outlines y XGrammar lo mitigan internamente; llama.cpp y otros requieren token healing explícito. Si el modelo cuela un caracter &amp;ldquo;extraño&amp;rdquo; después de la estructura, probablemente sea esto.&lt;/p>
&lt;p>&lt;strong>Bugs reales conocidos en vLLM 2025-26&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>vLLM 0.8.4: XGrammar rechaza &lt;code>minItems&lt;/code> en JSON Schema (issue #16880).&lt;/li>
&lt;li>vLLM con Qwen 2.5 VL: Outlines/XGrammar no respeta schema en algunos casos (issue #13038).&lt;/li>
&lt;li>vLLM bitmask backend &lt;code>apply_token_bitmask_inplace&lt;/code> siempre &lt;code>auto&lt;/code>, no expuesto al usuario.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>OpenAI subset limitations&lt;/strong>: si vas a portar un schema validado en OpenAI structured outputs a otro backend, comprobar que no usas constructs que OpenAI rechaza pero otros permiten (&lt;code>$ref&lt;/code> profundos, &lt;code>pattern&lt;/code>, &lt;code>default&lt;/code>, profundidad &amp;gt;5, &lt;code>anyOf&lt;/code> como root).&lt;/p>
&lt;h2 id="implicaciones-en-hardware-on-premise">Implicaciones en hardware on-premise&lt;/h2>
&lt;p>&lt;strong>En una RTX 4090 (24 GB).&lt;/strong> Cualquier modelo que sirvas con vLLM o llama.cpp puede llevar structured output con coste prácticamente despreciable. La latencia de máscara CPU-side (~40 µs) es trivial comparada con el TPOT típico (30-100 ms en consumer hardware). El caso interesante: function calling sobre Llama 3 8B o Qwen 3 14B INT4 → tool use fiable sin retries, sin necesidad de API hosted.&lt;/p>
&lt;p>&lt;strong>En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo).&lt;/strong> Aquí XGrammar como default de vLLM v1 / SGLang es el camino:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3 70B FP8 + XGrammar JSON Schema&lt;/strong>: TPOT P95 estable bajo 60 ms incluso con structured output activo en todo el batch. Soporta cientos de schemas distintos cacheados.&lt;/li>
&lt;li>&lt;strong>DeepSeek-V3 + XGrammar Structural Tag&lt;/strong>: agentic tool calling con MTP nativo y constrained decoding combinados; el coste de la máscara se solapa con el forward del MoE.&lt;/li>
&lt;li>&lt;strong>Multi-tenant function calling&lt;/strong>: cada cliente puede tener su set de tools (= sus schemas); compile-time se amortiza con el caching, runtime es invariante.&lt;/li>
&lt;/ul>
&lt;p>La regla de pulgar mayo 2026: &lt;strong>XGrammar por defecto, pre-cachear schemas del fleet al boot, two-pass para reasoning tasks&lt;/strong>.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Tool routing dinámico&lt;/strong> con XGrammar-2 Structural Tag: el detalle de cómo TagDispatch elige sub-grammars en runtime.&lt;/li>
&lt;li>&lt;strong>Constrained beam search&lt;/strong> y su interacción con grammar: degradación de calidad teórica vs greedy.&lt;/li>
&lt;li>&lt;strong>Grammar para code generation con AST completo&lt;/strong>: Python/Rust grammars production-grade, performance.&lt;/li>
&lt;li>&lt;strong>JSON Schema → Pydantic → grammar pipelines&lt;/strong>: tooling para reducir error humano.&lt;/li>
&lt;li>&lt;strong>Inhibition decoding&lt;/strong> (paper Inhibition Decoding, 2025): variante que penaliza pero no prohíbe ciertos tokens, útil para safety constraints suaves.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching: la peluquería con 8 sillones&lt;/a> — el scheduler donde structured output se aplica request a request; el cómputo de máscara CPU-side puede solaparse con el forward GPU-side.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding&lt;/a> — otra técnica que opera sobre el sampler; speculative + structured se combina pero requiere cuidado (la regla de aceptación tiene que respetar el bitmask).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a> — un adapter puede estar entrenado específicamente para function calling, complementa la garantía de structured output con afinidad del modelo a la tarea.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge: el corrector de oposiciones&lt;/a> — el judge produce un veredicto estructurado (&lt;code>{score, reasoning, decision}&lt;/code>) que puede asegurarse con structured output para evitar parseo manual.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs&lt;/a> — los evals con LLM-as-judge se benefician enormemente de structured output garantizado.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde Deploy es la etapa 4.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&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>