[{"role": …, "content": …}] es una comodidad de la librería: lo que entra al
modelo es una cadena con delimitadores especiales, y esa cadena tiene que
tener exactamente el formato con el que el modelo se entrenó.
apply_chat_template es la traducción, y vive en el tokenizador, no en el
modelo.
// Delimitadores de ejemplo. Los de tu modelo salen de su tokenizador, no de aquí.
prompt = tokenizer.apply_chat_template(
mensajes,
tokenize=False, # devuelve la cadena, no los ids
add_generation_prompt=True,
)
print(prompt)
tokenize=False es un instrumento de diagnóstico: te devuelve la cadena en vez
de los ids, así que puedes ver qué le estás pasando al modelo. Imprímela la
primera vez que uses un modelo nuevo. Es la diferencia entre depurar el formato y
adivinarlo.
Una cosa muy concreta: añade al final la cabecera del turno del asistente, vacía, para que
el modelo continúe ahí (es el último <|assistant|> del recuadro
de arriba, sin nada detrás).
Sin él, la cadena termina donde terminó el mensaje del usuario, y lo que el modelo continúa es el turno del usuario. De ahí el síntoma que la guía llama «el modelo habla solo»: no está alucinando, está completando exactamente lo que le has dado.
<|user|> … <|assistant|> …
[INST] … [/INST]
<|im_start|>user … <|im_end|>
Cada familia usa sus delimitadores, y viven en el tokenizer_config.json del
repositorio, en el campo chat_template. La plantilla se le pide al
tokenizador del modelo que vas a usar, nunca se copia de un ejemplo. Si aplicas
los delimitadores de otra familia, el modelo no reconoce el formato con el que fue
entrenado y se comporta como un completador base: sin error, solo peor.
El pipeline de generación devuelve, por defecto, el prompt más la continuación. Hay que quedarse con lo nuevo, y el cómo importa:
| Método | Qué pasa si cambias de modelo |
|---|---|
salida.split("<|assistant|>")[-1] |
El delimitador no aparece, el split devuelve la lista entera y te quedas con todo el texto sin enterarte |
return_full_text=False |
Sigue funcionando: la librería sabe dónde acababa tu prompt |
| Cortar por número de tokens del prompt | Sigue funcionando, y es lo que hay por debajo |
El split por el delimitador funciona hasta el día que cambias de modelo, y
ese día falla en silencio devolviendo texto plausible de más. Es el peor tipo de fallo.
Un modelo de chat cierra su turno emitiendo un token de fin. Si tu generación se pasa de largo e inventa un turno de usuario, hay dos causas típicas: la plantilla no se aplicó —así que el modelo nunca vio la estructura de turnos— o el criterio de parada no incluye el token de fin de turno de ese modelo. Las dos se diagnostican imprimiendo la cadena del paso 01.
La guía dice en sus Puntos clave: «usa siempre
apply_chat_template con modelos de chat». Y en su propio apartado de
generación de código hace lo contrario: pasa el prompt en crudo a
pipe_chat(...) y luego intenta partir la salida por
"<|assistant|>". Como la plantilla nunca se aplicó, ese delimitador no
aparece en el resultado y el split devuelve el texto entero. El código
contradice la regla que la propia guía acaba de dar.
En ese mismo bloque hay un fallo más simple: la variable prompt_codigo
aparece escrita en una línea suelta y nunca se le asigna nada antes de
usarse. Ese fragmento, copiado tal cual, no se ejecuta.
Y una cifra con truco. La guía dice de TinyLlama «1.1B parámetros» y «aprox 2.2 GB».
Los dos números solo encajan si los pesos están en 16 bits; en 32 bits serían
aproximadamente el doble. La guía no dice en qué precisión carga, así que la cifra de
descarga solo es cierta bajo un supuesto que no enuncia. Antes de dimensionar una
máquina con ese número: ¿qué torch_dtype estás usando de verdad?
Coge cualquier modelo de chat pequeño y haz solo esto: construye la lista de mensajes,
llama a apply_chat_template con tokenize=False e
imprime la cadena. Después vuelve a imprimirla con
add_generation_prompt=False y compara. La diferencia entre las dos es
literalmente la razón por la que un modelo «habla solo».
Luego cambia a un modelo de otra familia y repite la impresión sin tocar nada más. Si los
delimitadores han cambiado, ya sabes por qué ningún split escrito a mano
sobrevive a un cambio de modelo.