Por qué tu LLM trunca antes de llegar a max_tokens
Los LLMs no esperan a chocar con max_tokens: si sospechan que se van a pasar, truncan antes. Cómo lo descubrimos en DocuTray procesando documentos densos.
Alcanzar el límite de tokens de salida no es algo común, pero cuando pasa puede dañar la calidad de tu aplicación. Le pides un JSON y te devuelve uno sintácticamente válido pero con filas faltantes. O le pides que escriba un artículo y te manda una introducción, desarrollo, y conclusión de dos frases cada una.
Y hay un patrón menos obvio: los LLMs no esperan a chocar con el límite. Si sospechan que se van a pasar, truncan antes.
A la hora de escribir este post, Opus 4.7 tiene un máximo de 128K tokens de salida, Gemini 3 Pro de 64K, y GPT 5.5 de 128K. En el caso de Gemini, el default es de solo 8.192. Hay que subirlo explícito en maxOutputTokens para acercarse al techo del modelo. Es un número alto para llamados simples, pero en algunos casos es muy común pasarse de este límite.
Te quiero contar nuestras frustraciones con el max_tokens, cómo lo hemos enfrentado, y lo que hemos aprendido.
Profundizando en el problema
En DocuTray nos suele pasar cuando queremos procesar documentos densos, de muchas páginas con muchos registros. Y necesitamos un JSON de respuesta. JSON es el estándar para datos estructurados en este tipo de procesos, pero es muy intensivo en tokens, sobre todo cuando existen arreglos con muchos registros. Al procesar archivos densos, nuestras respuestas eran JSONs estructuralmente correctos, pero con filas faltantes, lo que entorpece el proceso de nuestro servicio. Por ejemplo, podía venir con los 20 primeros y el último elemento de un arreglo.
Al inicio no teníamos idea por qué pasaba esto. Sospechamos que no lograba leer bien algunas filas. Pero los logs de razonamiento mostraban lo contrario: el modelo sí leía todas las filas, las enumeraba correctamente antes de generar el JSON. El truncamiento aparecía recién en la salida estructurada. El modelo estaba estimando los tokens de salida (algo que, por cierto, hacen bastante mal), y cuando estimaba que no le iba a alcanzar, resolvía truncar. El JSON quedaba sintácticamente válido, pero incompleto.
Esto cambia cómo hay que pensar el max_tokens. Tuvimos que abrazar esta limitación dado que en el tiempo, con más clientes y más tipos de documentos que procesáramos, nos iba a tocar sí o sí enfrentarnos a esta situación.
¿Cómo lo enfrentamos?
Nuestra primera intuición fue que JSON era el problema. Si para cada elemento en un arreglo repetíamos puntuación y headers, seguro que íbamos a consumir muchos tokens. Así que lo primero que probamos fue Toon.
Toon
Toon (Token-Oriented Object Notation) es un formato de serialización pensado para LLMs. Para arreglos de objetos homogéneos, exactamente el caso de extraer filas de un documento, usa una cabecera con los nombres de las columnas y después una fila por registro, sin repetir las llaves en cada elemento. Puede reducir la cantidad de tokens usados en aproximadamente un 40% vs un JSON equivalente.
Escribimos un artículo aquí:

La ventaja: el mismo presupuesto le alcanza al modelo para más filas. En muchos de los ejemplos densos que teníamos funcionaba.
El trade-off: es un formato menos común, no está integrado directamente en los LLMs, por lo que en ocasiones podía devolverte un Toon con la estructura alterada, lo que impide devolver un JSON correcto como respuesta.
Por otro lado, con documentos lo suficientemente grandes, Toon también podía caer en max tokens. Y la idea de DocuTray es que procesemos cualquier documento.
Por eso Toon no nos pareció una alternativa real, más bien un parche.
Conversión agéntica de documentos
La segunda alternativa que probamos fue hacer la conversión de forma agéntica. En vez de un llamado API con un prompt y listo, abrir una sesión con un agente, darle la instrucción, y dejarlo iterar hasta que los resultados estuvieran listos.
La idea era atractiva: el agente puede llamar sub-agentes y ejecutar pasos adicionales. Por ejemplo, correr las reglas de validación de DocuTray (un módulo en la plataforma que aplica reglas lógicas sobre los datos extraídos para verificar que sean consistentes) y, si encuentra problemas, volver al documento a corregir.
Pero al probarlo nos chocamos con tres problemas. Primero, la falta de determinismo: ante un mismo request, los resultados podían variar entre corridas. Segundo, los tiempos de respuesta eran muchísimo más altos que un llamado API directo, incluso usando los modelos más rápidos (Haiku, Flash Lite). Y tercero, era un cambio arquitectónico grande en DocuTray sin que los resultados justificaran el costo.
Descartada esta alternativa, saltamos a la que sería la definitiva para nosotros..
Multiprompt
Nuestro enfoque final fue conceptualmente simple, pero en la práctica mucho más difícil: en vez de pedir todo en una llamada, partir el documento en chunks y hacer N llamadas. Cada llamada devuelve una parte, y al final combinas los resultados. Es la solución escalable para cualquier documento denso.
Algunas decisiones que tuvimos que hacer para implementar una solución productiva:
1. Hay que decidir cómo partir. ¿Siempre es necesario particionar? ¿Cómo nos aseguramos de que cada sub-llamada tenga toda la data necesaria para su parte? Por páginas es fácil pero rompe registros que cruzan páginas. Por bloques semánticos requiere otra pasada del modelo para identificar los bloques.
2. Cada llamada agrega latencia y costo. Si antes hacías una llamada, ahora hacés cuatro. Y si cada llamada tiene pensamiento, se va a sumar al total de tokens de output.
3. Hay que combinar los resultados, y eso puede introducir duplicados o huecos. Por lo mismo, el punto 1 es el fundamental.
En nuestro caso, el proceso final terminó siendo así: una primera llamada enfocada solo en detección de layout, que identifica en qué página está cada elemento relevante y devuelve un estimado de los tokens totales que va a requerir la extracción. Con esa información, el sistema decide cómo particionar y cuántas particiones hacer. Después se disparan varias llamadas en paralelo, cada una recibiendo solo las páginas relevantes para su parte. Al final se combinan los resultados y se devuelven al usuario.
Multiprompt terminó siendo nuestra solución para documentos densos. Toma un poco más de tiempo (esperable por el volumen de datos) y consume más tokens, pero hemos visto resultados muy buenos en documentos densos de incluso más de 100 páginas.
Conclusión
Lo más útil que aprendimos: tratar max_tokens como una variable que el modelo lee y sobre la cual toma decisiones, no como un límite duro que solo importa cuando lo cruzas. La gran mayoría de los documentos que procesamos en la plataforma no están ni cerca de llegar al límite, pero los que sí son justamente los documentos difíciles que nuestros clientes aman que podamos solucionar.
Estos límites terminan diseñando tu aplicación. Es lo que te hace más que un "ChatGPT Wrapper". Para nosotros la solución correcta terminó siendo multiprompt; puede que para ti sea otra.

