blog about dark mmfilesi
context engineering · ·30 min ·Advanced

Autopsia de una alucinación

¿Por qué fallan los prompts? Un recorrido por self-critique, logprobs, incertidumbre semántica e interpretabilidad para hacerle la autopsia a una alucinación.

1. ¿Por qué fallan los prompts?

Una de las grandes certezas del desarrollo de software es que, cuanto más complejo es un sistema, más posibilidades existen de que se produzcan errores inesperados por situaciones muy difíciles de prever. Por ejemplo, que el nombre de un usuario contenga caracteres en ogham —un alfabeto usado en la antigua Irlanda en el que las letras se componen de líneas y marcas talladas sobre una línea central— y el formulario de inscripción no esté preparado para ese 0,0001% de la población que va a usar la web.

Aunque parece algo muy sencillo, el prompting es en realidad uno de esos casos complejos. Cuando prompteamos, las palabras se trocean en tokens y cada token se convierte en un vector de miles de dimensiones. Esos vectores atraviesan decenas de capas en las que se reescriben unos a otros mediante mecanismos de atención y transformaciones aprendidas durante el entrenamiento. Al final del recorrido, el modelo tiene una probabilidad asignada a cada palabra candidata y escoge la siguiente a partir de ahí. Si el output es erróneo, el problema puede estar en cualquier punto de ese trayecto, y casi nunca es evidente cuál.

¿Cómo podemos debuggear qué sucede cuando falla un prompt? ¿Hay herramientas para eso? ¿Podemos contar con logs y trazas o, por el contrario, debemos intentar resolver el problema a ciegas, a fuerza de ensayo y error? ¿Cómo podemos realizar un análisis forense para evitar nuevos bugs similares en el futuro?

Trataré de responder estas preguntas explicando algunas técnicas que ya se están implementando, e incorporando alguna que otra de mi cosecha por el camino. Para entenderlas mejor, vamos a agruparlas en cuatro familias, según qué se observa, quién observa y en qué momento. Les pongo unos nombres más para entendernos que para fundar una taxonomía.

1. Interrogatorio conversacional. Consiste en presionar al modelo dentro de la propia conversación: contradicciones deliberadas, preguntas trampa, reformulaciones, giros lógicos. La idea es ver si el modelo razona o si se limita a complacer al usuario. Ocurre en el canal visible, en tiempo real, y la herramienta es el propio interlocutor.

2. Auditoría semántica automatizada. Aquí la revisión se delega en otros modelos o procesos que actúan como jueces, verificadores o filtros. Evalúan la respuesta antes de que llegue al usuario y pueden bloquearla, reescribirla o marcarla. Es invisible desde fuera y suele formar parte de la infraestructura, no del prompt.

3. Monitor de probabilidades. En lugar de mirar lo que el modelo dice, se mira con qué confianza lo dice. Las probabilidades asignadas a cada token (los logprobs) permiten detectar momentos de duda, transiciones bruscas o patrones anómalos que a veces correlacionan con alucinaciones. No es infalible, pero es una de las pocas señales cuantitativas que tenemos.

4. Inspección interna. Las técnicas más invasivas: abrir el modelo y observar qué circuitos se activan, qué cabezas de atención están atendiendo a qué, qué neuronas se encienden con qué conceptos. Es el territorio de la interpretabilidad mecanicista. Caro, lento y todavía bastante artesanal, pero apunta maneras.

Vamos con las primeras.

2. Interrogatorio conversacional.

2.1. Self-Critique

Dentro de este grupo, el concepto central del que derivan muchas estrategias es el Self-Critique, la autocrítica, que en algunos modelos da unos resultados espectaculares y consiste en esencia en pedir que critique la respuesta que se acaba de dar.

Por ejemplo, primero le formulamos la cuestión que queremos saber a un modelo:

«Explícame en un párrafo breve la relación entre Odín y Zeus».

Nos la explica:

«Odín y Zeus son figuras centrales de dos mitologías distintas —la nórdica y la griega, respectivamente—, por lo que no existe una relación directa entre ellos. Sin embargo, suelen compararse por ser ambos dioses supremos y padres de otros dioses. Odín, asociado a la sabiduría, la guerra y la muerte, gobierna el Asgard y busca conocimiento a través del sacrificio [...]».

Y, acto seguido, le indicamos que critique la respuesta que acaba de dar, con lo que él mismo encuentra sus fallos argumentales, entre ellos, no haber mencionado al posible origen común indoeropeo, que es clave para entender la relación entre los dos dioses.

«Falsa simetría al hablar de "relación": Pediste explícitamente la relación entre Odín y Zeus, y yo empecé diciendo que "no existe una relación directa". [...]

»Confusión entre relación histórica y tipológica: En estudios mitológicos, dos dioses pueden tener "relación" en términos de influencias culturales (ej. Zeus y Júpiter) o de origen común indoeuropeo. Odín y Zeus sí comparten un remoto origen protoindoeuropeo (como el Dyēus Pətēr del que desciende Zeus, y el Wōðanaz germánico), pero yo no mencioné eso y di a entender que solo se parecen por azar o por rol funcional, lo cual es impreciso».

Como apuntan Sam Silver et al. en Language Models can perform Single-Utterance Self-Correction of Perturbed Reasoning, esto mismo se podría reproducir en un mismo prompt, en el que conviene usar algún tipo de estructuración semántica, como la que permiten los tags xml, para evitar que se nos despiste el modelo:

[INSTRUCCIÓN CENTRAL]
Vas a responder a una pregunta en tres fases obligatorias: un borrador, una auditoría y una versión final. Ejecuta las fases secuencialmente en tu respuesta.

<fase_1_borrador>
Responde en un párrafo breve (máximo 4 oraciones): "Explícame la relación entre Odín y Zeus".
</fase_1_borrador>

<fase_2_autocritica>
Evalúa tu borrador anterior respondiendo estrictamente a estas 4 preguntas:
1. Precisión: ¿Mencioné que pertenecen a mitologías distintas sin parentesco original?
2. Comparación: ¿Expliqué una similitud funcional y una diferencia clave?
3. Claridad: ¿Es comprensible para un novato sin confundir cultos?
4. Teoría: ¿Existe alguna teoría que los relacione (ej. origen indoeuropeo)?
</fase_2_autocritica>

<fase_3_resultado_final>
Basándote en tu autocrítica, reescribe el párrafo corregido y mejorado (máximo 4 oraciones).
</fase_3_resultado_final>

El fenómeno es realmente desconcertante. ¿Cómo es posible que funcione este mecanismo? ¿Es que los modelos tienen una habilidad emergente que les permite reflexionar sobre sí mismos? ¿Es el momento de empezar a buscar otro planeta?

2.2. El cuarto de pensar

Como es sabido, una fórmula para que los niños salgan por unos minutos de una situación emocional intensa o de una conducta inapropiada, y así ayudarles a calmarse y recuperar el autocontrol, es enviarles al «cuarto de pensar». Y lo curioso es que los modelos tienen algo parecido, los thinking tokens.

Simplificándolo mucho, como traté de explicar en Consideraciones epistemológicas en los espacios latentes, el mecanismo por el que piensan los modelos consiste en identificar qué palabra o, mejor dicho, qué token, es más probable que sea el que sigue dado el contexto anterior. Por ejemplo, después de haber sido entrenados con cuanto texto ha sido posible, saben que es más probable que a «el cielo es» le siga la palabra «azul» que «saxofón».

Uno de los momentos cruciales de ese proceso sucede en la fase final, cuando los valores en bruto que ha calculado el modelo para cada token candidato —los logits— pasan por la función Softmax, que los convierte en probabilidades que suman en total el 100%. Así, en nuestro ejemplo, los dos candidatos podrían llegar a ese momento final con esta puntuación:

  • azul: 99%.
  • saxofón: 1%.

Si no hay modificadores, como la temperatura, el modelo no tiene dudas y escupe su output sin miedo: «el cielo es azul». Es como el siguiente micro vídeo que se ofrece a un usuario en redes sociales como TikTok o Instagram después de haber cartografiado sus preferencias.

Pero en muchas otras ocasiones la elección no está tan clara. ¿Qué sigue a «¿Por qué una tostada siempre cae del lado de la mermelada, pero si le pones mantequilla al gato, el universo se ríe en espiral?». Aquí la distribución podría quedar mucho más plana:

  • cosmología: 50%.
  • troll: 50%.

Cuando la respuesta no cabe en un solo token bien elegido, el problema es demasiado grande para una sola jugada. Y aquí los modelos que piensan hacen algo poco intuitivo: en vez de contestar, se ponen a escribir. Generan una larga tirada de tokens, los thinking tokens, ese «espera, no...», «déjame reconsiderar...» que algunas interfaces muestran antes de la respuesta. El modelo sigue prediciendo el token que viene; lo que cambia es que ahora predice su propio razonamiento.

Y escribir así ayuda por dos motivos. El primero es de cálculo. Cada token adicional permite otra ronda completa de cómputo sobre el contexto acumulado. Con doscientos tokens de razonamiento dispone de otras doscientas oportunidades para refinar el cálculo. El segundo es de contexto, cada token que escribe se suma a lo que lee para escribir el siguiente, así que cuando llega el momento de responder, el trabajo previo ya está sobre la mesa y el empate al 50% se ha deshecho por el camino.

Existe además otra familia de técnicas, que en lugar de dejar al modelo divagar hacia delante envuelve sus llamadas en una búsqueda explícita. La más conocida son los Árboles de Montecarlo (MCTS), un algoritmo de búsqueda que combina exploración con la construcción dinámica de un árbol de decisiones. Para entendernos: si una rama tiene pinta de prometedora, se explora; si deja de compensar, se retrocede (backtracking), se descarta esa rama antes de que el usuario la vea y se prueba otra. Es el algoritmo que hay detrás de AlphaGo, la primera máquina que derrotó al Go a un profesional de primer nivel, y algunos trabajos recientes, como rStar-Math, muestran que envolver un modelo de apenas 7.000 millones de parámetros en una búsqueda así ha bastado para plantarle cara a o1 en ciertos benchmarks matemáticos.

En cualquier caso, sea mediante una técnica u otra, cuando le pedimos a un modelo que critique su respuesta, que la analice antes de contestar, que reflexione, lo que conseguimos es algo más prosaico que la introspección: le hacemos generar todos esos tokens intermedios que, sin más magia, enriquecen el contexto. Y con un contexto más rico, el token bueno —los indoeuropeos— deja de empatar al 50% y pasa a ser, por fin, el más probable. No es que el modelo se conozca mejor a sí mismo. Es que, habiendo escrito más, tiene más sobre lo que apoyarse. Probabilística 1 - Hal 0.

chat de DeepSeek
En las interfaces suele habilitarse la opción de activar el modo autorreflexivo, como sucede con este caso de DeepSeek

2.3. Otras técnicas de prompting

Además de la autocrítica, hay otras técnicas para que el propio modelo identifique si está alucinando o se está despistando. En esencia, todas juegan con lo mismo, con romper la fluidez del texto y obligar al modelo a analizar los datos de forma aislada.

Es el caso de la técnica llamada Self-Consistency, que consiste en pedirle que genere, por ejemplo, 5 caminos de razonamiento diferentes para el mismo problema en lugar de uno solo. Si en 4 de ellos llega a la respuesta A y en uno a la B, hay una probabilidad alta de que la B sea una alucinación. La incongruencia es la primera alarma.

El prompt sería algo parecido a esto:

Quiero que resuelvas el siguiente problema. 
Para asegurarte de que no alucinas, genera 5 caminos de razonamiento diferentes y detallados de forma independiente. 
Al final, analiza los 5 resultados, identifica si alguno es inconsistente y dime cuál es la respuesta correcta basándote en la mayoría.

Un par de observaciones. La primera es que, en su formulación original, los caminos se generan en llamadas separadas, no de una tirada como en el prompt del ejemplo: si el modelo los escribe todos en la misma respuesta, el segundo ya ve el primero y tiende a imitarlo, con lo que la votación se contamina.

La segunda es más de fondo. Que 4 caminos coincidan en A no significa que A sea verdad, sino que es la respuesta más estable del modelo. Si el sesgo es consistente, los 5 caminos pueden converger en la misma alucinación con idéntico aplomo. Self-Consistency detecta inestabilidad, que ya es algo, pero una mentira firme la cruza sin despeinarse.

Otra técnica es la Chain-of-Verification (CoVe), emparentada con el self-critique y desarrollada por Dhuliawala et al. en Meta AI. El proceso es dar una respuesta inicial, luego listar qué afirmaciones de esa respuesta son hechos verificables, pedirle al modelo que valide esos hechos uno a uno de forma independiente y al final generar la respuesta corregida. Al fragmentar el pensamiento, es más fácil que el modelo detecte su propio error.

Esta técnica se desarrolla en 4 fases:

  1. Generar la respuesta inicial (Baseline Response). El modelo recibe la consulta del usuario y prepara una respuesta rápida que se asume de partida como potencialmente contaminada con alucinaciones.

  2. Planificar la verificación (Verification Plan). El modelo analiza su propia respuesta y genera una lista de preguntas de control sobre los datos duros (fechas, nombres, lugares, relaciones) que acaba de mencionar. Estas preguntas deben formularse de manera que requieran respuestas fácticas y atómicas (cortas y directas).

  3. Ejecutar la verificación (Verification Execution). El modelo responde a cada una de las preguntas del plan de forma independiente. Para evitar el sesgo de consistencia, que el modelo repita su mentira inicial para no contradecirse, en sistemas avanzados se borra el contexto de la respuesta inicial durante este paso, obligándolo a responder sin apoyarse en lo que dijo antes. Esta independencia es lo que de verdad hace funcionar el método.

  4. Generar la respuesta final (Final Verified Response). El modelo compara la respuesta inicial (Paso 1) con los hechos validados (Paso 3). Si encuentra discrepancias, corrige los errores y redacta una respuesta definitiva, con menos alucinaciones que la de partida.

Si se tuviera que formular en un solo prompt, el esquema podría ser algo parecido a esto.

Actúa como un sistema de alta fidelidad fáctica aplicando Chain-of-Verification (CoVe). Cuando te haga una pregunta, procesa la respuesta estrictamente en 4 bloques etiquetados:

1. [RESPUESTA INICIAL]: Da la respuesta directa a mi pregunta.
2. [PLAN DE VERIFICACIÓN]: Genera una lista de preguntas cortas para verificar cada dato, nombre, fecha o afirmación clave que hiciste en el paso anterior.
3. [EJECUCIÓN]: Responde a las preguntas del plan de forma directa y escueta, basándote estrictamente en hechos reales verificables.
4. [RESPUESTA FINAL]: Compara el bloque 1 y el bloque 3. Si detectas contradicciones o errores en tu respuesta inicial, corrígelos. Genera una respuesta final depurada y precisa.

Aquí está mi pregunta: [INSERTA TU PREGUNTA AQUÍ]

Otra técnica muy interesante, y sobre la que volveremos al hablar de QA en las pipelines, es la asignación de confianza, Confidence Scoring, en la que se le pide al modelo explícitamente que adjunte un porcentaje de certeza a las partes clave de la respuesta:

Responde a la pregunta y, entre corchetes, añade tu nivel de certeza (0-100%) después de cada afirmación factual. 

Marca con [VERIFICAR] cualquiera que baje del 70%.

Esto obliga al modelo a ponderar su respuesta y suele suavizar las alucinaciones más categóricas. Pero ese número conviene tomarlo con pinzas: es una confianza declarada, no medida. Los modelos están mal calibrados y son perfectamente capaces de estampar un «95%» sobre un dato falso sin que les tiemble el pulso. Sirve para que el modelo module el tono, no como probabilidad fiable de acierto. Más adelante veremos que existe una versión de esto mucho menos crédula, en la que la confianza no se le pregunta al modelo, sino que se mide por fuera.

Por mencionar otra entre varias más, me gusta mucho la validación cruzada por formato, Cross-examination, que consiste en pedirle al modelo la misma información en dos formatos distintos. Por ejemplo, primero una explicación narrativa y luego una tabla estructurada o código:

Explícame en un párrafo la cronología de los reyes sumerios de la antigua Mesopotamia. A continuación, vuelca exactamente la misma información en una tabla con columnas: rey, año de inicio y año de fin. 
Las fechas de ambas versiones deben coincidir.

Las alucinaciones suelen colarse en la prosa libre, pero saltan a la vista cuando el modelo intenta encajar datos inventados en una estructura rígida o en reglas lógicas estrictas. Lo que en un párrafo pasaba por una transición elegante, en una tabla se convierte en una celda que no cuadra.

En esencia, todas estas técnicas se basan en lo mismo, en forzar a que el modelo abra nuevas vías de exploración, y todas comparten el mismo inconveniente: aumentan el coste computacional y, por lo tanto, el económico. La pregunta, entonces, es si se pueden lanzar solo cuando se necesiten de verdad, cuando algo indique que una respuesta puede estar equivocada y solo en ese caso. ¿Es posible conseguir esto?

3. La incertidumbre semántica

3.1. Del logit al logprob

Todas las técnicas que hemos visto antes se pueden llevar a un flujo automatizado en el que otra llamada, ya sea al mismo modelo o a otro, actúe de juez supervisor. Esto tiene la ventaja de afinar la respuesta antes de que llegue al usuario y facilita la supervisión escalable, como expliqué en este texto sobre MAD, el Multi-Agent Debate; y, técnicamente, es tan sencillo como preparar un agente lineal que gestione las llamadas o usar un grafo con nodos agénticos, como los que podemos montar con LangGraph.

El problema, como decía, es que esto aumenta mucho los costes y, además, la latencia, el tiempo que el usuario espera hasta que empieza a leer la respuesta, pues todo se multiplica cuando menos por tres: la primera llamada, el análisis del juez y la segunda llamada con las apreciaciones del juez. Demasiados tokens, demasiado tiempo y demasiado dinero.

Una solución sería lanzar el proceso solo cuando hiciera falta de verdad, cuando el propio sistema detectase que se ha podido despistar. Y una manera de conseguirlo es observar los logprobs, de los que recuerdo un par de detalles por si no los conociéramos. Si ya los manejas, sáltate lo que sigue, claro :).

Logprob es la abreviatura de logaritmo de la probabilidad (logarithmic probability) y, en el contexto de los modelos de lenguaje, es una forma de medir la seguridad con la que el modelo predice un token concreto, que puede ser una palabra, parte de una palabra o incluso un solo carácter.

Como vimos antes, en la última capa del modelo, justo antes de la salida, se calcula una puntuación para cada token del vocabulario. Esas puntuaciones son los logits que ya conocemos: números reales, positivos o negativos, sin límite superior, que valoran en bruto a cada candidato. Conviene no confundirlos con los tokens: el logit no es la palabra, es la nota que el modelo le pone. Para la frase «En el Museo de Historia Natural vimos un esqueleto gigante de...», las notas podrían repartirse así:

  • dinosaurio: 5.2
  • mamut: 3.1
  • ballena: 1.8
  • refrigerador: -8.2
  • los otros ~50.000 tokens del vocabulario: entre -15 y 0.5

(Uso 50.000 porque es más manejable, aunque es el orden de magnitud del vocabulario de GPT-2; los modelos actuales manejan bastantes más, entre 100.000 y 200.000, pero la idea es la misma.)

Esos números en bruto no sirven todavía para hacer cálculos de probabilidad, así que se pasan por la función Softmax, que convierte el vector de logits en un vector de probabilidades con dos propiedades:

  • Cada valor está entre 0 y 1.
  • La suma de todos los valores es 1.

Si no se distorsiona con la Temperatura, la función hace tres cosas a la vez:

  • Exagera las diferencias (de ahí lo de max): el logit más alto no gana por poco, gana por goleada.
  • Aplasta los valores bajos: el logit -8.2 prácticamente desaparece.
  • Y lo hace de forma suave (soft): pequeños cambios en los logits producen pequeños cambios en las probabilidades.

Volviendo al ejemplo, tras pasar los candidatos por la función la competición queda así:

  • dinosaurio: 0.865
  • mamut: 0.106
  • ballena: 0.029
  • refrigerador: 0.0000014
  • el resto del vocabulario se reparte lo que queda

Y de esta manera mamut, que en logits parecía que podía hacerle sombra a dinosaurio, ve cómo sus opciones se desploman.

Queda un último paso. Esas probabilidades son incómodas de manejar en un ordenador: cuando se encadenan muchas, multiplicando una tras otra, el resultado se acerca tanto a cero que la máquina pierde precisión y empieza a redondear donde no debe. El truco es pasarlas a logaritmos. Un logaritmo convierte números minúsculos en números más manejables y, sobre todo, transforma las multiplicaciones en sumas, que son rápidas y estables. A ese logaritmo de la probabilidad es a lo que llamamos logprob.

En nuestro caso, los logprobs quedarían así:

  • dinosaurio: 0.865 -> -0.15
  • mamut: 0.106 -> -2.24
  • ballena: 0.029 -> -3.54
  • refrigerador: 0.0000014 -> -13.54
  • un token cualquiera del montón restante: ≈ 0.0000004 -> -14.7

En esta escala —llamémosla logpróbica, aunque me lo esté inventando— cuanto más cerca de cero, más seguro está el modelo. Para hacerse una idea:

  • +0.5: imposible. La probabilidad nunca pasa de 1, así que el logprob nunca es positivo.
  • 0: certeza total (probabilidad 1).
  • -0.16: muy buena confianza.
  • -0.69: mitad y mitad, el modelo se lo juega a cara o cruz.
  • -2.30: confianza floja.
  • -15: confianza pésima.

(Ese -0.69 es un viejo conocido. Cuando el modelo dudaba al 50% entre dos tokens en el cuarto de pensar, cada uno tenía probabilidad 0.5, y el logaritmo de 0.5 es precisamente -0.69. Es el empate de antes, ahora con su etiqueta numérica puesta.)

Y lo bueno es que estos valores no son un secreto del modelo: podemos pedirlos y leerlos token a token.

3.2. Un experimento casero

Entre los parámetros que admiten las APIs de muchos modelos están logprobs y top_logprobs, con los que podemos pedir que nos devuelvan los logprobs de los N candidatos mejor situados en cada posición.

Así, por ejemplo, subiendo la temperatura para que no se aplanen tanto las probabilidades, podríamos lanzar una llamada en LangChain contra un modelo de DeepSeek:

model = init_chat_model(
    "deepseek-chat",
    temperature=1,
    logprobs=True,
    top_logprobs=3
)

Con esta expresión:

«Responde en una palabra: Odín es más chamánico y orientado a la sabiduría oculta, ¿Zeus es más?»

Nos da estos candidatos:

  • 'Pol': -0.8156471
  • 'Solar': -1.2728939
  • 'Ap': -1.8646736

Como el formato logprob es poco legible, podemos pasarlo a porcentaje, que es más intuitivo. El código completo sería este:

import math
from langchain.chat_models import init_chat_model

# 1. Configuramos el modelo pidiendo explícitamente los logprobs
model = init_chat_model(
    "deepseek-chat",
    temperature=1,
    logprobs=True,
    top_logprobs=3
)

# 2. Lanzamos el prompt
prompt = "Responde en una palabra: Odín es más chamánico y orientado a la sabiduría oculta, ¿Zeus es más?"
response = model.invoke(prompt)

# 3. Recuperamos los logprobs
metadata = response.response_metadata
logprobs_content = metadata.get("logprobs", {}).get("content", [])

# 4. Iteramos sobre los tokens
for token_info in logprobs_content[:5]:  # Miramos los primeros 5 tokens como muestra
    chosen_token = token_info["token"]
    print(f"\nToken elegido: '{chosen_token}'")

    print("Candidatos que compitieron:")
    for candidate in token_info["top_logprobs"]:
        # Pasamos a porcentaje con la fórmula: e^logprob * 100
        probability = math.exp(candidate["logprob"]) * 100
        print(f"  - '{candidate['token']}': {probability:.2f}%")

Et voilà, ya tendríamos nuestra lista:

Token elegido: 'Pol'
Candidatos que compitieron:
  - 'Pol': 44.24%
  - 'Solar': 28.00%
  - 'Ap': 15.49%

Conviene fijarse en que los tres no suman 100%, sino apenas un 88%. Es normal: solo estamos viendo el podio, y el 12% restante se lo reparten los otros miles de tokens del vocabulario, cada uno con una miaja ínfima.

Bola extra

Es muy probable que ese «Pol» que ha ganado sea la primera parte de «político», un significado que se opone al «chamánico y oculto» con el que hemos calificado a Odín y que, por lo tanto, encaja con la intensificación de la locución «es más».

Podemos reforzar esta hipótesis consultando el diccionario de tokens de DeepSeek con el método AutoTokenizer de la librería transformers. Aquí hay que hacer una salvedad: el experimento de antes va contra deepseek-chat por la API, y este diccionario lo sacamos del modelo abierto DeepSeek-V3. Doy por hecho que ambos comparten tokenizador, que es lo razonable, pero no es algo que pueda garantizar al cien por cien.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(
    "deepseek-ai/DeepSeek-V3",
    trust_remote_code=True
)

for token_text, token_id in tokenizer.get_vocab().items():
    if token_text.startswith("ĠPol") or token_text.startswith("Pol"):
        print(f"ID: {token_id} -> Token: '{token_text}'")

Sabiendo que «Ġ» es una marca especial de este diccionario para indicar principio de palabra, nos quedaría una lista parecida a esta:

ID: 84025 -> Token: 'ĠPolynomial'
ID: 92524 -> Token: 'ĠPolitik'
ID: 24309 -> Token: 'ĠPoll'
ID: 18415 -> Token: 'Pol' # <--- por aquí anda el bueno de Pol
ID: 53939 -> Token: 'ĠPolar'
ID: 83791 -> Token: 'ĠPolsce'

Viendo la lista, parece más sensato pensar en una palabra del campo de la política que en algo polar o polinómico. Por pura lógica, «político». Pero aquí toca frenar: el logprob es un dato medido, mientras que el salto de «Pol» a «político» lo ponemos nosotros. El número nos dice qué token eligió el modelo y con cuánta seguridad; no nos dice qué quería decir. El significado es una conjetura nuestra, razonable pero conjetura, y por eso nunca estaremos seguros del todo.

Bueno, que me despisto yo solo. Volvamos con la incertidumbre semántica.

3.3. Su filtrado de confianza

Como vemos, los logprobs pueden ser un indicio poderoso de que una respuesta quizá no sea de fiar, tal y como ya apuntaban en una receta del Cookbook de OpenAI de 2023. Es lo que a veces se llama Confidence-based Filtering, un término paraguas para cualquier técnica que usa la confianza interna del modelo, los logprobs, para decidir si una respuesta pasa o se queda en cuarentena.

¿Y qué señales delatan que algo no va bien? Las hay de tres tipos, según dónde miremos: en una sola palabra, en la respuesta entera o en un punto concreto.

Lo primero que podemos mirar es la confianza en una palabra suelta en el momento de elegirla. Si el modelo tiene dos candidatos casi a la par, con uno apenas por encima del otro, es señal de que duda: cualquiera de los dos le parece igual de defendible. Cuando de verdad tiene clara la respuesta, en cambio, un candidato destaca con holgura sobre todos los demás.

Y hay un caso intermedio revelador: que el token elegido se imponga, sí, pero por poco, con muchas otras opciones pisándole los talones. Entonces el modelo ha tenido que decantarse por uno sin estar convencido, como quien en un examen tipo test marca una casilla solo porque hay que marcar alguna. Esa dispersión tiene hasta nombre técnico, entropía, pero la idea es sencilla: cuanto más repartida está la confianza entre muchos candidatos, menos seguro está el modelo. Eso sí, conviene no precipitarse, porque a veces se reparte no porque el modelo no sepa, sino porque hay varias respuestas igual de buenas. Si la frase admite «empezó» o «comenzó» sin que cambie nada, la duda entre las dos carecía de importancia.

Lo segundo que podemos mirar es la confianza media de toda la respuesta. Hay una medida para esto, la perplejidad, que viene a ser cuánto se sorprende el modelo de sus propias palabras. Si de media va eligiendo tokens que él mismo consideraba poco probables, es como si improvisara sobre un tema que no domina, y la perplejidad sube. Si en cambio todo le resulta predecible y va sobre seguro, la perplejidad es baja. Aquí también toca un matiz: una perplejidad alta no significa siempre que el modelo desbarre. En la propia receta de OpenAI, una pregunta sobre el pasado salía con perplejidad baja y otra sobre el futuro con perplejidad más alta, y es lógico: el futuro admite muchas más respuestas razonables. Más sorpresa no es lo mismo que más error.

Y lo tercero, quizá lo más útil en la práctica, es vigilar dónde cae la confianza dentro de una respuesta larga. Imaginemos un párrafo en el que el modelo avanza seguro, con buenos logprobs, hasta que llega a un dato concreto —una fecha, una cifra, un nombre propio— y de pronto la confianza se desploma justo ahí. Esa caída local es la señal más interesante, porque suele marcar el punto exacto en el que el modelo iba bien hasta que tuvo que inventarse algo que no sabía.

Con cualquiera de estas alarmas activada, ya podemos hacer varias cosas: lanzar los flujos de cuestionamiento que veíamos antes, ya sea con el propio modelo o con otro de juez, o simplemente avisar al usuario de que esa parte de la respuesta conviene tomarla con pinzas.

Ahora bien, es importante tener en cuenta que todo esto mide una sola cosa: lo seguro que está el modelo. Y un modelo puede estar segurísimo, pero estar equivocado por completo. La confianza no equivale a la verdad.

Hay además una trampa más sutil, derivada de la anterior, y es la más traicionera de todas. Puede ocurrir que la confianza sea alta en cada palabra por separado y que, aun así, el conjunto sea un disparate. Pensemos en «el rey de Francia en 2024 fue...»: el modelo completará la frase con seguridad, token a token, sin que ninguno le tiemble, porque cada palabra encaja perfectamente con la anterior. El problema no está en ninguna palabra suelta, sino en que toda la frase da por sentado algo falso, que Francia tiene rey. Ningún logprob va a delatar eso, porque para cada token todo es de lo más plausible.

Para cazar esa clase de error necesitaríamos algo más fino, como saber qué tokens son los que de verdad importan y separarlos del relleno. Pero ¿se puede hacer eso? ¿Hay alguna manera de distinguir las palabras clave del resto?

3.4. Identificando tokens

3.4.1. Heurísticas

Hay tres maneras de identificar los tokens relevantes para el filtrado de confianza. La más artesanal es aplicar reglas heurísticas, cálculos mecánicos simples como medir la longitud de una palabra.

En nuestro caso, podríamos intentar localizar:

  • Números: cualquier token que contenga un dígito (\d).
  • Mayúsculas: palabras que empiezan por mayúscula y no están al inicio de la frase, candidatas a ser nombres propios.
  • Palabras largas, de más de 8 letras, que a veces son nombres o términos clave.
  • Tokens con guiones o puntos, como las fechas (2024-01-30), dominios, etcétera.

En pseudocódigo:

import re

def es_token_importante(token_texto, idx, es_inicio_frase):
    if re.search(r'\d', token_texto):  # tiene número
        return True
    if token_texto[0].isupper() and not es_inicio_frase:  # mayúscula en medio
        return True
    if len(token_texto) > 8:
        return True
    return False

Bueno, no es la solución más elegante, y además tiene una trampa: estas reglas están pensadas para palabras, pero nosotros trabajamos con tokens, que son otra cosa. Como veremos en un momento, un nombre como «Pizarnik» puede partirse en trozos del tipo « Pi», «zar», «nik», ninguno de los cuales llega a 8 letras ni empieza por mayúscula limpia (alguno arrastra un espacio delante). Así que la heurística se nos escapará justo donde más falta hace. Para un apaño rápido podría servir; para algo serio, se queda corta.

3.4.2. Salidas estructuradas

La segunda manera es aprovechar las posibilidades de estructurar la salida. Por ejemplo, si le pedimos que nos dé la respuesta en formato JSON:

{
  "nombre": "Sargon",
  "reino": "Acad"
}

Podría devolver algo así (me invento el formato y los números, para que se vea la idea):

{
  "tokens": ["{", "\n", " ", " ", "\"", "nombre", "\"", ":", " ", "\"", "Sargon", "\"", ",", "\n", " ", " ", "\"", "reino", "\"", ":", " ", "\"", "Acad", "\"", "\n", "}"],
  "token_logprobs": [-0.01, -0.02, -0.01, -0.01, -0.01, -0.15, -0.01, -0.01, -0.01, -0.01, -1.8, -0.02, -0.01, -0.02, -0.01, -0.01, -0.01, -0.2, -0.01, -0.01, -0.01, -0.01, -3.4, -0.03, -0.02, -0.01]
}

Como los campos del JSON tienen siempre la misma estructura, sabemos que los valores que de verdad importan —«Sargon», «Acad»— caen en posiciones predecibles, y ahí es donde miramos el logprob. Todo lo demás (llaves, comillas, saltos de línea) es andamiaje con confianza altísima que podemos ignorar.

3.4.3. Etiquetas

La tercera fórmula es la más seria y se adapta a todo tipo de escenarios sin necesidad de estructurar la salida. Se basa en identificar qué tipo de palabra es cada token mediante alguna librería de NLP, Natural Language Processing, como spaCy, que es muy chula.

Se instala en el proyecto, se le pasa la cadena de texto plano que debe analizar y devuelve, entre otras cosas, dos datos clave:

  • POS Tagging (Part-of-Speech): etiqueta cada palabra según su categoría gramatical (sustantivo, verbo, artículo, preposición, número...).
  • NER (Named Entity Recognition): identifica entidades del mundo real. Aquí conviene una advertencia: el repertorio de etiquetas depende del idioma del modelo, y el español es bastante más pobre que el inglés. El modelo español que usamos solo distingue cuatro tipos:
    • PER: nombres de personas.
    • LOC: lugares (países, ciudades, ríos...).
    • ORG: empresas, universidades, instituciones.
    • MISC: un cajón de sastre para lo demás.

El modelo inglés, entrenado con otro corpus, hila mucho más fino y separa fechas, cantidades, nacionalidades o monedas en etiquetas propias. Pero los modelos de spaCy son monolingües: el inglés no entiende español, así que no podemos cargarlo para analizar una respuesta en castellano y esperar que acierte. Si un sistema responde en inglés, se puede cargar el modelo inglés y tener ese mayor repertorio; si responde en español, te toca apañarse con estas cuatro etiquetas, que no son pocas. Y aquí está el truco: lo que el NER español no marca —los números y las fechas— sí lo recoge el POS, que los etiqueta como NUM. Por eso, combinando ambas señales, no se nos escapan.

Podemos instalar la librería completa, que es algo pesada, o solo el paquete lingüístico que queramos. Por ejemplo, en español:

uv add spacy
uv run python -m spacy download es_core_news_sm

Le pasamos la cadena de texto plano a spaCy y luego la recorremos como necesitemos. En este caso, para sacar una tabla sosa, pero didáctica :P.

import spacy

nlp = spacy.load("es_core_news_sm")

# Tu frase de ejemplo
raw_text = "Alejandra Pizarnik nació el 29 de abril de 1936 en Argentina"
doc = nlp(raw_text)

# Cabecera de la tabla
header = f"{'WORD':<20} | {'START:END':<9} | {'POS TAG':<8} | {'NER LABEL':<10}"
print(header)
print("-" * len(header))

# Iteramos palabra por palabra para extraer sus metadatos
for token in doc:
    start_pos = token.idx
    end_pos = token.idx + len(token.text)
    pos_tag = token.pos_
    
    # Si la palabra no forma parte de una entidad, ponemos "NONE"
    ner_label = token.ent_type_ if token.ent_type_ else "NONE"
    
    print(f"{token.text:<20} | {start_pos:<3}:{end_pos:<5} | {pos_tag:<8} | {ner_label:<10}")

Y el resultado es que ya podríamos localizar los términos donde hay que poner el foco: los nombres propios (PROPN) que el NER marca como persona o lugar (PER, LOC) y los números (NUM), que como decíamos no caen del lado del NER en español, sino del POS. Si además el modelo ha titubeado con algún artículo (DET) o similar, probablemente no sea relevante.

WORD                 | START:END | POS TAG  | NER LABEL 
--------------------------------------------------------
Alejandra            | 0  :9     | PROPN    | PER       
Pizarnik             | 10 :18    | PROPN    | PER       
nació                | 19 :24    | VERB     | NONE      
el                   | 25 :27    | DET      | NONE      
29                   | 28 :30    | NUM      | NONE      
de                   | 31 :33    | ADP      | NONE      
abril                | 34 :39    | NOUN     | NONE      
de                   | 40 :42    | ADP      | NONE      
1936                 | 43 :47    | NUM      | NONE      
en                   | 48 :50    | ADP      | NONE      
Argentina            | 51 :60    | PROPN    | LOC 

Fíjate en que las fechas (29, 1936) salen como NONE en la columna NER. El modelo español no tiene una etiqueta para fechas, así que ahí no las reconoce como entidad. Pero no se nos pierden, porque el POS las ha marcado como NUM. Justo lo que decíamos: una señal cubre lo que la otra deja escapar.

Y ya casi estaría, pero, como nosotros trabajamos con tokens, hay que mapearlos de alguna manera. Algo así, por ejemplo:

import spacy

nlp = spacy.load("es_core_news_sm")

# La frase que se le pasa al modelo
response_text = "Alejandra Pizarnik nació el 29 de abril de 1936 en Argentina"

# Los tokens mockeados
llm_tokens = ["Alej", "andra", " Pi", "zar", "nik", " nació", " el", " 29", " de", " abril", " de", " 1936", " en", " Argentina"]

# Simulación de logprobs (pongo por didáctica  dudas  fuertes intencionadas en el nombre y en el año)
llm_logprobs = [-0.02, -0.05, -3.12, -2.85, -0.15, -0.01, -0.01, -0.04, -0.01, -0.02, -0.01, -4.20, -0.01, -0.05]

# 1. Mapeamos las coordenadas 
doc = nlp(response_text)

# 3. Dibujamos la súper tabla con toda la metadata unificada
header = f"{'LLM TOKEN':<12} | {'START:END':<9} | {'LOGPROB':<8} | {'ASSIGNED WORD':<15} | {'POS TAG':<8} | {'NER LABEL':<10}"
print(header)
print("-" * len(header))

current_pos = 0

for token, logprob in zip(llm_tokens, llm_logprobs):
    token_start = current_pos
    token_end = current_pos + len(token)
    
    assigned_word = "NONE"
    pos_tag = "NONE"
    ner_label = "NONE"
    
    # Buscamos qué tokens caen dentro de esta palabra completa
    for token_spacy in doc:
        word_start = token_spacy.idx
        word_end = token_spacy.idx + len(token_spacy.text)
        
        # Evaluamos intersección o contención de los caracteres
        # Caso especial por si el bloque del LLM incluye el espacio inicial de la palabra
        if (token_start >= word_start and token_start < word_end) or \
           (token_end > word_start and token_end <= word_end):
            
            assigned_word = token_spacy.text
            pos_tag = token_spacy.pos_
            ner_label = token_spacy.ent_type_ if token_spacy.ent_type_ else "NONE"
            break 
            
    print(f"{token:<12} | {token_start:<3}:{token_end:<5} | {logprob:<8.2f} | {assigned_word:<15} | {pos_tag:<8} | {ner_label:<10}")
    
    current_pos = token_end

Y ya tendríamos un mapa de los tokens relevantes. Mucho ojo, por ejemplo, con ese « Pi», que es un PROPN, parte de un nombre propio, y tiene un logprob bajísimo, -3.12. Red flag de manual.

LLM TOKEN    | START:END | LOGPROB  | ASSIGNED WORD   | POS TAG  | NER LABEL 
-----------------------------------------------------------------------------
Alej         | 0  :4     | -0.02    | Alejandra       | PROPN    | PER       
andra        | 4  :9     | -0.05    | Alejandra       | PROPN    | PER       
 Pi          | 9  :12    | -3.12    | Pizarnik        | PROPN    | PER       
zar          | 12 :15    | -2.85    | Pizarnik        | PROPN    | PER       
nik          | 15 :18    | -0.15    | Pizarnik        | PROPN    | PER       
 nació       | 18 :24    | -0.01    | nació           | VERB     | NONE      
 el          | 24 :27    | -0.01    | el              | DET      | NONE      
 29          | 27 :30    | -0.04    | 29              | NUM      | NONE      
 de          | 30 :33    | -0.01    | de              | ADP      | NONE      
 abril       | 33 :39    | -0.02    | abril           | NOUN     | NONE      
 de          | 39 :42    | -0.01    | de              | ADP      | NONE      
 1936        | 42 :47    | -4.20    | 1936            | NUM      | NONE      
 en          | 47 :50    | -0.01    | en              | ADP      | NONE      
 Argentina   | 50 :60    | -0.05    | Argentina       | PROPN    | LOC  

Ese mapa es exactamente lo que buscábamos: un token con etiqueta relevante (PROPN, NUM) y un logprob por los suelos es justo donde conviene desconfiar. En el ejemplo, el nombre y el año son los sospechosos; el resto, andamiaje gramatical que el modelo suelta sin dudar.

4. La inspección interna

Nos quedaría por ver la cuarta familia, la más ambiciosa. Todo lo anterior observa al modelo por fuera: lo que dice, con cuánta seguridad lo dice, dónde titubea. Pero ninguna de esas técnicas abre la caja ni nos dice por qué eligió lo que eligió. Recordemos la trampa pendiente, «el rey de Francia en 2024 fue...»: cada token es plausible, los logprobs van altos y aun así la frase presupone un disparate. Para cazar eso no basta con vigilar la salida; hay que mirar el mecanismo por dentro. Eso es la interpretabilidad mecanicista, que trata al modelo como un cerebro que diseccionar: qué circuitos se encienden, qué neuronas responden a qué conceptos.

En 2025 Anthropic publicó un par de trabajos en esta línea y, mirando ahí dentro, parece ser que encontraron algo revelador sobre las alucinaciones. El modelo tiene una especie de circuito que le dice «si no sabes, cállate», y la alucinación salta cuando otro circuito lo pisa y lo anula. O sea, a veces sabe que no sabe, y responde igual. Justo lo que los logprobs no alcanzan a ver, porque eso ocurre un piso más abajo.

Parece ser, como señalan sus autores, que solo traza parte de lo que ocurre; el modelo sigue siendo en buena medida una caja oscura, pero apuntan maneras.

En fin, quedan muchos temas en el tintero, pero espero que este artículo sirva de introducción para trabajar con el control de calidad de las respuestas de los modelos y nos haya ayudado a conocer un poco mejor cómo funcionan estos bichos sorprendentes.

Mapa conceptual del texto
Mapa conceptual del artículo

5. Para saber más

  • Becker, Evan y Soatto, Stefano. Cycles of Thought: Measuring LLM Confidence through Stable Explanations. Junio de 2024, arXiv:2406.03441

  • Dhuliawala, Shehzaad et al. Chain-of-Verification Reduces Hallucination in Large Language Models. Septiembre de 2023, arXiv:2309.11495

  • Feng, Austin et al. Multi-path reasoning on a budget: towards theoretically optimal hyperparameter-free adaptive self-consistency. Septiembre de 2025, OpenReview

  • Hills, James y Anadkat, Shyamal. Using logprobs. Diciembre de 2023, OpenAI Cookbook

  • Huang, Jie et al. Large Language Models Cannot Self-Correct Reasoning Yet. Octubre de 2023, arXiv:2310.01798

  • Lindsey, Jack et al. On the Biology of a Large Language Model. Marzo de 2025, Transformer Circuits

  • Marks Samuel et alt. Auditing language models for hidden objectives. Marzo de 2025. arXiv:2503.10965

  • Pawitan, Yudi y Holmes, Chris. Confidence in the Reasoning of Large Language Models. Enero de 2025, Harvard Data Science Review, 7(1)

  • Pres, Itamar et al. Position: It's Time to Optimize for Self-Consistency. Marzo de 2026, GitHub

  • Wang, Xuezhi et al. Self-Consistency Improves Chain of Thought Reasoning in Language Models. Marzo de 2022, arXiv:2203.11171

  • Wei, Jason et al. Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. Enero de 2022, arXiv:2201.11903

  • Xiong, Juming et al. Learning When to Sample: Confidence-Aware Self-Consistency for Efficient LLM Chain-of-Thought Reasoning. Marzo de 2026, arXiv:2603.08999

  • Xu, Beining y Lu, Yongming. TECP: Token-Entropy Conformal Prediction for LLMs. 2025, Mathematics 2025, 13(20), 3351

mmfilesi · 2026