Pásale un CV a un modelo de lenguaje, pídele una nota del 1 al 10 y te devolverá un número con una justificación muy segura. Pregúntale otra vez y el número puede cambiar. El consejo habitual es "pon la temperatura a 0", y es correcto, pero deja dos preguntas sin responder: ¿cuánto se mueve de verdad la nota con la configuración por defecto? ¿Y qué es lo que temperature 0 no arregla? Las dos cosas se pueden medir en un portátil en unos minutos, porque la nota es un solo token y el modelo te da su probabilidad directamente.

Para las pruebas usé Qwen2.5-3B-Instruct (4 bits, MLX) con mlx-lm 0.32.2 en un Mac M1 de 8 GB. Los prompts son los de una demo de evaluación de CVs en Models → Playground de la app de escritorio osFoundry: un CV de seis líneas para un puesto de analista de datos junior, seguido de "Score this CV from 1 to 10 for a junior data analyst role. Start with the score, then one reason." La app ejecuta los modelos locales con llama-server de llama.cpp, así que la configuración del muestreo sale de ahí.

Los valores por defecto cuando no eliges nada

Si una petición no lleva temperatura, llama-server muestrea con sus propios valores por defecto. La compilación que incluye la app (versión 10358) los muestra en --help:

--temp, --temperature N temperature (default: 0.80) --top-k N top-k sampling (default: 40, 0 = disabled) --top-p N top-p sampling (default: 0.95, 1.0 = disabled) --min-p N min-p sampling (default: 0.05, 0.0 = disabled) -s, --seed SEED RNG seed (default: -1, use random seed for -1) 

Se aplican a cualquier petición que no fije sus propios valores. Cuando la app arranca llama-server, solo añade --temp si se ha configurado una temperatura para el servidor (local_inference.rs), así que una petición sin temperatura se muestrea a 0.8 con una semilla aleatoria, que es también lo que hacen de serie la mayoría de interfaces basadas en llama.cpp.

Hacer que la nota sea el siguiente token

Una respuesta libre puede empezar con "Score: 7", "7/10" o "I would rate this CV a 7", y así cuesta localizar la nota. La solución es rellenar de antemano el inicio de la respuesta, para que el siguiente token que genere el modelo sea directamente el dígito:

import mlx.core as mx from mlx_lm import load, generate from mlx_lm.sample_utils import make_sampler model, tok = load("mlx-community/Qwen2.5-3B-Instruct-4bit") DIGITS = {d: tok.encode(str(d))[0] for d in range(1, 10)} def ids_for(cv, question, prefill="Score: "): msgs = [{"role": "user", "content": cv + "\n\n" + question}] text = tok.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False) return tok.encode(text + prefill) def digit_probs(ids, T=1.0): logits = model(mx.array([ids]))[0, -1].astype(mx.float32) p = mx.softmax(logits / T) return {d: float(p[t]) for d, t in DIGITS.items()} 

El tokenizador de Qwen separa los números en dígitos sueltos, así que "7" es un token y "10" empezaría por el token "1". Una sola pasada hacia delante sobre el prompt de 173 tokens da la distribución completa de la nota.

La nota es una distribución, no un número

Para el CV en inglés, con temperatura 1 (las probabilidades en bruto del modelo):

Nota Probabilidad
7 74.2%
6 23.3%
8 1.3%
5 1.2%

El modelo no "piensa" que el CV es un 7. Piensa que probablemente es un 7, con una posibilidad entre cuatro de que sea un 6. La temperatura divide los logits antes del softmax, lo que afila o aplana esta distribución:

Temperatura P(7) P(6) P(8)
1.0 74.2% 23.3% 1.3%
0.8 80.2% 18.9% 0.5%
0.5 91.0% 9.0% 0.0%
0.2 99.7% 0.3% 0.0%

llama-server no aplica solo la temperatura. Su cadena por defecto ejecuta top-k 40, luego top-p 0.95, luego min-p 0.05 y la temperatura al final. Con estos logits, top-p ya elimina todo lo que viene después del 7 y el 6 (entre los dos superan el 95%), así que el muestreador por defecto elige entre exactamente dos notas:

def llamacpp_default(logits, temp=0.8, top_k=40, top_p=0.95, min_p=0.05): order = mx.argsort(-logits)[:top_k] kept = logits[order] p = mx.softmax(kept) n = int(mx.sum(mx.cumsum(p) < top_p).item()) + 1 idx = [i for i in range(n) if p[i].item() >= min_p * p[0].item()] q = mx.softmax(kept[mx.array(idx)] / temp) return {tok.decode([int(order[i])]): float(q[j]) for j, i in enumerate(idx)} # -> {'7': 0.8093, '6': 0.1907} 

Después lo muestreé de verdad: 200 generaciones con el mismo prompt y el muestreador de mlx-lm configurado con esos mismos cuatro valores (make_sampler(temp=0.8, top_p=0.95, min_p=0.05, top_k=40)).

7 6
Previsto por la cadena 80.9% 19.1%
Observado, 200 ejecuciones 162 (81%) 38 (19%)

Es decir, con la configuración por defecto, más o menos una de cada cinco ejecuciones le pone un 6 a este CV en lugar de un 7. Si evalúas a dos candidatos una vez cada uno y uno saca un 6 y el otro un 7, hay una posibilidad real de que la diferencia sea puro sorteo.

Un caso más plano: el mismo CV en japonés

El mismo CV y la misma pregunta en japonés (222 tokens, con 「点数:」 como relleno inicial) dieron un modelo mucho menos decidido:

Nota Probabilidad con T=1 Observado, 200 ejecuciones con los valores por defecto
8 39.8% 108
9 23.8% 44
7 19.4% 40
10 (empieza por "1": 4.1%) 8

Aquí la nota más probable gana solo la mitad de las veces, más o menos, y en 200 ejecuciones salieron cuatro notas distintas. Cuánto baila una nota no es una propiedad fija del modelo. Depende de lo cerca que estén los primeros candidatos para esa entrada concreta, y eso solo se ve mirando las probabilidades.

Lo que temperature 0 sí arregla

Con temperatura 0 (decodificación greedy: siempre el token más probable), generé la respuesta completa de 40 tokens 20 veces en cada idioma. Salió 1 texto distinto por idioma, idéntico carácter a carácter. Para el CV en inglés es "7" seguido siempre de la misma justificación, y para el japonés 「8点」.

Una semilla fija consigue lo mismo al muestrear: 10 ejecuciones reiniciando mx.random.seed(42) antes de cada una dieron 1 texto distinto. Es útil cuando quieres variedad pero necesitas reproducir una ejecución concreta. Pero no sirve para ser justo. Con una semilla, un CV distinto recibe una secuencia de sorteos distinta, así que dos candidatos siguen evaluándose con suertes diferentes.

Todo esto tiene una condición: cada ejecución fue una única petición sobre un modelo sin otra carga. Los servidores que agrupan varias peticiones en lotes pueden producir diferencias mínimas de coma flotante entre ejecuciones, que solo importan cuando dos tokens están casi empatados. Eso no se midió aquí.

Lo que temperature 0 no arregla

La decodificación greedy convierte la salida en una función de la entrada. Lo que no garantiza es que sea una función estable. Hice cuatro cambios que no deberían alterar la nota de un CV y anoté cada vez la nota greedy y las probabilidades:

Cambio (inglés) Nota greedy P(primera) Segunda
ninguno 7 74.2% 6 con 23.3%
Línea de habilidades movida arriba 7 69.6% 6 con 28.1%
Nombre cambiado (Mia Chen → James Miller) 7 68.5% 6 con 28.5%
Una línea en blanco extra 7 71.0% 6 con 26.1%
Pregunta reformulada 6 67.5% 5 con 19.3%

La pregunta reformulada era "Rate this CV for a junior data analyst role on a 1 to 10 scale. Give the score first, then one reason." Misma tarea, mismo CV, y la nota greedy baja de 7 a 6. En japonés, las cinco versiones se quedaron en 8 con temperatura 0, pero la probabilidad de la primera nunca pasó del 44%, y la segunda alternaba entre 9 y 7 según el cambio.

Así que temperature 0 elimina el azar entre ejecuciones, no la sensibilidad al prompt. Para comparar CVs, eso significa:

  • Usa temperatura 0 y exactamente la misma pregunta para todo lo que compares, carácter a carácter. Si cambias la pregunta, vuelve a puntuarlo todo.
  • Guarda la probabilidad, no solo la nota. El endpoint /completion de llama-server devuelve las probabilidades de los tokens si le pasas n_probs, y con el truco del relleno inicial el primer token es la nota. Un 7 al 74% y un 8 al 40% no son el mismo tipo de respuesta.
  • Trata como empate una diferencia de un punto con poca confianza, y lee la justificación del modelo antes de fiarte de cualquiera de los dos números.

Hacerlo en la app

En la app de escritorio osFoundry, el mismo experimento son tres paneles con un mismo modelo local en Models → Playground. Ejecuta el prompt una vez con los valores por defecto para ver la dispersión, luego abre la fila de ajustes y pon Temp a 0.

Tres paneles del Playground con el mismo modelo y el mismo CV, que le ponen un 6, un 8 y un 7 con la configuración por defecto (ejemplo)

Las entradas del glosario sobre temperatura y top-p explican los dos ajustes con más detalle. La app es gratuita en la página de descarga de osFoundry.