
Tokenización Nativa con Qwen 2.5: Superando las Limitaciones de LM Studio en Proyecto PINSAPIA
¡Ha costado sus buenas horas de depuración, pero lo hemos conseguido! Cuando trabajamos con modelos de lenguaje locales y herramientas de servidor HTTP como LM Studio, no siempre todo sale a la primera. En esta entrega de la Línea Booth de PINSAPIA, analizamos el misterio del “Falso 200 OK” de LM Studio y cómo implementar la tokenización nativa y exacta de Qwen 2.5 directamente en Python.

1. El Diagnóstico: El Misterio del Falso 200 OK y los 0 tokens
Durante las pruebas iniciales, al intentar consultar a LM Studio para obtener los tokens de una frase en español o inglés, la aplicación devolvía un resultado extraño:
- Petición realizada:
POST /v1/internal/tokenizeoPOST /tokenize - Código de estado HTTP:
200 OK - Resultado:
num_tokens: 0ytokens: []
Al revisar los registros internos (logs) del servidor local de LM Studio, encontramos el verdadero motivo:
Plaintext
[ERROR] Unexpected endpoint or method. (POST /tokenize). Returning 200 anyway
¿Por qué ocurría esto?
LM Studio está diseñado como un servidor de inferencia compatible con la especificación OpenAPI de OpenAI (/v1/chat/completions, /v1/embeddings). Cuando recibe peticiones HTTP a rutas de tokenización interna (/tokenize), no responde con un error 404 Not Found, sino que responde con un código 200 OK enviando un objeto JSON vacío o de error.
Nuestra aplicación interpretaba ese 200 OK como una respuesta válida, generando el famoso falso positivo de los 0 tokens.
2. La Solución: Tokenización Nativa con transformers
En lugar de forzar a LM Studio a hacer algo para lo que su API HTTP REST no está expuesta, la solución más limpia y profesional consiste en cargar el tokenizador oficial de Qwen 2.5 directamente en Python mediante la librería transformers de Hugging Face.
Ventajas de este enfoque:
- Sin dependencia de servidores HTTP: No necesitas tener LM Studio encendido ni un modelo pesado cargado en VRAM solo para contar tokens.
- Descarga ligera: Solo se descarga el archivo de vocabulario y configuración del tokenizador (
~1.5 MB). - Exactitud del 100%: Utiliza las mismas reglas de división BPE (Byte-Pair Encoding) que el modelo Qwen 2.5 original.
- Rendimiento ultra-rápido: La tokenización se ejecuta en milisegundos en memoria local.
3. Instalación de Dependencias
Para ejecutar esta versión completa y funcional, asegúrate de instalar los siguientes paquetes en tu entorno virtual de Python:
Bash
pip install fastapi uvicorn tiktoken transformers
4. Código Fuente Completo (app_booth.py v2.0.0)
A continuación se presenta el código listo para producción, que integra el modo Tiktoken (OpenAI) y el modo Qwen 2.5 Nativo (Hugging Face) con el visor JSON crudo e interfaz visual interactiva.
Python
import json
import time
import urllib.request
import re
from typing import Optional, List, Dict, Any
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import uvicorn
# -----------------------------------------------------------------------------
# IMPORTACIÓN SEGURA DE MOTORES (TIKTOKEN Y TRANSFORMERS PARA QWEN)
# -----------------------------------------------------------------------------
TIKTOKEN_AVAILABLE = False
try:
import tiktoken
TIKTOKEN_AVAILABLE = True
except ImportError:
tiktoken = None
QWEN_TOKENIZER = None
QWEN_AVAILABLE = False
try:
from transformers import AutoTokenizer
# Carga el vocabulario oficial de Qwen 2.5 (descarga ~1.5 MB en la primera ejecución)
QWEN_TOKENIZER = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B", trust_remote_code=True)
QWEN_AVAILABLE = True
except Exception:
QWEN_AVAILABLE = False
app = FastAPI(
title="Proyecto PINSAPIA - Línea Booth",
description="Tokenización interactiva para IA con visor Raw de Qwen",
version="2.0.0"
)
# -----------------------------------------------------------------------------
# 1. PRESETS PEDAGÓGICOS
# -----------------------------------------------------------------------------
PRESETS = [
{
"id": "preset_1",
"categoria": "Español vs Inglés",
"titulo": "Opción 1: Frase Contextual",
"subtitulo": "Español vs Inglés",
"es": "La inteligencia artificial está transformando la educación",
"en": "Artificial intelligence is transforming education"
},
{
"id": "preset_2",
"categoria": "Término Técnico",
"titulo": "Opción 2: Término Técnico / Raro",
"subtitulo": "Misma palabra en ambos idiomas",
"es": "antidisestablishmentarianism electroencefalografista",
"en": "antidisestablishmentarianism electroencephalographer"
},
{
"id": "preset_3",
"categoria": "Símbolos y Números",
"titulo": "Opción 3: Caracteres Especiales",
"subtitulo": "Emojis, tildes y números 2026",
"es": "¡Hola! 😀 El año 2026 tiene 366 días #IA @Monterroso",
"en": "Hello! 😀 The year 2026 has 366 days #AI @Monterroso"
}
]
# -----------------------------------------------------------------------------
# 2. MOTORES DE TOKENIZACIÓN
# -----------------------------------------------------------------------------
def tokenizar_fallback(texto: str) -> Dict[str, Any]:
inicio = time.perf_counter()
if not texto:
texto = ""
pattern = r'\w+|[^\w\s]|\s+'
raw_tokens = re.findall(pattern, texto)
tokens_list = []
for idx, tok in enumerate(raw_tokens):
tokens_list.append({
"id": idx,
"token_id": 9000 + idx,
"texto": tok,
"bytes_hex": tok.encode('utf-8', errors='replace').hex()
})
num_palabras = len(texto.split())
num_tokens = len(tokens_list)
ratio = round(num_tokens / num_palabras, 2) if num_palabras > 0 else 0.0
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 3)
return {
"texto_original": texto,
"num_caracteres": len(texto),
"num_palabras": num_palabras,
"num_tokens": num_tokens,
"ratio_token_palabra": ratio,
"tokens": tokens_list,
"tiempo_ms": tiempo_ms,
"motor": "Respaldo Local (Python Sin Red)"
}
def tokenizar_tiktoken(texto: str, encoding_name: str = "cl100k_base") -> Dict[str, Any]:
if not TIKTOKEN_AVAILABLE:
res = tokenizar_fallback(texto)
res["motor"] = "Respaldo (tiktoken no instalado)"
return res
inicio = time.perf_counter()
if not texto:
texto = ""
try:
enc = tiktoken.get_encoding(encoding_name)
token_ids = enc.encode(texto)
except Exception:
res = tokenizar_fallback(texto)
res["motor"] = "Respaldo (Sin vocabulario Tiktoken)"
return res
tokens_list = []
for i, tid in enumerate(token_ids):
try:
raw_bytes = enc.decode_single_token_bytes(tid)
token_text = raw_bytes.decode('utf-8', errors='replace')
hex_bytes = raw_bytes.hex()
except Exception:
token_text = f"[{tid}]"
hex_bytes = ""
tokens_list.append({
"id": i,
"token_id": tid,
"texto": token_text,
"bytes_hex": hex_bytes
})
num_palabras = len(texto.split())
num_tokens = len(token_ids)
ratio = round(num_tokens / num_palabras, 2) if num_palabras > 0 else 0.0
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 3)
return {
"texto_original": texto,
"num_caracteres": len(texto),
"num_palabras": num_palabras,
"num_tokens": num_tokens,
"ratio_token_palabra": ratio,
"tokens": tokens_list,
"tiempo_ms": tiempo_ms,
"motor": f"Tiktoken ({encoding_name})"
}
def tokenizar_qwen_nativo(texto: str) -> Dict[str, Any]:
if not QWEN_AVAILABLE or QWEN_TOKENIZER is None:
raise HTTPException(
status_code=500,
detail="El tokenizador de Qwen no está disponible. Ejecuta 'pip install transformers' en la consola para habilitar la tokenización nativa de Qwen."
)
inicio = time.perf_counter()
if not texto:
texto = ""
token_ids = QWEN_TOKENIZER.encode(texto)
tokens_list = []
for i, tid in enumerate(token_ids):
try:
tok_text = QWEN_TOKENIZER.decode([tid])
raw_bytes = tok_text.encode('utf-8', errors='replace')
hex_bytes = raw_bytes.hex()
except Exception:
tok_text = f"[{tid}]"
hex_bytes = ""
tokens_list.append({
"id": i,
"token_id": tid,
"texto": tok_text,
"bytes_hex": hex_bytes
})
num_palabras = len(texto.split())
num_tokens = len(token_ids)
ratio = round(num_tokens / num_palabras, 2) if num_palabras > 0 else 0.0
tiempo_ms = round((time.perf_counter() - inicio) * 1000, 3)
return {
"texto_original": texto,
"num_caracteres": len(texto),
"num_palabras": num_palabras,
"num_tokens": num_tokens,
"ratio_token_palabra": ratio,
"tokens": tokens_list,
"tiempo_ms": tiempo_ms,
"motor": "Qwen 2.5 (Nativo / HuggingFace)"
}
# -----------------------------------------------------------------------------
# 3. ENDPOINTS DE LA API
# -----------------------------------------------------------------------------
class TokenizeRequest(BaseModel):
texto: str
texto_en: Optional[str] = None
encoding: str = "cl100k_base"
modo: str = "tiktoken"
@app.get("/api/presets")
def get_presets():
return PRESETS
@app.post("/api/tokenizar")
def api_tokenizar(req: TokenizeRequest):
if not req.texto.strip() and not (req.texto_en and req.texto_en.strip()):
raise HTTPException(status_code=400, detail="El texto de entrada no puede estar vacío.")
res_es = None
res_en = None
if req.modo == "lmstudio":
if req.texto.strip():
res_es = tokenizar_qwen_nativo(req.texto)
if req.texto_en and req.texto_en.strip():
res_en = tokenizar_qwen_nativo(req.texto_en)
else:
if req.texto.strip():
res_es = tokenizar_tiktoken(req.texto, req.encoding)
if req.texto_en and req.texto_en.strip():
res_en = tokenizar_tiktoken(req.texto_en, req.encoding)
return {
"es": res_es,
"en": res_en,
"modo": req.modo,
"encoding": "Qwen 2.5" if req.modo == "lmstudio" else req.encoding
}
# -----------------------------------------------------------------------------
# 4. FRONTEND HTML
# -----------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
def index():
return HTML_CONTENT
HTML_CONTENT = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PINSAPIA - Línea Booth: Tokenización</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-slate-100 font-sans min-h-screen pb-12">
<div class="max-w-7xl mx-auto px-4 py-6">
<header class="bg-slate-900 text-white rounded-2xl p-6 shadow-xl border border-slate-800 mb-6">
<div class="flex items-center justify-between">
<div>
<span class="text-xs font-bold uppercase tracking-widest text-emerald-400 bg-emerald-950/60 px-3 py-1 rounded-full border border-emerald-800">
TOKENIZACIÓN EN LLMs: LÍNEA BOOTH
</span>
<h1 class="text-2xl md:text-3xl font-extrabold mt-2 flex items-center gap-3">
<span>🧩 ¿Cómo Parte la Máquina las Palabras?</span>
</h1>
</div>
<i class="fa-solid fa-cubes text-emerald-400 text-4xl opacity-80 hidden sm:block"></i>
</div>
</header>
<section class="bg-white border border-slate-200 rounded-xl p-4 mb-6 shadow-sm">
<div class="font-bold text-slate-800 text-sm mb-3 flex items-center gap-2">
<i class="fa-solid fa-wand-magic-sparkles text-emerald-600"></i>
EJEMPLOS PRECONFIGURADOS
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3" id="presets-container"></div>
</section>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- ESPAÑOL -->
<div class="bg-white border-2 border-amber-400 rounded-2xl p-5 shadow-sm flex flex-col justify-between">
<div>
<span class="bg-amber-100 text-amber-900 px-2.5 py-1 rounded text-xs font-bold uppercase tracking-wide">TEXTO EN ESPAÑOL</span>
<textarea id="texto-es" class="w-full border border-slate-300 rounded-xl p-3 mt-3 text-slate-800 font-mono text-sm focus:ring-2 focus:ring-amber-400 focus:outline-none min-h-[90px]" placeholder="Escribe un texto aquí..."></textarea>
<span class="block font-bold text-slate-700 text-xs uppercase mt-4 mb-2">VISTA DE PÍLDORAS (VISUAL):</span>
<div id="tokens-box-es" class="bg-amber-50/50 border border-amber-200 rounded-xl p-4 min-h-[80px] flex flex-wrap gap-1.5 items-start">
<span class="text-slate-400 italic text-sm">Esperando tokenización...</span>
</div>
<span class="block font-bold text-slate-700 text-xs uppercase mt-4 mb-1 flex items-center gap-2">
<i class="fa-solid fa-align-left text-amber-600"></i>
DESGLOSE EN TEXTO PLANO ([#ID: "TOKEN"]):
</span>
<div id="text-view-es" class="bg-slate-900 text-emerald-300 font-mono text-xs p-4 rounded-xl min-h-[130px] w-full overflow-y-auto whitespace-pre-wrap break-all border border-slate-800 shadow-inner leading-relaxed">
<span class="text-slate-500 italic">Esperando datos...</span>
</div>
</div>
<div id="stats-es" class="mt-4 pt-3 border-t border-amber-200 text-xs text-amber-900 flex justify-between font-mono">
<span>Palabras: <strong>0</strong> | Tokens: <strong>0</strong></span>
<span>Ratio: <strong>0.0</strong> t/p</span>
</div>
</div>
<!-- INGLÉS -->
<div class="bg-white border-2 border-emerald-500 rounded-2xl p-5 shadow-sm flex flex-col justify-between">
<div>
<span class="bg-emerald-100 text-emerald-900 px-2.5 py-1 rounded text-xs font-bold uppercase tracking-wide">COMPARATIVA EN INGLÉS</span>
<textarea id="texto-en" class="w-full border border-slate-300 rounded-xl p-3 mt-3 text-slate-800 font-mono text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none min-h-[90px]" placeholder="Traducción opcional..."></textarea>
<span class="block font-bold text-slate-700 text-xs uppercase mt-4 mb-2">VISTA DE PÍLDORAS (VISUAL):</span>
<div id="tokens-box-en" class="bg-emerald-50/50 border border-emerald-200 rounded-xl p-4 min-h-[80px] flex flex-wrap gap-1.5 items-start">
<span class="text-slate-400 italic text-sm">Esperando tokenización...</span>
</div>
<span class="block font-bold text-slate-700 text-xs uppercase mt-4 mb-1 flex items-center gap-2">
<i class="fa-solid fa-align-left text-emerald-600"></i>
DESGLOSE EN TEXTO PLANO ([#ID: "TOKEN"]):
</span>
<div id="text-view-en" class="bg-slate-900 text-emerald-300 font-mono text-xs p-4 rounded-xl min-h-[130px] w-full overflow-y-auto whitespace-pre-wrap break-all border border-slate-800 shadow-inner leading-relaxed">
<span class="text-slate-500 italic">Esperando datos...</span>
</div>
</div>
<div id="stats-en" class="mt-4 pt-3 border-t border-emerald-200 text-xs text-emerald-900 flex justify-between font-mono">
<span>Palabras: <strong>0</strong> | Tokens: <strong>0</strong></span>
<span>Ratio: <strong>0.0</strong> t/p</span>
</div>
</div>
</div>
<!-- BOTONES DE ACCIÓN -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button id="btn-tiktoken" onclick="ejecutarTokenizacion('tiktoken')" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold p-4 rounded-xl shadow-md transition flex items-center justify-center gap-2">
<i id="icon-tiktoken" class="fa-solid fa-bolt"></i> TOKENIZAR CON TIKTOKEN (OPENAI)
</button>
<button id="btn-lmstudio" onclick="ejecutarTokenizacion('lmstudio')" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold p-4 rounded-xl shadow-md transition flex items-center justify-center gap-2">
<i id="icon-lmstudio" class="fa-solid fa-brain"></i> TOKENIZAR CON QWEN 2.5 (NATIVO)
</button>
</div>
<!-- PANEL INFERIOR: VISOR RAW JSON -->
<section class="bg-slate-900 text-slate-200 rounded-2xl p-6 shadow-xl border border-slate-800 mb-6">
<h3 class="text-xs font-bold uppercase tracking-widest text-emerald-400 mb-3 flex items-center gap-2">
<i class="fa-solid fa-code"></i>
VISOR RAW: RESPUESTA JSON DIRECTA DEL MODELO
</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 font-mono text-xs mb-4">
<div class="bg-slate-800 p-3 rounded-lg border border-slate-700">MOTOR: <span id="log-motor" class="text-emerald-400 font-bold">-</span></div>
<div class="bg-slate-800 p-3 rounded-lg border border-slate-700">TIEMPO: <span id="log-tiempo" class="text-amber-400 font-bold">0.0 ms</span></div>
<div class="bg-slate-800 p-3 rounded-lg border border-slate-700">DIFERENCIA: <span id="log-diferencia" class="text-sky-400 font-bold">0 tokens</span></div>
<div class="bg-slate-800 p-3 rounded-lg border border-slate-700">ESTADO: <span id="log-estado" class="text-emerald-400 font-bold">Listo</span></div>
</div>
<details open class="cursor-pointer">
<summary class="text-xs text-slate-300 font-bold hover:text-emerald-400 transition mb-2">
▼ Ver objeto JSON crudo devuelto por la API
</summary>
<pre id="raw-json" class="p-4 bg-slate-950 text-emerald-400 rounded-xl text-xs font-mono overflow-x-auto max-h-96 border border-slate-800 whitespace-pre-wrap break-all">{}</pre>
</details>
</section>
</div>
<script>
document.addEventListener("DOMContentLoaded", cargarPresets);
async function cargarPresets() {
try {
const res = await fetch("/api/presets");
const presets = await res.json();
const container = document.getElementById("presets-container");
container.innerHTML = "";
presets.forEach(p => {
const card = document.createElement("div");
card.className = "bg-slate-50 p-3 rounded-lg border border-slate-200 hover:border-emerald-500 cursor-pointer transition";
card.onclick = () => {
document.getElementById("texto-es").value = p.es;
document.getElementById("texto-en").value = p.en;
ejecutarTokenizacion('tiktoken');
};
card.innerHTML = `<div class="font-bold text-xs text-slate-800">${p.titulo}</div><div class="text-[11px] text-slate-500 truncate">${p.subtitulo}</div>`;
container.appendChild(card);
});
} catch (err) { console.error(err); }
}
function getPillColor(i) {
const colors = ['bg-indigo-100 text-indigo-900 border-indigo-300', 'bg-emerald-100 text-emerald-900 border-emerald-300', 'bg-amber-100 text-amber-900 border-amber-300', 'bg-rose-100 text-rose-900 border-rose-300', 'bg-sky-100 text-sky-900 border-sky-300'];
return colors[i % colors.length];
}
function renderTokens(boxId, textViewId, statsId, data) {
const box = document.getElementById(boxId);
const textView = document.getElementById(textViewId);
const stats = document.getElementById(statsId);
if (!data || !data.tokens || data.tokens.length === 0) {
box.innerHTML = '<span class="text-slate-400 italic text-sm">Sin datos para mostrar</span>';
textView.textContent = "Sin datos...";
return;
}
box.innerHTML = "";
let plainTextTokens = [];
data.tokens.forEach((tok, idx) => {
let rawText = tok.texto;
let visibleText = rawText;
if (visibleText === undefined || visibleText === null || visibleText === "") {
visibleText = `[#${tok.token_id}]`;
} else if (visibleText === " ") {
visibleText = "␣";
} else if (visibleText === "\\n") {
visibleText = "↵";
} else if (visibleText === "\\t") {
visibleText = "⇥";
}
plainTextTokens.push(`[#${tok.token_id}: "${rawText}"]`);
const pill = document.createElement("span");
pill.className = `inline-flex flex-col items-center font-mono px-2 py-1 rounded border shadow-sm ${getPillColor(idx)}`;
pill.innerHTML = `<span class="text-xs font-bold whitespace-pre">${escapeHtml(visibleText)}</span><span class="text-[9px] opacity-75">#${tok.token_id}</span>`;
box.appendChild(pill);
});
textView.textContent = plainTextTokens.join(" ");
stats.innerHTML = `<span>Palabras: <strong>${data.num_palabras}</strong> | Tokens: <strong>${data.num_tokens}</strong></span><span>Ratio: <strong>${data.ratio_token_palabra}</strong> t/p</span>`;
}
function renderError(boxId, textViewId, msg) {
document.getElementById(boxId).innerHTML = `<div class="p-3 bg-rose-100 text-rose-800 text-xs rounded-lg border border-rose-300 font-mono">⚠️ <strong>Error:</strong> ${escapeHtml(msg)}</div>`;
document.getElementById(textViewId).textContent = `ERROR: ${msg}`;
}
async function ejecutarTokenizacion(modo) {
const textoEs = document.getElementById("texto-es").value;
const textoEn = document.getElementById("texto-en").value;
document.getElementById("log-estado").innerText = "Procesando...";
try {
const response = await fetch("/api/tokenizar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ texto: textoEs, texto_en: textoEn, modo: modo })
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.detail || "Error en el servidor");
}
const data = await response.json();
renderTokens("tokens-box-es", "text-view-es", "stats-es", data.es);
renderTokens("tokens-box-en", "text-view-en", "stats-en", data.en);
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
const motor = data.es ? data.es.motor : (data.en ? data.en.motor : modo);
document.getElementById("log-motor").innerText = motor;
document.getElementById("log-tiempo").innerText = `${((data.es?.tiempo_ms || 0) + (data.en?.tiempo_ms || 0)).toFixed(2)} ms`;
let diffText = "0 tokens";
if (data.es && data.en) {
const diff = data.es.num_tokens - data.en.num_tokens;
diffText = diff > 0 ? `+${diff} tokens en ES` : `${diff} tokens en ES`;
}
document.getElementById("log-diferencia").innerText = diffText;
document.getElementById("log-estado").innerText = "Completado";
} catch (err) {
renderError("tokens-box-es", "text-view-es", err.message);
if (textoEn.trim()) renderError("tokens-box-en", "text-view-en", err.message);
document.getElementById("raw-json").textContent = JSON.stringify({ error: err.message }, null, 2);
document.getElementById("log-estado").innerText = "Error";
}
}
function escapeHtml(str) {
return String(str || '').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
</script>
</body>
</html>
"""
if __name__ == "__main__":
uvicorn.run("app_booth:app", host="0.0.0.0", port=8088, reload=True)
5. Instrucciones de Ejecución
- Guarda el código anterior en un archivo llamado
app_booth.py. - Abre la terminal y ejecuta el servidor:
Bashpython app_booth.py - Navega en tu navegador a:
http://localhost:8088
Al probar la frase “La inteligencia artificial está transformando la educación”, observarás cómo Qwen 2.5 desglosa cada sub-palabra con una eficiencia lingüística superior y te ofrece el objeto JSON crudo listo para ser inspeccionado en el laboratorio de la Línea Booth.
¿Qué te parece la comparación entre las ratios de tokens entre el vocabulario de OpenAI y el de Qwen 2.5?
Guía de Exploración Práctica: “Desmontando el Idioma Token a Token”
Esta guía está diseñada para que el alumnado explore la aplicación de forma interactiva, comprenda el funcionamiento del algoritmo de tokenización (Byte-Pair Encoding) y analice el comportamiento de distintos modelos de lenguaje.
🎯 Objetivo del Laboratorio
Comprender que los modelos de Inteligencia Artificial no leen palabras ni letras como las personas, sino que fragmentan el texto en unidades numéricas (tokens). A través de la interfaz web, el alumnado identificará cómo influyen el idioma, la longitud de las palabras y los caracteres especiales en el consumo de memoria y coste computacional del modelo.
🧪 Misiones y Retos para el Alumnado
Reto 1: El “Impuesto Lingüístico” (Español vs. Inglés)
- Selecciona la Opción 1: Frase Contextual en los botones de ejemplo superiores (o escribe una frase idéntica en español e inglés).
- Pulsa el botón TOKENIZAR CON TIKTOKEN (OPENAI).
- Observa las estadísticas en la parte inferior de cada caja (
Palabras,TokensyRatio t/p).
- Pregunta de análisis: ¿Qué idioma ha necesitado más tokens para expresar la misma idea? ¿Por qué la ratio tokens/palabra es mayor en español si usamos un tokenizador entrenado mayoritariamente en inglés?
Reto 2: Detectives de Subpalabras (Descomposición BPE)
- Borra el texto e ingresa una palabra larga, técnica o inventada en español (ej.
electroencefalografista,anticonstitucionalmente,desoxirribonucleico). - Pulsa cualquiera de los botones de tokenización.
- Examina la Vista de Píldoras (Visual).
- Pregunta de análisis: ¿La máquina ha mantenido la palabra entera o la ha fragmentado? Identifica los prefijos, raíces o sufijos que la máquina ha reconocido como piezas independientes (
#ID).
Reto 3: El Coste Oculto de Emojis, Símbolos y Formato
- Selecciona la Opción 3: Caracteres Especiales o escribe una frase que contenga emojis (😀🚀), tildes, números largos (
2026), hashtags (#) y saltos de línea. - Tokeniza el texto y observa la Vista de Píldoras y el Desglose en Texto Plano.
- Pregunta de análisis:
- ¿Cuántos tokens consume un solo emoji?
- Busca los símbolos invisibles como
␣(espacio) y↵(salto de línea). ¿Tienen su propio código de identificación (#ID)? ¿Cuestan procesamiento?
Reto 4: Duelo de Gigantes (Tiktoken vs. Qwen 2.5 Nativo)
- Introduce un párrafo mediano en español dentro del cuadro de texto.
- Primero, pulsa TOKENIZAR CON TIKTOKEN (OPENAI) y anota el número total de tokens devueltos.
- A continuación, pulsa TOKENIZAR CON QWEN 2.5 (NATIVO) y revisa el Visor RAW JSON en la parte inferior.
- Fíjate en la métrica
DIFERENCIAen el panel de control.
- Pregunta de análisis: ¿Qué modelo ha fragmentado menos el texto en español? ¿Qué implicaciones tiene esto en la velocidad de respuesta y en el límite de ventana de contexto del modelo?
📋 Cuestionario de Autoevaluación (Ficha de Entrega)
| Pregunta de Reflexión | Tu Respuesta |
| 1. ¿Qué es un Token? Explícalo con tus propias palabras basándote en las “píldoras” que has visto en pantalla. | |
| 2. Eficiencia: ¿Por qué un modelo con un vocabulario multilingual como Qwen 2.5 suele procesar mejor el español que los modelos más antiguos de OpenAI? | |
| 3. Optimización de Prompts: Si tuvieras que diseñar una instrucción corta (prompt) para una IA y no quieres malgastar tokens, ¿qué elementos o formatos deberías evitar? |
¿Te gustaría incluir algún reto enfocado a lenguajes de programación (código Python/HTML) o prefieres adaptarlo a un nivel educativo específico?



