<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Anonymize on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/anonymize/</link><description>Recent content in Anonymize on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 01 Jun 2026 05:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/anonymize/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM Guard: el traductor jurado con cuaderno de equivalencias — anatomía, scanners y su integración con Langfuse, vLLM y LiteLLM</title><link>https://blog.lo0.es/posts/llm-guard-fundamentos/</link><pubDate>Mon, 01 Jun 2026 05:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/llm-guard-fundamentos/</guid><description>&lt;blockquote>
&lt;p>Este post es &lt;strong>deep-dive de una sola pieza&lt;/strong> dentro de la capa cubierta en el &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post sobre guardrails y safety LLM&lt;/a>. Aquel mapea las cuatro líneas de defensa (input, retrieval, tool, output) y el catálogo OSS 2026 a vista de pájaro; éste baja al ras de &lt;strong>LLM Guard&lt;/strong> porque su patrón Anonymize/Deanonymize, su modelo de scanners composables y sus cuatro modos de despliegue merecen tratamiento propio. Las analogías que se construyeron arriba (cocina HACCP, cuatro CCP) siguen valiendo: este post amplía el zoom sobre la herramienta que ocupa el cinturón de PII y de scanners individuales dentro de esa arquitectura.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>LLM Guard es la herramienta OSS (MIT, Protect AI) que materializa la capa de guardrails LLM con un modelo radicalmente distinto al de NeMo Guardrails y al de Guardrails AI: en lugar de un DSL declarativo (Colang) o de un framework de validators con LLM-as-judge externos, ofrece un &lt;strong>catálogo de detectores compactos especializados&lt;/strong> —15 input scanners, 21 output scanners— componibles como pipeline Python, con un mecanismo único distintivo: el patrón &lt;strong>Anonymize → LLM → Deanonymize con Vault&lt;/strong>. El Vault es un almacén centralizado del mapping entre entidades reales (&lt;code>John Doe&lt;/code>, &lt;code>12345678X&lt;/code>) y placeholders (&lt;code>[REDACTED_PERSON_1]&lt;/code>, &lt;code>[REDACTED_DNI_1]&lt;/code>); en input, las entidades se redactan y el mapping se guarda; el LLM nunca ve datos personales reales; en output, el Deanonymize scanner restituye los originales antes de devolver la respuesta al usuario. Este post desmonta: la anatomía interna (Vault + scanners + orquestador con &lt;code>fail_fast&lt;/code> y caché TTL), los cuatro patrones de despliegue con sus matemáticas (librería in-process, API FastAPI, sidecar OTel sobre vLLM, plugin de AI Gateway — LiteLLM, Envoy AI Gateway, Kong AI Gateway), los diagramas de integración con Langfuse (vía OTel HTTP exporter de LLM Guard + &lt;code>langfuse.score()&lt;/code> desde el AI Gateway), las matemáticas con benchmarks del proyecto (Anonymize en 177 ms CPU → 128 ms ONNX-CPU → 125 ms GPU FP16 → 38 ms GPU+ONNX, escalado x4.6 cuando combinas ONNX + GPU), el patrón ONNX como aceleración por defecto sin GPU dedicada, la comparativa con NeMo Guardrails (DSL Colang declarativo orientado a flujo conversacional) y Guardrails AI (validators tipo contrato JSON con judges externos), la aplicación a hardware on-premise (qué scanners aguantan CPU, cuáles necesitan GPU compartida) y las siete trampas operativas específicas de la herramienta.&lt;/p>
&lt;h2 id="la-analogía-el-traductor-jurado-con-cuaderno-de-equivalencias">La analogía: el traductor jurado con cuaderno de equivalencias&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="LLM Guard como traductor jurado con cuaderno de equivalencias">
&lt;style>
.t-user{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.t-trad{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:8}
.t-model{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.t-vault{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:8}
.tl{font:600 13px sans-serif;fill:#222}
.ts{font:400 11px sans-serif;fill:#555}
.tn{font:italic 11px sans-serif;fill:#555}
.tar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mt1)}
.tcb{stroke:#7a5;stroke-width:1.4;fill:none;stroke-dasharray:5 3;marker-end:url(#mt2)}
&lt;/style>
&lt;defs>
&lt;marker id="mt1" 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;marker id="mt2" 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="#7a5"/>&lt;/marker>
&lt;/defs>
&lt;rect x="20" y="40" width="120" height="60" class="t-user"/>
&lt;text x="80" y="64" text-anchor="middle" class="tl">Cliente&lt;/text>
&lt;text x="80" y="82" text-anchor="middle" class="ts">"Mi DNI es 12345678X,&lt;/text>
&lt;text x="80" y="96" text-anchor="middle" class="ts">¿paga IVA?"&lt;/text>
&lt;rect x="180" y="40" width="160" height="60" class="t-trad"/>
&lt;text x="260" y="64" text-anchor="middle" class="tl">Traductor (Anonymize)&lt;/text>
&lt;text x="260" y="82" text-anchor="middle" class="ts">redacta entidades sensibles&lt;/text>
&lt;text x="260" y="96" text-anchor="middle" class="ts">+ inscribe en cuaderno&lt;/text>
&lt;rect x="380" y="40" width="160" height="60" class="t-model"/>
&lt;text x="460" y="64" text-anchor="middle" class="tl">LLM&lt;/text>
&lt;text x="460" y="82" text-anchor="middle" class="ts">recibe texto saneado:&lt;/text>
&lt;text x="460" y="96" text-anchor="middle" class="ts">"Mi DNI es [DNI_1], ¿paga IVA?"&lt;/text>
&lt;rect x="580" y="40" width="160" height="60" class="t-trad"/>
&lt;text x="660" y="64" text-anchor="middle" class="tl">Traductor (Deanonymize)&lt;/text>
&lt;text x="660" y="82" text-anchor="middle" class="ts">restituye originales&lt;/text>
&lt;text x="660" y="96" text-anchor="middle" class="ts">desde el cuaderno&lt;/text>
&lt;rect x="680" y="40" width="120" height="60" class="t-user" transform="translate(-20 130)"/>
&lt;text x="720" y="194" text-anchor="middle" class="tl" transform="translate(-20 0)">Cliente recibe&lt;/text>
&lt;text x="720" y="212" text-anchor="middle" class="ts" transform="translate(-20 0)">respuesta con&lt;/text>
&lt;text x="720" y="226" text-anchor="middle" class="ts" transform="translate(-20 0)">"12345678X" restituido&lt;/text>
&lt;path class="tar" d="M140,70 L180,70"/>
&lt;path class="tar" d="M340,70 L380,70"/>
&lt;path class="tar" d="M540,70 L580,70"/>
&lt;path class="tar" d="M660,100 Q660,150 700,170"/>
&lt;rect x="280" y="220" width="220" height="80" class="t-vault"/>
&lt;text x="390" y="244" text-anchor="middle" class="tl">Vault (cuaderno compartido)&lt;/text>
&lt;text x="390" y="262" text-anchor="middle" class="ts">[PERSON_1] = "Marta García"&lt;/text>
&lt;text x="390" y="276" text-anchor="middle" class="ts">[DNI_1] = "12345678X"&lt;/text>
&lt;text x="390" y="290" text-anchor="middle" class="ts">[IBAN_1] = "ES91 2100 0418..."&lt;/text>
&lt;path class="tcb" d="M260,100 L320,218"/>
&lt;path class="tcb" d="M460,218 L660,100"/>
&lt;text x="200" y="160" class="tn">guarda mapping&lt;/text>
&lt;text x="500" y="160" class="tn">consulta para restituir&lt;/text>
&lt;text x="410" y="340" text-anchor="middle" class="tn">El LLM nunca ve la PII original. El cuaderno (Vault) es el único punto que conoce la equivalencia.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Un traductor jurado serio que trabaja con documentos sensibles —un contrato laboral, una historia clínica, una declaración fiscal— no envía el texto crudo al traductor automático que tiene en la nube. Lleva un &lt;strong>cuaderno de equivalencias&lt;/strong> abierto sobre la mesa. Cuando recibe el documento original, abre el cuaderno y va apuntando: &amp;ldquo;Marta García&amp;rdquo; → &lt;code>[PERSONA-1]&lt;/code>, &amp;ldquo;12345678X&amp;rdquo; → &lt;code>[DNI-1]&lt;/code>, &amp;ldquo;ES91 2100 0418&amp;hellip;&amp;rdquo; → &lt;code>[IBAN-1]&lt;/code>. Sustituye cada aparición en el texto por su etiqueta y pasa el texto &lt;strong>anonimizado&lt;/strong> al servicio de traducción. El servicio devuelve una traducción que sigue conteniendo las etiquetas. El traductor abre de nuevo el cuaderno, restituye cada etiqueta por su valor original, y entrega al cliente la traducción final con la PII intacta. Para el servicio de traducción, esos datos personales &lt;strong>nunca existieron&lt;/strong>: sólo vio placeholders.&lt;/p>
&lt;p>Esta es la operación exacta que define el carácter de LLM Guard frente al resto del ecosistema. NeMo Guardrails resuelve safety con un &lt;strong>grafo declarativo&lt;/strong> de reglas en Colang; Guardrails AI con &lt;strong>validators&lt;/strong> que invocan a un LLM-as-judge para verificar contratos; LLM Guard con un &lt;strong>catálogo de detectores compactos especializados&lt;/strong> + el patrón Vault. Los tres son válidos en distintos escenarios. La elección no es de gusto: es estructural según cómo se construye el sistema y dónde está el cuello.&lt;/p>
&lt;p>El traductor también revisa, claro, que el texto no contenga otros problemas además de la PII: insultos, instrucciones para reprogramarse, links a páginas hostiles, código que no debería estar ahí. Para eso tiene el resto del catálogo de scanners. Pero la firma de la casa, lo que la distingue, es ese cuaderno.&lt;/p>
&lt;h2 id="anatomía-interna-de-llm-guard">Anatomía interna de LLM Guard&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Anatomía interna de LLM Guard">
&lt;style>
.a-orch{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.a-in{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:6}
.a-out{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:6}
.a-vault{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:6}
.a-obs{fill:#f8a8d8;stroke:#444;stroke-width:1.4;rx:6}
.al{font:600 12px sans-serif;fill:#222}
.as{font:400 10px sans-serif;fill:#444}
.an{font:italic 10px sans-serif;fill:#555}
.aar{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#ma1)}
&lt;/style>
&lt;defs>&lt;marker id="ma1" 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;rect x="20" y="20" width="780" height="40" class="a-orch"/>
&lt;text x="410" y="42" text-anchor="middle" class="al">Orquestador: scan_prompt() / scan_output() · fail_fast · caché TTL · timeout · OTel spans&lt;/text>
&lt;text x="410" y="56" text-anchor="middle" class="as">Itera scanners en orden, agrega is_valid y risk_score, emite trace y métricas Prometheus&lt;/text>
&lt;text x="50" y="90" class="al">Input scanners (15)&lt;/text>
&lt;rect x="30" y="100" width="120" height="22" class="a-in"/>&lt;text x="90" y="115" text-anchor="middle" class="as">Anonymize ⓥ&lt;/text>
&lt;rect x="30" y="125" width="120" height="22" class="a-in"/>&lt;text x="90" y="140" text-anchor="middle" class="as">PromptInjection&lt;/text>
&lt;rect x="30" y="150" width="120" height="22" class="a-in"/>&lt;text x="90" y="165" text-anchor="middle" class="as">Toxicity&lt;/text>
&lt;rect x="30" y="175" width="120" height="22" class="a-in"/>&lt;text x="90" y="190" text-anchor="middle" class="as">Secrets&lt;/text>
&lt;rect x="30" y="200" width="120" height="22" class="a-in"/>&lt;text x="90" y="215" text-anchor="middle" class="as">TokenLimit&lt;/text>
&lt;rect x="30" y="225" width="120" height="22" class="a-in"/>&lt;text x="90" y="240" text-anchor="middle" class="as">BanTopics&lt;/text>
&lt;rect x="30" y="250" width="120" height="22" class="a-in"/>&lt;text x="90" y="265" text-anchor="middle" class="as">BanCompetitors&lt;/text>
&lt;rect x="30" y="275" width="120" height="22" class="a-in"/>&lt;text x="90" y="290" text-anchor="middle" class="as">BanCode / Code&lt;/text>
&lt;rect x="30" y="300" width="120" height="22" class="a-in"/>&lt;text x="90" y="315" text-anchor="middle" class="as">Sentiment&lt;/text>
&lt;rect x="30" y="325" width="120" height="22" class="a-in"/>&lt;text x="90" y="340" text-anchor="middle" class="as">Gibberish&lt;/text>
&lt;rect x="30" y="350" width="120" height="22" class="a-in"/>&lt;text x="90" y="365" text-anchor="middle" class="as">Language&lt;/text>
&lt;rect x="30" y="375" width="120" height="22" class="a-in"/>&lt;text x="90" y="390" text-anchor="middle" class="as">InvisibleText&lt;/text>
&lt;rect x="30" y="400" width="120" height="22" class="a-in"/>&lt;text x="90" y="415" text-anchor="middle" class="as">Regex · BanSubstrings&lt;/text>
&lt;rect x="180" y="120" width="160" height="170" class="a-vault"/>
&lt;text x="260" y="142" text-anchor="middle" class="al">Vault&lt;/text>
&lt;text x="260" y="160" text-anchor="middle" class="as">Diccionario in-memory&lt;/text>
&lt;text x="260" y="174" text-anchor="middle" class="as">por sesión / request&lt;/text>
&lt;text x="260" y="200" text-anchor="middle" class="as">[PERSON_1]→"Marta García"&lt;/text>
&lt;text x="260" y="214" text-anchor="middle" class="as">[DNI_1]→"12345678X"&lt;/text>
&lt;text x="260" y="228" text-anchor="middle" class="as">[IBAN_1]→"ES91..."&lt;/text>
&lt;text x="260" y="252" text-anchor="middle" class="as">.placeholder() / .get()&lt;/text>
&lt;text x="260" y="266" text-anchor="middle" class="as">opcional: persistencia&lt;/text>
&lt;text x="260" y="280" text-anchor="middle" class="as">Redis / cliente sticky&lt;/text>
&lt;path class="aar" d="M150,110 L186,140"/>
&lt;text x="360" y="90" class="al">Output scanners (21)&lt;/text>
&lt;rect x="350" y="100" width="120" height="22" class="a-out"/>&lt;text x="410" y="115" text-anchor="middle" class="as">Deanonymize ⓥ&lt;/text>
&lt;rect x="350" y="125" width="120" height="22" class="a-out"/>&lt;text x="410" y="140" text-anchor="middle" class="as">Sensitive (PII out)&lt;/text>
&lt;rect x="350" y="150" width="120" height="22" class="a-out"/>&lt;text x="410" y="165" text-anchor="middle" class="as">Toxicity · Bias&lt;/text>
&lt;rect x="350" y="175" width="120" height="22" class="a-out"/>&lt;text x="410" y="190" text-anchor="middle" class="as">NoRefusal&lt;/text>
&lt;rect x="350" y="200" width="120" height="22" class="a-out"/>&lt;text x="410" y="215" text-anchor="middle" class="as">Relevance&lt;/text>
&lt;rect x="350" y="225" width="120" height="22" class="a-out"/>&lt;text x="410" y="240" text-anchor="middle" class="as">FactualConsistency&lt;/text>
&lt;rect x="350" y="250" width="120" height="22" class="a-out"/>&lt;text x="410" y="265" text-anchor="middle" class="as">JSON validator&lt;/text>
&lt;rect x="350" y="275" width="120" height="22" class="a-out"/>&lt;text x="410" y="290" text-anchor="middle" class="as">MaliciousURLs&lt;/text>
&lt;rect x="350" y="300" width="120" height="22" class="a-out"/>&lt;text x="410" y="315" text-anchor="middle" class="as">URLReachability&lt;/text>
&lt;rect x="350" y="325" width="120" height="22" class="a-out"/>&lt;text x="410" y="340" text-anchor="middle" class="as">LanguageSame&lt;/text>
&lt;rect x="350" y="350" width="120" height="22" class="a-out"/>&lt;text x="410" y="365" text-anchor="middle" class="as">ReadingTime&lt;/text>
&lt;rect x="350" y="375" width="120" height="22" class="a-out"/>&lt;text x="410" y="390" text-anchor="middle" class="as">BanCompetitors&lt;/text>
&lt;rect x="350" y="400" width="120" height="22" class="a-out"/>&lt;text x="410" y="415" text-anchor="middle" class="as">Regex · BanSubstrings&lt;/text>
&lt;path class="aar" d="M340,140 L350,140"/>
&lt;text x="540" y="90" class="al">Modelos backend&lt;/text>
&lt;rect x="510" y="100" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="120" text-anchor="middle" class="al">ONNX runtime&lt;/text>
&lt;text x="590" y="136" text-anchor="middle" class="as">modelos cuantizados&lt;/text>
&lt;text x="590" y="152" text-anchor="middle" class="as">CPU + GPU compatibles&lt;/text>
&lt;rect x="510" y="170" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="190" text-anchor="middle" class="al">Transformers (HF)&lt;/text>
&lt;text x="590" y="206" text-anchor="middle" class="as">BERT NER, distilbert&lt;/text>
&lt;text x="590" y="222" text-anchor="middle" class="as">deberta, bge, etc.&lt;/text>
&lt;rect x="510" y="240" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="260" text-anchor="middle" class="al">Presidio Analyzer&lt;/text>
&lt;text x="590" y="276" text-anchor="middle" class="as">spaCy / flair / regex&lt;/text>
&lt;text x="590" y="292" text-anchor="middle" class="as">~50 entidades base&lt;/text>
&lt;rect x="510" y="310" width="160" height="60" class="a-obs"/>
&lt;text x="590" y="330" text-anchor="middle" class="al">Validators puros&lt;/text>
&lt;text x="590" y="346" text-anchor="middle" class="as">regex, JSON schema,&lt;/text>
&lt;text x="590" y="362" text-anchor="middle" class="as">stdlib URL parsing&lt;/text>
&lt;text x="700" y="90" class="al">Telemetría&lt;/text>
&lt;rect x="690" y="100" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="120" text-anchor="middle" class="al">OTel exporter&lt;/text>
&lt;text x="747" y="136" text-anchor="middle" class="as">traces (HTTP)&lt;/text>
&lt;text x="747" y="152" text-anchor="middle" class="as">metrics (HTTP)&lt;/text>
&lt;rect x="690" y="170" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="190" text-anchor="middle" class="al">Prometheus&lt;/text>
&lt;text x="747" y="206" text-anchor="middle" class="as">/metrics endpoint&lt;/text>
&lt;text x="747" y="222" text-anchor="middle" class="as">counters + histograms&lt;/text>
&lt;rect x="690" y="240" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="260" text-anchor="middle" class="al">structured logs&lt;/text>
&lt;text x="747" y="276" text-anchor="middle" class="as">stdout JSON,&lt;/text>
&lt;text x="747" y="292" text-anchor="middle" class="as">parseable Loki/ELK&lt;/text>
&lt;rect x="690" y="310" width="115" height="60" class="a-obs"/>
&lt;text x="747" y="330" text-anchor="middle" class="al">FastAPI&lt;/text>
&lt;text x="747" y="346" text-anchor="middle" class="as">/analyze/prompt&lt;/text>
&lt;text x="747" y="362" text-anchor="middle" class="as">/analyze/output&lt;/text>
&lt;text x="410" y="445" text-anchor="middle" class="an">El Vault es la pieza única: lo comparten Anonymize (input) y Deanonymize (output) en la misma request o sesión. Sin él, la PII se filtraría al LLM.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las tres piezas estructurales son:&lt;/p>
&lt;p>&lt;strong>1. El orquestador&lt;/strong> (&lt;code>scan_prompt&lt;/code>, &lt;code>scan_output&lt;/code>). Recibe una lista de scanners en orden y los ejecuta secuencialmente sobre el texto. Devuelve la terna &lt;code>(sanitized_text, results_valid, results_score)&lt;/code> donde:&lt;/p>
&lt;ul>
&lt;li>&lt;code>sanitized_text&lt;/code> es el texto transformado por los scanners que mutan (Anonymize, BanSubstrings con redaction).&lt;/li>
&lt;li>&lt;code>results_valid&lt;/code> es un dict &lt;code>{scanner_name: bool}&lt;/code> indicando qué scanners pasaron.&lt;/li>
&lt;li>&lt;code>results_score&lt;/code> es un dict &lt;code>{scanner_name: float}&lt;/code> con el risk score reportado (0 limpio, 1 violación máxima).&lt;/li>
&lt;/ul>
&lt;p>Soporta &lt;code>fail_fast=True&lt;/code> para cortar tras el primer fail. Soporta &lt;code>timeout&lt;/code> por scanner para no bloquearse en un detector lento. Cuando se expone como API FastAPI, soporta caché TTL para evitar reescanear prompts repetidos (caso de bots con preguntas idénticas).&lt;/p>
&lt;p>&lt;strong>2. El catálogo de scanners.&lt;/strong> Quince input scanners y veintiún output scanners, cada uno con su propio modelo backend y su umbral configurable:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Familia&lt;/th>
&lt;th>Input&lt;/th>
&lt;th>Output&lt;/th>
&lt;th>Backend dominante&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>PII&lt;/strong>&lt;/td>
&lt;td>Anonymize&lt;/td>
&lt;td>Deanonymize, Sensitive&lt;/td>
&lt;td>Presidio + BERT-NER&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Inyección y jailbreak&lt;/strong>&lt;/td>
&lt;td>PromptInjection&lt;/td>
&lt;td>—&lt;/td>
&lt;td>DeBERTa fine-tuned (Protect AI propio)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Toxicidad y bias&lt;/strong>&lt;/td>
&lt;td>Toxicity, Sentiment&lt;/td>
&lt;td>Toxicity, Bias, Sentiment&lt;/td>
&lt;td>RoBERTa / BERT fine-tuned&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tópicos prohibidos&lt;/strong>&lt;/td>
&lt;td>BanTopics, BanCompetitors&lt;/td>
&lt;td>BanTopics, BanCompetitors&lt;/td>
&lt;td>Zero-shot classifier BART-MNLI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Substrings y regex&lt;/strong>&lt;/td>
&lt;td>BanSubstrings, Regex&lt;/td>
&lt;td>BanSubstrings, Regex&lt;/td>
&lt;td>string matching + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secrets&lt;/strong>&lt;/td>
&lt;td>Secrets&lt;/td>
&lt;td>—&lt;/td>
&lt;td>detect-secrets (Yelp) + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Estructura&lt;/strong>&lt;/td>
&lt;td>TokenLimit, Language, InvisibleText, Gibberish&lt;/td>
&lt;td>JSON, Language, LanguageSame, Gibberish, ReadingTime&lt;/td>
&lt;td>tokenizer, lang-detect, JSON schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Código&lt;/strong>&lt;/td>
&lt;td>BanCode, Code&lt;/td>
&lt;td>BanCode, Code&lt;/td>
&lt;td>classifier de lenguaje + regex&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>URLs&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>MaliciousURLs, URLReachability&lt;/td>
&lt;td>block-list + DNS lookup&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Calidad de respuesta&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>NoRefusal, Relevance, FactualConsistency&lt;/td>
&lt;td>NLI-cross-encoder + cosine similarity&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cada scanner se importa y se instancia individualmente, con su umbral propio:&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">llm_guard.input_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Anonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Secrets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.vault&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Vault&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">vault&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Vault&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">scanners&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">Anonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.5&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.85&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">threshold&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">0.7&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Secrets&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. El Vault.&lt;/strong> Pieza única no encontrada en NeMo Guardrails ni en Guardrails AI con el mismo modelo. Es un diccionario in-memory por sesión o request que guarda el mapping &lt;code>placeholder → valor_original&lt;/code>. Lo escribe el scanner &lt;code>Anonymize&lt;/code> en input y lo lee el scanner &lt;code>Deanonymize&lt;/code> en output. Si el Vault es compartido entre múltiples requests del mismo usuario, el mapping persiste (útil para conversaciones multi-turno). Si es por request, se descarta tras la respuesta.&lt;/p>
&lt;p>El Vault básico es &lt;code>dict&lt;/code> Python; para entornos distribuidos con múltiples pods, se sustituye por un Redis sticky (mismo usuario → mismo pod) o por un Vault custom que lea/escriba a un Redis externo, descartado tras un TTL. Esto es operacional, no de la librería core.&lt;/p>
&lt;h2 id="el-flujo-anonymize--llm--deanonymize-en-detalle">El flujo Anonymize → LLM → Deanonymize en detalle&lt;/h2>
&lt;p>El patrón canónico de uso de LLM Guard se descompone en seis pasos exactos:&lt;/p>
&lt;pre tabindex="0">&lt;code>1. Recibir prompt del usuario:
&amp;#34;Mi nombre es Marta García y mi IBAN es ES9121000418450200051332,
¿podéis revisar el cargo del 14 de marzo?&amp;#34;
2. scan_prompt() con [Anonymize(vault), PromptInjection(), Toxicity()]
→ Anonymize redacta entidades y las guarda en vault:
vault[&amp;#34;[REDACTED_PERSON_1]&amp;#34;] = &amp;#34;Marta García&amp;#34;
vault[&amp;#34;[REDACTED_IBAN_1]&amp;#34;] = &amp;#34;ES9121000418450200051332&amp;#34;
→ PromptInjection comprueba que no haya jailbreak (no lo hay)
→ Toxicity comprueba que no haya insultos (no los hay)
→ results_valid = {Anonymize: True, PromptInjection: True, Toxicity: True}
→ sanitized_prompt:
&amp;#34;Mi nombre es [REDACTED_PERSON_1] y mi IBAN es [REDACTED_IBAN_1],
¿podéis revisar el cargo del 14 de marzo?&amp;#34;
3. Llamar al LLM con sanitized_prompt:
→ vLLM recibe el prompt sin PII real
→ genera respuesta:
&amp;#34;Sí, [REDACTED_PERSON_1], voy a revisar el cargo en la cuenta
[REDACTED_IBAN_1]. ¿Puedes confirmar el importe?&amp;#34;
4. scan_output() con [Deanonymize(vault), Toxicity(), Relevance(), Sensitive()]
→ Deanonymize sustituye placeholders por valores del vault:
[REDACTED_PERSON_1] → &amp;#34;Marta García&amp;#34;
[REDACTED_IBAN_1] → &amp;#34;ES9121000418450200051332&amp;#34;
→ Toxicity comprueba que la respuesta no sea ofensiva
→ Relevance comprueba que responde al prompt
→ Sensitive comprueba que no aparezca PII no autorizada
(en este caso, la PII restituida está autorizada porque la trajo
el propio usuario y la firma el Vault → la regla aplica solo a
PII nueva inventada por el LLM)
→ sanitized_response:
&amp;#34;Sí, Marta García, voy a revisar el cargo en la cuenta
ES9121000418450200051332. ¿Puedes confirmar el importe?&amp;#34;
5. Devolver al usuario sanitized_response.
6. Si la sesión sigue, el vault persiste y los próximos turnos reutilizan
los mismos placeholders. Cuando termina la sesión, el vault se descarta.
&lt;/code>&lt;/pre>&lt;p>Tres detalles que importan operativamente:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Las entidades persistentes&lt;/strong> (&lt;code>[REDACTED_PERSON_1]&lt;/code> para &amp;ldquo;Marta García&amp;rdquo;) se mantienen constantes durante la sesión. Si el usuario menciona otra persona (&amp;ldquo;hablé con Juan Pérez&amp;rdquo;), Anonymize asignará &lt;code>[REDACTED_PERSON_2]&lt;/code>. La coherencia inter-turno la asegura el Vault.&lt;/li>
&lt;li>&lt;strong>El LLM nunca ve los datos originales&lt;/strong> durante la sesión. Esto es la propiedad clave para casos donde el LLM se sirve desde un modelo en cloud o cuando se loguea el prompt (Langfuse, OTel) sin acceso confidencial.&lt;/li>
&lt;li>&lt;strong>El logging de LLM Guard registra los placeholders&lt;/strong>, no los valores originales. Para auditoría con valores originales hace falta una capa adicional (acceso al Vault con permisos privilegiados) — esto es por diseño, no por defecto.&lt;/li>
&lt;/ul>
&lt;h2 id="cuatro-modos-de-despliegue">Cuatro modos de despliegue&lt;/h2>
&lt;h3 id="modo-1--librería-python-in-process">Modo 1 — Librería Python in-process&lt;/h3>
&lt;p>El más simple: &lt;code>pip install llm-guard&lt;/code>, importar los scanners en el código de la aplicación, llamar a &lt;code>scan_prompt&lt;/code>/&lt;code>scan_output&lt;/code> directamente. Los modelos se cargan en el proceso. La ventaja es latencia mínima; la desventaja es que cada réplica de la aplicación carga sus propios modelos en memoria.&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"># en el servidor de la app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">scan_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">scan_output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.input_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Anonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.output_scanners&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Deanonymize&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Toxicity&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">OutToxicity&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Relevance&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">llm_guard.vault&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Vault&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">vault&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Vault&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">input_scanners&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">Anonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">PromptInjection&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">Toxicity&lt;/span>&lt;span class="p">()]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">output_scanners&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">Deanonymize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">vault&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">OutToxicity&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">Relevance&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"># en el handler de la request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">sanitized_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">valid_in&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">score_in&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scan_prompt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">input_scanners&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">user_prompt&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="nb">all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">valid_in&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">()):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">error_response&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score_in&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">vllm_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">complete&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">sanitized_prompt&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">sanitized_resp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">valid_out&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">score_out&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">scan_output&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">output_scanners&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sanitized_prompt&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">response&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="nb">all&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">valid_out&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">()):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">error_response&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">score_out&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="k">return&lt;/span> &lt;span class="n">sanitized_resp&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Encaja con el &lt;strong>patrón A (sidecar)&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post de guardrails&lt;/a> cuando la app y el sidecar comparten proceso. Y con el &lt;strong>patrón C (in-process)&lt;/strong> si la app es directamente la capa de inferencia.&lt;/p>
&lt;h3 id="modo-2--api-fastapi-propia">Modo 2 — API FastAPI propia&lt;/h3>
&lt;p>El proyecto incluye un servidor FastAPI listo (&lt;code>llm-guard-api&lt;/code>) que expone los scanners detrás de dos endpoints REST:&lt;/p>
&lt;pre tabindex="0">&lt;code>POST /analyze/prompt
body: {&amp;#34;prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;scanners&amp;#34;: [...] (opcional)}
response: {&amp;#34;sanitized_prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;is_valid&amp;#34;: bool, &amp;#34;scanners&amp;#34;: {scanner: {is_valid, risk_score}}}
POST /analyze/output
body: {&amp;#34;prompt&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;scanners&amp;#34;: [...]}
response: análoga
&lt;/code>&lt;/pre>&lt;p>Configuración por &lt;code>config/scanners.yml&lt;/code> con variables de entorno (&lt;code>SCAN_FAIL_FAST&lt;/code>, &lt;code>CACHE_MAX_SIZE&lt;/code>, &lt;code>CACHE_TTL&lt;/code>, &lt;code>SCAN_PROMPT_TIMEOUT&lt;/code>&amp;hellip;). Lleva métricas Prometheus en &lt;code>/metrics&lt;/code> y traces OTel HTTP exporter por defecto.&lt;/p>
&lt;p>Encaja con el &lt;strong>patrón B (servicio centralizado tras AI Gateway)&lt;/strong> del post de guardrails.&lt;/p>
&lt;h3 id="modo-3--sidecar-otel-sobre-el-pod-del-motor-de-inferencia">Modo 3 — Sidecar OTel sobre el pod del motor de inferencia&lt;/h3>
&lt;p>Para deployments de vLLM en Kubernetes, una variante del modo 2 es desplegar la API de LLM Guard como &lt;strong>sidecar container&lt;/strong> en el mismo pod del vLLM, hablando por localhost. El AI Gateway delante invoca al sidecar antes y después de la inferencia. El OTel collector del nodo agrega los spans de vLLM con los spans &lt;code>gen_ai.guardrail.*&lt;/code> de LLM Guard automáticamente porque comparten &lt;code>trace_id&lt;/code> propagado por baggage HTTP.&lt;/p>
&lt;p>Esto encaja con el &lt;strong>patrón A (sidecar)&lt;/strong> del post de guardrails, pero con la disciplina de la API REST para no acoplar lenguaje (el AI Gateway puede ser Envoy en C++, LLM Guard en Python).&lt;/p>
&lt;h3 id="modo-4--plugin-dentro-de-un-ai-gateway">Modo 4 — Plugin dentro de un AI Gateway&lt;/h3>
&lt;p>Tres AI Gateways soportan LLM Guard como plugin nativo en 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LiteLLM Proxy&lt;/strong> (MIT, BerriAI) — plugin &lt;code>llm_guard&lt;/code> activable en config con &lt;code>guardrails: [&amp;quot;llm_guard&amp;quot;]&lt;/code>. Llama internamente a la API.&lt;/li>
&lt;li>&lt;strong>Envoy AI Gateway&lt;/strong> (CNCF, Apache 2.0) — filtro &lt;code>ai-guardrails&lt;/code> con backend pluggable apuntando al servicio LLM Guard.&lt;/li>
&lt;li>&lt;strong>Kong AI Gateway&lt;/strong> (Apache 2.0) — plugin &lt;code>ai-proxy&lt;/code> con post-procesador que invoca LLM Guard.&lt;/li>
&lt;/ul>
&lt;p>En los tres casos, el AI Gateway es el punto único de entrada de la app cliente al LLM; el gateway llama a LLM Guard antes/después de pasar al motor de inferencia. Ventaja: lock-in cero en el código de la aplicación; cambiar de LLM Guard a NeMo Guardrails es cambiar el plugin del gateway, no reescribir la app. Desventaja: el hop adicional añade latencia (típicamente 5-15 ms intra-cluster).&lt;/p>
&lt;h2 id="integración-gráfica-con-langfuse-vllm-y-el-stack-otel">Integración gráfica con Langfuse, vLLM y el stack OTel&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Integración de LLM Guard con Langfuse, vLLM y el stack OTel">
&lt;style>
.b-app{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.b-gw{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:8}
.b-lg{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:8}
.b-vllm{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.b-otel{fill:#c8b8ff;stroke:#444;stroke-width:1.4;rx:8}
.b-langfuse{fill:#f8a8d8;stroke:#444;stroke-width:1.4;rx:8}
.b-storage{fill:#f0e8c0;stroke:#444;stroke-width:1.4;rx:8}
.bl{font:600 13px sans-serif;fill:#222}
.bs{font:400 11px sans-serif;fill:#444}
.bn{font:italic 10px sans-serif;fill:#555}
.bar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mb1)}
.bart{stroke:#5a5;stroke-width:1.4;fill:none;stroke-dasharray:5 3;marker-end:url(#mb2)}
&lt;/style>
&lt;defs>
&lt;marker id="mb1" 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;marker id="mb2" 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="#5a5"/>&lt;/marker>
&lt;/defs>
&lt;rect x="20" y="40" width="140" height="50" class="b-app"/>
&lt;text x="90" y="60" text-anchor="middle" class="bl">App cliente&lt;/text>
&lt;text x="90" y="78" text-anchor="middle" class="bs">chatbot · backend · agente&lt;/text>
&lt;rect x="200" y="40" width="180" height="50" class="b-gw"/>
&lt;text x="290" y="60" text-anchor="middle" class="bl">AI Gateway&lt;/text>
&lt;text x="290" y="78" text-anchor="middle" class="bs">LiteLLM · Envoy AI · Kong AI&lt;/text>
&lt;rect x="430" y="20" width="160" height="40" class="b-lg"/>
&lt;text x="510" y="38" text-anchor="middle" class="bl">LLM Guard API&lt;/text>
&lt;text x="510" y="54" text-anchor="middle" class="bs">scan_prompt + scan_output&lt;/text>
&lt;rect x="430" y="70" width="160" height="40" class="b-vllm"/>
&lt;text x="510" y="88" text-anchor="middle" class="bl">vLLM&lt;/text>
&lt;text x="510" y="104" text-anchor="middle" class="bs">motor inferencia + adapter&lt;/text>
&lt;rect x="640" y="40" width="160" height="50" class="b-storage"/>
&lt;text x="720" y="60" text-anchor="middle" class="bl">Vault Redis&lt;/text>
&lt;text x="720" y="78" text-anchor="middle" class="bs">mapping placeholder→PII&lt;/text>
&lt;path class="bar" d="M160,65 L200,65"/>
&lt;path class="bar" d="M380,55 L430,40"/>
&lt;path class="bar" d="M380,75 L430,90"/>
&lt;path class="bar" d="M510,60 L640,65"/>
&lt;text x="170" y="55" class="bn">1: prompt&lt;/text>
&lt;text x="390" y="35" class="bn">2: pre-scan&lt;/text>
&lt;text x="390" y="105" class="bn">3: inferencia&lt;/text>
&lt;text x="555" y="55" class="bn">vault R/W&lt;/text>
&lt;rect x="20" y="180" width="240" height="80" class="b-otel"/>
&lt;text x="140" y="202" text-anchor="middle" class="bl">OTel Collector (DaemonSet)&lt;/text>
&lt;text x="140" y="220" text-anchor="middle" class="bs">recibe spans gen_ai.* y&lt;/text>
&lt;text x="140" y="234" text-anchor="middle" class="bs">gen_ai.guardrail.* desde:&lt;/text>
&lt;text x="140" y="250" text-anchor="middle" class="bs">vLLM, LLM Guard, AI Gateway&lt;/text>
&lt;path class="bart" d="M510,110 Q260,140 140,178"/>
&lt;path class="bart" d="M510,60 Q330,140 200,178"/>
&lt;path class="bart" d="M290,90 Q230,140 140,178"/>
&lt;text x="320" y="135" class="bn">spans OTel HTTP&lt;/text>
&lt;rect x="300" y="180" width="200" height="80" class="b-langfuse"/>
&lt;text x="400" y="202" text-anchor="middle" class="bl">Langfuse&lt;/text>
&lt;text x="400" y="220" text-anchor="middle" class="bs">/api/public/otel ingestion&lt;/text>
&lt;text x="400" y="236" text-anchor="middle" class="bs">+ /api/public/scores&lt;/text>
&lt;text x="400" y="252" text-anchor="middle" class="bs">+ datasets + sessions&lt;/text>
&lt;path class="bar" d="M260,220 L300,220"/>
&lt;text x="270" y="215" class="bn">OTLP&lt;/text>
&lt;rect x="540" y="180" width="120" height="40" class="b-otel"/>
&lt;text x="600" y="200" text-anchor="middle" class="bl">Tempo / Jaeger&lt;/text>
&lt;text x="600" y="216" text-anchor="middle" class="bs">trace storage&lt;/text>
&lt;rect x="540" y="225" width="120" height="40" class="b-otel"/>
&lt;text x="600" y="245" text-anchor="middle" class="bl">VictoriaMetrics&lt;/text>
&lt;text x="600" y="261" text-anchor="middle" class="bs">métricas Prom&lt;/text>
&lt;path class="bar" d="M260,210 L540,200"/>
&lt;path class="bar" d="M260,235 L540,240"/>
&lt;rect x="700" y="180" width="100" height="80" class="b-storage"/>
&lt;text x="750" y="202" text-anchor="middle" class="bl">Grafana&lt;/text>
&lt;text x="750" y="220" text-anchor="middle" class="bs">datasource&lt;/text>
&lt;text x="750" y="234" text-anchor="middle" class="bs">Tempo + VM&lt;/text>
&lt;text x="750" y="252" text-anchor="middle" class="bs">+ Langfuse&lt;/text>
&lt;path class="bar" d="M660,220 L700,220"/>
&lt;rect x="20" y="320" width="780" height="50" class="b-gw"/>
&lt;text x="410" y="340" text-anchor="middle" class="bl">Plano scoring de Langfuse: el AI Gateway postea langfuse.score(trace_id, name="guardrail.PromptInjection", value=risk_score)&lt;/text>
&lt;text x="410" y="356" text-anchor="middle" class="bs">por cada scanner ejecutado; eso permite a Langfuse construir dashboards de "% bloqueos por categoría" y series temporales&lt;/text>
&lt;path class="bar" d="M290,90 Q290,290 400,320"/>
&lt;text x="305" y="200" class="bn">scores HTTP&lt;/text>
&lt;text x="410" y="400" text-anchor="middle" class="bn">Tres planos de telemetría se mezclan: traces (OTel → Tempo + Langfuse), métricas (Prometheus → VictoriaMetrics), scores (Langfuse SDK).&lt;/text>
&lt;text x="410" y="418" text-anchor="middle" class="bn">Grafana los une por trace_id; Langfuse los une por session_id + trace_id propagado.&lt;/text>
&lt;text x="410" y="438" text-anchor="middle" class="bn">El Vault Redis tiene su propio plano de datos y NO se exporta a observabilidad — la PII original nunca sale de él.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Las &lt;strong>tres rutas de integración con Langfuse&lt;/strong> que importan operativamente:&lt;/p>
&lt;p>&lt;strong>Ruta A — OTel HTTP exporter de LLM Guard.&lt;/strong> LLM Guard tiene exporter OTel HTTP nativo. Configurando &lt;code>OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://langfuse.cluster/api/public/otel&lt;/code>, los spans &lt;code>gen_ai.guardrail.*&lt;/code> que emite cada scanner llegan directamente a Langfuse y aparecen como spans hijos del span LLM principal (siempre que el &lt;code>trace_id&lt;/code> se propague vía baggage HTTP desde el AI Gateway). Esta es la ruta canónica en 2026.&lt;/p>
&lt;p>&lt;strong>Ruta B — Langfuse scoring API desde el AI Gateway.&lt;/strong> El AI Gateway (LiteLLM, Envoy AI, Kong AI), al recibir la respuesta de LLM Guard con los &lt;code>risk_score&lt;/code> por scanner, emite una llamada &lt;code>langfuse.score(trace_id, name=&amp;quot;guardrail.PromptInjection&amp;quot;, value=0.87, comment=&amp;quot;blocked&amp;quot;)&lt;/code> por cada scanner. En Langfuse aparece como scores enganchados al mismo trace que la inferencia. Permite dashboards &amp;ldquo;bloqueos por categoría&amp;rdquo; y series temporales por scanner. Es &lt;strong>complementaria&lt;/strong> a la ruta A: la A trae los spans, la B trae el score numérico fácil de agregar en SQL.&lt;/p>
&lt;p>&lt;strong>Ruta C — Sessions de Langfuse + Vault metadata.&lt;/strong> En modo conversacional, el AI Gateway propaga &lt;code>langfuse_session_id&lt;/code> al Vault como su clave. Cuando un usuario tiene una sesión multi-turno, Langfuse muestra la traza completa de la sesión, con los placeholders que se reutilizan turno a turno. La PII original sigue sin viajar a Langfuse — sólo los placeholders y sus categorías.&lt;/p>
&lt;p>El &lt;strong>OTel Collector&lt;/strong> del nodo es el pegamento: recibe spans de vLLM (por OpenLLMetry o instrumentación nativa), de LLM Guard (por su exporter OTel) y del AI Gateway (instrumentación HTTP estándar), los &lt;strong>une por trace_id&lt;/strong>, y los envía paralelamente a Langfuse (vía OTLP HTTP) y a Tempo/Jaeger. Las métricas Prometheus de LLM Guard van a VictoriaMetrics por scraping normal. Grafana ofrece la vista unificada para investigación cross-trace; Langfuse ofrece la vista LLM-céntrica con sessions y scores. El &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">post sobre tracing OTel GenAI&lt;/a> detalla la mecánica completa del Collector.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan">Las matemáticas que importan&lt;/h2>
&lt;h3 id="latencia-por-scanner--los-números-reales">Latencia por scanner — los números reales&lt;/h3>
&lt;p>El proyecto publica benchmarks reproducibles. Para el scanner Anonymize (input length 317 chars, batch 5), los datos de referencia son:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Plataforma&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Latencia avg&lt;/th>
&lt;th>p99&lt;/th>
&lt;th>QPS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>AWS m5.xlarge (CPU)&lt;/td>
&lt;td>Transformers&lt;/td>
&lt;td>177 ms&lt;/td>
&lt;td>326 ms&lt;/td>
&lt;td>1.789&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS m5.xlarge (CPU)&lt;/td>
&lt;td>&lt;strong>ONNX runtime&lt;/strong>&lt;/td>
&lt;td>&lt;strong>128 ms&lt;/strong>&lt;/td>
&lt;td>180 ms&lt;/td>
&lt;td>2.464&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS r6a.xlarge (AMD CPU)&lt;/td>
&lt;td>Transformers&lt;/td>
&lt;td>244 ms&lt;/td>
&lt;td>284 ms&lt;/td>
&lt;td>1.298&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS g5.xlarge (NVIDIA A10G)&lt;/td>
&lt;td>Transformers FP16&lt;/td>
&lt;td>125 ms&lt;/td>
&lt;td>498 ms&lt;/td>
&lt;td>2.532&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS g5.xlarge (A10G)&lt;/td>
&lt;td>&lt;strong>ONNX + GPU&lt;/strong>&lt;/td>
&lt;td>&lt;strong>38 ms&lt;/strong>&lt;/td>
&lt;td>99 ms&lt;/td>
&lt;td>8.317&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tres observaciones operativas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ONNX siempre gana.&lt;/strong> Incluso en CPU, ONNX baja el avg de 177 a 128 ms (factor 1,4×). En GPU con ONNX, baja de 177 a 38 ms (factor 4,6×). La regla práctica: &lt;strong>siempre exportar el modelo del scanner a ONNX antes de producción&lt;/strong>. La preview del SaaS oficial lo usa por defecto.&lt;/li>
&lt;li>&lt;strong>GPU sin ONNX no rinde tanto como uno espera.&lt;/strong> Una A10G sin ONNX (125 ms) es comparable a m5.xlarge con ONNX (128 ms). La GPU sola no compensa si el grafo de inferencia no está optimizado. El binomio relevante es ONNX + GPU.&lt;/li>
&lt;li>&lt;strong>La latencia p99 sin ONNX explota.&lt;/strong> En GPU sin ONNX, el p99 de 498 ms triplica el avg de 125 ms — colas y batching producen tail latencies altas. Con ONNX, el ratio p99/avg cae a 2,6× (99/38), mucho más predecible.&lt;/li>
&lt;/ol>
&lt;p>Para una capa de guardrails con cinco scanners ejecutados secuencialmente (Anonymize, PromptInjection, Toxicity, Secrets, BanTopics), la suma del p99 es lo que determina el budget de la línea 1 (input). Cinco scanners a ~100 ms p99 cada uno = 500 ms p99 acumulado — fuera de presupuesto para chat interactivo. Con ONNX bajamos a ~50 ms cada uno = 250 ms p99 — manejable. &lt;strong>Con &lt;code>fail_fast=True&lt;/code>&lt;/strong>, el tiempo esperado es menor (el más probable es que pasen los más baratos y fallen los caros sólo si se ejecutan).&lt;/p>
&lt;p>Para un cálculo más fino, la latencia esperada del pipeline con &lt;code>fail_fast&lt;/code> es:&lt;/p>
&lt;p>[
\mathbb{E}[L] = \sum_{i=1}^{N} L_i \cdot \prod_{j=1}^{i-1} p_j
]&lt;/p>
&lt;p>donde (L_i) es la latencia del scanner (i) y (p_j) la probabilidad de que el scanner (j) devuelva válido. En tráfico bien comportado (la mayoría de prompts pasan todos los scanners), (\prod p_j \approx 1) y la fórmula colapsa a la suma directa. En tráfico adversarial, los scanners más rápidos al principio del pipeline cortan antes y la latencia esperada baja drásticamente.&lt;/p>
&lt;h3 id="coste-computacional-por-scanner">Coste computacional por scanner&lt;/h3>
&lt;p>El tamaño del modelo backend determina el coste y la posibilidad de correr en CPU vs requerir GPU:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Modelo backend típico&lt;/th>
&lt;th>Parámetros&lt;/th>
&lt;th>VRAM FP16 / ONNX-INT8&lt;/th>
&lt;th>CPU viable&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Anonymize (BERT-NER)&lt;/td>
&lt;td>dslim/bert-base-NER&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí (con ONNX)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Anonymize (BERT-large)&lt;/td>
&lt;td>dslim/bert-large-NER&lt;/td>
&lt;td>335 M&lt;/td>
&lt;td>670 MB / 170 MB&lt;/td>
&lt;td>Sí pero lento (~500 ms CPU)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PromptInjection&lt;/td>
&lt;td>DeBERTa-v3-base fine-tuned&lt;/td>
&lt;td>184 M&lt;/td>
&lt;td>370 MB / 90 MB&lt;/td>
&lt;td>Sí (con ONNX)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Toxicity&lt;/td>
&lt;td>unitary/toxic-bert&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sentiment&lt;/td>
&lt;td>distilbert-sst2&lt;/td>
&lt;td>67 M&lt;/td>
&lt;td>130 MB / 35 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gibberish&lt;/td>
&lt;td>small distilbert&lt;/td>
&lt;td>67 M&lt;/td>
&lt;td>130 MB / 35 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BanTopics&lt;/td>
&lt;td>BART-MNLI zero-shot&lt;/td>
&lt;td>407 M&lt;/td>
&lt;td>815 MB / 200 MB&lt;/td>
&lt;td>Lento en CPU (~400 ms)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bias (output)&lt;/td>
&lt;td>RoBERTa-bias&lt;/td>
&lt;td>125 M&lt;/td>
&lt;td>250 MB / 65 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FactualConsistency&lt;/td>
&lt;td>cross-encoder/nli-deberta&lt;/td>
&lt;td>184 M&lt;/td>
&lt;td>370 MB / 90 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Relevance&lt;/td>
&lt;td>sentence-transformers&lt;/td>
&lt;td>110 M&lt;/td>
&lt;td>220 MB / 55 MB&lt;/td>
&lt;td>Sí&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TokenLimit, Regex, JSON, BanSubstrings, Secrets&lt;/td>
&lt;td>(sin modelo)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>0&lt;/td>
&lt;td>Trivial&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Patrón razonable on-premise&lt;/strong>: scanners sin modelo (TokenLimit, Regex, BanSubstrings, Secrets) corren en CPU sin pestañear. Anonymize, PromptInjection, Toxicity, Sentiment, Relevance corren cómodamente en CPU con ONNX-INT8 con ~50-150 ms p99. BanTopics y los basados en cross-encoder grandes (FactualConsistency) son los candidatos a vivir en una GPU compartida si quieres p99 &amp;lt; 100 ms.&lt;/p>
&lt;h3 id="throughput-de-la-api-en-cluster">Throughput de la API en cluster&lt;/h3>
&lt;p>Una instancia de la API FastAPI con 4 workers Uvicorn sobre un nodo con 8 vCPUs alcanza ~600-1.200 RPS sobre un pipeline típico de 5 scanners en CPU + ONNX. Para escalar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Horizontal&lt;/strong>: replicar pods detrás de un Service ClusterIP — escalado lineal porque los scanners son stateless (excepto el Vault, que es por sesión y se externaliza a Redis si se quiere sticky o compartido).&lt;/li>
&lt;li>&lt;strong>Vertical con GPU&lt;/strong>: 1 H100 sirve ~5.000-10.000 RPS con todos los scanners en ONNX-GPU. Es overkill para la mayoría de deployments excepto en multi-tenant con miles de QPS sostenidos.&lt;/li>
&lt;/ul>
&lt;p>La regla práctica del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post sobre guardrails&lt;/a> (1 GPU guardrails por 4-8 GPUs LLM) se mantiene aquí: con cluster 4×H100 SXM sirviendo Llama 70B en TP=4, una L4 o RTX 4090 dedicada al servicio LLM Guard cubre la carga.&lt;/p>
&lt;h2 id="comparativa-con-nemo-guardrails-y-guardrails-ai">Comparativa con NeMo Guardrails y Guardrails AI&lt;/h2>
&lt;p>Las tres herramientas resuelven el mismo problema desde tres modelos arquitectónicos distintos. La elección entre ellas no es de calidad —las tres están maduras—, es de &lt;strong>encaje con el resto del stack&lt;/strong>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimensión&lt;/th>
&lt;th>LLM Guard&lt;/th>
&lt;th>NeMo Guardrails&lt;/th>
&lt;th>Guardrails AI&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Modelo conceptual&lt;/strong>&lt;/td>
&lt;td>Pipeline de scanners compactos&lt;/td>
&lt;td>Grafo declarativo Colang (flujo conversacional)&lt;/td>
&lt;td>Validators tipo contrato JSON&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Detección dominante&lt;/strong>&lt;/td>
&lt;td>Modelos ML especializados (BERT, DeBERTa) por categoría&lt;/td>
&lt;td>Reglas + LLM-as-judge&lt;/td>
&lt;td>Validators heurísticos + LLM-as-judge externo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>PII workflow&lt;/strong>&lt;/td>
&lt;td>Anonymize + Vault + Deanonymize&lt;/td>
&lt;td>Vía Presidio integrado, sin Vault built-in&lt;/td>
&lt;td>Validators de PII, sin restitución automática&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Licencia&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0 (+ Hub paid)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Lenguaje&lt;/strong>&lt;/td>
&lt;td>Python&lt;/td>
&lt;td>Python + Colang DSL&lt;/td>
&lt;td>Python&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Madurez API&lt;/strong>&lt;/td>
&lt;td>API FastAPI built-in, OTel built-in&lt;/td>
&lt;td>Server FastAPI built-in, OTel parcial&lt;/td>
&lt;td>API server externo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Despliegue cluster&lt;/strong>&lt;/td>
&lt;td>Lib + API + sidecar + plugin gateways&lt;/td>
&lt;td>Lib + server&lt;/td>
&lt;td>Lib + server + Hub SaaS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Latencia típica (5 scanners ONNX-GPU)&lt;/strong>&lt;/td>
&lt;td>50-200 ms&lt;/td>
&lt;td>100-500 ms (más si hay LLM judge)&lt;/td>
&lt;td>100-300 ms (depende del validator)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo brilla&lt;/strong>&lt;/td>
&lt;td>Apps con PII fuerte, multi-tenant con sesiones, requisitos GDPR/HIPAA&lt;/td>
&lt;td>Sistemas conversacionales con flujos definidos, agentes con dialog policy&lt;/td>
&lt;td>Apps con contratos JSON estrictos, structured output con validación adicional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cuándo no encaja&lt;/strong>&lt;/td>
&lt;td>Si necesitas dialog policy declarativa&lt;/td>
&lt;td>Si quieres detectores compactos sin LLM judge&lt;/td>
&lt;td>Si quieres Vault y Deanonymize automático&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Los tres son &lt;strong>complementarios en deployments grandes&lt;/strong>. Un patrón maduro en 2026 es:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>NeMo Guardrails&lt;/strong> orquesta el flujo de diálogo (qué tools puede invocar el agente, en qué orden, con qué cooldowns).&lt;/li>
&lt;li>&lt;strong>LLM Guard&lt;/strong> ocupa la línea de PII + scanners compactos en input y output, con su Vault haciendo el trabajo sucio de anonimización.&lt;/li>
&lt;li>&lt;strong>Guardrails AI&lt;/strong> valida outputs estructurados (JSON Schema, function calling) con sus validators.&lt;/li>
&lt;/ul>
&lt;p>La separación de responsabilidades evita el solapamiento y permite cambiar piezas sin reescribir todo. Las tres exponen API FastAPI y emiten spans OTel; el AI Gateway las orquesta secuencialmente.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise">Aplicado a hardware on-premise&lt;/h2>
&lt;h3 id="en-la-rtx-4090-24-gb">En la RTX 4090 (24 GB)&lt;/h3>
&lt;p>Una 4090 dedicada al pod del servicio LLM Guard sirve cómodamente el pipeline completo en producción media:&lt;/p>
&lt;ul>
&lt;li>Anonymize (BERT-NER ONNX-INT8): ~50 MB VRAM.&lt;/li>
&lt;li>PromptInjection (DeBERTa ONNX-INT8): ~90 MB.&lt;/li>
&lt;li>Toxicity, Sentiment, Gibberish: ~150 MB total.&lt;/li>
&lt;li>BanTopics (BART-MNLI ONNX-INT8): ~200 MB.&lt;/li>
&lt;li>Bias, Relevance, FactualConsistency (output): ~250 MB total.&lt;/li>
&lt;/ul>
&lt;p>Total ~750 MB. Resto de la VRAM ociosa o aprovechable para batching agresivo. Throughput sostenido a 3.000-6.000 RPS sobre el pipeline completo. Para deployments con &amp;lt; 500 RPS sostenidos, la 4090 está sub-utilizada y se puede compartir con otra carga (embeddings de RAG, reranker BGE).&lt;/p>
&lt;h3 id="en-el-cluster-4h100-sxm-320-gb-total-nvlink">En el cluster 4×H100 SXM (320 GB total, NVLink)&lt;/h3>
&lt;p>Sobra capacidad por orden de magnitud. Patrón razonable:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>3 H100&lt;/strong> sirviendo el LLM principal en TP=3 (Llama 70B FP8).&lt;/li>
&lt;li>&lt;strong>1 H100 dividida en MIG instances&lt;/strong> (1g.10gb o similar) — una porción para LLM Guard (~10 GB MIG es más que suficiente), otra para el reranker, otra para embeddings.&lt;/li>
&lt;/ul>
&lt;p>Throughput agregado para LLM Guard a esa escala: 15.000-30.000 RPS. Sobra para multi-tenant grande con sesiones largas.&lt;/p>
&lt;h2 id="las-trampas-operativas-específicas">Las trampas operativas específicas&lt;/h2>
&lt;p>&lt;strong>Trampa 1 — Vault sin TTL.&lt;/strong> El Vault crece sin freno si no se limpia. En modo lib in-process por request, no hay problema (el objeto se destruye). En modo servicio centralizado con Redis, &lt;strong>falta poner TTL&lt;/strong> y el Redis se llena. Trampa silenciosa que se descubre cuando el pod de Redis OOM-killea en producción a las 6 semanas.&lt;/p>
&lt;p>&lt;strong>Trampa 2 — Vault no compartido entre pods + AI Gateway sin sticky session.&lt;/strong> Si el AI Gateway distribuye round-robin entre múltiples pods de LLM Guard, el Vault local de un pod no sabe del mapping creado por otro. Resultado: en el turno 2 de una sesión, el Deanonymize no encuentra los placeholders del turno 1 y deja &lt;code>[REDACTED_PERSON_1]&lt;/code> literal en la respuesta. Solución: Vault Redis compartido &lt;strong>o&lt;/strong> sticky session por user_id.&lt;/p>
&lt;p>&lt;strong>Trampa 3 — Modelos no exportados a ONNX en producción.&lt;/strong> Se despliega con la config por defecto (Transformers) y la latencia es 3-5× peor que la que reportan los benchmarks. Equipo asume que LLM Guard &amp;ldquo;es lento&amp;rdquo;. La solución es exportar a ONNX (built-in en el proyecto) y configurar &lt;code>recognizer_conf&lt;/code> con la ruta al &lt;code>.onnx&lt;/code> del modelo.&lt;/p>
&lt;p>&lt;strong>Trampa 4 — &lt;code>fail_fast=False&lt;/code> con muchos scanners.&lt;/strong> Sin &lt;code>fail_fast&lt;/code>, todos los scanners corren siempre, incluso si el primero ya bloqueó. Latencia 3-5× peor en tráfico adversarial. Para producción, salvo razón explícita (querer métricas completas por scanner aun bloqueando), &lt;code>fail_fast=True&lt;/code> es el default razonable.&lt;/p>
&lt;p>&lt;strong>Trampa 5 — &lt;code>cache_ttl&lt;/code> infinito + prompts con PII variable.&lt;/strong> Si la caché de la API guarda el &lt;code>sanitized_prompt&lt;/code> indefinidamente, dos sesiones distintas con misma estructura de prompt pero diferentes PII pueden colidir si la clave de caché no incluye el Vault hash. Hay que verificar que la clave de caché incluya o bien el contenido completo (sin PII) o un hash del prompt original.&lt;/p>
&lt;p>&lt;strong>Trampa 6 — Logs estructurados con PII original.&lt;/strong> Los logs stdout JSON de LLM Guard registran por defecto sólo placeholders. Pero si se añaden hooks custom para debug, es fácil filtrar la PII original al log. Auditoría regulatoria (RGPD, ENS) detecta esto y es incumplimiento. Disciplina: nunca añadir hooks que lean del Vault sin permiso explícito.&lt;/p>
&lt;p>&lt;strong>Trampa 7 — &lt;code>scan_output&lt;/code> sin &lt;code>prompt&lt;/code> original.&lt;/strong> El método &lt;code>scan_output&lt;/code> espera (&lt;code>prompt&lt;/code>, &lt;code>output&lt;/code>) para validadores que comparan ambos (Relevance, LanguageSame, FactualConsistency). Si se le pasa sólo el output, esos scanners fallan silenciosamente o devuelven &lt;code>is_valid=True&lt;/code> por defecto. Hay que conservar el &lt;code>sanitized_prompt&lt;/code> en el AI Gateway y pasarlo al scan_output.&lt;/p>
&lt;h2 id="cuándo-elegir-llm-guard-y-cuándo-no">Cuándo elegir LLM Guard (y cuándo no)&lt;/h2>
&lt;p>&lt;strong>Elegir LLM Guard cuando&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>El requisito de &lt;strong>anonimización PII con restitución automática&lt;/strong> está en la lista. Es la razón #1 para usarlo. Banca, salud, asesoría legal, RRHH — cualquier caso con PII fuerte que no debe llegar al LLM aunque éste sea local.&lt;/li>
&lt;li>Quieres un &lt;strong>pipeline pythonic&lt;/strong> sin DSL nuevo. Si el equipo es Python-puro y prefiere componer scanners como objetos antes que aprender Colang.&lt;/li>
&lt;li>El stack ya tiene un &lt;strong>AI Gateway&lt;/strong> (LiteLLM, Envoy AI, Kong AI) y se integra como plugin sin tocar la app.&lt;/li>
&lt;li>Necesitas &lt;strong>OTel y Prometheus built-in&lt;/strong> sin instrumentación adicional.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>No elegir LLM Guard cuando&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>El sistema es un &lt;strong>agente conversacional con flujos de diálogo complejos&lt;/strong> (políticas, fallbacks, escalado a humano). Ahí NeMo Guardrails con Colang es estructuralmente mejor.&lt;/li>
&lt;li>La capa de safety se reduce a &lt;strong>validar outputs estructurados&lt;/strong> (JSON, function calling). Guardrails AI con sus validators es más natural.&lt;/li>
&lt;li>Tu &lt;strong>latencia budget es ultra-agresivo&lt;/strong> (&amp;lt; 30 ms para toda la capa). Habrá que reducir scanners y aceptar cobertura menor; quizás un único PromptGuard 2 + Presidio en sidecar (patrón del &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">post de guardrails&lt;/a>) sea más simple.&lt;/li>
&lt;li>No quieres cargar con &lt;strong>el peso operativo del Vault distribuido&lt;/strong> (Redis, TTL, sticky session). Para sistemas pequeños sin requerimiento fuerte de PII, sobra-dimensiona.&lt;/li>
&lt;/ul>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-posts">Lo que no hemos cubierto (próximos posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Custom scanners&lt;/strong>: cómo escribir tu propio scanner cuando ninguno del catálogo encaja (regex compleja de dominio, classifier fine-tuned propio). El proyecto admite scanners custom heredando de &lt;code>InputScanner&lt;/code> / &lt;code>OutputScanner&lt;/code> con tres métodos.&lt;/li>
&lt;li>&lt;strong>Integración con SLSA / supply chain&lt;/strong>: cómo firmar el contenedor de LLM Guard con cosign, attestations SLSA, y verificación en cluster antes de admitirlo. Tema operativo de &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">seguridad de supply chain&lt;/a> (OWASP LLM03).&lt;/li>
&lt;li>&lt;strong>Red teaming contra LLM Guard&lt;/strong>: técnicas conocidas que evaden detectores (homoglyphs, Unicode confusables, encoding base64 dentro del prompt). El proyecto publica un suite de tests adversariales para hacer benchmarking propio. Cómo se monta como gate continuo en CI.&lt;/li>
&lt;li>&lt;strong>Benchmark comparativo con Bedrock Guardrails y Azure AI Content Safety&lt;/strong>: F1 por categoría sobre tráfico real cruzando tres deployments distintos. El &lt;a href="https://blog.lo0.es/posts/oss-vs-hyperscalers-llmops/">post de OSS vs hyperscalers&lt;/a> tiene la comparativa estratégica; falta el comparativo técnico de detección.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>LLM Guard (Protect AI)&lt;/strong>: &lt;a href="https://llm-guard.com">https://llm-guard.com&lt;/a> — documentación oficial, lista de scanners, benchmarks.&lt;/li>
&lt;li>&lt;strong>Repositorio&lt;/strong>: &lt;a href="https://github.com/protectai/llm-guard">https://github.com/protectai/llm-guard&lt;/a>.&lt;/li>
&lt;li>&lt;strong>LLM Guard API&lt;/strong>: &lt;a href="https://github.com/protectai/llm-guard/tree/main/llm_guard_api">https://github.com/protectai/llm-guard/tree/main/llm_guard_api&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Presidio (Microsoft)&lt;/strong>: &lt;a href="https://microsoft.github.io/presidio/">https://microsoft.github.io/presidio/&lt;/a> — base del scanner Anonymize.&lt;/li>
&lt;li>&lt;strong>detect-secrets (Yelp)&lt;/strong>: &lt;a href="https://github.com/Yelp/detect-secrets">https://github.com/Yelp/detect-secrets&lt;/a> — base del scanner Secrets.&lt;/li>
&lt;li>&lt;strong>Langfuse OTel ingestion&lt;/strong>: &lt;a href="https://langfuse.com/docs/opentelemetry/get-started">https://langfuse.com/docs/opentelemetry/get-started&lt;/a>.&lt;/li>
&lt;li>&lt;strong>LiteLLM guardrails&lt;/strong>: &lt;a href="https://docs.litellm.ai/docs/proxy/guardrails">https://docs.litellm.ai/docs/proxy/guardrails&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Envoy AI Gateway&lt;/strong>: &lt;a href="https://aigateway.envoyproxy.io">https://aigateway.envoyproxy.io&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Kong AI Gateway&lt;/strong>: &lt;a href="https://docs.konghq.com/hub/kong-inc/ai-prompt-guard/">https://docs.konghq.com/hub/kong-inc/ai-prompt-guard/&lt;/a>.&lt;/li>
&lt;li>&lt;strong>OWASP Top 10 for LLM Applications 2025&lt;/strong>: &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;li>&lt;strong>ONNX Runtime&lt;/strong>: &lt;a href="https://onnxruntime.ai">https://onnxruntime.ai&lt;/a> — exportación de modelos HF a ONNX para acelerar.&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/guardrails-safety-llm/">Guardrails y safety en LLMs: las cuatro líneas de defensa&lt;/a> — el marco que ubica LLM Guard como una de las herramientas dentro de la capa. Aquel post explica las cuatro líneas (input, retrieval, tool, output), OWASP LLM Top 10 y compara a vista de pájaro NeMo Guardrails, Llama Guard 4, ShieldGemma, Granite Guardian, PromptGuard 2 y LLM Guard.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">El catálogo OSS para LLMOps en seis etapas&lt;/a> — ficha extendida de LLM Guard entre el resto de herramientas OSS por etapa del pipeline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation: el bibliotecario activo&lt;/a> — la prevención en ingest comparte el detector PII de Presidio con LLM Guard; el patrón Vault es la pieza nueva que se añade en runtime.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — el plano OTel sobre el que LLM Guard emite spans &lt;code>gen_ai.guardrail.*&lt;/code> que Langfuse y Tempo consumen.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow&lt;/a> — el &lt;code>prompt_id+version&lt;/code> viaja como atributo de span aunque el contenido del prompt esté anonimizado; complementa el blindaje PII de este post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs: la capa después del tracing&lt;/a> — la pareja offline de LLM Guard. Cuando un scanner reporta tasa alta de FP sobre tráfico real, el ejercicio offline contra golden anotado identifica si afinar threshold o cambiar modelo backend.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle feedback → dataset → adapter&lt;/a> — los incidentes severity HIGH que LLM Guard emite con &lt;code>risk_score &amp;gt; umbral&lt;/code> alimentan el bucle de incident-driven retrain.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/oss-vs-hyperscalers-llmops/">OSS vs hyperscalers en LLMOps&lt;/a> — la columna OSS de la fila &amp;ldquo;Guardrails&amp;rdquo; (NeMo + Presidio + Llama Guard 4 + &lt;strong>LLM Guard&lt;/strong>) frente a Bedrock Guardrails, Azure AI Content Safety, Vertex Model Armor.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">Structured output: function calling y constrained decoding&lt;/a> — el scanner JSON de LLM Guard valida estructura del output como red de seguridad cuando el motor de inferencia ya hizo constrained decoding.&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 Guardrails (este post incluido) es la pareja online de la etapa Eval.&lt;/li>
&lt;/ul></description></item></channel></rss>