Curso / Aplicado a tus proyectos / Gate anti-alucinación
🔧 basado en tu código real · 3 de 5

Aplicado a tus proyectos · Pipeline (TheLedger)

El gate que no deja que un LLM invente números

El Capítulo 1 te dijo que los LLM "alucinan con total confianza" y lo dejó ahí. Este es el problema resuelto en producción de verdad: dos capas de defensa, cada una cerrando el hueco que la otra no puede ver, disparadas por un incidente real con plata real de por medio.

Antes de leerHF cap. 11 §4, evaluación y LLM-as-judge · LLM 2026 · 4 §6, verificar fuera del modelo

Incidente: Micron Technology 10-Q, 2026-07-09 Capa 1: gate determinístico (regex, sin LLM) Capa 2: verificación con recibos (LLM + prueba verbatim) Verificador: Gemma-4 vía vLLM (analyst.py)

🎯 Al terminar vas a poder

  • Explicar por qué las alucinaciones son estructurales en un modelo autoregresivo, y distinguir fidelidad (faithfulness) de factualidad.
  • Ubicar un gate determinístico, un verificador con citas y un LLM-as-judge dentro de la familia de defensas, con los límites de cada uno.
  • Implementar una banda de precisión por cifras significativas y explicar por qué es más estricta que una tolerancia relativa.
  • Aplicar fail-closed como principio de diseño, separando error de infraestructura de veredicto negativo.
  • Diseñar un caso de aceptación para un pipeline propio a partir de un incidente real, en vez de un benchmark genérico.

0. El incidente Micron

Tu pipeline genera artículos financieros a partir de filings de la SEC con un LLM. El 2026-07-09, sobre un 10-Q de Micron Technology, ese LLM produjo dos afirmaciones falsas en la misma corrida — y son dos clases distintas de falla, que terminaron necesitando dos mecanismos de defensa distintos:

Fabricación pura

"Las ganancias se vieron infladas por un reembolso de aranceles único de $14.5 mil millones." Un evento que no existe en ningún lado del filing.

Número real, atribución falsa

El flujo de caja operativo del período actual declarado como $11.8 mil millones — cuando la columna del período actual en el filing decía $45.7 mil millones. $11.8B sí aparece en el filing: es el dato del año anterior.

La primera es fácil de imaginar cómo detectar: ese número no está en ningún lado del filing. La segunda es mucho más traicionera — el número sí existe, letra por letra, en el documento fuente. Solo está pegado a la columna equivocada.

Por qué esto es estructural, no un bug del modelo

Un LLM autoregresivo hace una sola cosa en cada paso: elegir el siguiente token según qué tan probable es dado el texto anterior. No tiene un paso separado de "verificar contra la fuente" — la fuente es solo más texto en el contexto, que influye en las probabilidades pero no las manda. "Un reembolso de aranceles único de $14.5 mil millones" es una frase perfectamente probable en un artículo financiero: tiene la forma correcta, el número tiene la magnitud correcta, y eso es todo lo que el modelo optimiza. Que el evento no exista no cambia la probabilidad de la frase. Por eso el entrenamiento no lo "cura": mejora la probabilidad de decir cosas ciertas, pero nunca la lleva a 1, y el modelo no tiene forma de saber en qué casos cayó del lado equivocado — de ahí la confianza total.

📐 Teoría de la información de la alucinación autoregresiva

En un modelo de lenguaje autoregresivo parametrizado por pesos θ, la probabilidad de una secuencia generada C=(w1,…,wT) dado un documento de contexto D se descompone por la regla de la cadena:

P(C∣D,θ)=∏t=1TP(wt∣w<t,D,θ)

La fidelidad (faithfulness) formal se cuantifica como la información mutua entre la afirmación C y el contexto D, condicionada a los priors paramétricos θ:

I(C;D∣θ)=H(C∣θ)-H(C∣D,θ)

Cuando el modelo incurre en una alucinación pura (como inventar «un reembolso de aranceles de $14.5B»), la distribución de predicción se desacopla del texto fuente:

P(wt∣w<t,D,θ)≈P(wt∣w<t,θ)⟹I(C;D∣θ)→0

Dado que el entrenamiento minimiza la divergencia de Kullback-Leibler sobre corpus masivos donde el estilo financiero fluido correlaciona con la sintaxis de balance contable, el modelo no optimiza una función indicatriz de respaldo documental: maximiza verosimilitud condicional. Por ello, la alucinación es un fenómeno intrínseco al muestreo sobre P(wt∣·).

Dos palabras que conviene separar, porque los dos errores de Micron caen en lugares distintos:

PropiedadPregunta que respondeLos dos errores de Micron
Fidelidad (faithfulness)¿Lo que dice el artículo está respaldado por el documento fuente que se le dio?Los dos fallan: el reembolso no está en el filing; el $11.8B está, pero no dice lo que el artículo dice que dice.
Factualidad¿Lo que dice el artículo es cierto en el mundo?Los dos fallan también — pero podrían no haberlo hecho: un artículo puede ser infiel al filing y aun así cierto (si el modelo "sabe" el dato de otro lado), o fiel a un filing que tiene un error.

Todo lo que sigue en esta página verifica fidelidad: que cada número tenga respaldo en el filing que se le dio al modelo. Es lo único que un sistema puede verificar mecánicamente — la factualidad "en el mundo" no tiene una fuente contra la cual comparar. Y es lo que importa en este pipeline: el contrato con el lector es "esto sale del filing", no "esto es verdad porque el modelo lo dice".

1. Capa 1: el gate determinístico

numeric_gate.py no usa ningún LLM. Es puro regex y aritmética: extrae todos los números del filing crudo en tres "pools" (exactos, escalables por unidad implícita, porcentajes), y después chequea que cada número que aparece en el artículo generado tenga una contraparte en esos pools.

La primera versión usaba una tolerancia relativa fija (±0.8%) para permitir redondeos normales de prosa ("$15.3 mil millones" describiendo una tabla que dice "15,378"). Ese mismo margen es lo que dejó pasar el fraude de Micron: la tabla real tenía una línea "Additional capital 14,442" (en millones = $14.442B) — nada que ver con un reembolso de aranceles, pero a solo 0.4% de "$14.5 billion", dentro de la tolerancia.

🔧 El fix real: bandas de precisión

La v2 (post-incidente) reemplaza la tolerancia fija por una idea más simple y más estricta: un número solo "funda" un reclamo si redondea o trunca a él, a la precisión que el reclamo mismo declara. "$14.5 billion" tiene 3 cifras significativas → su banda es [14.45B, 14.55B). 14,442 en millones es $14.442B — cae afuera de esa banda por muy poco. Nunca podría redondear o truncarse a "14.5". La tolerancia vieja medía distancia; la nueva pregunta es distinta: "¿este número podría legítimamente haberse escrito así?"

📐 Formulación matemática del intervalo por cifras significativas

Para un número de reclamo c∈R expresado con k cifras significativas, la magnitud decimal se expresa en notación científica como c=m×10p, donde 1≤|m|<10 y p=⌊log10|c|⌋. La unidad en el último lugar decimal significativo (ULP) es:

δ(c,k)=10p-k+1

El conjunto de valores de referencia x∈R que legítimamente redondean a c define el intervalo semiabierto de aceptación:

Isig(c,k)=c-12δ(c,k),c+12δ(c,k)

Para el reclamo de Micron c=14.5×109 (k=3, p=10): δ=1010-3+1=108=0.1B. El intervalo exacto es [14.45B,14.55B). Como el dato de la tabla era 14.442B<14.45B, queda rechazado automáticamente. En contraste, la tolerancia porcentual relativa Irel=[c(1-ϵ),c(1+ϵ)] con ϵ=0.008 admitía [14.384B,14.616B], tolerando una desviación masiva de 232 millones de dólares.

Tolerancia relativa contra banda de precisión, en los dos casos Arriba, el reclamo de 14,5 mil millones: la tolerancia de más menos 0,8 por ciento va de 14,384 a 14,616 y deja pasar el 14,442 real de la tabla; la banda de tres cifras significativas va de 14,45 a 14,55 y lo rechaza. Abajo, el reclamo de 2,1 mil millones con 2,149 en la tabla: la tolerancia lo rechaza porque está a 2,3 por ciento, la banda de dos cifras, de 2,05 a 2,15, lo acepta porque 2,149 sí redondea a 2,1. Micron: el artículo dice «$14.5 billion» ±0,8 % → pasa ✗ banda → no pasa ✓ 14,3 14,4 14,5 14,6 14,7 reclamo 14,442 en la tabla («Additional capital») El caso inverso: «$2.1 billion» con 2,149 en la tabla ±0,8 % → no pasa banda → pasa ✓ (2,149 redondea a 2,1) 2 2,05 2,1 2,15 2,2 reclamo 2,149 en la tabla
La tolerancia vieja medía distancia; la banda pregunta otra cosa: «¿este número podría haberse escrito así, a la precisión que el reclamo declara?». No es más estricta en general, como muestra el caso inverso; es la pregunta correcta, y por eso atrapa el 14,442 que la distancia dejaba pasar.

Tres detalles de implementación que valen la pena por lo que enseñan de ingeniería de texto:

  • Escalas implícitas: una tabla dice "15,378" con un header "(in millions)" en algún lado; la prosa dice "$15.4 billion". El gate prueba mil/millón/billón como escalas candidatas antes de descartar un número.
  • Tokens "seguros" nunca cuentan: años (2026), enteros chicos (conteos, "10-Q"), nunca entran al pool ni se chequean como reclamos — de lo contrario cualquier año mencionado "fundaría" cualquier claim numérico cercano.
  • Todo corre sobre texto, nunca sobre JSON parseado a mano: el mismo _NUMERIC_TOKEN_RE se aplica recorriendo cada string del payload — así el gate no necesita saber la forma exacta del JSON que genera el LLM, que cambia con cada versión del prompt.

2. Por qué la capa 1 no alcanza

Un gate por presencia, por más preciso que sea, tiene un techo estructural: solo puede preguntar "¿este número existe en el filing?" — nunca "¿este número describe lo que el artículo dice que describe?". $11.8 mil millones pasa el gate de la capa 1 sin problema: es un número real del filing de Micron. El gate no tiene forma de saber que está en la columna del año anterior, no en la del actual.

Detectar que un número existe es aritmética. Detectar que describe lo correcto es lectura.

Leer una tabla financiera — entender qué columna es el período actual y cuál es el anterior, qué línea corresponde a qué concepto — es exactamente el tipo de tarea donde un LLM le gana a cualquier regex. El problema es el inverso: ¿cómo confiás en el juicio de un sistema que sabés que puede alucinar, para que sea el que certifique que otro LLM no alucinó?

La familia de defensas, y el límite de cada una

Esa pregunta tiene varias respuestas conocidas, y conviene ubicar la de tu pipeline entre ellas — ninguna es completa, y las buenas soluciones apilan varias:

TécnicaQué haceDónde se queda corta
Grounding con RAGLe da al modelo la fuente en el contexto para que responda leyendo, no recordando.Reduce alucinaciones, no las elimina: Micron es exactamente un modelo que tenía el filing en el contexto y fabricó igual. Grounding es la precondición de la verificación, no la verificación.
Atribución / citasExigir que cada afirmación venga con un pasaje de la fuente que la respalde (la idea de "attributable to identified sources", Rashkin et al.).Un modelo puede inventar la cita con la misma confianza que inventa el dato. Solo sirve si alguien — código, no el modelo — comprueba que la cita existe y prueba lo que se dice.
LLM-as-judgeUn segundo modelo (o el mismo, en otra pasada) lee la salida y dicta un veredicto; variantes como Chain-of-Verification hacen que el modelo se haga preguntas de chequeo a sí mismo.El juez alucina igual que el juzgado. Su juicio semántico es valioso (leer columnas de una tabla), pero su veredicto no es una prueba. Sin una verificación en código del respaldo, es voto, no evidencia.
AutoconsistenciaMuestrear varias respuestas y desconfiar de las afirmaciones que no se repiten (SelfCheckGPT).Detecta inseguridad, no error: una fabricación que el modelo repite en todas las muestras pasa limpia. Y multiplica el costo de inferencia.
Gate determinísticoCódigo sin LLM que chequea propiedades mecánicas: números que existen en la fuente, formatos, rangos.Solo ve lo que se puede expresar como regla. Existencia sí; significado no — es el techo del §2.

Tu pipeline es la combinación de las últimas cuatro filas con el hueco de cada una tapado por la siguiente: grounding (el filing en el prompt), gate determinístico (capa 1), y un juez con citas cuya cita se verifica en código (capa 2). Lo que la sección siguiente explica es esa última pieza: qué significa que la verificación sea del código y no del modelo.

Las dos capas del gate y dónde cae cada error de Micron El LLM genera el artículo a partir del filing. La capa 1, numeric gate sin LLM, extrae los números del filing en tres pools y comprueba que cada número del artículo exista a la precisión declarada; el 14,5 mil millones fabricado no existe y va a cuarentena. La capa 2, claim verifier, recorta fragmentos verbatim del filing, pide a un LLM veredicto y cita, y el código comprueba que la cita exista carácter por carácter y contenga el número; el 11,8 mil millones existe pero está en la columna del período anterior y cae acá. Si vLLM no responde, no es cuarentena sino reintento. filing (10-Q) texto crudo de la SEC LLM generador filing en el contexto artículo (JSON) «$14.5B reembolso» · «$11.8B actual» capa 1 · numeric_gate.py · sin LLM, regex y aritmética pools del filing: exactos · escalables por unidad (mil / millón / billón) · porcentajes · sin años ni conteos ¿cada número del artículo existe en el filing a la precisión que declara? banda de «14.5B» = [14,45B; 14,55B) cuarentena $14.5B: no existe $11.8B pasa: sí existe en el filing capa 2 · claim_verifier.py · juicio del LLM, prueba del código 1. el código recorta fragmentos verbatim del filing con un número compatible con el reclamo 2. el LLM verificador (Gemma-4 vía vLLM) responde supported / unsupported + una cita exacta 3. el código exige que la cita exista carácter por carácter en los fragmentos y contenga el número cuarentena $11.8B: es el año anterior publicar vLLM no responde VerifierUnavailable no es cuarentena: bloquea, conserva, reintenta fail-closed en cada bifurcación, con dos tipos de «no»
Las dos capas cierran huecos distintos. La primera sabe si un número existe, y para eso no hace falta un modelo; la segunda sabe si describe lo que el artículo dice que describe, y para eso hace falta leer una tabla, pero el veredicto solo vale con un recibo que el código pueda comprobar.

3. Capa 2: verificar con recibos, no por voto

La solución de claim_verifier.py es no confiar en el veredicto del LLM verificador por sí solo. Se le muestra cada reclamo numérico junto a fragmentos copiados verbatim del filing crudo, y se le pide una cita textual exacta que lo respalde — pero el código, no el modelo, es quien decide si esa cita realmente prueba algo:

  1. Se buscan en el filing crudo todos los fragmentos que contienen un número compatible con el reclamo (misma lógica de bandas de precisión que la capa 1).
  2. El LLM recibe el reclamo + esos fragmentos, y debe responder con un veredicto (supported/unsupported) más una cita exacta.
  3. El código verifica dos cosas sobre esa cita, sin confiar en el modelo: que exista carácter por carácter (normalizando espacios) dentro de los fragmentos que el código mismo recortó, y que contenga un número que efectivamente matchea el reclamo.

🔧 Por qué esto es fail-closed contra un verificador que alucina

Un modelo que "alucina su verificación" — que inventa que vio una cita que nunca existió — no puede pasar: el string-match del código no va a encontrar esa cita inventada dentro de los fragmentos reales. El LLM aporta el juicio (¿esta columna es la correcta?); el código aporta la prueba (¿esa cita existe de verdad?). Ninguno de los dos solo alcanza.

El prompt al verificador es explícito sobre el problema exacto del incidente: "los estados financieros suelen imprimir el período actual en la primera columna numérica y los períodos anteriores a su derecha; un valor que aparece solo en una columna de período anterior no respalda un reclamo sobre el período actual." Esa instrucción no es genérica — está escrita para el error puntual que ya pasó.

4. Fail-closed en cada bifurcación

Todo el sistema comparte un principio: ante cualquier ambigüedad, no publicar es la opción por defecto. Pero hay una distinción fina que vale la pena remarcar, porque es fácil de pasar por alto: no toda falla es un veredicto.

SituaciónTratamiento
El filing crudo no existe o está corruptoCuarentena — no se puede verificar, no se publica
El verificador responde JSON malformadoCuarentena — respuesta inutilizable cuenta como "no soportado"
El verificador dice "unsupported"Cuarentena — veredicto negativo real
La cita no matchea verbatim en los fragmentosCuarentena — el modelo no probó lo que dijo
El servidor vLLM del verificador no respondeNo es cuarentena — VerifierUnavailableError, reintentar, artefactos intactos

Esa última fila es la más sutil de todas: un timeout de vLLM no es lo mismo que un modelo diciendo "esto no está probado". Si un hipo de infraestructura borrara y descartara una síntesis válida como si el modelo la hubiera rechazado, terminarías perdiendo trabajo bueno por un problema que no tiene nada que ver con la calidad del contenido. La distinción entre "no pude verificar por una falla técnica" y "verifiqué y no está soportado" está modelada explícitamente en el tipo de excepción que se lanza.

Fail-closed con dos tipos de «no»: veredicto y falla de infraestructura Una cadena de preguntas. Sin filing válido, cuarentena. Si el servidor vLLM del verificador no responde, error de verificador no disponible: se bloquea la publicación, se conservan los artefactos y se reintenta. Si el JSON del verificador es inválido, si el veredicto es unsupported o si la cita no matchea verbatim, cuarentena. Solo si todo pasa se publica. ¿filing crudo válido? sí ¿el vLLM del verificador responde? sí ¿JSON bien formado? sí ¿veredicto supported? sí ¿la cita existe verbatim? sí publicar no no no no cuarentena: «evalué y no está probado» · veredicto negativo real el artículo no se publica y queda marcado para revisión no responde VerifierUnavailableError: «no pude evaluar» · bloquea la publicación, conserva los artefactos, reintenta
Todas las salidas bloquean la publicación: eso es fail-closed. Pero un timeout de vLLM no es un veredicto, y tratarlo como tal destruiría síntesis válidas por un hipo de red hasta que alguien «arregle» eso relajando el gate. La distinción vive en el tipo de excepción, no en un comentario.

Fail-closed y fail-open, como principio general

La disyuntiva no es de LLMs: es de cualquier sistema con un guardián. Fail-open significa que, si el guardián no puede decidir, deja pasar — la puerta de emergencia que se destraba cuando se corta la luz, o un antivirus que deja correr un archivo cuando no logra escanearlo. Fail-closed significa lo contrario: sin decisión, no hay paso — la bóveda del banco. Cuál corresponde depende de qué error cuesta más: en la puerta de emergencia, atrapar gente adentro es peor que dejar entrar a alguien; en la bóveda, al revés. Para un pipeline que publica cifras financieras con una firma, un artículo falso publicado cuesta más que un artículo bueno demorado, así que la elección es fail-closed en cada bifurcación.

Lo que la tabla de arriba agrega es la segunda mitad del principio, la que se suele olvidar: fail-closed no significa que toda falla se trate como un veredicto negativo. Hay que separar dos tipos de "no": "no pude evaluar" (infraestructura: timeout, servidor caído, red) y "evalué y falla" (veredicto). Los dos bloquean la publicación — eso es fail-closed — pero el primero conserva el trabajo y reintenta, mientras que el segundo lo manda a cuarentena. Si los confundís en una sola rama, cada hipo de red te destruye síntesis válidas, y con el tiempo alguien "arregla" eso relajando el gate — y ahí sí perdiste la garantía. Modelar la distinción en el tipo de excepción es lo que hace que el principio sobreviva a la operación diaria.

5. El harness que decide si un modelo nuevo puede servir

scripts/model_acceptance.py resuelve una pregunta que conecta directo con tu pipeline de cuantización: cuando cambiás el modelo o la cuantización que sirve de verificador, ¿cómo sabés que el nuevo sigue detectando lo que tiene que detectar? La respuesta acá no es "correr un benchmark genérico y mirar el score" — es volver a correr el incidente real de Micron contra el candidato.

La barra de aceptación tiene dos requisitos, y el segundo es tan importante como el primero:

  • La síntesis de Micron tiene que fallar, y entre las fallas tienen que aparecer las dos firmas exactas del incidente: el "$14.5 billion" fabricado y el "$11.8 billion" mal atribuido. Esto último es lo que tu propio comentario en el script llama "the acid test" — detectar la mala atribución exige que el modelo lea los headers de columna de una tabla, algo que ningún gate determinístico puede hacer.
  • Dos casos limpios conocidos (HIMS, VST) tienen que pasar, sin falsos positivos. Un modelo que rechaza todo es, para este propósito, tan inútil como uno que no rechaza nada.

🟢 La idea que generaliza

"Un modelo que no puede reproducir estos resultados no debe gatear producción, sin importar qué diga su porcentaje de recuperación en el benchmark" — tu propio comentario en el script. Es la misma lección que ya viste en la página de cuantización: un log que dice "salió bien" no es lo mismo que un artefacto verificado. Acá el equivalente es: un benchmark genérico que dice "95% de recuperación" no es lo mismo que reproducir el incidente real que te importa.

6. Bugs reales que ya encontraste

El código está lleno de comentarios fechados que documentan hallazgos de revisión adversarial — vale la pena leerlos como casos de estudio de lo difícil que es este problema en la práctica:

"16 basis points" → 16 mil millones

Sin un límite de palabra en el regex, el sufijo de escala ("b" de billion) se comía la primera letra de la palabra siguiente: "16 b[asis points]" se parseaba como "16 B". Un bug de regex, no de IA — pero envenenaba tanto el pool de referencia como los reclamos.

Una fecha como "prueba" de un reclamo en dólares

"2026" multiplicado por la escala implícita de millones cae dentro de la banda de un reclamo de "$2.0 mil millones". Los tokens "seguros" (años, conteos chicos) tuvieron que excluirse explícitamente de poder actuar como respaldo.

Metadata de producción leída como cifra financiera

Un cue de video a los 35 segundos, o la duración de una escena, son metadata de renderizado — no reclamos sobre el filing. 9 de 13 fallas del gate de media en una corrida real (E2) fueron por esto, antes de excluir esas claves.

El título de un artículo citado, leído como cifra del filing

Un artículo externo citado como fuente, titulado "...Up +27.9%...", exponía ese porcentaje como si el modelo lo hubiera narrado. Las claves de citación/atribución (source_map, web_sources) tuvieron que podarse como subárbol completo, sin importar su forma interna.

Ninguno de estos cuatro es un problema de "el LLM alucinó" — son todos formas en las que la infraestructura de verificación misma puede fallar en silencio si no se la revisa con la misma sospecha que al modelo que está vigilando.

7. Lo que generaliza

Sin filings ni Micron, esto es lo que vale para cualquier pipeline que publique salidas de un LLM:

LecciónAcáEn cualquier otro pipeline
Verificá fidelidad, no "verdad"Cada número tiene que tener respaldo en el filingDefiní cuál es la fuente contra la que se verifica; sin fuente no hay verificación, hay opinión.
Lo mecánico va en código; lo semántico en el modelo; la prueba, siempre en códigoRegex para existencia, LLM para columnas, string-match para la citaNunca dejes que el modelo sea a la vez juez y notario: pedile el juicio y hacé que el código exija el recibo.
La tolerancia tiene que preguntar lo correcto±0.8% dejó pasar 14.442 como 14.5; la banda por cifras significativas noAntes de poner un umbral numérico, escribí en palabras qué pregunta responde ("¿podría haberse escrito así?") y derivá el umbral de ahí.
Fail-closed, con dos tipos de "no"VerifierUnavailableError vs cuarentenaToda falla bloquea, pero infraestructura reintenta y veredicto descarta; modelalo en tipos, no en comentarios.
El incidente real es el test de aceptaciónMicron tiene que fallar, HIMS y VST tienen que pasarCada incidente que te costó algo se convierte en un caso fijo que cualquier cambio de modelo, prompt o cuantización tiene que reproducir.
La infraestructura de verificación también falla en silencio"16 basis points", fechas como respaldo, metadata de video, títulos citadosRevisá el verificador con la misma sospecha que al modelo: loggeá qué respaldó a qué, y leé esos logs.

8. Resumen

El Capítulo 1 nombra el problema; esto es la respuesta de ingeniería completa:

  1. Capa 1 (determinística): ¿este número existe en el filing, a la precisión que el reclamo declara? Rápida, sin LLM, cero costo de inferencia.
  2. Capa 2 (verificación con recibos): ¿este número existente describe lo que el artículo dice que describe? Necesita juicio semántico (un LLM), pero el código exige una prueba verbatim que un modelo que alucina no puede falsificar.
  3. Fail-closed everywhere, con una distinción cuidadosa entre "no pude verificar" (infraestructura, reintentar) y "verifiqué y está mal" (veredicto real, cuarentena).
  4. Un harness de aceptación que gatea cualquier cambio de modelo o cuantización contra el incidente real, no contra un benchmark genérico — la disciplina de verificar el artefacto, aplicada a comportamiento en vez de a pesos.

Alucinar con total confianza deja de ser una limitación que solo "tenés que tener en mente" cuando hay una capa de código que no te deja publicar sin la prueba.

9. Ponete a prueba

Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.

1. ¿Por qué darle el filing completo en el contexto no alcanzó para que el modelo no fabricara el reembolso de aranceles?

Porque el modelo elige el token más probable dado el texto anterior, y el filing es solo más texto que influye en esa probabilidad — no un paso de verificación. Una frase con la forma y la magnitud correctas es probable aunque el evento no exista. Grounding baja la tasa, no la lleva a cero. (§0, §2)

2. Un artículo dice un dato cierto que no aparece en el filing. ¿Es fiel? ¿Es factual? ¿Qué hace el gate?

Es factual pero no fiel: no tiene respaldo en la fuente entregada. El gate verifica fidelidad, así que lo bloquea — correctamente, porque el contrato del pipeline es "esto sale del filing", y un sistema no puede verificar la verdad en el mundo, solo el respaldo en una fuente. (§0)

3. "$2.1 billion" en el artículo; la tabla dice "2,149" en millones. ¿Pasa la tolerancia del ±0.8%? ¿Pasa la banda de precisión?

Distancia relativa: 49/2100 ≈ 2.3% → no pasa la tolerancia vieja. Banda de "2.1" con dos cifras significativas: [2.05B, 2.15B) → 2.149B sí cae adentro, pasa. Es el ejemplo inverso a Micron: la banda no es "más estricta" en general, sino que responde otra pregunta — 2,149 sí podría escribirse legítimamente como 2.1. (§1)

4. El verificador responde "supported" con una cita que no aparece en los fragmentos. ¿Qué pasa y por qué eso es lo que protege contra un verificador que alucina?

Cuarentena: el código busca la cita carácter por carácter dentro de los fragmentos que él mismo recortó, y no está. El veredicto del modelo no cuenta sin prueba; una cita inventada no puede matchear texto real. El LLM aporta juicio, el código exige el recibo. (§3)

5. ¿Por qué un timeout del servidor vLLM no manda la síntesis a cuarentena, si el sistema es fail-closed?

Porque fail-closed significa "sin decisión no se publica", no "toda falla es un veredicto negativo". Un timeout es "no pude evaluar" — se bloquea la publicación, se conservan los artefactos y se reintenta (VerifierUnavailableError). Tratarlo como "evalué y falla" destruiría trabajo válido por un hipo de red, y con el tiempo llevaría a relajar el gate. (§4)

6. Un modelo candidato saca 97% en un benchmark de detección de alucinaciones pero no marca el "$11.8 billion" de Micron. ¿Puede gatear producción?

No. El harness exige reproducir las dos firmas del incidente real (fabricación y mala atribución) y no dar falsos positivos en HIMS y VST. Un benchmark genérico no prueba que el modelo lea columnas de una tabla; el incidente sí. Es la misma disciplina de "verificá el artefacto, no el log". (§5)

10. Para practicar

✏️ Ejercicio 1 — Bandas de precisión por cifras significativas

La idea del §1, aislada: dado un reclamo escrito como texto ("$14.5 billion"), devolver el intervalo de valores que redondean a ese número tal como está escrito, y usarlo para decidir si un valor del documento fuente lo funda. Python puro, sin dependencias. Decimal conserva cuántos decimales escribió el autor — float no.

import re
from decimal import Decimal

ESCALAS = {"thousand": 10**3, "k": 10**3, "million": 10**6, "m": 10**6,
           "billion": 10**9, "bn": 10**9, "b": 10**9}
# el \b final: "16 b" solo cuenta como "16 billion" si la "b" termina palabra
_RE = re.compile(r"\$?\s*(\d[\d,]*(?:\.\d+)?)\s*(thousand|million|billion|bn|k|m|b)?\b", re.I)

def banda(texto):
    """Intervalo [lo, hi) de valores que redondean al número tal como está escrito."""
    m = _RE.search(texto)
    cifra = Decimal(m.group(1).replace(",", ""))
    escala = ESCALAS.get((m.group(2) or "").lower(), 1)
    paso = Decimal(1).scaleb(cifra.as_tuple().exponent)   # "14.5" -> 0.1 ; "15,378" -> 1
    return (cifra - paso / 2) * escala, (cifra + paso / 2) * escala

def funda(valor, texto):
    lo, hi = banda(texto)
    return lo <= Decimal(valor) < hi

print(banda("$14.5 billion"))                                # (14450000000, 14550000000)
print(funda(Decimal("14442") * 10**6, "$14.5 billion"))    # False: 14.442B no redondea a 14.5
print(funda(Decimal("15378") * 10**6, "$15.4 billion"))    # True: 15.378B sí redondea a 15.4
print(abs(Decimal("14.442") - Decimal("14.5")) / Decimal("14.5"))  # 0.004: el ±0.8 % lo dejaba pasar
print(_RE.search("16 basis points").groups())                 # ('16', None): la "b" no se come
→ (Decimal('14450000000.00'), Decimal('14550000000.00')) · False · True · 0.004 · ('16', None)
Pista / para seguir

Tres extensiones, en orden de dificultad: (1) el §1 dice "redondea o trunca" — agregá la banda de truncamiento [x, x + paso) y uní las dos; (2) "2,000" tiene exponente 0 en Decimal, así que la banda es [1999.5, 2000.5) — decidí qué querés que signifique un número con ceros finales y escribí el test; (3) excluí los "tokens seguros" (años entre 1900 y 2100, enteros chicos) tanto del lado del reclamo como del respaldo, y verificá que "2026" ya no pueda fundar "$2.0 billion".

✏️ Ejercicio 2 — Diseñá el caso de aceptación de tu propio pipeline

Sin código nuevo: tomá un pipeline tuyo que publique salidas de un LLM (un resumen, una clasificación, un artículo, una extracción) y escribí su model_acceptance en un archivo de texto, siguiendo el esqueleto del §5. Tiene que tener las dos mitades:

  1. Lo que tiene que fallar: el peor error real que ya viste (o el peor que podés imaginar, escrito como caso concreto con la entrada exacta y la firma exacta del error). Si no tenés un incidente real todavía, fabricá uno editando una entrada válida para plantarle un error — y anotá que es sintético.
  2. Lo que tiene que pasar: dos o tres entradas limpias, conocidas, que el modelo no puede rechazar. Sin esta mitad, un modelo que rechaza todo aprueba.

Después respondé por escrito: ¿qué cambio en tu sistema dispara esta corrida (modelo, prompt, cuantización, versión de librería)? ¿Cuánto tarda? ¿Quién mira el resultado? Si la respuesta a la última es "nadie", el harness no existe todavía.

Qué tendría que salir

Un archivo que cualquiera de tu equipo pueda leer y decir "sí, esos son los errores que nos importan". La prueba de que está bien diseñado: si mañana cambiás el modelo por otro que saca mejor puntaje en un benchmark público, el harness te dice en minutos si perdiste la detección que te importa — antes de que lo haga producción.

11. Fuentes y para seguir