Inicio / Artículos / La tokenización es una decisión de diseño de sistemas, no un paso de preprocesamiento

La tokenización es una decisión de diseño de sistemas, no un paso de preprocesamiento

Cómo el BPE a nivel de bytes construye el vocabulario que ve tu LLM, por qué el número de tokens cambia según el idioma, qué fallos provoca y qué medir antes de culpar al modelo.

2081 palabras

Cuatro síntomas con una sola causa

Tarde o temprano, casi todo el que construye sobre LLM se topa con alguno de estos:

  • Un modelo anunciado con una ventana de contexto de 128k se queda sin espacio mucho antes de lo esperado con entradas que no están en inglés o que llevan mucho código.
  • Una misma función cuesta bastante más por petición a quienes escriben en japonés, hindi o árabe que a quienes escriben en inglés.
  • El modelo falla con total seguridad al contar letras o invertir una palabra, se formule como se formule el prompt.
  • Dos equipos obtienen recuentos de tokens distintos para un texto que parece idéntico, y nadie sabe explicar por qué.

Reformular el prompt no resuelve nada de esto, y el modelo no es simplemente torpe. La culpable es la tokenización, la etapa del pipeline que los ingenieros suelen conocer peor. Vive entre tu código y el modelo, hace su trabajo sin que se vea y solo llama la atención cuando falla. A continuación se explica el mecanismo, su lugar en un sistema real, el daño que puede causar y una lista de comprobación para repasar antes de culpar al modelo.

Lo que el modelo recibe de verdad

Un transformer no puede procesar texto directamente, así que lo primero que ocurre es convertirlo en enteros. Esa conversión es la tokenización, y se apoya en un vocabulario fijado de antemano. Nada en tu prompt puede cambiarlo mientras el modelo se ejecuta.

Imagina un juego fijo de piezas de Lego. Las palabras inglesas frecuentes como «the», «is» y «running» suelen ser una sola pieza. Las más raras se dividen, así que «tokenization» podría quedar en «token» más «ization». El texto que el tokenizador vio poco, como un idioma minoritario, un símbolo Unicode inusual o el nombre de un producto poco conocido, se rompe en más piezas y más pequeñas.

Tus caracteres originales nunca llegan al modelo. Lo que llega es la segmentación que eligió el tokenizador, y la facturación, el uso del contexto y ciertos errores de razonamiento tienen su origen en dónde cortó. La palabra «strawberry» es un buen ejemplo: el modelo ve unos pocos tokens, no diez letras.

Cómo se construye el vocabulario

La mayoría de los vocabularios salen de Byte Pair Encoding (BPE). Su origen está en la compresión, no en el lenguaje: Philip Gage lo describió en 1994 como una forma de reducir datos sustituyendo por un símbolo nuevo el par de símbolos que aparece junto con más frecuencia.

Sennrich, Haddow y Birch lo llevaron a la traducción automática neuronal en 2016 con el artículo «Neural Machine Translation of Rare Words with Subword Units». Los vocabularios a nivel de palabra se atascan con palabras raras, erratas y formas flexionadas, y su respuesta fue un vocabulario abierto en el que las palabras raras se montan a partir de piezas de subpalabra más pequeñas y frecuentes.

El trabajo de OpenAI sobre GPT-2 (Radford et al., 2019, «Language Models are Unsupervised Multitask Learners») llevó la idea al nivel de bytes. Las fusiones empiezan desde bytes UTF-8 en bruto en lugar de caracteres, lo que da un alfabeto inicial de exactamente 256 símbolos. Ese único cambio permite a un tokenizador moderno representar cualquier cadena en cualquier idioma, incluido texto mal formado y emojis, sin chocar con un token desconocido. Casi todas las grandes familias de modelos actuales, GPT y Llama entre ellas, usan alguna variante.

El entrenamiento se hace una vez, sin conexión, con estos pasos:

  1. Empezar con unidades base, los 256 bytes o los caracteres en la formulación original.
  2. Recorrer un corpus grande y contar cuántas veces aparece cada par adyacente de símbolos.
  3. Tomar el par más frecuente, fusionarlo en un símbolo nuevo y añadir ese símbolo al vocabulario.
  4. Sustituir cada aparición de ese par en el corpus por el símbolo nuevo.
  5. Repetir los pasos 2 a 4 hasta que el vocabulario alcance un tamaño objetivo, normalmente entre decenas de miles y un par de cientos de miles de tokens.

Terminado el entrenamiento, la codificación es rápida y totalmente determinista. Al texto nuevo se le aplican las fusiones aprendidas en el mismo orden en que se aprendieron, uniendo piezas de forma voraz hasta que no se puede fusionar nada más. No interviene ninguna comprensión. Es búsqueda y fusión, y la misma entrada siempre produce los mismos tokens.

SentencePiece, una biblioteca aparte de Google (2018), sigue otro camino que conviene conocer. Entrena directamente sobre texto en bruto y trata el espacio como un símbolo corriente, no como un delimitador especial. Eso importa en chino, japonés y tailandés, que no separan palabras con espacios: ahí un pretokenizador basado en espacios se desmorona y SentencePiece funciona de forma uniforme.

Dónde se sitúa en una petición

La tokenización se ejecuta antes del transformer. El recorrido de una petición real es este:

User Text
   ↓
Pretokenizer (split on whitespace/punctuation rules, handle Unicode)
   ↓
BPE Encoder (apply learned merges → token IDs)
   ↓
Token IDs (integers)
   ↓
Embedding Lookup Table
   ↓
Transformer (attention + feedforward layers)
   ↓
Output Token IDs
   ↓
Decoder (token IDs → text)
   ↓
Response Text

Hay dos componentes fáciles de olvidar. Primero, la tabla de embeddings se indexa por ID de token, así que el tamaño del vocabulario fija el tamaño de las capas de embedding y de proyección de salida. Un vocabulario más grande gasta más parámetros en convertir tokens en vectores antes de que se ejecute cualquier capa de razonamiento. Segundo, el decodificador es el tokenizador al revés. Emitir una respuesta token a token y convertirla de nuevo en texto legible depende del mismo vocabulario fijo que se usa para codificar.

Verlo con tiktoken

En lugar de creerlo sin más, puedes comprobarlo con tiktoken, la biblioteca de tokenización de OpenAI:

import tiktoken

enc = tiktoken.encoding_for_model("gpt-4o")

samples = {
    "english": "The invoice was approved yesterday.",
    "japanese": "請求書は昨日承認されました。",
}

for label, text in samples.items():
    tokens = enc.encode(text)
    print(f"{label}: {len(tokens)} tokens -> {tokens}")
    print([enc.decode([t]) for t in tokens])

Al hacer una comparación así, hay dos cosas que mirar. Primero, los tokens por carácter o por palabra varían mucho según el idioma. La misma información puede costar bastantes más tokens según lo bien representado que estuviera el idioma al entrenar el tokenizador. La propia documentación de OpenAI sobre su tokenizador lo muestra: una frase corta se rompe en más piezas con las codificaciones antiguas, centradas en el inglés, que con las nuevas, y las escrituras no latinas suelen usar más tokens por carácter que la prosa inglesa. Segundo, los límites de los tokens rara vez coinciden con caracteres o siquiera con sílabas. Por eso contar letras e invertir cadenas les cuesta a los LLM por defecto: la representación con la que trabajan no expone los caracteres individuales como unidad de primera clase.

Un ejemplo de presupuesto multilingüe

Imagina un agente de soporte que atiende a hablantes de inglés, japonés y árabe con un presupuesto estricto de tokens por petición. Limitar los tokens de entrada y salida por llamada para acotar coste y latencia es una práctica normal en producción. Si dimensionas ese presupuesto probando solo en inglés, perjudicarás sistemáticamente a todos los demás. El mismo historial de conversación y el mismo prompt de sistema consumen más presupuesto en japonés o árabe y dejan menos espacio para la respuesta.

No hay nada exótico en esto. Los tokenizadores BPE entrenados sobre todo con inglés se comportan así de serie, y el resultado es una pérdida silenciosa de calidad, por contexto recortado, justo para los usuarios internacionales a los que el producto quiere llegar. Un prompt más ingenioso no lo arregla. Lo que ayuda es contar tokens reales por idioma durante las pruebas y luego fijar presupuestos y reglas de recorte o resumen por separado para cada uno.

Un experimento barato

Antes de fiarte de suposiciones sobre el comportamiento del tokenizador, prueba esto. Toma entre 20 y 30 frases de contenido equivalente, traducidas profesionalmente al inglés, japonés, árabe e hindi (las transcripciones de soporte funcionan bien). Codifica cada una con un mismo tokenizador, por ejemplo o200k_base mediante tiktoken, y mide los tokens por frase y por carácter de cada idioma.

La hipótesis a comprobar (no es un hallazgo establecido) es que el japonés, el árabe y el hindi salen con una proporción de tokens por carácter claramente mayor que el inglés. Eso significaría que la misma ventana de contexto y el mismo presupuesto por petición dejan a esos usuarios menos espacio útil para el mismo contenido. La proporción exacta depende de la versión del tokenizador y de los idiomas, así que mídela con tus propios datos.

Fallos que te encontrarás en producción

La tokenización produce un conjunto concreto y recurrente de errores.

  • Fallos en tareas a nivel de carácter. Contar letras, invertir cadenas, deletrear y parte de la aritmética con varios dígitos fallan porque el modelo razona sobre tokens de subpalabra y no sobre caracteres. El síntoma es un modelo que se equivoca con aplomo en algo trivial para una persona. Mejorar el prompt por sí solo no lo arregla. Funciona mejor pedirle al modelo que deletree la palabra con separadores, lo que fuerza tokens a nivel de carácter, o pasar la tarea a ejecución de código.
  • Desajuste entre tokenizador y modelo. Usar el tokenizador de otro modelo para estimar coste o uso de contexto da cifras sistemáticamente erróneas. Verás recuentos locales que no cuadran con la factura de la API. Usa la codificación documentada para el modelo al que llamas.
  • Desbordamientos que nadie nota. Un prompt que cabe sobrado en inglés puede pasarse del límite al traducirlo, y lo mismo ocurre con contenido lleno de código, JSON o símbolos raros. El síntoma es contexto recortado y peores respuestas, en algunos pipelines sin ningún error explícito. Mide los tokens reales sobre contenido real de producción.
  • Sensibilidad a espacios y formato. Los espacios iniciales, los espacios repetidos y un formato incoherente pueden desplazar los límites y el número de tokens, a veces mucho, de modo que dos prompts que a una persona le parecen iguales difieren en tokens y coste.
  • Desajuste de vocabulario tras un ajuste fino o un cambio de modelo. Pasar a otra familia de modelos con otro tokenizador sin revisar los presupuestos de prompt ni la longitud de los ejemplos few-shot puede romper en silencio supuestos en los que se apoyaba el sistema. Algo que funcionaba bien empeora tras un cambio supuestamente transparente.

Qué hacer en producción

  • Coste: los tokens son la unidad real de facturación, así que los productos multilingües deben presupuestar por idioma y no suponer una media global.
  • Planificación del contexto: dimensiona los chunks de RAG, el recorte del historial de conversación y los ejemplos few-shot en tokens del tokenizador concreto que usas, nunca en palabras ni en caracteres.
  • Velocidad: la tokenización en sí es rápida y rara vez es el cuello de botella, pero estimar la longitud del prompt con la codificación equivocada antes de una llamada puede provocar reintentos o recortes innecesarios, y ambos añaden retraso.
  • Observabilidad: registra el número real de tokens de entrada y salida por petición, desglosado por idioma si atiendes varios. Es barato de añadir y detecta problemas de coste y contexto antes de que los usuarios los reporten.
  • Coherencia: usa la misma versión del tokenizador para las pruebas locales, la estimación de costes y la llamada a la API en producción. Una codificación distinta da cifras erróneas con total seguridad.

Cuándo puedes ignorar todo esto

En una aplicación solo en inglés, de textos cortos y poco volumen, como una herramienta interna, un prototipo o un chatbot con poco tráfico, las cuestiones del tokenizador como las diferencias de coste entre idiomas no merecen tiempo de ingeniería. Los valores por defecto de la biblioteca sirven, y el esfuerzo rinde más en otra parte.

Del mismo modo, si el producto nunca necesita trabajo a nivel de carácter, como deletrear, contar letras o invertir cadenas con exactitud, no hay nada que sortear. Solo importa cuando el caso de uso le pide de verdad esas tareas al modelo, y entonces la solución habitual es delegarlas en un paso de ejecución de código en lugar de pelearse con el tokenizador a base de prompts.

La versión corta

Los modelos nunca ven caracteres ni palabras, solo tokens de un vocabulario fijo construido de antemano, normalmente con BPE a nivel de bytes. BPE nació en 1994 como algoritmo de compresión, llegó al PLN en 2016 y el equipo de GPT-2 lo extendió a bytes en 2019, por eso los tokenizadores modernos prácticamente nunca emiten tokens desconocidos. Lo que fija tu coste real y tu uso de contexto es el número de tokens, no el de palabras ni el de caracteres. La misma información puede costar más tokens en algunos idiomas, lo que perjudica en silencio a los usuarios que no hablan inglés si no presupuestas por idioma. Los fallos al contar caracteres, deletrear o invertir vienen de la representación del tokenizador y no de un déficit de razonamiento, y los tokenizadores desajustados causan errores evitables en producción. Mide la tokenización y sigue vigilándola como parte del diseño, en lugar de olvidarla tras la primera integración.

Referencias:

  • Gage (1994), «A New Algorithm for Data Compression», The C Users Journal, el algoritmo de compresión BPE original.
  • Sennrich, Haddow y Birch (2016), «Neural Machine Translation of Rare Words with Subword Units», ACL 2016, DOI 10.18653/v1/P16-1162.
  • Radford et al. (2019), «Language Models are Unsupervised Multitask Learners», el artículo de GPT-2 que introdujo el BPE a nivel de bytes.
  • Kudo y Richardson (2018), el artículo de Google sobre SentencePiece, un tokenizador y detokenizador de subpalabras independiente del idioma.
  • La guía del OpenAI Cookbook para contar tokens con tiktoken y la documentación de tiktoken sobre las codificaciones cl100k_base y o200k_base.