¿Puede el formato importar más que el contenido? Midiendo el ahorro de tokens con TOON en pipelines de IA
Cuando construyes sistemas que usan LLMs, el costo no lo determina solo qué le preguntas al modelo — lo determina cómo se lo preguntas. Cada token que entra tiene un precio, y en producción eso se acumula rápido.
Este artículo documenta un experimento concreto que compara JSON contra TOON (Token-Oriented Object Notation) en dos escenarios distintos: texto en lenguaje natural y respuestas de herramientas MCP.
Qué es TOON
TOON es un formato de serialización diseñado desde cero para minimizar el consumo de tokens. Su ventaja principal está en los arrays de objetos uniformes: en lugar de repetir los nombres de campo en cada objeto (como hace JSON), los declara una sola vez en el encabezado y lista únicamente los valores.
JSON (verbose):
[
{ "id": "t-001", "nombre": "Migrar auth", "estado": "abierto", "asignado": "ana" },
{ "id": "t-002", "nombre": "Escribir tests", "estado": "hecho", "asignado": "luis" }
]
TOON (compacto):
tareas[2]{id,nombre,estado,asignado}:
t-001,Migrar auth,abierto,ana
t-002,Escribir tests,hecho,luis
El ahorro es lineal: a más filas, mayor reducción.
Experimento 1: Comprimiendo texto en lenguaje natural con spaCy
Hipótesis: usar NLP para extraer el esqueleto semántico (entidades, frases nominales, pares verbo-objeto) y codificarlo en TOON debería reducir los tokens.
El pipeline
spaCy extrae entidades nombradas, noun chunks y acciones del texto, que luego se serializan como tablas TOON:
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Refactor the authentication module to use httpOnly cookies instead of localStorage.")
for ent in doc.ents:
print(ent.text, "→", ent.label_)
# localStorage → ORG
Salida en TOON:
entities[2]{text,label}:
localStorage,ORG
httpOnly cookies,PRODUCT
actions[2]{verb,tense,object}:
refactor,unknown,module
use,unknown,cookies
Resultados
| Variante | Tokens | Diferencia |
|---|---|---|
| A — Prompt crudo (baseline) | 515 | — |
| B — TOON + bloque de código | 1,188 | +131% |
| C — TOON sin código | 846 | +64% |
La hipótesis fue refutada. El pipeline multiplica los tokens.
Por qué falla
spaCy no comprime el texto — lo expande. Para "BYD superó a Tesla por tercer trimestre consecutivo" genera filas de entidad para BYD y Tesla, noun chunks para ambos, una acción, y la oración original. El mismo hecho codificado cinco veces. El tokenizador de Claude ya maneja el lenguaje natural de forma óptima — no hay redundancia que explotar.
Aprendizaje clave: El texto en lenguaje natural ya es una representación token-eficiente para los LLMs modernos. Intentar "comprimirlo" produce el efecto contrario.
Experimento 2: Comprimiendo respuestas JSON de herramientas MCP
En lugar de comprimir el input del usuario, este experimento comprime las respuestas de herramientas — el JSON que devuelven servidores MCP como ClickUp, Gmail o Google Calendar antes de que entre al contexto del modelo.
El conversor JSON → TOON
def _is_uniform_array(items: list) -> bool:
"""True si todos los elementos son dicts con las mismas claves."""
if not items or not all(isinstance(i, dict) for i in items):
return False
first_keys = set(items[0].keys())
return all(set(i.keys()) == first_keys for i in items)
def array_to_toon_table(name: str, items: list[dict]) -> str:
fields = list(items[0].keys())
header = f"{name}[{len(items)}]{{{','.join(fields)}}}:"
rows = [
" " + ",".join(str(item.get(f, "")).replace(",", "\\,") for f in fields)
for item in items
]
return header + "\n" + "\n".join(rows)
Resultados
| Payload | Tokens JSON | Tokens TOON | Reducción |
|---|---|---|---|
| Lista de tareas (15 items, 8 campos) | 1,484 | 546 | −63.2% |
| Bandeja de entrada (10 emails, 9 campos) | 1,477 | 820 | −44.5% |
| Calendario (8 eventos, 8 campos) | 1,260 | 666 | −47.1% |
La hipótesis se confirma. Entre 44% y 63% de ahorro dependiendo del payload.
¿Se preserva la accuracy?
Se envió cada payload en ambos formatos con preguntas factuales de respuesta determinista. Los resultados fueron idénticos en los tres payloads. Claude lee TOON tan bien como JSON para datos estructurados.
El contraste entre experimentos
| Tipo de dato | Formato origen | Resultado |
|---|---|---|
| Texto en lenguaje natural | Prosa | TOON falla — overhead de +131% a +283% |
| Respuestas JSON de APIs/herramientas | JSON estructurado | TOON funciona — ahorro de 44% a 63% |
TOON es eficiente exactamente donde JSON es ineficiente: en arrays uniformes de datos estructurados. No ofrece ninguna ventaja sobre la prosa.
La regla práctica:
- Pasando instrucciones, contexto o documentos → déjalos como están.
- Pasando resultados de herramientas, queries a bases de datos o listas → TOON puede reducir el costo a la mitad.
Implementación en un pipeline MCP real
El punto de conversión natural es entre la respuesta de la herramienta y el bloque tool_result:
Usuario → LLM → [tool_use] → Servidor MCP
↓
respuesta JSON
↓
[conversor JSON→TOON] ← aquí
↓
LLM ← [tool_result en TOON]
Un conversor completo:
import json
def convert_payload(payload: dict | list, name: str = "data") -> str:
def escape(v):
return str(v).replace(",", "\\,").replace("\n", " ")
def serialize_cell(v):
if isinstance(v, list):
return "[" + "|".join(escape(i) for i in v) + "]"
return escape(v)
def is_uniform(items):
if not items or not all(isinstance(i, dict) for i in items):
return False
keys = set(items[0].keys())
return all(set(i.keys()) == keys for i in items)
def to_toon(key, value):
if isinstance(value, list) and is_uniform(value):
fields = list(value[0].keys())
header = f"{key}[{len(value)}]{{{','.join(fields)}}}:"
rows = [" " + ",".join(serialize_cell(row.get(f)) for f in fields)
for row in value]
return header + "\n" + "\n".join(rows)
if isinstance(value, dict):
return "\n".join(to_toon(k, v) for k, v in value.items())
return f"{key}: {escape(value)}"
if isinstance(payload, list) and is_uniform(payload):
fields = list(payload[0].keys())
header = f"{name}[{len(payload)}]{{{','.join(fields)}}}:"
rows = [" " + ",".join(serialize_cell(row.get(f)) for f in fields)
for row in payload]
return header + "\n" + "\n".join(rows)
if isinstance(payload, dict):
return "\n\n".join(to_toon(k, v) for k, v in payload.items())
return json.dumps(payload)
Uso:
raw_response = mcp_client.call_tool("clickup_filter_tasks", {...})
json_payload = json.loads(raw_response)
# Conversión a TOON antes de enviarlo al modelo
toon_payload = convert_payload(json_payload, name="tareas")
¿Cuándo aplicarlo?
Vale la pena:
- Resultados de búsqueda con muchos items (Gmail, Notion, ClickUp)
- Queries a bases de datos con múltiples filas
- Listados de recursos de APIs REST
- Cualquier array con más de ~5 objetos y campos repetidos
No vale la pena (o puede ser contraproducente):
- Respuestas de un solo objeto
- JSON con estructura irregular o altamente anidada
- Texto en lenguaje natural en cualquier forma
- Payloads pequeños donde el overhead del encabezado TOON no se amortiza
Conclusión
La pregunta inicial era si el formato de los datos puede tener impacto en el costo de uso de un LLM. La respuesta es: sí, pero solo cuando el formato origen es verboso por naturaleza.
JSON es verboso para arrays uniformes. TOON no lo es. El ahorro en un pipeline MCP real ronda el 44–63% en los tipos de datos más comunes, sin pérdida measurable de accuracy.
El texto en lenguaje natural, en cambio, ya es eficiente. Intentar comprimirlo con extracción NLP produce el efecto contrario: más tokens, no menos.
La optimización correcta siempre depende de entender qué parte del contexto es redundante. En el caso de herramientas MCP que devuelven listas de datos estructurados, la respuesta está en el formato.