Ingeniería de Contexto para IA en Producción: Guía Práctica 2026

Guía práctica sobre ingeniería de contexto en 2026: los cuatro pilares fundamentales, arquitecturas de memoria para agentes, gestión de ventanas de contexto, RAG 2.0, prompt caching y patrones de diseño con código Python listo para producción.

Introducción: La Era de la Ingeniería de Contexto

Durante años, la comunidad de inteligencia artificial estuvo obsesionada con escribir el prompt perfecto. Se crearon frameworks, plantillas y metodologías enteras dedicadas a encontrar esas "palabras mágicas" que desbloquearan todo el potencial de los modelos de lenguaje. Pero, seamos honestos, a medida que las aplicaciones de IA han madurado y pasado de prototipos a sistemas reales en producción, ha quedado bastante claro que el prompt engineering tradicional es solo la punta del iceberg.

La ingeniería de contexto va mucho más allá. Es la disciplina que diseña, gestiona y optimiza el entorno completo de información — la ventana de contexto — que alimenta a los modelos de IA. No se trata solo de qué le dices al modelo, sino de todo lo que el modelo puede ver cuando genera una respuesta: las instrucciones del sistema, los resultados de herramientas, los ejemplos recuperados dinámicamente, el historial de conversación comprimido, los documentos extraídos mediante RAG y las restricciones estructurales que guían la salida.

En 2026, con modelos como Claude 4 de Anthropic y GPT-5 de OpenAI ofreciendo ventanas de contexto de cientos de miles de tokens, la ingeniería de contexto se ha convertido en la competencia central de cualquier ingeniero de IA que trabaje en producción. Como señaló Andrej Karpathy, ya no basta con ser un buen escritor de prompts; ahora necesitas ser un arquitecto de contexto.

"La ingeniería de contexto es el arte sutil de llenar la ventana de contexto con exactamente la información correcta que un modelo de lenguaje necesita para producir el resultado ideal. No se trata de instrucciones ingeniosas — se trata de sistemas que ensamblan dinámicamente toda la información, herramientas y ejemplos que el modelo necesita."

Este artículo es una guía práctica para dominar la ingeniería de contexto en aplicaciones de IA en producción. Vamos a cubrir desde los fundamentos teóricos hasta implementaciones con código, patrones de diseño probados y las mejores prácticas que están definiendo el estado del arte este año.

De Prompt Engineering a Ingeniería de Contexto

Para entender bien la ingeniería de contexto, primero hay que comprender por qué el prompt engineering tradicional se queda corto para sistemas en producción.

Las limitaciones del prompt engineering clásico

El prompt engineering se centra en optimizar una cadena de texto estática: cómo formular una pregunta, qué rol asignar al modelo, qué formato pedir. Funciona bien para interacciones puntuales, pero presenta problemas fundamentales cuando intentamos construir aplicaciones robustas:

  • Estaticidad: Un prompt fijo no puede adaptarse a la diversidad de consultas que recibe un sistema en producción. Cada usuario, cada sesión y cada caso de uso requiere información contextual diferente.
  • Escala: Cuando tu aplicación maneja miles de conversaciones concurrentes, cada una con su propio historial, documentos y estado, necesitas sistemas que gestionen el contexto dinámicamente.
  • Complejidad del dominio: Las aplicaciones empresariales requieren integrar múltiples fuentes de datos, herramientas externas y reglas de negocio que simplemente no caben en un prompt estático.
  • Agentes autónomos: Los sistemas agénticos que ejecutan múltiples pasos, invocan herramientas y toman decisiones necesitan un contexto que evolucione con cada acción.

El salto paradigmático

La ingeniería de contexto reconoce que el rendimiento de un modelo de IA en producción depende de un ecosistema completo de información. Y este ecosistema incluye bastante más de lo que imaginarías a primera vista:

  1. Instrucciones del sistema que definen comportamiento, restricciones y personalidad.
  2. Definiciones de herramientas que el modelo puede invocar, con descripciones claras y parámetros bien tipados.
  3. Ejemplos dinámicos seleccionados en función de la consulta actual del usuario.
  4. Información recuperada de bases de datos vectoriales, APIs y sistemas de archivos.
  5. Historial de conversación gestionado inteligentemente para preservar contexto sin desperdiciar tokens.
  6. Estado de memoria que persiste información crítica entre sesiones.

El ingeniero de contexto no escribe un prompt — diseña un sistema que construye el contexto óptimo para cada interacción, en tiempo real, considerando restricciones de costo, latencia y calidad. Es un cambio de mentalidad importante.

Los Cuatro Pilares de la Ingeniería de Contexto

Anthropic ha articulado un framework que organiza la ingeniería de contexto en cuatro pilares fundamentales. Estos pilares forman la base sobre la cual se construyen sistemas de IA robustos y confiables. Vamos a verlos uno por uno.

Pilar 1: System Prompts — El Contrato Fundacional

El system prompt es el cimiento de cualquier aplicación de IA. Define quién es el modelo, qué puede hacer, qué no debe hacer y cómo debe comportarse. En producción, un system prompt bien diseñado no es un texto que escribes una vez y olvidas — es un documento vivo que evoluciona con el producto.

Principios clave para system prompts efectivos:

  • Organización con etiquetas XML o Markdown: Usar marcadores estructurales permite que el modelo identifique secciones con claridad. Las etiquetas XML como <instrucciones>, <restricciones> y <formato> crean delimitadores inequívocos.
  • Equilibrio entre especificidad y flexibilidad: Un system prompt demasiado restrictivo genera respuestas rígidas. Uno demasiado vago produce inconsistencias. El arte está en definir límites claros dejando espacio para la adaptación contextual.
  • Jerarquía de prioridades: Cuando las instrucciones pueden entrar en conflicto (y créeme, eventualmente lo harán), establecer una jerarquía explícita evita comportamientos impredecibles.
# Ejemplo de un system prompt estructurado con etiquetas XML
SYSTEM_PROMPT = """
<rol>
Eres un asistente experto en análisis financiero para la empresa AcmeCorp.
Tu objetivo es ayudar a los analistas a interpretar datos financieros
y generar informes precisos.
</rol>

<capacidades>
- Analizar estados financieros y métricas clave
- Generar visualizaciones descriptivas de datos
- Comparar rendimiento entre períodos
- Identificar tendencias y anomalías
</capacidades>

<restricciones>
- NUNCA proporciones asesoría de inversión directa
- SIEMPRE indica cuando los datos están incompletos
- NO generes proyecciones sin datos históricos suficientes
- Mantén confidencialidad de datos internos
</restricciones>

<formato_respuesta>
- Usa tablas Markdown para datos comparativos
- Incluye siempre las fuentes de datos citadas
- Estructura las respuestas con encabezados claros
- Proporciona un resumen ejecutivo al inicio de análisis largos
</formato_respuesta>

<prioridades>
En caso de conflicto entre instrucciones:
1. La seguridad y confidencialidad de datos son prioritarias
2. La precisión de datos prevalece sobre la velocidad de respuesta
3. La claridad prevalece sobre la exhaustividad
</prioridades>
"""

Pilar 2: Diseño de Herramientas — Eficiencia en Tokens

Las herramientas (tools) que exponemos a un modelo de IA consumen tokens de la ventana de contexto. Cada definición de herramienta — con su nombre, descripción y esquema de parámetros — ocupa espacio valioso. Así que el diseño eficiente de herramientas es un componente crítico de la ingeniería de contexto, aunque a menudo se subestima.

Principios de diseño de herramientas:

  • Descripciones concisas pero claras: Cada token en la descripción de una herramienta cuenta. Elimina redundancias y usa lenguaje preciso.
  • Parámetros de entrada bien tipados: Los esquemas JSON con tipos estrictos, enumeraciones y descripciones breves reducen errores del modelo y tokens desperdiciados en intentos fallidos.
  • Minimizar solapamiento: Si dos herramientas hacen cosas similares, el modelo invertirá tokens deliberando cuál usar. Consolida funcionalidades cuando sea posible.
  • Resultados compactos: Diseña las herramientas para devolver solo la información necesaria, no volcados completos de datos.

Pilar 3: Few-Shot Examples — Ejemplos que Valen Mil Tokens

Los ejemplos son, en mi experiencia, el mecanismo más poderoso para comunicar expectativas a un modelo de lenguaje. Un ejemplo bien elegido transmite formato, tono, nivel de detalle y lógica de razonamiento de forma mucho más eficiente que párrafos y párrafos de instrucciones explícitas.

Estrategias para few-shot examples en producción:

  • Curación de ejemplos canónicos: Mantén una biblioteca de ejemplos de alta calidad, etiquetados por categoría, dificultad y caso de uso. Estos ejemplos deben revisarse y actualizarse periódicamente.
  • Selección dinámica: En lugar de incluir siempre los mismos ejemplos, selecciona dinámicamente aquellos que son más relevantes para la consulta actual del usuario, usando similitud semántica o clasificación por categoría.
  • Ejemplos negativos: A veces es tan importante mostrar qué NO hacer como mostrar qué hacer. Los contra-ejemplos, etiquetados como tales, ayudan a definir los límites del comportamiento deseado.

Pilar 4: Estrategia de Recuperación de Contexto

La recuperación de contexto (context retrieval) determina qué información externa se inyecta en la ventana de contexto para cada interacción. Es aquí donde la ingeniería de contexto se conecta con sistemas de RAG, bases de datos y APIs externas.

Enfoques clave:

  • Just-in-time: Recuperar información solo cuando es necesaria, no cargar todo el contexto posible de antemano. Esto conserva tokens para la información verdaderamente relevante.
  • Descubrimiento progresivo: En sistemas agénticos, permitir que el modelo solicite información adicional cuando la necesite, en lugar de intentar anticipar todas las necesidades posibles.
  • Identificadores ligeros: En lugar de inyectar documentos completos, proporcionar primero títulos, resúmenes o metadatos que permitan al modelo decidir qué necesita ver en detalle.

Arquitectura de Memoria para Agentes de IA

Los agentes de IA en producción necesitan diferentes tipos de memoria para funcionar bien. Esta arquitectura se inspira en los modelos de memoria humana (sí, hay bastante paralelismo) y adapta sus principios al contexto computacional de los LLMs.

Memoria de Trabajo (Working Memory)

La memoria de trabajo es lo que el modelo "ve" en este momento: el contenido actual de la ventana de contexto. Incluye el system prompt, el historial de conversación reciente, los resultados de herramientas y cualquier contexto inyectado.

Es limitada por el tamaño de la ventana de contexto del modelo y es la más costosa en términos de tokens.

Memoria Episódica (Episodic Memory)

La memoria episódica registra qué sucedió en interacciones anteriores. Almacena resúmenes de conversaciones pasadas, decisiones tomadas, errores cometidos y sus correcciones. Se comprime para ahorrar tokens y se recupera selectivamente cuando resulta relevante para la interacción actual.

Memoria Semántica (Semantic Memory)

La memoria semántica almacena conocimiento indexado por tema, entidad y relevancia. Incluye hechos sobre el usuario, documentación del dominio, políticas empresariales y cualquier información que no cambia con frecuencia. Se organiza en bases de datos vectoriales o grafos de conocimiento para una recuperación eficiente.

Veamos cómo se implementa todo esto en código:

"""
Gestor de memoria para agentes de IA en producción.
Implementa los tres tipos de memoria: trabajo, episódica y semántica.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import json
import hashlib


@dataclass
class Recuerdo:
    """Representa una unidad de memoria almacenada."""
    contenido: str
    tipo: str  # "episodico" o "semantico"
    marca_temporal: datetime = field(default_factory=datetime.now)
    relevancia: float = 1.0
    etiquetas: list[str] = field(default_factory=list)
    entidad: Optional[str] = None

    @property
    def identificador(self) -> str:
        """Genera un identificador único basado en el contenido."""
        return hashlib.md5(self.contenido.encode()).hexdigest()[:12]


class GestorDeMemoria:
    """
    Gestor de memoria completo para agentes de IA.
    Coordina los tres tipos de memoria y gestiona
    la ventana de contexto de forma eficiente.
    """

    def __init__(self, limite_tokens_trabajo: int = 8000):
        self.memoria_trabajo: list[dict] = []
        self.limite_tokens_trabajo = limite_tokens_trabajo
        self.memoria_episodica: list[Recuerdo] = []
        self.memoria_semantica: dict[str, list[Recuerdo]] = {}

    def agregar_a_trabajo(self, mensaje: dict) -> None:
        """Agrega un mensaje a la memoria de trabajo."""
        self.memoria_trabajo.append(mensaje)
        self._gestionar_desbordamiento()

    def _estimar_tokens(self, texto: str) -> int:
        """Estimación rápida de tokens (aprox. 4 caracteres por token)."""
        return len(texto) // 4

    def _calcular_tokens_trabajo(self) -> int:
        """Calcula el total de tokens en la memoria de trabajo."""
        total = 0
        for mensaje in self.memoria_trabajo:
            total += self._estimar_tokens(
                json.dumps(mensaje, ensure_ascii=False)
            )
        return total

    def _gestionar_desbordamiento(self) -> None:
        """
        Comprime mensajes antiguos a memoria episódica
        cuando se excede el límite de tokens.
        """
        while (self._calcular_tokens_trabajo() > self.limite_tokens_trabajo
               and len(self.memoria_trabajo) > 2):
            mensaje_antiguo = self.memoria_trabajo.pop(1)
            self._comprimir_a_episodica(mensaje_antiguo)

    def _comprimir_a_episodica(self, mensaje: dict) -> None:
        """Comprime un mensaje y lo almacena en memoria episódica."""
        resumen = Recuerdo(
            contenido=f"[{mensaje.get('role', 'unknown')}]: "
                      f"{mensaje.get('content', '')[:200]}...",
            tipo="episodico",
            relevancia=0.7
        )
        self.memoria_episodica.append(resumen)

    def almacenar_semantico(
        self, contenido: str, tema: str,
        entidad: Optional[str] = None,
        etiquetas: list[str] = None
    ) -> None:
        """Almacena conocimiento en la memoria semántica."""
        recuerdo = Recuerdo(
            contenido=contenido,
            tipo="semantico",
            entidad=entidad,
            etiquetas=etiquetas or []
        )
        if tema not in self.memoria_semantica:
            self.memoria_semantica[tema] = []
        self.memoria_semantica[tema].append(recuerdo)

    def recuperar_contexto_relevante(
        self, consulta: str, max_resultados: int = 5
    ) -> list[str]:
        """
        Recupera contexto relevante de todas las memorias
        para enriquecer la ventana de contexto actual.
        """
        contexto = []

        # Buscar en memoria episódica reciente
        episodicos_recientes = sorted(
            self.memoria_episodica,
            key=lambda r: r.marca_temporal,
            reverse=True
        )[:max_resultados]
        for recuerdo in episodicos_recientes:
            contexto.append(f"[Episódico] {recuerdo.contenido}")

        # Buscar en memoria semántica por coincidencia
        palabras_consulta = set(consulta.lower().split())
        for tema, recuerdos in self.memoria_semantica.items():
            if any(p in tema.lower() for p in palabras_consulta):
                for recuerdo in recuerdos[:2]:
                    contexto.append(
                        f"[Semántico/{tema}] {recuerdo.contenido}"
                    )

        return contexto[:max_resultados]

    def construir_ventana_contexto(
        self, consulta_actual: str
    ) -> list[dict]:
        """
        Construye la ventana de contexto completa,
        combinando todos los tipos de memoria.
        """
        contexto_adicional = self.recuperar_contexto_relevante(
            consulta_actual
        )
        ventana = list(self.memoria_trabajo)

        if contexto_adicional:
            mensaje_contexto = {
                "role": "system",
                "content": (
                    "<contexto_recuperado>\n"
                    + "\n".join(contexto_adicional)
                    + "\n</contexto_recuperado>"
                )
            }
            ventana.insert(1, mensaje_contexto)

        return ventana


# --- Ejemplo de uso ---
gestor = GestorDeMemoria(limite_tokens_trabajo=4000)

gestor.agregar_a_trabajo({
    "role": "system",
    "content": "Eres un asistente de soporte técnico para CloudX."
})

gestor.almacenar_semantico(
    contenido="El plan Enterprise incluye 99.9% SLA y soporte 24/7.",
    tema="planes",
    etiquetas=["enterprise", "sla", "soporte"]
)

gestor.agregar_a_trabajo({
    "role": "user",
    "content": "¿Qué planes tienen disponibles?"
})

ventana = gestor.construir_ventana_contexto("planes disponibles")
print(f"Ventana construida con {len(ventana)} mensajes")

Gestión de la Ventana de Contexto en Producción

Uno de los desafíos más críticos de la ingeniería de contexto en producción es gestionar la ventana de contexto a lo largo de conversaciones extensas y flujos de trabajo complejos. Aunque los modelos de 2026 ofrecen ventanas de hasta 200.000 tokens, llenarlas indiscriminadamente degrada el rendimiento, aumenta los costos y dispara la latencia. Más no siempre es mejor — y esto aplica perfectamente aquí.

Trimming vs. Summarization

Existen dos estrategias fundamentales para gestionar el crecimiento del contexto:

  • Trimming (recorte): Eliminar los mensajes más antiguos del historial cuando se supera un umbral. Es rápido y simple, pero puede perder información crítica mencionada al inicio de la conversación.
  • Summarization (resumen): Comprimir bloques de mensajes antiguos en resúmenes concisos. Preserva la información esencial pero introduce latencia adicional, ya que requiere una llamada extra al modelo para generar el resumen.

En la práctica, lo ideal suele ser combinar ambas.

Compactación de contexto

La compactación es justamente esa combinación. En lugar de eliminar o resumir mensajes individuales, se compacta periódicamente toda la conversación manteniendo un documento de estado que captura las decisiones clave, preferencias del usuario y hechos relevantes descubiertos. Funciona sorprendentemente bien.

Notas estructuradas fuera de la ventana

Una técnica avanzada (y que personalmente encuentro muy elegante) consiste en mantener un archivo de notas externo — un scratchpad persistente — que el agente actualiza a lo largo de la sesión. Este archivo se lee al inicio de cada turno para restaurar el estado mental del agente sin necesidad de cargar todo el historial.

Sub-agentes con ventanas limpias

Para tareas complejas que involucran múltiples dominios, una arquitectura de sub-agentes permite delegar subtareas a agentes especializados con ventanas de contexto limpias. Cada sub-agente recibe solo la información necesaria para su tarea específica y devuelve un resumen compacto de entre 1.000 y 2.000 tokens al agente principal.

"""
Gestor de ventana de contexto con estrategias de recorte
y resumen adaptativo para producción.
"""
from typing import Callable, Optional


class GestorVentanaContexto:
    """
    Implementa estrategias de trimming con fallback a
    summarización para gestionar la ventana de contexto.
    """

    def __init__(
        self,
        limite_tokens: int = 16000,
        umbral_resumen: float = 0.75,
        funcion_resumen: Optional[Callable] = None
    ):
        self.limite_tokens = limite_tokens
        self.umbral_resumen = umbral_resumen
        self.mensajes: list[dict] = []
        self.resumen_acumulado: Optional[str] = None
        self.funcion_resumen = (
            funcion_resumen or self._resumen_por_defecto
        )
        self.notas_externas: dict = {
            "decisiones_clave": [],
            "preferencias_usuario": {},
            "hechos_descubiertos": [],
            "tareas_pendientes": []
        }

    def _estimar_tokens(self, texto: str) -> int:
        """Estimación rápida de tokens."""
        return len(texto) // 4

    def _tokens_totales(self) -> int:
        """Calcula los tokens totales en la ventana actual."""
        total = 0
        for msg in self.mensajes:
            total += self._estimar_tokens(
                str(msg.get("content", ""))
            )
        if self.resumen_acumulado:
            total += self._estimar_tokens(self.resumen_acumulado)
        return total

    def _resumen_por_defecto(self, mensajes: list[dict]) -> str:
        """
        Genera un resumen simple de los mensajes.
        En producción, esto llamaría a un LLM.
        """
        puntos = []
        for msg in mensajes:
            rol = msg.get("role", "desconocido")
            contenido = str(msg.get("content", ""))[:100]
            puntos.append(f"- [{rol}] {contenido}")
        return (
            "RESUMEN DE CONVERSACIÓN ANTERIOR:\n"
            + "\n".join(puntos)
        )

    def agregar_mensaje(self, mensaje: dict) -> dict:
        """
        Agrega un mensaje y aplica gestión de contexto
        si es necesario. Retorna un informe del estado.
        """
        self.mensajes.append(mensaje)
        tokens_actuales = self._tokens_totales()
        umbral = int(self.limite_tokens * self.umbral_resumen)

        informe = {
            "tokens_actuales": tokens_actuales,
            "limite": self.limite_tokens,
            "accion_tomada": "ninguna"
        }

        if tokens_actuales > umbral:
            informe["accion_tomada"] = self._aplicar_estrategia()

        return informe

    def _aplicar_estrategia(self) -> str:
        """
        Aplica la estrategia de gestión según la presión
        de tokens. Primero trimming; si no basta, resume.
        """
        tokens_actuales = self._tokens_totales()

        # Estrategia 1: Trimming de resultados de herramientas largos
        if tokens_actuales < self.limite_tokens:
            recortables = self._identificar_recortables()
            if recortables:
                for idx in recortables[:3]:
                    if idx < len(self.mensajes):
                        self.mensajes.pop(idx)
                return "trimming_simple"

        # Estrategia 2: Resumen de la primera mitad
        punto_medio = len(self.mensajes) // 2
        if punto_medio > 1:
            mensajes_a_resumir = self.mensajes[1:punto_medio]
            nuevo_resumen = self.funcion_resumen(mensajes_a_resumir)

            if self.resumen_acumulado:
                self.resumen_acumulado += "\n\n" + nuevo_resumen
            else:
                self.resumen_acumulado = nuevo_resumen

            self.mensajes = (
                [self.mensajes[0]]
                + [{"role": "system",
                    "content": self.resumen_acumulado}]
                + self.mensajes[punto_medio:]
            )
            return "resumen_aplicado"

        return "sin_accion_posible"

    def _identificar_recortables(self) -> list[int]:
        """Identifica mensajes que pueden recortarse."""
        recortables = []
        for i, msg in enumerate(self.mensajes):
            if i == 0:
                continue
            contenido = str(msg.get("content", ""))
            if (msg.get("role") == "tool" and
                    self._estimar_tokens(contenido) > 500):
                recortables.append(i)
        return recortables

    def registrar_nota(self, categoria: str, nota: str) -> None:
        """Registra una nota en almacenamiento externo."""
        if categoria in self.notas_externas:
            if isinstance(self.notas_externas[categoria], list):
                self.notas_externas[categoria].append(nota)
            elif isinstance(self.notas_externas[categoria], dict):
                if ":" in nota:
                    clave, valor = nota.split(":", 1)
                    self.notas_externas[categoria][
                        clave.strip()
                    ] = valor.strip()


# --- Ejemplo de uso ---
gestor = GestorVentanaContexto(limite_tokens=8000)

gestor.agregar_mensaje({
    "role": "system",
    "content": "Eres un asistente de programación en Python."
})

for i in range(20):
    gestor.agregar_mensaje({
        "role": "user",
        "content": f"Pregunta {i} sobre optimización de código..."
    })
    informe = gestor.agregar_mensaje({
        "role": "assistant",
        "content": f"Respuesta detallada {i} con ejemplos..."
    })
    if informe["accion_tomada"] != "ninguna":
        print(
            f"Turno {i}: Acción -> {informe['accion_tomada']}"
        )

RAG 2.0 como Componente de Ingeniería de Contexto

La Generación Aumentada por Recuperación (RAG) ha cambiado bastante desde sus primeras implementaciones. En 2026, RAG ya no es simplemente una búsqueda vectorial seguida de una inyección de texto. Es un componente sofisticado dentro del ecosistema de ingeniería de contexto, con múltiples estrategias de recuperación y filtrado que trabajan en conjunto.

De la búsqueda vectorial simple a la recuperación multi-hop

El RAG tradicional realiza una sola búsqueda vectorial contra una consulta del usuario y devuelve los k fragmentos más similares. Este enfoque tiene limitaciones evidentes cuando la respuesta requiere sintetizar información de múltiples documentos o cuando la consulta del usuario no se alinea semánticamente con los fragmentos relevantes.

El RAG multi-hop aborda esto ejecutando múltiples rondas de recuperación, donde cada ronda refina la consulta basándose en los resultados anteriores. Esto permite navegar cadenas de razonamiento complejas y descubrir información que simplemente no aparecería con una sola búsqueda.

Filtrado semántico y reranking

No todos los fragmentos recuperados merecen ocupar espacio en la ventana de contexto. El filtrado semántico aplica un segundo nivel de evaluación — ya sea mediante un modelo de reranking tipo cross-encoder o reglas basadas en metadatos — para quedarse solo con los fragmentos verdaderamente relevantes. Este paso marca una diferencia enorme en la calidad final.

Recuperación híbrida

Los sistemas de producción en 2026 combinan múltiples estrategias de recuperación: búsqueda vectorial densa para similitud semántica, búsqueda léxica dispersa (BM25) para coincidencias exactas de términos, y búsqueda estructurada en grafos de conocimiento para relaciones entre entidades.

"""
Pipeline de recuperación RAG 2.0 con búsqueda multi-hop,
filtrado semántico y recuperación híbrida.
"""
from dataclasses import dataclass
from typing import Optional
from enum import Enum


class TipoBusqueda(Enum):
    VECTORIAL = "vectorial"
    LEXICA = "lexica"
    GRAFO = "grafo"


@dataclass
class Fragmento:
    """Representa un fragmento de documento recuperado."""
    contenido: str
    fuente: str
    puntuacion: float
    metadatos: dict
    tipo_busqueda: TipoBusqueda


@dataclass
class ResultadoRAG:
    """Resultado final del pipeline de recuperación."""
    fragmentos: list[Fragmento]
    consulta_original: str
    consultas_expandidas: list[str]
    total_candidatos: int
    total_seleccionados: int


class PipelineRAG:
    """
    Pipeline RAG 2.0 que combina múltiples
    estrategias de búsqueda y filtrado.
    """

    def __init__(
        self,
        almacen_vectorial,
        indice_lexico,
        grafo_conocimiento=None,
        modelo_reranking=None,
        umbral_relevancia: float = 0.7,
        max_fragmentos: int = 10,
        max_hops: int = 3
    ):
        self.almacen_vectorial = almacen_vectorial
        self.indice_lexico = indice_lexico
        self.grafo_conocimiento = grafo_conocimiento
        self.modelo_reranking = modelo_reranking
        self.umbral_relevancia = umbral_relevancia
        self.max_fragmentos = max_fragmentos
        self.max_hops = max_hops

    def recuperar(
        self, consulta: str,
        contexto_previo: Optional[str] = None
    ) -> ResultadoRAG:
        """Ejecuta el pipeline completo de recuperación."""
        # Paso 1: Expandir la consulta
        consultas = self._expandir_consulta(consulta, contexto_previo)

        # Paso 2: Recuperación híbrida
        candidatos = []
        for q in consultas:
            candidatos.extend(self._busqueda_hibrida(q))

        # Paso 3: Deduplicación
        candidatos_unicos = self._deduplicar(candidatos)

        # Paso 4: Reranking y filtrado semántico
        filtrados = self._filtrar_y_reordenar(
            consulta, candidatos_unicos
        )

        # Paso 5: Recuperación multi-hop
        finales = self._multi_hop(consulta, filtrados)

        return ResultadoRAG(
            fragmentos=finales[:self.max_fragmentos],
            consulta_original=consulta,
            consultas_expandidas=consultas,
            total_candidatos=len(candidatos),
            total_seleccionados=len(finales)
        )

    def _expandir_consulta(
        self, consulta: str, contexto: Optional[str]
    ) -> list[str]:
        """Genera variaciones de la consulta."""
        consultas = [consulta]
        if contexto:
            consultas.append(f"{contexto} {consulta}")
        palabras_clave = [
            p for p in consulta.split() if len(p) > 4
        ]
        if palabras_clave:
            consultas.append(" ".join(palabras_clave))
        return consultas

    def _busqueda_hibrida(self, consulta: str) -> list[Fragmento]:
        """Ejecuta búsqueda en múltiples índices."""
        resultados = []

        # Búsqueda vectorial densa
        for doc, punt in self.almacen_vectorial.buscar(consulta, k=20):
            resultados.append(Fragmento(
                contenido=doc.contenido, fuente=doc.fuente,
                puntuacion=punt, metadatos=doc.metadatos,
                tipo_busqueda=TipoBusqueda.VECTORIAL
            ))

        # Búsqueda léxica BM25
        for doc, punt in self.indice_lexico.buscar(consulta, k=10):
            resultados.append(Fragmento(
                contenido=doc.contenido, fuente=doc.fuente,
                puntuacion=punt, metadatos=doc.metadatos,
                tipo_busqueda=TipoBusqueda.LEXICA
            ))

        # Búsqueda en grafo de conocimiento
        if self.grafo_conocimiento:
            entidades = self.grafo_conocimiento.extraer_entidades(consulta)
            for entidad in entidades:
                for rel in self.grafo_conocimiento.obtener_relaciones(entidad):
                    resultados.append(Fragmento(
                        contenido=rel.descripcion,
                        fuente=f"grafo:{entidad.nombre}",
                        puntuacion=rel.peso,
                        metadatos={"entidad": entidad.nombre},
                        tipo_busqueda=TipoBusqueda.GRAFO
                    ))

        return resultados

    def _deduplicar(self, fragmentos: list[Fragmento]) -> list[Fragmento]:
        """Elimina fragmentos duplicados."""
        vistos = set()
        unicos = []
        for frag in fragmentos:
            huella = frag.contenido[:200]
            if huella not in vistos:
                vistos.add(huella)
                unicos.append(frag)
        return unicos

    def _filtrar_y_reordenar(
        self, consulta: str, fragmentos: list[Fragmento]
    ) -> list[Fragmento]:
        """Aplica reranking con cross-encoder y filtra."""
        if self.modelo_reranking:
            pares = [(consulta, f.contenido) for f in fragmentos]
            puntuaciones = self.modelo_reranking.puntuar(pares)
            for frag, punt in zip(fragmentos, puntuaciones):
                frag.puntuacion = punt

        filtrados = [
            f for f in fragmentos
            if f.puntuacion >= self.umbral_relevancia
        ]
        filtrados.sort(key=lambda f: f.puntuacion, reverse=True)
        return filtrados

    def formatear_para_contexto(self, resultado: ResultadoRAG) -> str:
        """Formatea los resultados para inyección en contexto."""
        secciones = []
        for i, frag in enumerate(resultado.fragmentos, 1):
            secciones.append(
                f'<documento id="{i}" fuente="{frag.fuente}" '
                f'relevancia="{frag.puntuacion:.2f}">\n'
                f'{frag.contenido}\n</documento>'
            )
        return (
            "<documentos_recuperados>\n"
            + "\n\n".join(secciones)
            + "\n</documentos_recuperados>"
        )

Prompt Caching y Optimización de Costos

En producción, el costo de las llamadas a modelos de lenguaje puede escalar rápidamente. Y cuando digo rápidamente, me refiero a que te puede sorprender la factura a fin de mes si no tienes cuidado. La ingeniería de contexto incluye estrategias específicas para reducir costos sin sacrificar calidad, y el prompt caching es una de las más impactantes.

Cómo funciona el prompt caching

El prompt caching permite reutilizar la computación de la parte estática del contexto entre llamadas sucesivas. Cuando múltiples solicitudes comparten el mismo prefijo — system prompt, definiciones de herramientas, ejemplos few-shot — el modelo puede cachear el procesamiento de ese prefijo y solo computar la parte nueva de cada solicitud.

Anthropic introdujo esta funcionalidad en su API, permitiendo reducciones de costo de hasta un 90% en los tokens cacheados y mejoras significativas en latencia (reducción del time-to-first-token de hasta un 85%). Son números que vale la pena tomarse en serio.

Estructurar prompts para aprovechar el caché

Para maximizar la eficiencia del caché, hay que organizar el contexto estratégicamente:

  1. Contenido estático al principio: System prompt, definiciones de herramientas y ejemplos fijos deben estar al inicio del mensaje, ya que el caché funciona con prefijos.
  2. Contenido semi-dinámico después: Documentos de RAG y contexto de sesión, que cambian con menor frecuencia que el mensaje del usuario.
  3. Contenido dinámico al final: El mensaje actual del usuario y los últimos turnos de conversación van al final.
"""
Ejemplo de uso de prompt caching con la API de Anthropic.
Estructura optimizada: estático → semi-dinámico → dinámico.
"""
import anthropic


def crear_cliente_con_cache():
    """Configura un cliente optimizado para prompt caching."""
    cliente = anthropic.Anthropic()

    # Contenido estático que se cacheará entre solicitudes
    system_prompt_estatico = """
    <rol>
    Eres un asistente experto en análisis de datos para
    la plataforma DataInsights Pro.
    </rol>

    <herramientas_disponibles>
    - ejecutar_sql: Ejecuta consultas SQL
    - crear_grafico: Genera visualizaciones
    - exportar_csv: Exporta resultados a CSV
    - calcular_estadisticas: Estadísticas descriptivas
    </herramientas_disponibles>

    <ejemplos_canonicos>
    <ejemplo tipo="consulta_simple">
    Usuario: ¿Cuántos usuarios activos tenemos este mes?
    Asistente: Consulto la base de datos para obtener el
    conteo de usuarios activos del mes actual.
    [Ejecuta: SELECT COUNT(DISTINCT user_id) FROM events
     WHERE fecha >= DATE_TRUNC('month', CURRENT_DATE)
     AND tipo_evento = 'login']
    Resultado: 15,432 usuarios activos únicos este mes.
    </ejemplo>
    </ejemplos_canonicos>

    <directrices>
    - Explica tu razonamiento antes de ejecutar consultas
    - Valida que los datos tengan sentido
    - Sugiere análisis complementarios cuando sea relevante
    </directrices>
    """

    return cliente, system_prompt_estatico


def consulta_con_cache(
    cliente: anthropic.Anthropic,
    system_prompt: str,
    documentos_contexto: str,
    historial: list[dict],
    mensaje_usuario: str
) -> str:
    """Realiza una consulta con prompt caching optimizado."""
    respuesta = cliente.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        system=[
            {
                # Bloque estático: se cachea entre solicitudes
                "type": "text",
                "text": system_prompt,
                "cache_control": {"type": "ephemeral"}
            },
            {
                # Documentos de sesión: se cachea por sesión
                "type": "text",
                "text": documentos_contexto,
                "cache_control": {"type": "ephemeral"}
            }
        ],
        messages=historial + [
            {"role": "user", "content": mensaje_usuario}
        ]
    )

    # Métricas de uso del caché para monitoreo
    uso = respuesta.usage
    tokens_cache = getattr(uso, "cache_read_input_tokens", 0)
    tokens_creacion = getattr(
        uso, "cache_creation_input_tokens", 0
    )
    total = tokens_cache + tokens_creacion + uso.input_tokens
    porcentaje = (
        (tokens_cache / total * 100) if total > 0 else 0
    )

    print(f"Tokens entrada: {total}")
    print(f"Desde caché: {tokens_cache} ({porcentaje:.1f}%)")

    return respuesta.content[0].text


# --- Ejemplo de uso ---
cliente, system_prompt = crear_cliente_con_cache()

documentos = """
<esquema_base_datos>
Tabla: usuarios (id, nombre, email, fecha_registro, plan)
Tabla: eventos (id, user_id, tipo_evento, fecha, metadatos)
Tabla: suscripciones (id, user_id, plan, fecha_inicio, fecha_fin)
</esquema_base_datos>
"""

# Primera consulta: crea el caché
r1 = consulta_con_cache(
    cliente, system_prompt, documentos,
    historial=[],
    mensaje_usuario="¿Cuántos usuarios se registraron esta semana?"
)

# Segunda consulta: reutiliza el caché (más rápida y barata)
r2 = consulta_con_cache(
    cliente, system_prompt, documentos,
    historial=[
        {"role": "user",
         "content": "¿Cuántos usuarios se registraron esta semana?"},
        {"role": "assistant", "content": r1}
    ],
    mensaje_usuario="¿Cuál es su tasa de conversión a plan de pago?"
)

Patrones de Diseño para Contexto Estructurado

La estructura del contexto tiene un impacto directo en la calidad de las respuestas del modelo. Un contexto bien organizado reduce ambigüedades, mejora la adherencia a instrucciones y facilita el seguimiento de conversaciones complejas. Así que vale la pena invertir tiempo en esto.

Etiquetas XML para secciones de contexto

Las etiquetas XML son el formato preferido por Anthropic para estructurar el contexto de Claude. Crean delimitadores claros e inequívocos que el modelo puede identificar fácilmente, incluso en contextos extensos.

# Plantilla de contexto estructurado con XML
PLANTILLA_CONTEXTO = """
<contexto_sistema>
  <identidad>
    Eres {nombre_agente}, especializado en {dominio}.
    Tu objetivo principal es {objetivo}.
  </identidad>

  <capacidades>
    {lista_capacidades}
  </capacidades>

  <restricciones>
    {lista_restricciones}
  </restricciones>
</contexto_sistema>

<informacion_usuario>
  <perfil>
    Nombre: {nombre_usuario}
    Rol: {rol_usuario}
    Nivel: {nivel_experiencia}
  </perfil>
  <sesion_actual>
    Inicio: {inicio_sesion}
    Objetivo: {objetivo_sesion}
  </sesion_actual>
</informacion_usuario>

<documentos_referencia>
  {documentos_recuperados}
</documentos_referencia>

<historial_resumido>
  {resumen_conversacion}
</historial_resumido>
"""


def construir_contexto(config: dict) -> str:
    """Construye contexto estructurado desde configuración."""
    capacidades = "\n    ".join(
        f"<capacidad>{c}</capacidad>"
        for c in config.get("capacidades", [])
    )
    restricciones = "\n    ".join(
        f"<restriccion>{r}</restriccion>"
        for r in config.get("restricciones", [])
    )
    documentos = "\n  ".join(
        f'<documento fuente="{d["fuente"]}">'
        f'\n    {d["contenido"]}\n  </documento>'
        for d in config.get("documentos", [])
    )

    return PLANTILLA_CONTEXTO.format(
        nombre_agente=config.get("nombre_agente", "Asistente"),
        dominio=config.get("dominio", "general"),
        objetivo=config.get("objetivo", "ayudar al usuario"),
        lista_capacidades=capacidades,
        lista_restricciones=restricciones,
        nombre_usuario=config.get("nombre_usuario", "Usuario"),
        rol_usuario=config.get("rol_usuario", "general"),
        nivel_experiencia=config.get("nivel", "intermedio"),
        inicio_sesion=config.get("inicio_sesion", "ahora"),
        objetivo_sesion=config.get("objetivo", "no especificado"),
        documentos_recuperados=documentos or "Sin documentos",
        resumen_conversacion=config.get("resumen", "Nueva sesión")
    )

Esquemas JSON para salidas estructuradas

Cuando la aplicación necesita consumir la salida del modelo programáticamente, los esquemas JSON dentro del contexto guían al modelo para generar respuestas en el formato exacto esperado. Incluir el esquema como parte del contexto es más confiable que simplemente describir el formato deseado en lenguaje natural — y si has trabajado con esto, sabes que la diferencia puede ser enorme.

# Esquema JSON para guiar la salida estructurada
ESQUEMA_ANALISIS = {
    "tipo": "objeto",
    "propiedades": {
        "resumen_ejecutivo": {
            "tipo": "cadena",
            "descripcion": "Resumen en 2-3 oraciones"
        },
        "metricas_clave": {
            "tipo": "lista",
            "elementos": {
                "nombre": {"tipo": "cadena"},
                "valor": {"tipo": "numero"},
                "tendencia": {
                    "valores": ["alza", "baja", "estable"]
                }
            }
        },
        "recomendaciones": {
            "tipo": "lista",
            "elementos": {
                "accion": {"tipo": "cadena"},
                "prioridad": {
                    "valores": ["alta", "media", "baja"]
                },
                "impacto_estimado": {"tipo": "cadena"}
            }
        },
        "nivel_confianza": {
            "tipo": "numero", "minimo": 0, "maximo": 1
        }
    }
}

# Instrucción para inyectar en el contexto
INSTRUCCION_FORMATO = f"""
<formato_salida>
Responde EXCLUSIVAMENTE con un objeto JSON válido
que cumpla con el siguiente esquema:

{ESQUEMA_ANALISIS}

Ejemplo de respuesta válida:
{{
  "resumen_ejecutivo": "Las ventas del Q3 muestran crecimiento...",
  "metricas_clave": [
    {{"nombre": "Ingresos", "valor": 1250000, "tendencia": "alza"}}
  ],
  "recomendaciones": [
    {{"accion": "Aumentar inversión en canal digital",
      "prioridad": "alta",
      "impacto_estimado": "15% incremento en conversiones"}}
  ],
  "nivel_confianza": 0.85
}}
</formato_salida>
"""

Mejores Prácticas y Anti-patrones

La ingeniería de contexto es una disciplina donde la experiencia práctica es invaluable. Después de miles de iteraciones en sistemas de producción, han emergido patrones claros de lo que funciona y lo que no. Aquí van los más importantes.

Mejores prácticas

1. Empieza simple, añade complejidad solo cuando sea necesario

La regla de oro: "Haz lo más simple que funcione." Es tentador diseñar sistemas elaborados con múltiples capas de memoria, recuperación multi-hop y compactación sofisticada desde el inicio. Pero cada capa de complejidad introduce puntos de falla, aumenta la latencia y dificulta la depuración.

Comienza con un system prompt directo y un historial de conversación simple. Mide el rendimiento. Identifica los fallos específicos. Solo entonces añade la capa de complejidad que aborda ese fallo particular.

2. Más contexto no siempre es mejor

Uno de los anti-patrones más comunes (y tentadores) es la suposición de que más información en la ventana de contexto produce mejores resultados. La investigación ha demostrado que los modelos sufren degradación de atención en contextos largos: la información en el medio tiende a recibir menos atención que la del inicio y final (el famoso efecto "Lost in the Middle").

En producción, es más efectivo inyectar 5 fragmentos altamente relevantes que 50 fragmentos vagamente relacionados. La precisión de la recuperación importa más que el recall.

3. Mide, itera y refina

La ingeniería de contexto requiere un enfoque experimental. Implementa métricas para evaluar la calidad de las respuestas, la relevancia de los fragmentos recuperados y la eficiencia del uso de tokens.

  • Registra las llamadas completas al modelo para depuración.
  • Mide tasas de éxito antes y después de cambios en el contexto.
  • Monitorea el uso de tokens y costos para detectar ineficiencias.
  • Realiza pruebas A/B con diferentes estrategias de contexto.

4. Separación clara entre instrucciones y datos

Mantén una separación explícita entre las instrucciones del sistema (qué hacer), los datos de referencia (con qué) y la entrada del usuario (sobre qué). Esta separación reduce la posibilidad de inyección de prompts y facilita el mantenimiento del sistema.

5. Versiona tu contexto

Trata tus system prompts y plantillas de contexto como código. Almacénalos en control de versiones, revisa los cambios y mantén un registro de qué versión se usó para cada evaluación. Esto permite reproducibilidad y rollback cuando algo sale mal — y algo siempre sale mal eventualmente.

Anti-patrones comunes

El contexto omnisciente: Intentar cargar toda la información posible en la ventana de contexto "por si acaso". Esto desperdicia tokens, aumenta costos y diluye la atención del modelo.

La instrucción contradictoria: System prompts que han crecido orgánicamente a lo largo de meses, con instrucciones añadidas ad-hoc que se contradicen entre sí. El resultado es un comportamiento impredecible del modelo.

El ejemplo desactualizado: Few-shot examples que ya no reflejan el formato o la lógica actual del sistema. Los ejemplos desactualizados son peores que no tener ejemplos, porque enseñan activamente el comportamiento incorrecto.

La recuperación sin filtrado: Inyectar fragmentos de RAG directamente en el contexto sin verificar su relevancia. Un fragmento con baja puntuación puede contaminar el razonamiento del modelo y generar respuestas incorrectas que parecen bien fundamentadas.

El historial infinito: Enviar el historial completo de la conversación sin ningún tipo de gestión. En conversaciones largas, esto mezcla temas resueltos con el problema actual, confundiendo al modelo.

"El mejor contexto no es el más grande — es el más preciso. Cada token en tu ventana de contexto debe ganarse su lugar demostrando que contribuye a la calidad de la respuesta."

Framework de evaluación iterativa

Para aplicar estas mejores prácticas de forma sistemática, recomiendo seguir un ciclo de refinamiento:

  1. Línea base: Implementa la versión más simple posible y mide su rendimiento en tu conjunto de evaluación.
  2. Diagnóstico: Analiza los fallos específicos. ¿El modelo no tiene suficiente información? ¿Tiene demasiada? ¿Las instrucciones son ambiguas?
  3. Hipótesis: Identifica qué cambio en el contexto podría resolver los fallos observados.
  4. Implementación: Realiza el cambio mínimo necesario.
  5. Evaluación: Mide de nuevo. ¿Mejoró la métrica objetivo? ¿Introdujo regresiones?
  6. Documentación: Registra qué cambio se hizo, por qué y qué impacto tuvo.

Conclusión: El Futuro de la Ingeniería de Contexto

La ingeniería de contexto se ha consolidado en 2026 como la disciplina central del desarrollo de aplicaciones de IA. Ya no es una habilidad complementaria — es la responsabilidad principal de cualquier ingeniero que trabaje con modelos de lenguaje en producción.

Las tendencias que están definiendo el futuro de esta disciplina son bastante claras:

  • Ventanas de contexto crecientes pero no infinitas: Aunque los modelos continúan expandiendo sus ventanas de contexto, las limitaciones de atención, costo y latencia hacen que la gestión inteligente siga siendo esencial. Más capacidad no elimina la necesidad de ingeniería — la amplifica.
  • Agentes cada vez más autónomos: A medida que los sistemas agénticos ejecutan flujos de trabajo más largos y complejos — desplegando código, investigando documentación, coordinándose con otros agentes — la arquitectura de memoria y la gestión de contexto se vuelven infraestructura crítica.
  • Herramientas especializadas emergentes: Frameworks como LangChain, LlamaIndex, y las herramientas nativas de los proveedores de modelos están integrando primitivas de ingeniería de contexto como funcionalidades de primera clase. La compactación automática, la selección dinámica de herramientas y la gestión de memoria pasan de ser implementaciones ad-hoc a componentes estandarizados.
  • Evaluaciones como pilar fundamental: La comunidad ha reconocido que la ingeniería de contexto sin evaluaciones rigurosas es ingeniería a ciegas. Los frameworks de evaluación automatizada están emergiendo como herramientas esenciales.
  • Convergencia con la ingeniería de datos: La ingeniería de contexto está convergiendo con la ingeniería de datos tradicional. Los pipelines que preparan, transforman, filtran y entregan datos a la ventana de contexto requieren las mismas disciplinas de calidad, observabilidad y gobernanza que cualquier pipeline de datos empresarial.

Para los profesionales que buscan dominar esta disciplina, el camino es claro: comprender profundamente cómo los modelos procesan y priorizan información dentro de la ventana de contexto, dominar las técnicas de recuperación y filtrado, implementar arquitecturas de memoria robustas y, sobre todo, adoptar un enfoque experimental e iterativo.

La diferencia entre una aplicación de IA que funciona en demos y una que funciona en producción rara vez está en el modelo subyacente — está en la ingeniería de contexto que alimenta ese modelo. Los ingenieros que dominen esta disciplina no solo construirán mejores productos de IA; definirán los estándares de calidad de una industria que apenas empieza a descubrir el verdadero potencial de los modelos de lenguaje cuando se les proporciona el contexto adecuado.

"En el futuro, no recordaremos a los ingenieros de IA por los modelos que entrenaron, sino por los contextos que diseñaron. La ingeniería de contexto es donde la ciencia de los LLMs se encuentra con el arte de construir productos que realmente funcionan."

Sobre el Autor Editorial Team

Our team of expert writers and editors.