Módulo 6: Prompt Composition y Chaining

5. Context Window Management

Descripción

El context window (ventana de contexto) es uno de los recursos más críticos y limitados cuando trabajas con LLMs. Entender cómo funciona, cómo medirlo con precisión, y qué estrategias usar cuando el contenido supera el límite es fundamental para construir sistemas robustos.

Esta cápsula cubre las tres estrategias principales: summarization chains, sliding window, y hierarchical summarization, junto con criterios para elegir cuándo usar cada una.


El Problema del Context Window

gpt-4o-mini: 128K tokens de contexto
gpt-4o: 128K tokens de contexto
claude-3-5-haiku: 200K tokens de contexto

¿Cuánto es un token? Aproximadamente:
- 1 token ≈ 0.75 palabras en inglés
- 1 token ≈ 0.5 palabras en español (más tokens por carácter)
- 1 página A4 de texto ≈ 500 tokens
- Un libro (200 páginas) ≈ 100,000 tokens

Problema real:
- Reporte anual de empresa: 150,000 tokens → NO CABE en un solo prompt
- Código base de 10K líneas: ~100,000 tokens → No cabe con el prompt de análisis
- Conversación de 2 horas en chat: ~30,000 tokens → Puede quedarse sin contexto

Pero incluso cuando CABE, hay otro problema: "Lost in the Middle"
El modelo tiene peor recall para información en el MEDIO del context window.
Lo mejor recordado: principio y final.

Contar Tokens con Precisión

from openai import OpenAI
import tiktoken
from typing import Union

client = OpenAI()

def contar_tokens(texto: str, modelo: str = "gpt-4o-mini") -> int:
    """
    Cuenta tokens de forma precisa usando tiktoken.
    Mucho más preciso que la regla heurística de palabras.
    
    Args:
        texto: Texto a tokenizar
        modelo: Modelo de OpenAI para el que contar tokens
    
    Returns:
        Número exacto de tokens
    """
    try:
        encoding = tiktoken.encoding_for_model(modelo)
    except KeyError:
        # Fallback para modelos nuevos
        encoding = tiktoken.get_encoding("cl100k_base")
    
    return len(encoding.encode(texto))

def contar_tokens_mensajes(mensajes: list[dict], modelo: str = "gpt-4o-mini") -> int:
    """
    Cuenta tokens para una lista de mensajes (formato chat).
    Incluye los tokens overhead del formato de chat.
    """
    try:
        encoding = tiktoken.encoding_for_model(modelo)
    except KeyError:
        encoding = tiktoken.get_encoding("cl100k_base")
    
    # Overhead por mensaje (aproximado para GPT-4)
    tokens = 3  # Sistema base
    for msg in mensajes:
        tokens += 4  # Overhead por mensaje
        tokens += len(encoding.encode(msg.get("content", "")))
        tokens += len(encoding.encode(msg.get("role", "")))
    
    return tokens

def chunks_por_tokens(
    texto: str,
    max_tokens_por_chunk: int = 4000,
    overlap_tokens: int = 200,
    modelo: str = "gpt-4o-mini"
) -> list[str]:
    """
    Divide un texto en chunks de tamaño máximo en tokens.
    Incluye overlap para mantener coherencia entre chunks.
    
    Args:
        texto: Texto a dividir
        max_tokens_por_chunk: Máximo tokens por chunk
        overlap_tokens: Tokens de overlap entre chunks consecutivos
        modelo: Modelo para calcular tokens
    
    Returns:
        Lista de chunks de texto
    """
    try:
        encoding = tiktoken.encoding_for_model(modelo)
    except KeyError:
        encoding = tiktoken.get_encoding("cl100k_base")
    
    tokens = encoding.encode(texto)
    
    chunks = []
    inicio = 0
    
    while inicio < len(tokens):
        fin = min(inicio + max_tokens_por_chunk, len(tokens))
        
        chunk_tokens = tokens[inicio:fin]
        chunk_texto = encoding.decode(chunk_tokens)
        chunks.append(chunk_texto)
        
        inicio = fin - overlap_tokens  # Overlap para continuidad
        
        if inicio >= len(tokens):
            break
    
    return chunks


# Ejemplo de uso:
if __name__ == "__main__":
    texto_largo = "Este es un texto de ejemplo. " * 1000  # ~6K tokens
    
    total_tokens = contar_tokens(texto_largo)
    print(f"Total tokens: {total_tokens:,}")
    
    chunks = chunks_por_tokens(texto_largo, max_tokens_por_chunk=1000, overlap_tokens=100)
    print(f"Número de chunks: {len(chunks)}")
    for i, chunk in enumerate(chunks[:3]):
        print(f"Chunk {i+1}: {contar_tokens(chunk)} tokens")

Estrategia 1: Summarization Chain

La estrategia más simple: dividir el texto en chunks, resumir cada uno, y concatenar los resúmenes.

def summarization_chain(
    texto: str,
    max_tokens_chunk: int = 4000,
    tokens_por_resumen: int = 200,
    instruccion_adicional: str = "",
    verbose: bool = False
) -> str:
    """
    Estrategia básica de summarization:
    1. Dividir en chunks
    2. Resumir cada chunk
    3. Concatenar resúmenes
    
    Mejor para: Documentos donde el resumen de cada sección es suficiente.
    No ideal cuando: Necesitas preservar detalles específicos.
    
    Args:
        texto: Texto largo a resumir
        max_tokens_chunk: Tokens máximos por chunk
        tokens_por_resumen: Tokens del resumen de cada chunk
        instruccion_adicional: Qué preservar del resumen (ej: "mantén cifras exactas")
    
    Returns:
        Texto resumido concatenado
    """
    chunks = chunks_por_tokens(texto, max_tokens_chunk)
    
    if len(chunks) == 1:
        return texto  # No hace falta dividir
    
    if verbose:
        print(f"Dividido en {len(chunks)} chunks para summarization")
    
    resumenes = []
    for i, chunk in enumerate(chunks):
        if verbose:
            print(f"  Resumiendo chunk {i+1}/{len(chunks)}...")
        
        prompt = f"""Resume el siguiente fragmento de texto en {tokens_por_resumen} palabras.
{f'Instrucción adicional: {instruccion_adicional}' if instruccion_adicional else ''}
Preserva: nombres propios, fechas, números específicos, ideas principales.

Texto:
{chunk}"""
        
        resumen = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=tokens_por_resumen * 2
        ).choices[0].message.content
        
        resumenes.append(resumen)
    
    return "\n\n".join(resumenes)


# Ejemplo con instrucción específica:
texto_informe = "El informe anual de TechCorp..." * 500  # Simular texto largo
resumen = summarization_chain(
    texto_informe,
    instruccion_adicional="mantén todas las cifras financieras y porcentajes",
    verbose=True
)
print(f"Texto original: {contar_tokens(texto_informe):,} tokens")
print(f"Resumen: {contar_tokens(resumen):,} tokens")

Estrategia 2: Sliding Window

Procesa el texto en ventanas deslizantes con overlap, útil cuando necesitas mantener coherencia contextual.

def sliding_window_analysis(
    texto: str,
    pregunta: str,
    window_tokens: int = 3000,
    overlap_tokens: int = 300,
    combinar: bool = True
) -> Union[list[str], str]:
    """
    Análisis con ventana deslizante.
    Útil para: búsqueda de información específica, análisis local coherente.
    
    Args:
        texto: Texto largo a analizar
        pregunta: La pregunta o tarea a aplicar en cada ventana
        window_tokens: Tamaño de la ventana en tokens
        overlap_tokens: Tokens de overlap entre ventanas (para coherencia)
        combinar: Si True, combina resultados en una síntesis final
    
    Returns:
        Lista de resultados por ventana, o síntesis combinada
    """
    chunks = chunks_por_tokens(texto, window_tokens, overlap_tokens)
    
    resultados = []
    
    for i, chunk in enumerate(chunks):
        # Indicar al modelo el contexto de la ventana
        contexto = f"[Fragmento {i+1} de {len(chunks)} del documento]"
        
        prompt = f"""{contexto}
        
{pregunta}

Fragmento:
{chunk}

Responde basándote SOLO en este fragmento. Si la información no está en este fragmento, indica "No encontrado en este fragmento"."""
        
        resultado = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=400
        ).choices[0].message.content
        
        resultados.append({
            "fragmento": i + 1,
            "total_fragmentos": len(chunks),
            "resultado": resultado
        })
    
    if not combinar:
        return resultados
    
    # Combinar resultados de todas las ventanas
    resultados_str = "\n\n".join([
        f"Fragmento {r['fragmento']}:\n{r['resultado']}"
        for r in resultados
        if "No encontrado" not in r['resultado']
    ])
    
    if not resultados_str.strip():
        return "La información solicitada no fue encontrada en ningún fragmento del documento."
    
    sintesis = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"""Sintetiza estos resultados de análisis por fragmentos:

{resultados_str}

Genera una respuesta coherente y sin repeticiones para: {pregunta}"""}],
        temperature=0,
        max_tokens=500
    ).choices[0].message.content
    
    return sintesis

# Ejemplo: Buscar información específica en documento largo
documento = "Texto largo del contrato..." * 300
pregunta = "¿Cuáles son las penalizaciones por incumplimiento mencionadas en el documento?"
resultado = sliding_window_analysis(documento, pregunta, window_tokens=2000)
print(resultado)

Estrategia 3: Hierarchical Summarization

Para documentos muy largos, hacer resúmenes en múltiples niveles.

def hierarchical_summarize(
    texto: str,
    nivel_1_tokens: int = 3000,
    nivel_1_resumen_tokens: int = 300,
    nivel_2_resumen_tokens: int = 500,
    objetivo: str = "resumen general"
) -> dict:
    """
    Summarization jerárquica para documentos muy largos.
    
    Nivel 1: Resumir cada chunk (granularidad fina)
    Nivel 2: Resumir los resúmenes del nivel 1 (granularidad media)
    Nivel 3: Resumen final de los resúmenes de nivel 2 (granularidad gruesa)
    
    Args:
        texto: Texto muy largo
        nivel_1_tokens: Tamaño de chunks del nivel 1
        nivel_1_resumen_tokens: Tokens por resumen en nivel 1
        nivel_2_resumen_tokens: Tokens para el resumen de nivel 2
        objetivo: El objetivo del resumen (orienta el foco)
    
    Returns:
        dict con resúmenes de cada nivel
    """
    # NIVEL 1: Resumir cada chunk
    print("Nivel 1: Resumiendo chunks individuales...")
    chunks_n1 = chunks_por_tokens(texto, nivel_1_tokens)
    resumenes_n1 = []
    
    for i, chunk in enumerate(chunks_n1):
        print(f"  Chunk {i+1}/{len(chunks_n1)}")
        resumen = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"""Resume este fragmento para el objetivo: {objetivo}
Sé conciso pero preserva datos clave (números, nombres, fechas).
Target: {nivel_1_resumen_tokens} palabras.

Fragmento:
{chunk}"""}],
            temperature=0,
            max_tokens=nivel_1_resumen_tokens * 2
        ).choices[0].message.content
        resumenes_n1.append(resumen)
    
    # Verificar si el nivel 1 ya es suficientemente pequeño
    texto_n1 = "\n\n---\n\n".join(resumenes_n1)
    tokens_n1 = contar_tokens(texto_n1)
    
    if tokens_n1 <= 4000:
        # El nivel 1 ya cabe en contexto, hacer solo un nivel adicional
        print(f"Nivel 2: Síntesis final (nivel 1 ya es manejable: {tokens_n1} tokens)")
        resumen_final = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"""Objetivo: {objetivo}
            
Basándote en estos resúmenes de secciones del documento, genera un resumen final coherente:

{texto_n1}

El resumen final debe ser de {nivel_2_resumen_tokens} palabras y sin repeticiones."""}],
            temperature=0,
            max_tokens=nivel_2_resumen_tokens * 2
        ).choices[0].message.content
        
        return {
            "niveles": 2,
            "n_chunks_nivel1": len(chunks_n1),
            "resumenes_nivel1": resumenes_n1,
            "resumen_final": resumen_final
        }
    
    # NIVEL 2: Necesitamos otro nivel de summarization
    print(f"Nivel 2: Los resúmenes de nivel 1 aún son grandes ({tokens_n1} tokens). Summarizando nivel 2...")
    chunks_n2 = chunks_por_tokens(texto_n1, nivel_1_tokens)
    resumenes_n2 = []
    
    for i, chunk in enumerate(chunks_n2):
        resumen = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"Sintetiza estos resúmenes parciales (objetivo: {objetivo}):\n\n{chunk}"}],
            temperature=0,
            max_tokens=nivel_2_resumen_tokens
        ).choices[0].message.content
        resumenes_n2.append(resumen)
    
    # NIVEL FINAL: Síntesis de nivel 2
    texto_n2 = "\n\n".join(resumenes_n2)
    resumen_final = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"""Genera el resumen ejecutivo final del documento.
Objetivo: {objetivo}

Basado en:
{texto_n2}

Sé exhaustivo pero conciso. Resalta los puntos más importantes."""}],
        temperature=0,
        max_tokens=nivel_2_resumen_tokens * 2
    ).choices[0].message.content
    
    return {
        "niveles": 3,
        "n_chunks_nivel1": len(chunks_n1),
        "n_chunks_nivel2": len(chunks_n2),
        "resumenes_nivel1": resumenes_n1,
        "resumenes_nivel2": resumenes_n2,
        "resumen_final": resumen_final
    }

Cuándo Usar Cada Estrategia

def seleccionar_estrategia_contexto(
    texto: str,
    tarea: str,
    modelo: str = "gpt-4o-mini"
) -> dict:
    """
    Recomienda la estrategia de context management según el tamaño del texto
    y el tipo de tarea.
    
    Returns:
        dict con estrategia recomendada y justificación
    """
    tokens_texto = contar_tokens(texto, modelo)
    
    LIMITES_MODELO = {
        "gpt-4o-mini": 128_000,
        "gpt-4o": 128_000,
        "claude-3-5-haiku-20241022": 200_000
    }
    
    limite = LIMITES_MODELO.get(modelo, 128_000)
    tokens_disponibles = limite - 2000  # Reservar para el prompt y respuesta
    
    # Tipos de tarea que requieren consulta por secciones
    tareas_busqueda = ["encontrar", "buscar", "extraer específico", "¿dónde", "¿cuándo", "¿quién"]
    es_busqueda = any(t in tarea.lower() for t in tareas_busqueda)
    
    if tokens_texto <= tokens_disponibles * 0.8:
        # El texto cabe cómodamente en contexto
        return {
            "estrategia": "contexto_directo",
            "descripcion": "El texto cabe en el context window. Usarlo directamente.",
            "tokens_texto": tokens_texto,
            "tokens_disponibles": tokens_disponibles,
            "chunks_estimados": 1,
            "llamadas_estimadas": 1
        }
    
    if tokens_texto <= tokens_disponibles * 2:
        # Ligeramente excede el contexto
        if es_busqueda:
            return {
                "estrategia": "sliding_window",
                "descripcion": "Búsqueda por secciones con overlap para encontrar información específica.",
                "tokens_texto": tokens_texto,
                "chunks_estimados": 2,
                "llamadas_estimadas": 3  # 2 chunks + 1 síntesis
            }
        return {
            "estrategia": "summarization_chain",
            "descripcion": "Resumir en 2-3 chunks, concatenar.",
            "tokens_texto": tokens_texto,
            "chunks_estimados": 2,
            "llamadas_estimadas": 3
        }
    
    if tokens_texto <= tokens_disponibles * 10:
        # Moderadamente largo
        if es_busqueda:
            return {
                "estrategia": "sliding_window",
                "descripcion": "Ventana deslizante con síntesis final.",
                "tokens_texto": tokens_texto,
                "chunks_estimados": tokens_texto // 3000,
                "llamadas_estimadas": (tokens_texto // 3000) + 1
            }
        return {
            "estrategia": "hierarchical_summarization_2_niveles",
            "descripcion": "Resumir chunks, luego sintetizar resúmenes.",
            "tokens_texto": tokens_texto,
            "chunks_estimados": tokens_texto // 3000,
            "llamadas_estimadas": tokens_texto // 3000 + 1
        }
    
    # Muy largo: necesita jerarquía completa
    return {
        "estrategia": "hierarchical_summarization_3_niveles",
        "descripcion": "Summarization en 3 niveles jerárquicos.",
        "tokens_texto": tokens_texto,
        "chunks_estimados": tokens_texto // 3000,
        "llamadas_estimadas": tokens_texto // 2000  # Estimación gruesa
    }

Tabla de Comparación de Estrategias

EstrategiaTokens máximosPreserva detallesCostoCuándo usar
Contexto directo< 100K✓✓✓1xSiempre que quepa
Summarization chainIlimitado✓✓ (con instrucciones)N× chunksResúmenes donde la síntesis es suficiente
Sliding windowIlimitado✓✓✓ por secciónN× chunks + síntesisBúsqueda de info específica
HierarchicalIlimitado✓ (pierde detalle)Documentos extremadamente largos

Integración con Anthropic (Claude)

import anthropic

client_anthropic = anthropic.Anthropic()

def summarization_chain_claude(
    texto: str,
    max_chars_chunk: int = 15000,
    instruccion: str = "Resumen ejecutivo preservando datos clave"
) -> str:
    """
    Summarization chain usando Claude.
    Claude tiene ventana de 200K tokens, pero la misma estrategia
    aplica para documentos extremadamente largos.
    """
    # Claude-3-5-haiku soporta hasta 200K tokens de contexto
    if len(texto) < 150000:  # Aproximadamente 100K tokens
        # El texto cabe directamente
        message = client_anthropic.messages.create(
            model="claude-3-5-haiku-20241022",
            max_tokens=1000,
            messages=[{"role": "user", "content": f"{instruccion}:\n\n{texto}"}]
        )
        return message.content[0].text
    
    # Dividir en chunks si es demasiado largo
    chunks = [texto[i:i+max_chars_chunk] for i in range(0, len(texto), max_chars_chunk)]
    resumenes = []
    
    for chunk in chunks:
        message = client_anthropic.messages.create(
            model="claude-3-5-haiku-20241022",
            max_tokens=400,
            messages=[{"role": "user", "content": f"Resume en 300 palabras preservando datos importantes:\n\n{chunk}"}]
        )
        resumenes.append(message.content[0].text)
    
    # Síntesis final
    sintesis_input = "\n\n---\n\n".join(resumenes)
    sintesis_msg = client_anthropic.messages.create(
        model="claude-3-5-haiku-20241022",
        max_tokens=800,
        messages=[{"role": "user", "content": f"Sintetiza estos resúmenes parciales en un informe ejecutivo coherente:\n\n{sintesis_input}"}]
    )
    return sintesis_msg.content[0].text

Manejo de "Lost in the Middle"

def procesar_con_posicion_optima(
    contexto_largo: str,
    pregunta: str,
    n_fragmentos_clave: int = 3
) -> str:
    """
    Mitiga el problema "Lost in the Middle" colocando la información
    más relevante al principio y al final del contexto.
    
    El modelo recuerda mejor el inicio y el final del context window.
    """
    # Paso 1: Identificar los fragmentos más relevantes
    chunks = chunks_por_tokens(contexto_largo, max_tokens_por_chunk=1000)
    
    relevancia = []
    for i, chunk in enumerate(chunks):
        score_resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"¿Qué tan relevante es este fragmento para responder: '{pregunta}'?\nScore 0-10. Solo el número.\n\nFragmento: {chunk[:300]}"}],
            temperature=0,
            max_tokens=5
        ).choices[0].message.content.strip()
        
        try:
            score = float(score_resp[:3])
        except ValueError:
            score = 5.0
        relevancia.append((score, i, chunk))
    
    # Ordenar por relevancia y tomar los mejores
    relevancia.sort(reverse=True)
    fragmentos_clave = relevancia[:n_fragmentos_clave]
    
    # Paso 2: Colocar fragmentos clave al principio del contexto
    contexto_optimizado = "FRAGMENTOS MÁS RELEVANTES (para tu referencia):\n\n"
    for score, i, chunk in sorted(fragmentos_clave, key=lambda x: x[0], reverse=True):
        contexto_optimizado += f"[Fragmento {i+1}, relevancia {score:.1f}/10]:\n{chunk}\n\n"
    
    # Paso 3: Responder con el contexto optimizado
    respuesta = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"""{contexto_optimizado}

Con base en estos fragmentos relevantes, responde:
{pregunta}"""
        }],
        temperature=0,
        max_tokens=500
    ).choices[0].message.content
    
    return respuesta

Troubleshooting

Problema 1: Pérdida de información en chunks

Síntoma: El resumen de un chunk no menciona datos importantes que sí estaban en él.

Causa: El prompt de resumen es demasiado genérico y el modelo selecciona qué preservar.

Solución:

def resumir_preservando_especificos(
    chunk: str,
    tipos_a_preservar: list[str] = None
) -> str:
    """Resumen que garantiza preservación de tipos de datos específicos."""
    if tipos_a_preservar is None:
        tipos_a_preservar = ["números y porcentajes", "nombres propios", "fechas", "URLs o referencias"]
    
    tipos_str = ", ".join(tipos_a_preservar)
    
    return client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"""Resume el siguiente texto (máx 200 palabras).
        
CRÍTICO: Preserva LITERALMENTE todos los: {tipos_str}
Si no puedes incluirlos en el resumen, listarlos al final bajo "Datos clave:"

Texto: {chunk}"""}],
        temperature=0,
        max_tokens=350
    ).choices[0].message.content

Problema 2: Incoherencia entre resúmenes de chunks consecutivos

Síntoma: El resumen final parece desconectado; cada chunk parece no estar relacionado con el anterior.

Solución: Pasar contexto del chunk anterior al siguiente:

def summarization_chain_con_contexto(texto: str, max_tokens_chunk: int = 3000) -> str:
    """Summarization donde cada chunk conoce el contexto del anterior."""
    chunks = chunks_por_tokens(texto, max_tokens_chunk)
    resumenes = []
    contexto_previo = ""
    
    for i, chunk in enumerate(chunks):
        prompt = f"""Resume este fragmento ({i+1} de {len(chunks)}).
{'Contexto de fragmentos anteriores: ' + contexto_previo[:300] if contexto_previo else ''}

Fragmento actual:
{chunk}

Genera un resumen de 150-200 palabras que sea coherente con el contexto previo."""
        
        resumen = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
            max_tokens=300
        ).choices[0].message.content
        
        resumenes.append(resumen)
        contexto_previo = resumen  # El resumen actual se convierte en contexto para el siguiente
    
    return "\n\n".join(resumenes)

Problema 3: Costo muy alto con muchos chunks

Síntoma: Un documento de 50K tokens con chunks de 2K = 25 llamadas solo para el primer nivel.

Solución: Usar chunks más grandes y resumir en 2 pasos si es necesario:

def summarization_eficiente(texto: str, budget_llamadas: int = 10) -> str:
    """
    Summarization que respeta un budget de llamadas API.
    Ajusta el tamaño de chunk automáticamente.
    """
    tokens_total = contar_tokens(texto)
    tokens_por_llamada = tokens_total // budget_llamadas
    tokens_por_llamada = max(tokens_por_llamada, 2000)  # Mínimo 2K por chunk
    
    print(f"Texto: {tokens_total:,} tokens, Budget: {budget_llamadas} llamadas")
    print(f"Tamaño de chunk ajustado: {tokens_por_llamada:,} tokens")
    
    return summarization_chain(texto, max_tokens_chunk=tokens_por_llamada)

Ejercicios

Ejercicio 1: Implementar extractor de información específica

Crea una función que use sliding window para extraer TODAS las menciones de fechas, importes, y nombres de personas de un documento largo. El resultado debe ser una lista deduplicada y ordenada.

Ver solución
import re

def extraer_entidades_sliding_window(documento: str) -> dict:
    """Extrae entidades específicas usando sliding window."""
    chunks = chunks_por_tokens(documento, max_tokens_por_chunk=2000, overlap_tokens=200)
    
    fechas = set()
    importes = set()
    personas = set()
    
    for i, chunk in enumerate(chunks):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"""Extrae del texto:
1. Fechas (formato: DD/MM/YYYY o texto como "enero 2026")
2. Importes monetarios (€/$) con valor exacto
3. Nombres de personas

Responde en JSON: {{"fechas": [], "importes": [], "personas": []}}

Texto: {chunk}"""}],
            temperature=0,
            response_format={"type": "json_object"}
        ).choices[0].message.content
        
        try:
            datos = json.loads(response)
            fechas.update(datos.get("fechas", []))
            importes.update(datos.get("importes", []))
            personas.update(datos.get("personas", []))
        except Exception:
            pass
    
    return {
        "fechas": sorted(list(fechas)),
        "importes": sorted(list(importes)),
        "personas": sorted(list(personas))
    }

Ejercicio 2: Comparar estrategias

Para un documento de ~10K tokens, mide y compara:

  • Contexto directo (si cabe)
  • Summarization chain (chunks de 2K)
  • Hierarchical (2 niveles)

Compara: calidad del resumen, número de llamadas, tokens usados.

Ver solución
import time

def comparar_estrategias(texto: str, pregunta_evaluacion: str) -> dict:
    """Compara las 3 estrategias para el mismo documento."""
    tokens = contar_tokens(texto)
    resultados = {}
    
    # Estrategia 1: Directo (si cabe)
    if tokens < 100000:
        t0 = time.time()
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": f"Resume: {texto}"}],
            temperature=0, max_tokens=400
        ).choices[0].message.content
        resultados["directo"] = {
            "resumen": resp, "tiempo": time.time()-t0, "llamadas": 1,
            "tokens_input": tokens
        }
    
    # Estrategia 2: Summarization chain
    t0 = time.time()
    resumen_sc = summarization_chain(texto, max_tokens_chunk=2000, verbose=False)
    resultados["summarization_chain"] = {
        "resumen": resumen_sc, "tiempo": time.time()-t0,
        "llamadas": len(chunks_por_tokens(texto, 2000)),
        "tokens_input": tokens
    }
    
    # Comparar
    print(f"Tokens originales: {tokens:,}")
    for estrategia, datos in resultados.items():
        print(f"\n{estrategia}:")
        print(f"  Tiempo: {datos['tiempo']:.2f}s")
        print(f"  Llamadas: {datos['llamadas']}")
        print(f"  Resumen: {datos['resumen'][:100]}...")
    
    return resultados

Resumen

  • Contexto directo: Siempre que el texto quepa (~80% del límite para dejar margen). Mejor calidad.
  • Summarization chain: Para documentos moderadamente largos (2-3x el límite). Simple y efectivo.
  • Sliding window: Para búsquedas de información específica. Preserva detalles locales.
  • Hierarchical: Para documentos muy largos (>10x el límite). Pierde algo de detalle en cada nivel.
  • Contar tokens: Usar tiktoken, no estimaciones heurísticas.
  • Lost in the Middle: Colocar información crítica al inicio o final del contexto.

Recursos adicionales

  1. tiktoken - OpenAI tokenizer
  2. Lost in the Middle (Liu et al., 2023) - Paper original sobre el fenómeno
  3. OpenAI Token counting cookbook
  4. Anthropic Claude context window documentation
  5. Long Context Best Practices - Anthropic
  6. LangChain text splitters