Inicio / Artículos

Presupuestos de tokens versus límites semánticos: lograr divisiones correctas de los fragmentos en RAG

Aprenda cómo difieren la tokenización y el chunking, cuál papel desempeñan en un pipeline RAG, y cómo dividir documentos por significado al mismo tiempo que se mide el tamaño con el tokenizador real.

Editor: REACTAPP.TOP11 min de lectura2515 palabras

Fuentes: Chunking vs Tokenization: The Two Splits Every AI Engineer Keeps Confusing

Un pipeline RAG puede parecer funcional en cada capa visible, con embeddings razonables, puntuaciones de similitud plausibles y un almacén de vectores bien poblado, y aun así entregar al modelo fragmentos que se interrumpen a mitad de oración. En muchos de esos casos, el defecto radica en el código de ingestión, donde una rutina de particionamiento se basó en una suposición errónea sobre cómo el texto se convierte en tokens. Esta guía separa claramente estas dos concepciones, muestra dónde se encuentra cada una en el pipeline y explica un enfoque de división que prioriza el significado mientras mantiene un control estricto sobre los presupuestos de tokens.

Los ingenieros suelen decir frases como “divídelo en 512 tokens” o “fragmentación basada en tokens”, y esta expresión combina dos operaciones diferentes en una sola. Ellas resuelven problemas distintos, se ejecutan en etapas diferentes y se descomponen de maneras distintas. Saber exactamente dónde termina una y comienza la otra es lo que permite analizar un sistema de recuperación en lugar de ajustar parámetros hasta que mejore la puntuación de evaluación.

Dos operaciones, dos responsables

Las definiciones más breves y útiles son estas:

  • Tokenización convierte el texto en unidades enteras discretas que un modelo consume realmente. Pertenece al modelo.
  • Fragmentación divide los documentos en partes autónomas que pueden ser incrustadas, indexadas y recuperadas. Pertenece a tu flujo de trabajo y a tus datos.

La tokenización se ejecuta cada vez que alguna cadena llega al modelo, ya sea que usted lo considere o no. El particionamiento es una decisión que toma de antemano sobre cómo estructurar un corpus antes de que el modelo lo vea. La primera es fija y en su mayor parte invisible; la segunda es algo que diseña, mide y perfecciona a lo largo de semanas.

Los dos interactúan constantemente. Un divisor que ignora el comportamiento del tokenizador generará silenciosamente fragmentos de mala calidad. Aun así, se trata de pasos separados, y tratarlos como lo mismo es la causa de una cantidad sorprendente de errores sutiles en la recuperación de información.

Cómo lee el texto un modelo

Ningún modelo LLM de tipo transformador, ya sea GPT-4o, Claude, Llama o cualquier otro en uso real, lee palabras. En realidad, lee una secuencia de enteros, cada uno de los cuales apunta a un vocabulario fijo aprendido durante el entrenamiento. El tokenizador es el algoritmo determinista que convierte el texto en bruto en dicha secuencia. Se elige una sola vez, al entrenar el modelo, y se incluye como parte de los archivos del mismo. No es posible cambiarlo según cada solicitud.

Vocabularios subpalabras: BPE, WordPiece, SentencePiece

La mayoría de los modelos actuales dependen de la tokenización subpalabra, generalmente proveniente de las familias BPE, WordPiece o SentencePiece. El principio común es partir de caracteres o bytes en bruto y fusionar repetidamente las parejas vecinas más frecuentes para crear nuevas entradas en el vocabulario, hasta alcanzar un tamaño deseado, que suele estar entre 32k y 128k entradas o más.

Lo que esto significa en el trabajo diario:

  • Las palabras en inglés más frecuentes, como the, is o model, suelen corresponder a un token cada una.
  • Las palabras menos frecuentes se dividen en partes subpalabras, por lo que la tokenización puede resultar en token más ization.
  • Las cadenas inusuales, incluidos los identificadores de código, los sistemas de escritura no latinos, los emojis y los nombres de productos inventados, pueden generar muchos tokens por palabra e, ocasionalmente, casi un token por carácter.

Esa es la razón por la cual una oración en hindi o tamil puede costar de dos a tres veces más tokens que una oración en inglés con el mismo significado, y por qué un identificador como xgboost_lstm_ensemble_v2 puede consumir alrededor de ocho tokens cuando se planeaban dos. Las proporciones exactas dependen del tokenizador, y los vocabularios más recientes han reducido la diferencia para algunos idiomas, por lo que es necesario medirlo con el modelo que se utilice. Para cualquier producto destinado a mercados no en inglés, como por ejemplo una capa de LLM que resume los informes de la bolsa de valores india o los registros de órdenes de corretaje, esto representa un costo real y un elemento del presupuesto relacionado con el contexto, y no solo una curiosidad.

Por qué el conteo de tokens es importante más allá de la factura

El conteo de tokens hace algo más que determinar el precio de una solicitud. Regula tres aspectos:

  1. Cuánto de la ventana de contexto se utiliza. Una ventana de 128 k se mide en tokens, no en palabras o caracteres. Un bloque de 1,000 palabras no tiene un costo fijo; su cantidad de tokens varía según el vocabulario, el idioma y el tipo de contenido, ya que el código, la prosa y los números se tokenizan de manera muy diferente.
  2. Sobre qué opera la atención. Los Transformers prestan atención a través de las posiciones de los tokens, por lo que la forma en que un texto se divide en tokens determina cuán coherentemente el modelo puede relacionar sus partes. Las divisiones problemáticas que afectan la comprensión son poco comunes, pero ciertas codificaciones pueden generarlas.
  3. Dónde ocurre la truncación. Cuando la entrada supera el tamaño de la ventana, se corta a nivel de tokens, no en los límites de oraciones o bloques, a menos que usted mismo lo gestione. Una truncación descuidada puede dejar una palabra incompleta.

Mire sus propias cadenas una vez

Si nunca ha procesado sus cadenas reales de producción con el tiktoken de OpenAI o el tokenizador de Anthropic, vale la pena hacerlo al menos una vez. Ver cómo su propio modelo de prompt se divide en fragmentos inesperados convierte una abstracción en algo concreto. La sintaxis de Markdown, los corchetes JSON y los marcos de código son especialmente problemáticos: tokenizan con menos eficiencia que la prosa ordinaria.

El fragmento a continuación carga la codificación cl100k_base y cuenta los tokens en una orden de comandos típica. Observe cómo las banderas y las extensiones de archivo suelen ocupar dos o tres tokens cada una, lo cual es exactamente el tipo de contenido que aumenta el presupuesto sin que nadie se dé cuenta.

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode("pip-compile requirements.in --output-file requirements.txt")
print(len(tokens), tokens)
# CLI flags and file extensions often split into 2-3 tokens each —
# this is exactly the kind of string that silently inflates your token budget

Cómo decide qué verá el modelo

El fragmentado ocurre en un nivel superior, dentro del pipeline y no en el modelo. Toma un documento grande, como un PDF, una página de Confluence, un repositorio de código o una circular regulatoria de 40 páginas, y lo divide en unidades más pequeñas que se incrustan, indexan y recuperan de forma independiente. El fragmento es la unidad básica en la capa de recuperación.

Existen dos limitaciones que hacen necesario el fragmentado, y ninguna de ellas tiene que ver realmente con la tokenización:

  1. Los embeddings largos se vuelven poco precisos. Si se incluyen demasiadas ideas no relacionadas en un único vector, este se convierte en el promedio de todas ellas, sin coincidir con nada de manera exacta. Un buen fragmento contiene una idea coherente, no todo un documento.
  • El contexto sigue siendo finito y costoso. Incluso con ventanas de 128k tokens o más que ahora son comunes, incluir toda la base de conocimientos en cada solicitud no constituye una recuperación de información. Se trata de una mala imitación de la inferencia con contexto completo, que presenta más latencia, mayor costo y menor precisión que un paso de recuperación adecuado.
  • Estrategias de fragmentación y cómo fallan cada una

    Ventanas de tamaño fijo

    El enfoque más simple consiste en dividir el texto cada N tokens o caracteres, generalmente con algo de solapamiento entre los fragmentos adyacentes. La mayoría de los equipos comienzan aquí porque solo requiere unas pocas líneas de código. La función a continuación codifica todo el texto, divide la lista de tokens en ventanas de chunk_size, decodifica cada ventana de vuelta a texto y avanza chunk_size - overlap unidades, de modo que los fragmentos consecutivos compartan un margen.

    def fixed_chunk(text, chunk_size=512, overlap=50):
        tokens = enc.encode(text)
        chunks = []
        start = 0
        while start < len(tokens):
            end = start + chunk_size
            chunk_tokens = tokens[start:end]
            chunks.append(enc.decode(chunk_tokens))
            start += chunk_size - overlap
        return chunks
    

    La debilidad radica en que esta función no tiene concepto de oración. Coloca un límite en medio de una cláusula, separando una afirmación de la información que la respalda, o un ejemplo de código de la oración que lo explica. Esos fragmentos de media oración se incluyen en vectores situados entre temas y no coinciden bien, lo cual es precisamente el síntoma descrito al principio. Dos consideraciones adicionales: overlap debe mantenerse por debajo de chunk_size o el bucle nunca avanzará, y la decodificación de un segmento arbitrario de tokens puede dividir a un carácter multibyte por la mitad, generando un carácter de reemplazo en el borde de un fragmento.

    División recursiva en límites naturales

    Un divisor recursivo prefiere los saltos de párrafo, recurre a los saltos de oración y luego a los saltos de palabra, y solo realiza un corte definitivo cuando ya no hay otra opción viable. RecursiveCharacterTextSplitter de LangChain y los analizadores de nodos en LlamaIndex siguen este patrón. Para corpus con mucho texto en prosa, es una opción predeterminada mucho mejor, ya que se respeta la estructura propia del documento antes que el límite de tamaño.

    Fragmentación semántica

    En lugar de fijar un tamaño, la fragmentación semántica incluye oraciones individuales o pequeños grupos de ellas y coloca un límite donde la similitud coseno entre las representaciones vecinales disminuye, es decir, donde cambia el tema. Los fragmentos resultantes se mantienen unidos por su significado y no por su longitud. La desventaja es que se requiere un proceso adicional de generación de representaciones vecinales para cada documento durante su procesamiento.

    Fragmentación consciente de la estructura

    El contenido con estructura integrada, como Markdown con encabezados, archivos de código fuente, especificaciones de API o documentación de esquemas, debe dividirse siguiendo dicha estructura. En el código, la definición de una función constituye un límite evidente. En una base de conocimientos en Markdown, una sección ## lo es también. En el caso del README de un sistema multiagente, dividir el contenido según los encabezados de las secciones permite obtener un significado mucho más fácilmente recuperable que cualquier conteo de tokens. Un análisis más profundo sobre cómo mantener juntas las pruebas de apoyo se encuentra en chunking que preserva evidencia.

    Chunking impulsado por LLM

    En el rango de mayor costo, se puede pedir a un LLM que lea el documento y proponga él mismo los límites, con el objetivo de obtener fragmentos que funcionen de forma independiente y respondan a una pregunta coherente. Es un proceso lento y costoso, pero resulta útil en corpus de alto valor como documentos regulatorios o contratos, donde un error en la recuperación tiene consecuencias reales.

    Donde chocan las dos ideas

    Lo interesante es el punto en el que se encuentran estos conceptos, y se puede resumir en una sola frase: el tamaño de los fragmentos se expresa normalmente en tokens, pero los límites entre ellos deben seguir el sentido del texto, y estos dos objetivos apuntan en direcciones opuestas.

    Cortar estrictamente en el límite de 512 tokens con chunk_size=512 cumple con el presupuesto pero ignora el significado. Dividir solo en los saltos de párrafo sin límite máximo respeta el significado, pero no tiene en cuenta que un párrafo pueda tener 40 tokens y el siguiente 4,000. Este último puede superar la longitud máxima de secuencia del modelo de embedding, y el contexto recuperado termina siendo extremadamente desigual en cuanto a la cantidad de información que contiene cada fragmento.

    Por lo tanto, un divisor adecuado aborda ambos problemas en un orden fijo:

    1. Primero, identificar los límites semánticos: secciones, párrafos, oraciones, bloques de código.
    2. Combinar las unidades pequeñas adyacentes hasta que agregar la siguiente supere el presupuesto de tokens.
  • Divida una sola unidad de forma forzada solo cuando su tamaño supere por sí mismo el presupuesto establecido, y al hacerlo, tokenícela usted mismo para decidir dónde se realizará el corte, en lugar de depender de una estimación basada en el recuento de caracteres que no tiene en cuenta los límites entre tokens.
  • La función a continuación implementa ese orden de operaciones. Exige que las secciones ya estén divididas según párrafos o encabezados. Para cada una de ellas se mide la longitud real del token; si su adición provocaría que el bloque actual se sobrepase, las secciones acumuladas se envían como un bloque separado. Una sección que por sí sola exceda max_tokens se divide según los límites entre tokens; todo lo demás se agrega al bloque en curso. Lo que quede al final constituirá el bloque final.

    def chunk_by_tokens_with_semantic_bias(sections, max_tokens=512):
        chunks, current, current_len = [], [], 0
        for section in sections:  # pre-split by paragraph/header
            section_len = len(enc.encode(section))
            if current_len + section_len > max_tokens and current:
                chunks.append("\n".join(current))
                current, current_len = [], 0
            if section_len > max_tokens:
                # single section too big — hard split on token boundaries, not chars
                section_tokens = enc.encode(section)
                for i in range(0, len(section_tokens), max_tokens):
                    chunks.append(enc.decode(section_tokens[i:i+max_tokens]))
            else:
                current.append(section)
                current_len += section_len
        if current:
            chunks.append("\n".join(current))
        return chunks
    

    La decisión clave en el diseño es que el tamaño se mide con el tokenizador del modelo de embedding objetivo, y no se estima a partir de los caracteres; además, una división forzada solo puede producirse en un límite de token que el código controle. Una mejora digna de considerar: el salto de línea utilizado en "\n".join agrega aproximadamente un token por sección que no se incluye en el recuento actual, por lo que es necesario dejar algo de margen por debajo del límite estricto del modelo.

    Dónde se sitúa cada paso en el pipeline

    Desde un punto de vista espacial, el chunking es un problema en la fase inicial. Ocurre durante la ingestión, antes de que intervenga cualquier modelo, y su resultado se almacena. La tokenización es un problema en la fase posterior. Se produce dentro de la capa de entrada del modelo en cada solicitud, incluidas las llamadas a embeddings realizadas durante la ingestión y las llamadas de generación en el momento de la consulta. Tener esto en mente te indica qué código abrir cuando surge un problema.

    Diferenciar los dos modos de fallo

    Cuando analizas un resultado de recuperación deficiente, hay dos síntomas similares pero que se refieren a capas diferentes:

    • El fragmento recuperado está incompleto o termina a mitad de una idea. Esto es un fallo de chunking. La lógica de delimitación dividió una unidad semántica, y la solución debe buscarse en el módulo de división, no en la elección del modelo de embeddings.
  • El fragmento parece completo, pero la respuesta ignora la mitad de él, o el prompt se trunca inesperadamente. Se trata de un error en la tokenización y en el presupuesto de contexto. La contabilidad de tokens está incorrecta en algún lugar, muy a menudo porque los tokens se estimaron con len(text) / 4 y el recuento real resultó ser mayor.
  • A menudo se culpa a “un modelo de embedding deficiente” por ambos problemas. Rara vez esa es la causa real; casi siempre se debe a la lógica de límites o a la contabilidad de tokens.

    Lista de verificación para producción

    Al crear o revisar un pipeline RAG o de agentes, revise los siguientes puntos:

    • Cuente los tokens con el tokenizador real del modelo que utilice. Use tiktoken para los modelos de OpenAI y el tokenizador específico del modelo para aquellos de código abierto. Nunca utilice len(text) // 4 como método de conteo en producción.
    • Mida el tamaño del bloque en tokens del modelo de embedding. Los modelos de embedding suelen utilizar un tokenizador diferente al del modelo de generación y imponen una longitud máxima de secuencia más estricta.
    • Calcule explícitamente la superposición. La superposición consume parte de la ventana efectiva, por lo que debe incluirse en los cálculos relacionados con el límite de entrada del modelo de embedding, en lugar de descubrirla a través de una advertencia de truncamiento.
    • Dé prioridad a la estructura sobre el tamaño. Utilice por defecto una división basada en la estructura o recursiva, y reserve ventanas de tamaño fijo para textos realmente no estructurados.
    • Registre los conteos de tokens y las decisiones relativas a los límites durante la ingestión. Cuando la calidad disminuye, es necesario saber de inmediato si un bloque fue cortado por cuestiones de tamaño o de estructura.
    • Volver a tokenizar después de cambiar los modelos de embedding. Una estrategia ajustada para un vocabulario específico no se transfiere de manera efectiva; 512 tokens abarcan una cantidad diferente de texto con un tokenizador distinto.

    Conclusión

    La tokenización responde a la pregunta del modelo sobre cómo interpretar una cadena de texto. El particionamiento en fragmentos responde a la pregunta sobre cuál es la unidad más pequeña y autónoma que vale la pena recuperar. La primera se determina en el momento en que eliges un modelo; la segunda es un aspecto del diseño completamente bajo tu control, y suele ser donde radica el verdadero punto de influencia en la ingeniería de un sistema RAG.

    Estos dos tipos de errores también se manifiestan de manera diferente. Los errores en el cómputo de tokens se detectan mediante una truncación extraña y presupuestos excedidos, lo cual al menos es visible. Los errores de segmentación generan un sistema que funciona sin ningún error y, aun así, obtiene la mitad incorrecta de la oración correcta, lo cual es mucho más difícil de percibir. Mida con el tokenizador real, divida primero según el significado y registre por qué cada límite se colocó donde lo hizo.