agents101 · Ingeniería de Agentes

Prólogo: Del LLM al Agente — Una ruta completa de ingeniería

Los Modelos de Lenguaje de Gran Escala (Large Language Model, LLM) han transformado la forma en que construimos software. Pero limitarse a llamar a una API y obtener respuestas de texto es solo el primer paso. El verdadero desafío de ingeniería consiste en hacer que el modelo evolucione de “saber conversar” a “saber hacer”: que pueda invocar herramientas externas, planificar tareas de forma autónoma, colaborar en equipo, recordar las preferencias del usuario y, en última instancia, funcionar de manera estable en entornos de producción.

Esta ruta abarca ocho áreas fundamentales:

  1. Fundamentos de LLM: comprender Tokenization, la arquitectura Transformer, la ventana de contexto y la ingeniería de prompts
  2. Principios y práctica de RAG: dominar la generación aumentada por recuperación para que el modelo acceda a conocimiento privado
  3. Uso de herramientas por el Agente: Function Calling, ciclo ReAct, protocolo MCP
  4. Planificación y ejecución del Agente: mecanismos de reflexión, Plan & Execute, orquestación de flujos de trabajo
  5. Colaboración multi-agente: colaboración jerárquica, patrón de pizarra, metodología de colaboración
  6. Memory y Skill: gestión de memoria a corto/largo plazo, diseño de sistemas de Skill
  7. Evaluación de Agentes: evaluación end-to-end, evaluación de caja blanca, iteración guiada por evaluación
  8. Puesta en producción: despliegue de modelos, optimización de inferencia, barreras de seguridad, Harness Engineering

Esta guía está dirigida a desarrolladores que desean dominar sistemáticamente la ingeniería de agentes, ofreciendo una referencia técnica integral desde los principios fundamentales hasta la práctica en producción.


Principios de Tokenization

Por qué necesitamos Tokenization

Las computadoras no pueden entender directamente el lenguaje humano. El primer paso que da un modelo grande al procesar texto es convertir el lenguaje natural en un formato numérico que la máquina pueda procesar. Este proceso se denomina Tokenization (tokenización).

Un token es la unidad básica que se obtiene después de que el tokenizador (Tokenizer) codifica el texto; cada token corresponde a un ID entero en el vocabulario. La clave conceptual: un token suele ser un fragmento de subpalabra o un fragmento de carácter, no necesariamente equivale a una “palabra” completa ni siempre posee significado semántico independiente.

Texto de entrada: "ACP is a very"
          ↓ Tokenizer
Secuencia de tokens: [347, 1186, 374, 1134]

Tomando el inglés como ejemplo, las palabras pueden dividirse en raíz y sufijos; el chino puede segmentarse por carácter o por grupos de palabras comunes; los espacios y signos de puntuación también pueden codificarse como tokens. Los Tokenizers varían enormemente entre modelos: la serie GPT utiliza BPE (Byte Pair Encoding), LLaMA emplea SentencePiece BPE y ciertos modelos chinos realizan optimizaciones especiales para corpus en chino.

Vectorización de tokens y codificación posicional

El valor numérico del ID entero en sí no tiene significado semántico. Que el ID sea 500 no implica que sea “más importante” que el ID 50. Por lo tanto, estos ID discretos deben mapearse a vectores densos mediante una matriz de Embedding.

Token ID → Consulta en la matriz de Embedding → vector d-dimensional
  1186   →   [0.023, -0.451, 0.789, ..., -0.312]  (d=4096 o mayor)

Al mismo tiempo, el orden del lenguaje es crucial: “yo te ayudo” y “tú me ayudas” son completamente distintos. Dado que el mecanismo de Self-Attention del Transformer no posee capacidad intrínseca de percibir secuencialidad, es necesario añadir codificación posicional (Positional Encoding). Los modelos modernos suelen usar codificación posicional rotatoria (RoPE, Rotary Position Embedding), que introduce información posicional relativa en el cálculo de atención, permitiendo al modelo procesar secuencias largas de forma natural.

Estrategias de decodificación: de probabilidades a salida

Después de la inferencia, el modelo produce logits (vectores de puntuación) que se transforman mediante softmax en una distribución de probabilidad P(siguiente_token | contexto). Luego se selecciona el token de salida mediante una estrategia de decodificación:

EstrategiaPrincipioCaso de uso
Decodificación voraz (Greedy)En cada paso elige el token con mayor probabilidadTareas que requieren salida determinista (generación de código, extracción estructurada)
Beam SearchMantiene múltiples rutas candidatas y elige la secuencia con mayor probabilidad globalTraducción, resumen y otras tareas que requieren óptimo global
Top-k SamplingMuestrea aleatoriamente entre los k tokens de mayor probabilidadEscritura creativa, generación de diálogos
Top-p (Nucleus)Muestrea del conjunto mínimo de tokens cuya probabilidad acumulada supera pDiálogo general, equilibrando diversidad y calidad

Dos parámetros fundamentales controlan la aleatoriedad de la salida:

Temperature (temperatura): controla el grado de “agudeza” de la distribución de probabilidad de softmax.

# Comparación del efecto de temperature
# logits originales → softmax(logits / temperature)
# T=0.1: probabilidad altamente concentrada, salida casi determinista
# T=0.7: moderada, mantiene diversidad razonable
# T=1.5: probabilidad tiende a ser uniforme, salida altamente aleatoria

Top_p (umbral de muestreo por núcleo): controla el rango de tokens candidatos que participan en el muestreo. Por ejemplo, top_p=0.9 significa que solo se muestrea del conjunto mínimo de tokens cuya probabilidad acumulada alcanza el 90%.

# Ejemplo de configuración típica
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Escribe un poema sobre inteligencia artificial"}],
    temperature=0.8,   # Creatividad moderada
    top_p=0.9,         # Muestreo por núcleo
    max_tokens=200
)

Generación autorregresiva y condiciones de parada

El modelo emplea generación autorregresiva (Autoregressive Generation): cada vez genera un nuevo token, lo añade al final de la entrada y luego continúa prediciendo el siguiente token basándose en la nueva secuencia.

"ACP is a very" → predice "informative"
"ACP is a very informative" → predice "course"
"ACP is a very informative course" → ... → predice <EOS> → se detiene

Las condiciones de parada incluyen:

  • Generar un token de fin especial (EOS token)
  • Alcanzar el límite preestablecido de max_tokens
  • Generar una secuencia de parada especificada por el usuario

Los tokens de fin varían entre familias de modelos:

ModeloToken de fin
Serie GPT<|endoftext|>
LLaMA/Mistral</s>
DeepSeek<|end▁of▁sentence|>
Algunos modelos chinos grandes<|im_end|>

La salida en streaming (Streaming) consiste esencialmente en que el servidor decodifica y envía incrementalmente cada token (o cada pocos tokens) tan pronto como se generan, en lugar de esperar a que todos los tokens estén listos para devolver la respuesta.


Arquitectura Transformer y mecanismo de Attention

Panorama de la arquitectura

El núcleo de los modelos de lenguaje grandes modernos es la arquitectura Transformer. Aunque el Transformer completo incluye codificador (Encoder) y decodificador (Decoder), los LLM autorregresivos actuales (GPT, LLaMA, etc.) utilizan únicamente la arquitectura de decodificador (Decoder-only).

Arquitectura decodificadora Transformer

Atención causal (Causal Self-Attention)

La fórmula central del mecanismo de Attention:

Attention(Q, K, V) = softmax(QK^T / √d_k) · V

Donde Q (Query), K (Key) y V (Value) se obtienen a partir de la misma secuencia de entrada mediante transformaciones lineales. Diseños clave:

(1) Factor de escala √d_k: evita que el producto punto sea excesivamente grande y provoque la desaparición del gradiente en softmax. Cuando la dimensión del vector d_k es grande, la varianza del producto punto también aumenta; dividir por √d_k normaliza la varianza.

(2) Máscara causal (Causal Mask): en los modelos autorregresivos, cada token solo puede “ver” los tokens anteriores, no puede “espiar” el contenido futuro. Esto se implementa llenando las posiciones de la matriz triangular superior con -∞:

Entrada: ["ACP", "is", "a", "very"]
Matriz de Attention (tras máscara causal):
        ACP   is    a   very
ACP   0.8    -∞   -∞    -∞
is    0.3   0.7   -∞    -∞
a     0.2   0.3  0.5    -∞
very  0.1   0.2  0.3   0.4

(3) Atención multi-cabeza (Multi-Head Attention): no se realiza un solo cálculo de atención, sino que se ejecutan en paralelo múltiples conjuntos de proyecciones Q/K/V, donde cada conjunto se enfoca en diferentes relaciones semánticas (estructura gramatical, relaciones de referencia, similitud semántica, etc.) y finalmente se concatenan todas las salidas.

# Pseudocódigo de atención multi-cabeza
def multi_head_attention(x, num_heads=8, d_model=512):
    d_head = d_model // num_heads  # dimensión por cabeza
    outputs = []
    for h in range(num_heads):
        Q = linear_projection(x, d_head)
        K = linear_projection(x, d_head)
        V = linear_projection(x, d_head)
        attn_out = softmax(Q @ K.T / sqrt(d_head)) @ V
        outputs.append(attn_out)
    return concat(outputs)  # concatenar todas las cabezas

Red feed-forward (Feed-Forward Network)

Después de cada capa de Attention sigue inmediatamente una FFN:

FFN(x) = GELU(x·W₁ + b₁) · W₂ + b₂

La FFN típicamente expande primero la dimensión oculta (por ejemplo 4x) y luego la comprime de vuelta a la dimensión original. Esta estructura de “expansión-compresión” proporciona al modelo capacidad de transformación no lineal y es un componente clave para que el modelo almacene y aplique conocimiento.

Conexiones residuales y normalización de capa

Cada subcapa (Attention y FFN) se suma a su entrada mediante una conexión residual:

output = LayerNorm(x + Sublayer(x))

Las conexiones residuales permiten que el gradiente se propague directamente a las capas superficiales, resolviendo la dificultad de entrenamiento de redes profundas. La normalización de capa (LayerNorm) en arquitecturas modernas suele adoptar el diseño Pre-Norm (normalización antes de la subcapa), que es más estable que el Post-Norm original.


Ventana de contexto y presupuesto de tokens

La esencia de la ventana de contexto

El lugar donde el modelo grande recibe la entrada se denomina ventana de contexto (Context Window). Puede entenderse como la memoria RAM de una computadora: tiene capacidad limitada y afecta directamente el rendimiento.

Composición de la ventana de contexto

Las ventanas de contexto de los modelos modernos se han expandido enormemente:

  • GPT-4 Turbo: 128K tokens
  • Claude 3: 200K tokens
  • Gemini 1.5 Pro: 1M+ tokens
  • Modelos open-source (LLaMA 3, algunos modelos chinos, etc.): 32K–128K tokens

Pero una ventana grande no significa que se pueda abusar de ella. Los estudios demuestran la existencia del efecto “Lost in the Middle”: la capacidad del modelo para procesar información en la parte central del contexto disminuye significativamente, prestando más atención al inicio (efecto de primacía) y al final (efecto de recencia).

Gestión del presupuesto de tokens

En entornos de producción, necesitas gestionar el contexto como gestionas la memoria. Estas son las estrategias principales:

(1) Cálculo preciso del consumo de tokens

import tiktoken

def count_tokens(text: str, model: str = "gpt-4") -> int:
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

# Ejemplo: calcular el total de tokens de los mensajes
def count_message_tokens(messages):
    encoding = tiktoken.encoding_for_model("gpt-4")
    total = 0
    for msg in messages:
        # Cada mensaje tiene un overhead fijo (~4 tokens)
        total += 4
        total += len(encoding.encode(msg["content"]))
    total += 2  # priming de la respuesta
    return total

(2) Estrategia de asignación de la ventana de contexto

┌──────────────────────────────────────────────┐
│ Asignación recomendada del presupuesto de tokens  │
│ (ejemplo para ventana de 128K)                  │
├──────────────────────────────────────────────┤
│ System Prompt:      2-5K  (definición de rol, reglas) │
│ Resultados RAG:     3-8K  (fragmentos de conocimiento relevantes) │
│ Historial de diálogo: 10-20K (últimas N rondas)   │
│ Entrada actual del usuario: 1-3K                  │
│ Reserva para respuesta: 4-8K                      │
│ Margen de seguridad:  restante (~80K)             │
└──────────────────────────────────────────────┘

(3) Ingeniería de contexto (Context Engineering)

La ingeniería de contexto es la práctica de diseñar, construir y optimizar sistemáticamente el contexto. No se trata solo de “meter información en el prompt”, sino que abarca cuatro técnicas fundamentales:

TécnicaProblema que resuelveMétodo principal
RAGConocimiento privado insuficienteRecuperar información relevante de bases de conocimiento externas e inyectarla en el contexto
Prompt EngineeringInstrucciones poco precisasGuiar el comportamiento del modelo mediante instrucciones cuidadosamente diseñadas
Tool UseEl modelo no puede ejecutar accionesDotar al modelo de la capacidad de invocar herramientas externas
MemoryOlvido entre sesionesEstablecer mecanismos de memoria a corto y largo plazo

Muchos fracasos en aplicaciones con modelos grandes no se deben a que el modelo no sea suficientemente inteligente, sino a un fallo en el “contexto”. La ingeniería de contexto es precisamente la clave para liberar el potencial de los modelos grandes.


Metodología de ingeniería de prompts

Diseño del System Prompt

El System Prompt es la “constitución” del modelo: define los límites de comportamiento del rol, el estilo de respuesta y las restricciones de la tarea. Un buen System Prompt debe contener:

# Plantilla de estructura del System Prompt
definición_del_rol: |
  Eres un revisor senior de documentación técnica de Python,
  enfocado en la corrección del código y la efectividad pedagógica.

normas_de_conducta:
  - No modificar nombres de variables ni números de versión de API en el código
  - Al detectar problemas, indicar la ubicación concreta y sugerencias de corrección
  - Si la información es insuficiente para juzgar, decir explícitamente "no estoy seguro"

formato_de_salida:
  ## Informe de revisión
  ### Problemas críticos
  - **[Línea N]**: descripción del problema
    - Gravedad: crítica|media|leve
    - Sugerencia de corrección: recomendación concreta

restricciones:
  - Prohibidos los chistes y descripciones situacionales innecesarias
  - Los términos técnicos deben explicarse en su primera aparición
  - Los bloques de código deben incluir las sentencias import necesarias

Few-Shot y salida estructurada

Ejemplos Few-Shot: proporcionar pares de entrada-salida como modelo para que el modelo imite el formato y el estilo.

examples = [
    {
        "input": "Explica qué es un decorador en Python",
        "output": "### Introducción del problema\n¿Alguna vez has querido añadir la misma lógica de registro en múltiples funciones?..."
    },
    {
        "input": "Explica qué es una list comprehension",
        "output": "### Introducción del problema\n¿Alguna vez has escrito código así — un bucle for de 5 líneas solo para filtrar números pares de una lista?..."
    }
]

prompt = f"""
Responde la pregunta del usuario siguiendo el estilo de los ejemplos a continuación:

{examples}

Pregunta del usuario: {user_question}
"""

Salida estructurada: restringir el formato de salida mediante JSON Schema o modelos Pydantic.

from pydantic import BaseModel
from typing import List, Optional

class CodeReview(BaseModel):
    file_name: str
    issues: List[dict]
    overall_score: int  # 1-5
    requires_rewrite: bool

# Adjuntar el Schema en el prompt
prompt = f"""
Proporciona el resultado de la revisión según el siguiente JSON Schema:

{CodeReview.model_json_schema()}

Código a revisar:
{code}
"""

Chain-of-Thought (cadena de pensamiento)

Para tareas complejas que requieren razonamiento en múltiples pasos, guiar al modelo para que “verbalice su proceso de pensamiento” mejora significativamente la precisión.

# ❌ Solicitud directa (baja precisión)
prompt_simple = "Calcula: en una clase hay 30 estudiantes, hay 4 chicos más que chicas, ¿cuántos chicos hay?"

# ✅ Prompt CoT (alta precisión)
prompt_cot = """
Calcula: en una clase hay 30 estudiantes, hay 4 chicos más que chicas, ¿cuántos chicos hay?

Razona paso a paso:
Paso 1: sea x el número de chicas, entonces el número de chicos es x + 4
Paso 2: el total es x + (x + 4) = 30
Paso 3: resolviendo 2x + 4 = 30, se obtiene x = 13
Paso 4: número de chicos = x + 4 = 17
Respuesta: 17 chicos
"""

Las variantes de CoT incluyen también:

  • ToT (Tree of Thoughts): explorar simultáneamente múltiples rutas de razonamiento y seleccionar la óptima
  • GoT (Graph of Thoughts): representar el razonamiento como un grafo dirigido, permitiendo topologías de razonamiento más complejas
  • Self-Consistency: muestrear múltiples rutas CoT y tomar el resultado por voto mayoritario

Meta Prompting: hacer que el modelo optimice sus propios prompts

Escribir un prompt perfecto de una sola vez es casi imposible. La idea central del Meta Prompting es: hacer que el modelo grande actúe como “experto revisor de prompts”, analizando y optimizando el propio prompt.

meta_prompt = """
Eres un experto en ingeniería de prompts. Analiza los defectos del siguiente prompt y genera una versión optimizada.

Prompt actual:
{current_prompt}

Salida de este prompt:
{current_output}

Salida deseada:
{desired_output}

Analiza la brecha y genera el prompt optimizado.
"""

# Este ciclo puede automatizarse: generar → evaluar → optimizar → regenerar

Un flujo completo de Meta Prompting también puede incorporar “respuesta de referencia” y puntuación cuantitativa:

  1. Establecer respuesta de referencia: definir la salida ideal
  2. Analizar la brecha: hacer que el modelo “evaluador” compare el resultado generado con la respuesta de referencia
  3. Optimizar el prompt: reescribir el prompt basándose en el informe de análisis de brecha
  4. Verificación cuantitativa: usar un calificador (Grader) para puntuar múltiples versiones

Embedding y recuperación vectorial

Cómo funcionan los modelos de Embedding

Los modelos de Embedding convierten texto en vectores de alta dimensión, de modo que textos semánticamente similares queden cerca en el espacio vectorial.

"Me gusta comer manzanas"   →  [0.12, -0.34, 0.56, ..., 0.78]  (1024 dimensiones)
"Me encanta comer manzanas"  →  [0.11, -0.33, 0.55, ..., 0.79]  ← muy cercanos
"Manual de reparación de autos" →  [-0.78, 0.45, -0.23, ..., 0.01] ← muy lejanos

El entrenamiento de modelos de Embedding suele incluir una etapa de aprendizaje contrastivo (Contrastive Learning): la entrada consiste en muchos pares de texto etiquetados como relevantes/irrelevantes, y el objetivo de entrenamiento es maximizar la similitud vectorial de los pares relevantes y minimizar la de los irrelevantes.

# Calcular la similitud coseno entre dos vectores de texto
import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Ejemplo
query_vec = embedding_model.encode("¿Cómo solicitar vacaciones anuales?")
doc_vec = embedding_model.encode("Procedimiento de solicitud de vacaciones anuales para empleados")

similarity = cosine_similarity(query_vec, doc_vec)
print(f"Similitud: {similarity:.4f}")  # 0.92 — altamente relevante

Selección de base de datos vectorial

La base de datos vectorial es la infraestructura central de un sistema RAG. Al elegir, hay que sopesar:

SoluciónProductos representativosVentajasDesventajasCaso de uso
Almacenamiento en memoriaIntegrado en LlamaIndexCero configuración, prototipado rápidoDatos no persistentes, limitado por memoriaDesarrollo y pruebas
Base vectorial localMilvus, Qdrant, ChromaFuncionalidad completa, datos bajo controlRequiere despliegue y mantenimiento propiosAplicaciones pequeñas/medianas
Servicio gestionadoPinecone, Weaviate CloudSin operaciones, escalado automáticoMayor coste, datos en externoProducción, necesidades elásticas
Extensión de BD existentePostgreSQL + pgvector, ElasticsearchAprovecha infraestructura existenteRendimiento vectorial inferior a BD dedicadasEquipos que ya usan esa BD
# Ejemplo usando Chroma (base vectorial local ligera)
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.create_collection(
    name="company_docs",
    embedding_function=embedding_functions.OpenAIEmbeddingFunction(
        api_key="your-api-key",
        model_name="text-embedding-3-small"
    )
)

# Añadir documentos
collection.add(
    documents=["Procedimiento de solicitud de vacaciones anuales...", "Normas de reembolso de viajes..."],
    metadatas=[{"source": "hr_policy.pdf"}, {"source": "finance_policy.pdf"}],
    ids=["doc_1", "doc_2"]
)

# Recuperar
results = collection.query(
    query_texts=["¿Cómo solicitar vacaciones anuales?"],
    n_results=3
)

Estrategias de segmentación de documentos

La contradicción fundamental de la segmentación

La efectividad de la recuperación en un sistema RAG depende en gran medida de la calidad de la segmentación de documentos. La contradicción central es:

Segmentos demasiado grandes → introducen demasiado ruido en la recuperación, la atención del modelo se diluye
Segmentos demasiado pequeños → la información clave puede truncarse, se pierde contexto

No existe una estrategia de segmentación óptima universal. Debes elegir según el tipo de documento, el escenario de recuperación y la capacidad del modelo.

Cinco métodos principales de segmentación

Segmentación por Token

Segmenta por número fijo de tokens, adecuado para escenarios que requieren control preciso del consumo de tokens.

from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=256,     # número de tokens por segmento
    chunk_overlap=30    # tokens de solapamiento entre segmentos adyacentes
)

nodes = splitter.get_nodes_from_documents(documents)

Ventajas: control preciso del tamaño del contexto, adecuado para modelos con ventanas de contexto más pequeñas. Desventajas: puede cortar en medio de una oración, rompiendo la integridad semántica.

Segmentación por oración

Mantiene la integridad de las oraciones; es la opción por defecto en la mayoría de escenarios.

from llama_index.core.node_parser import SentenceSplitter

splitter = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50
)

Ventajas: preserva la integridad de las unidades semánticas del lenguaje natural. Desventajas: no percibe la estructura del documento, puede separar párrafos relacionados en distintos segmentos.

Segmentación con ventana de oración

Usa diferente granularidad en indexación y recuperación: la indexación usa granularidad fina para coincidencia precisa, y al recuperar se devuelve el segmento junto con su ventana de contexto adyacente.

from llama_index.core.node_parser import SentenceWindowNodeParser

parser = SentenceWindowNodeParser(
    window_size=3,          # expandir 3 oraciones adyacentes al recuperar
    window_metadata_key="window",
    original_text_metadata_key="original"
)

Ventaja principal: equilibra precisión de recuperación e integridad contextual.

Segmentación semántica

Elige los puntos de corte de forma adaptativa según la relevancia semántica, manteniendo la continuidad semántica del documento.

from llama_index.core.node_parser import SemanticSplitterNodeParser

splitter = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=95,  # cortar cuando la similitud cae por debajo de este umbral
    embed_model=embed_model
)

Caso de uso: documentos largos con buena estructura lógica y contenido especializado.

Segmentación por Markdown

Optimizada específicamente para documentos estructurados en Markdown, segmenta según los niveles de encabezados.

from llama_index.core.node_parser import MarkdownNodeParser

parser = MarkdownNodeParser()
# Reconoce automáticamente los niveles de encabezado #, ##, ###, etc., cortando en cada sección

Mejor práctica: convertir documentos de PDF/Word a Markdown antes de segmentar, aprovechando la estructura de encabezados para mejorar la precisión de la recuperación.

Guía de selección de estrategia de segmentación

Tipo de documentoEstrategia recomendadaMotivo
Manual técnico (estructura clara)Segmentación MarkdownAprovecha la jerarquía de encabezados para mantener la estructura
Contrato legal (lógica rigurosa)Segmentación semánticaMantiene la integridad semántica de las cláusulas
Registros de conversaciónSegmentación con ventana de oraciónNecesita contexto anterior y posterior para entender la semántica
Documentación de códigoSegmentación por Token + semánticaNecesita control preciso de longitud
Noticias/blogsSegmentación por oraciónAsociaciones más débiles entre párrafos

Pipeline de generación aumentada por recuperación

Arquitectura en dos fases de RAG

RAG (Retrieval-Augmented Generation) es la arquitectura central para resolver la “falta de conocimiento” de los modelos grandes. Divide el proceso en dos fases:

Fase uno: Construcción del índice

Pipeline de generación aumentada por recuperación

  1. Parseo de documentos: convertir PDF, Word, Markdown y otros formatos a texto plano
  2. Segmentación de texto: dividir el documento en párrafos según la estrategia elegida
  3. Vectorización: transformar cada segmento en vector usando el modelo de Embedding
  4. Almacenamiento del índice: guardar los vectores en la base de datos vectorial y construir el índice

Fase dos: Recuperación y generación

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ Pregunta  │ → │ Recuperación │ → │ Ensamblado  │ → │ Generación │
│ del usuario│    │ vectorial │    │ del Prompt│    │ del modelo │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
  1. Pregunta del usuario: recibir la pregunta
  2. Recuperación vectorial: vectorizar la pregunta y buscar los segmentos más similares en la base de datos vectorial
  3. Ensamblado del Prompt: combinar los fragmentos de conocimiento recuperados + la pregunta original + instrucciones en un Prompt completo
  4. Generación del modelo: el modelo grande genera la respuesta basándose en el contexto aumentado

Ejemplo completo de pipeline RAG

from openai import OpenAI
import numpy as np

client = OpenAI()

class SimpleRAG:
    def __init__(self, embed_model="text-embedding-3-small"):
        self.embed_model = embed_model
        self.documents = []      # almacenar textos de documentos
        self.embeddings = []     # almacenar vectores de documentos

    def add_documents(self, docs: list[str]):
        """Construir índice: vectorizar y almacenar documentos"""
        for doc in docs:
            vec = self._embed(doc)
            self.documents.append(doc)
            self.embeddings.append(vec)

    def _embed(self, text: str) -> np.ndarray:
        resp = client.embeddings.create(
            model=self.embed_model,
            input=text
        )
        return np.array(resp.data[0].embedding)

    def retrieve(self, query: str, top_k: int = 3) -> list[str]:
        """Recuperar los fragmentos de documento más relevantes"""
        query_vec = self._embed(query)
        similarities = [
            np.dot(query_vec, doc_vec) /
            (np.linalg.norm(query_vec) * np.linalg.norm(doc_vec))
            for doc_vec in self.embeddings
        ]
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        return [self.documents[i] for i in top_indices]

    def query(self, question: str) -> str:
        """Consulta RAG completa"""
        contexts = self.retrieve(question)
        prompt = f"""Responde la siguiente pregunta basándote en la información de referencia:

Información de referencia:
{' '.join(contexts)}

Pregunta: {question}

Si la información de referencia no es suficiente para responder, indícalo explícitamente."""

        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        return resp.choices[0].message.content

Reescritura de preguntas en diálogo multi-turno con RAG

Implementar diálogo multi-turno en un escenario RAG presenta desafíos únicos. Si en la segunda ronda el usuario dice “¿Quién es su supervisor?”, usar esta frase directamente para recuperar fallará por completo: el sistema no sabe a quién se refiere “su”.

Solución: reescritura de preguntas (Query Rewriting)

def rewrite_query(conversation_history: list, current_query: str) -> str:
    """Usar el modelo grande para transformar una pregunta dependiente del contexto en una pregunta independiente"""
    rewrite_prompt = f"""
    Basándote en el historial de la conversación, transforma la pregunta actual en una pregunta independiente que no dependa del contexto.

    Historial de conversación:
    {format_history(conversation_history)}

    Pregunta actual: {current_query}

    Pregunta reescrita:"""

    resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": rewrite_prompt}],
        temperature=0.1
    )
    return resp.choices[0].message.content

# Ejemplo
# Historial: usuario pregunta "¿Dónde está el puesto de trabajo de Zhang San?"
#             asistente responde "En el edificio A, planta 5"
# Actual: "¿Quién es su supervisor?"
# Reescrita: "¿Quién es el supervisor de Zhang San?"

Patrones avanzados de RAG

RAG avanzado

HyDE (Hypothetical Document Embeddings)

La idea central de HyDE: primero hacer que el modelo “invente” una respuesta hipotética y luego usar esa respuesta hipotética para recuperar, en lugar de usar la pregunta original. La intuición es que una respuesta hipotética es semánticamente más cercana a los documentos reales que la pregunta.

def hyde_retrieve(query: str, top_k: int = 3) -> list[str]:
    """Recuperar usando el método HyDE"""
    # Paso 1: generar respuesta hipotética
    hyde_prompt = f"""
    Question: {query}
    Please write a passage that answers this question.
    Passage:"""

    hyde_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": hyde_prompt}]
    )
    hypothetical_doc = hyde_resp.choices[0].message.content

    # Paso 2: recuperar usando la respuesta hipotética, no la pregunta original
    query_vec = embed(hypothetical_doc)
    results = vector_db.search(query_vec, top_k=top_k)
    return results

HyDE es especialmente adecuado para escenarios donde la consulta original es muy corta pero semánticamente compleja, ya que la respuesta hipotética proporciona más pistas semánticas.

Reordenamiento (Re-Ranking)

La recuperación vectorial inicial (fase gruesa) es rápida pero tiene precisión limitada. Se puede introducir un modelo de reordenamiento (fase fina) para ordenar de nuevo los segmentos candidatos:

from sentence_transformers import CrossEncoder

reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')

def rerank(query: str, candidates: list[str], top_k: int = 3):
    """Reordenar los segmentos candidatos"""
    pairs = [(query, doc) for doc in candidates]
    scores = reranker.predict(pairs)

    # Ordenar por puntuación de relevancia
    ranked = sorted(
        zip(candidates, scores),
        key=lambda x: x[1],
        reverse=True
    )
    return [doc for doc, _ in ranked[:top_k]]

Panorama de optimización de estrategias de recuperación

MomentoEstrategia de mejoraDescripción
Antes de la recuperaciónReescritura de preguntaTransformar pregunta dependiente del contexto en independiente
Antes de la recuperaciónExpansión de preguntaAñadir más información semántica para mejorar recall
Antes de la recuperaciónExtracción de etiquetasFiltrar primero por etiquetas, luego recuperación vectorial
Antes de la recuperaciónDescomposición de consulta en múltiples pasosDividir pregunta compleja en varias subconsultas
Después de la recuperaciónReRankReordenar con un modelo más preciso
Después de la recuperaciónVentana deslizanteAl recuperar un segmento, añadir los segmentos adyacentes

Estrategia de preparación de documentos para RAG

La clave para construir un sistema RAG de alta calidad es la preparación de documentos. Debes entender la relación entre el “espacio de intención” y el “espacio de conocimiento”:

Espacio de intención           Espacio de conocimiento
(lo que el usuario puede preguntar)  (lo que la base de conocimiento cubre)
     ┌─────────┐
     │  Zona de   │ ← RAG puede responder
     │solapamiento│
     └─────────┘
     ↑            ↑
  Intenciones no    Conocimiento no
  cubiertas →       aprovechado →
  añadir conocimiento  optimizar recall

Principios fundamentales:

  • Antes de optimizar el algoritmo, completa el conocimiento faltante
  • Antes de mejorar el recall, mejora la calidad de los documentos
  • Recopila continuamente las intenciones de los usuarios para formar un ciclo cerrado de “recopilación de datos - actualización de conocimiento - validación experta”

El protocolo Function Calling

AutoGen Studio — constructor de flujos multi-agente AutoGen Studio — el constructor no-code de flujos multi-agente de Microsoft — vía microsoft/autogen

DSPy — programación declarativa de LLMs DSPy — programa LLMs declarativamente (no con prompts); usado por la pipeline de auto-evolución de Hermes — vía stanfordnlp/dspy

Qué es Function Calling

Function Calling (invocación de funciones, también llamado Tool Calling) es una capacidad estándar que ofrecen las API de modelos grandes. Permite que el modelo, cuando sea necesario, emita instrucciones estructuradas de invocación de herramientas en lugar de respuestas de texto plano.

El flujo de trabajo es el siguiente:

Protocolo de Function Calling

Definición de herramientas con JSON Schema

# Definir lista de herramientas
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Buscar en la base de conocimiento interna de la empresa para obtener documentos de políticas y guías de procedimientos",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Palabra clave o pregunta de búsqueda"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["hr", "it", "finance", "general"],
                        "description": "Categoría de conocimiento"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "Enviar un correo electrónico",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {
                        "type": "string",
                        "description": "Dirección de correo del destinatario"
                    },
                    "subject": {
                        "type": "string",
                        "description": "Asunto del correo"
                    },
                    "body": {
                        "type": "string",
                        "description": "Cuerpo del correo"
                    }
                },
                "required": ["to", "subject", "body"]
            }
        }
    }
]

Ciclo completo de Function Calling

from openai import OpenAI
import json

client = OpenAI()

def execute_function_call(tool_call):
    """Ejecutar la invocación de herramienta y devolver el resultado"""
    func_name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)

    if func_name == "search_knowledge_base":
        # Lógica real de búsqueda
        result = knowledge_base.search(args["query"])
        return json.dumps(result)
    elif func_name == "send_email":
        # Lógica real de envío de correo
        result = email_service.send(
            to=args["to"],
            subject=args["subject"],
            body=args["body"]
        )
        return json.dumps({"status": "sent" if result else "failed"})
    else:
        return json.dumps({"error": f"Unknown function: {func_name}"})

def chat_with_tools(user_message: str, messages: list = None):
    """Diálogo con soporte para Function Calling"""
    if messages is None:
        messages = [
            {"role": "system", "content": "Eres un asistente empresarial que puede buscar en la base de conocimiento y enviar correos."}
        ]

    messages.append({"role": "user", "content": user_message})

    # Primera llamada: el modelo decide si usar herramientas
    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools
    )

    assistant_msg = response.choices[0].message

    # Si el modelo quiere invocar herramientas
    if assistant_msg.tool_calls:
        messages.append(assistant_msg)

        for tool_call in assistant_msg.tool_calls:
            # Ejecutar herramienta
            result = execute_function_call(tool_call)
            # Devolver resultado al modelo
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

        # Segunda llamada: el modelo genera la respuesta final basándose en los resultados
        final_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )
        return final_response.choices[0].message.content

    # Si el modelo responde directamente
    return assistant_msg.content

Mejores prácticas para la definición de herramientas

  1. Descripciones precisas: el modelo decide cuándo invocar una herramienta basándose en description; una descripción ambigua provoca invocaciones erróneas
  2. Parámetros con restricciones: usa enum, required y restricciones de tipo para reducir errores en los parámetros
  3. Funciones de responsabilidad única: no agrupes múltiples operaciones en una sola función
  4. Resultados estructurados: la salida de la herramienta debe ser fácil de entender para el modelo; se recomienda JSON
# ❌ Mala descripción de herramienta
{"name": "do_stuff", "description": "ejecutar operación", "parameters": {...}}

# ✅ Descripción precisa de herramienta
{
    "name": "cancel_meeting",
    "description": "Cancelar una reunión especificada; requiere ID de reunión y motivo de cancelación",
    "parameters": {
        "properties": {
            "meeting_id": {"type": "string", "description": "Identificador único de la reunión"},
            "reason": {"type": "string", "description": "Motivo de cancelación, se notificará a todos los asistentes"}
        },
        "required": ["meeting_id"]
    }
}

El ciclo ReAct de razonamiento-acción

La idea central de ReAct

ReAct (Reasoning + Acting) es un patrón que hace que el modelo alterne entre razonamiento (Thought) y acción (Action). No genera una respuesta final de una sola vez, sino que resuelve problemas mediante un ciclo de “pensar → actuar → observar → pensar…”.

Bucle de razonamiento-acción ReAct

Un proceso típico de ejecución ReAct:

Usuario: "Busca el departamento de Zhang San y envía un correo a su supervisor"

Thought 1: Necesito buscar primero la información del departamento de Zhang San
Action 1: search_knowledge_base(query="Zhang San departamento")
Observation 1: "Zhang San pertenece al Departamento de I+D, supervisor es Li Si ([email protected])"

Thought 2: He obtenido la información, ahora necesito escribir un correo a Li Si
Action 2: send_email(to="[email protected]", subject="Sobre Zhang San",
                      body="...")
Observation 2: {"status": "sent"}

Thought 3: Tarea completada
Final Answer: "He encontrado que Zhang San está en el Departamento de I+D y he enviado un correo a su supervisor Li Si."

Implementación manual de un ReAct Agent

class ReActAgent:
    def __init__(self, tools: dict, max_iterations: int = 10):
        self.tools = tools
        self.max_iterations = max_iterations

    def run(self, task: str) -> str:
        messages = [
            {"role": "system", "content": self._build_system_prompt()},
            {"role": "user", "content": task}
        ]

        for i in range(self.max_iterations):
            response = client.chat.completions.create(
                model="gpt-4",
                messages=messages,
                tools=self._format_tools()
            )

            msg = response.choices[0].message

            if msg.content and not msg.tool_calls:
                # El modelo ha dado la respuesta final
                return msg.content

            if msg.tool_calls:
                # Añadir la invocación de herramienta del asistente al historial
                messages.append(msg)

                for tc in msg.tool_calls:
                    tool_name = tc.function.name
                    args = json.loads(tc.function.arguments)

                    # Ejecutar herramienta
                    result = self.tools[tool_name](**args)

                    # Añadir el resultado de la observación al historial
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": json.dumps(result)
                    })
                    print(f"  [Tool: {tool_name}({args}) → {result}]")

        return "Ciclo ReAct alcanzó el máximo de iteraciones"

    def _build_system_prompt(self) -> str:
        return """Eres un asistente inteligente capaz de usar herramientas.
Sigue el patrón ReAct: primero piensa, luego actúa, observa el resultado y decide el siguiente paso.
Si la tarea está completada, da directamente la respuesta final."""

    def _format_tools(self) -> list:
        return [
            {
                "type": "function",
                "function": {
                    "name": name,
                    "description": func.__doc__ or "",
                    "parameters": get_schema(func)
                }
            }
            for name, func in self.tools.items()
        ]

Ventajas y limitaciones de ReAct

Ventajas:

  • Observable: cada paso de pensamiento y acción deja rastro
  • Corregible: al observar un resultado erróneo, puede ajustar la estrategia
  • Componible: combina automáticamente múltiples herramientas en una solución

Limitaciones:

  • Número de ciclos impredecible (puede entrar en bucle infinito)
  • Múltiples llamadas API aumentan latencia y coste
  • Depende de la calidad de las observaciones devueltas por las herramientas

Protocolo MCP y ecosistema de herramientas

Protocolo MCP y ecosistema de herramientas

Por qué necesitamos MCP

Function Calling tiene un problema fundamental: la definición y el consumo de herramientas están acoplados. Cada desarrollador de Agent necesita codificar manualmente el JSON Schema de las herramientas en su propio código. Cuando la API de una herramienta se actualiza, todos los Agents que la integran deben actualizarse manualmente.

Modelo tradicional de Function Calling:
  Agent A ──Schema hardcodeado──→ web_search v1
  Agent B ──Schema hardcodeado──→ web_search v1  ← definición repetida
  Agent C ──Schema hardcodeado──→ web_search v1  ← definición repetida

Modelo MCP:
  Agent A ──┐
  Agent B ──┼── MCP Client ──→ MCP Server (web_search)
  Agent C ──┘                    ↑
                          El proveedor de la herramienta define el Schema

La idea central de MCP (Model Context Protocol) es “quien proporciona la herramienta, define la herramienta”. Transfiere la responsabilidad de la definición de herramientas del Agent (consumidor) al servicio de herramientas (proveedor).

Roles en la arquitectura MCP

RolResponsabilidadAnalogía
MCP ServerDeclarar herramientas (nombre, descripción, parámetros), ejecutar la lógicaDispositivo USB
MCP ClientConectarse al MCP Server, obtener definiciones de herramientas, enviar solicitudes de invocaciónControlador host USB
AgentUsar el MCP Client para obtener la lista de herramientas, decidir las invocacionesAplicación

Construcción de MCP Server y Client

Ejemplo de MCP Server:

from mcp.server import Server, stdio_server
from mcp.types import Tool, TextContent

app = Server("web-search")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="web_search",
            description="Buscar en internet para obtener información actualizada",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Palabra clave de búsqueda"},
                    "num_results": {"type": "integer", "default": 5}
                },
                "required": ["query"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "web_search":
        results = search_engine.search(
            arguments["query"],
            num=arguments.get("num_results", 5)
        )
        return [TextContent(type="text", text=json.dumps(results))]
    raise ValueError(f"Unknown tool: {name}")

# Iniciar Server vía stdio
async def main():
    async with stdio_server() as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Ejemplo de integración de MCP Client:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run_with_mcp_tools(user_query: str):
    server_params = StdioServerParameters(
        command="python",
        args=["web_search_server.py"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Obtener definiciones de herramientas del MCP Server
            tools_result = await session.list_tools()
            tools = tools_result.tools

            # Convertir al formato tools de OpenAI
            openai_tools = [
                {
                    "type": "function",
                    "function": {
                        "name": tool.name,
                        "description": tool.description,
                        "parameters": tool.inputSchema
                    }
                }
                for tool in tools
            ]

            # Flujo estándar de Function Calling
            response = client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": user_query}],
                tools=openai_tools
            )

            # Si el modelo quiere invocar herramientas, ejecutar vía MCP Client
            if response.choices[0].message.tool_calls:
                for tc in response.choices[0].message.tool_calls:
                    result = await session.call_tool(
                        tc.function.name,
                        json.loads(tc.function.arguments)
                    )
                    # ... devolver resultado al modelo

Valor de ingeniería de MCP

  • Desacoplamiento: la definición de herramientas y la implementación del servicio se separan, iterando de forma independiente
  • Descubrimiento dinámico: el Agent obtiene automáticamente la lista de herramientas más reciente al iniciarse, con coste de mantenimiento cero
  • Efecto ecosistema: terceros pueden proporcionar MCP Servers estandarizados; los desarrolladores de Agents solo necesitan integrar un MCP Client
  • Múltiples protocolos de transporte: soporta stdio (comunicación entre procesos locales) y HTTP/SSE (comunicación remota)

Reflexión y autocorrección

Por qué necesitamos reflexión

El contenido generado por el modelo grande no siempre es utilizable. Puede que:

  • “Corrija” silenciosamente nombres de variables en el código causando errores de ejecución
  • Continúe razonando sobre “hechos” erróneos, produciendo errores en cascada
  • En una salida muy larga, olvide las restricciones anteriores

La idea central de la reflexión (Reflection) es: dar al modelo la oportunidad de examinar y evaluar el contenido completo que ya ha generado, para así descubrir y corregir errores.

Dos modos de autoevaluación

Modo uno: reflexión en una sola instrucción

En una sola llamada, indicar al modelo mediante el Prompt que genere la respuesta y al mismo tiempo reflexione:

prompt_with_reflection = """
## Tarea
1. Mejora la expresión lingüística del siguiente contenido del curso, mostrando el texto completo mejorado.
2. Reflexiona sobre el contenido mejorado:
   - ¿Cumple con las normas de redacción?
   - Aparte de la expresión lingüística, ¿se ha modificado accidentalmente algún otro contenido?
   Muestra el resultado de la reflexión y las sugerencias de modificación.
3. Modifica el curso según las sugerencias y muestra el texto completo modificado.

## Borrador del curso
{original_content}
"""

Ventajas: implementación simple, se completa en una sola llamada. Desventajas: el modelo tiende a autoverificarse con el mismo sesgo de pensamiento, cayendo en “autoconfirmación”.

Modo dos: “generación-evaluación” en dos pasos

Separar la generación y la revisión en dos llamadas independientes:

def generate_and_review(content: str) -> str:
    # Primer paso: generar
    draft_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": "Eres un redactor de cursos. Mejora el siguiente contenido para hacerlo más atractivo."
        }, {
            "role": "user",
            "content": content
        }]
    )
    draft = draft_resp.choices[0].message.content

    # Segundo paso: revisar (¡con un system prompt diferente!)
    review_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{
            "role": "system",
            "content": """Eres un revisor técnico riguroso.
Compara el [contenido original] y el [contenido mejorado]:
- Si solo se ha modificado la expresión del texto y el contenido técnico como el código es completamente idéntico → responde "Aprobado"
- Si el contenido técnico ha sido modificado → responde "No aprobado", indicando la ubicación concreta"""
        }, {
            "role": "user",
            "content": f"Contenido original:\n{content}\n\nContenido mejorado:\n{draft}"
        }]
    )

    # Tercer paso: si no se aprueba, enviar la revisión al Agent redactor para corregir
    review = review_resp.choices[0].message.content
    if "No aprobado" in review:
        # ... reenviar la retroalimentación al Agent redactor para corrección
        pass

    return draft

Ventaja principal: la perspectiva del Agent revisor es diferente a la del Agent redactor, evitando el sesgo de rol. Incluso se pueden configurar múltiples Agents revisores especializados: revisión de hechos, revisión de lógica, revisión de estilo, revisión de seguridad.

Retroalimentación externa

La autoevaluación tiene una limitación natural: el modelo no puede verificar la corrección del contenido en un entorno real. La idea de la retroalimentación externa es ejecutar el resultado generado en un entorno real y usar hechos objetivos para verificarlo.

def generate_code_and_validate(spec: str) -> str:
    # Primer paso: generar código
    code_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": f"Escribe código Python según los siguientes requisitos:\n{spec}"}]
    )
    code = extract_code(code_resp.choices[0].message.content)

    # Segundo paso: validación por ejecución externa
    import subprocess, tempfile
    with tempfile.NamedTemporaryFile(suffix=".py", mode="w") as f:
        f.write(code)
        f.flush()
        result = subprocess.run(
            ["python", f.name],
            capture_output=True,
            text=True,
            timeout=30
        )

    # Tercer paso: enviar el error al modelo para corrección
    if result.returncode != 0:
        fix_prompt = f"""El siguiente código produce un error al ejecutarse:

Código:
{code}

Mensaje de error:
{result.stderr}

Corrige el código y muestra la versión completa corregida."""
        fix_resp = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": fix_prompt}]
        )
        return fix_resp.choices[0].message.content

    return code

Escenarios de aplicación de la retroalimentación externa:

  • Validación por ejecución de código: ejecutar código con un intérprete y capturar errores en tiempo de ejecución
  • Validación de JSON Schema: verificar salidas estructuradas con librerías como Pydantic
  • Validación de cálculos numéricos: verificar resultados matemáticos con herramientas de cálculo
  • Validación de renderizado visual: tras generar un gráfico, hacer que el modelo “vea” el resultado del renderizado para inspección visual

Patrón Plan & Execute

Por qué necesitamos planificación explícita

Cuando se le pide directamente a un Agent que ejecute tareas complejas, los problemas comunes son:

  • Olvido: al llegar a pasos posteriores “olvida” las restricciones anteriores
  • Errores en cascada: los errores tempranos se convierten en la base del razonamiento posterior, amplificándose exponencialmente
  • Pérdida de estructura: el modelo tiende al procesamiento lineal y no puede identificar relaciones de paralelismo/dependencia entre tareas

La idea central del patrón Plan & Execute es: primero planificar y luego ejecutar — primero elaborar un plan de acción completo, confirmarlo mediante revisión y luego ejecutarlo paso a paso.

Implementación del Plan Mode

from typing import List
from pydantic import BaseModel

class PlanStep(BaseModel):
    step_id: int
    description: str
    dependencies: List[int] = []  # IDs de pasos de los que depende
    tool: str = ""               # herramienta a utilizar
    expected_output: str = ""    # salida esperada

class ExecutionPlan(BaseModel):
    goal: str
    steps: List[PlanStep]

def plan_and_execute(task: str) -> str:
    # Fase 1: Elaborar plan
    plan_prompt = f"""
    Eres un experto en planificación de proyectos. Elabora un plan de ejecución detallado para la siguiente tarea.

    Requisitos:
    1. Descomponer la tarea en pasos concretos
    2. Indicar las dependencias entre pasos
    3. Cada paso debe indicar la salida esperada

    Tarea: {task}

    Proporciona el plan en formato JSON."""

    plan_resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": plan_prompt}],
        response_format={"type": "json_object"}
    )
    plan = ExecutionPlan.model_validate_json(
        plan_resp.choices[0].message.content
    )

    # Fase 2: Ejecutar según dependencias
    results = {}
    executed = set()

    while len(executed) < len(plan.steps):
        for step in plan.steps:
            if step.step_id in executed:
                continue
            # Verificar que todas las dependencias se han ejecutado
            if all(dep in executed for dep in step.dependencies):
                # Ejecutar paso
                result = execute_step(step, results)
                results[step.step_id] = result
                executed.add(step.step_id)

    # Fase 3: Consolidar resultados
    return summarize_results(plan, results)

Flujo de trabajo consolidado: patrón Pipeline

Cuando los pasos de una tarea son deterministas y repetibles, deben consolidarse en un pipeline:

Input → Step 1 → Step 2 → Step 3 → ... → Output
class Pipeline:
    """Pipeline fijo: la salida de cada paso es la entrada del siguiente"""

    def __init__(self):
        self.steps = []

    def add_step(self, name: str, func):
        self.steps.append({"name": name, "func": func})

    def run(self, input_data):
        result = input_data
        for step in self.steps:
            print(f"  [Ejecutando] {step['name']}")
            result = step["func"](result)
        return result

# Ejemplo: pipeline de procesamiento de documentos
pipeline = Pipeline()
pipeline.add_step("Parsear PDF", parse_pdf_to_text)
pipeline.add_step("Limpiar texto", clean_text)
pipeline.add_step("Segmentar", split_sections)
pipeline.add_step("Vectorizar", vectorize_chunks)
pipeline.add_step("Almacenar índice", store_to_vectordb)

pipeline.run("document.pdf")

Patrones de orquestación de flujos de trabajo

Cinco patrones fundamentales de flujo de trabajo

Las tareas complejas requieren organizar los nodos Agent en topologías específicas. Estos son los cinco patrones fundamentales:

Enrutamiento por rama (Branching/Router)

En el nodo de entrada se determina el tipo de tarea y se deriva a diferentes rutas de procesamiento.

Patrones de orquestación de flujos de trabajo

def router_agent(user_input: str):
    """Derivar a diferentes pipelines según la intención"""
    classify_prompt = f"""
    Analiza el tipo de la siguiente solicitud del usuario, responde solo una palabra:
    - code_review: revisar/verificar código
    - style_review: mejorar/optimizar lenguaje
    - fact_check: verificar precisión de hechos/conceptos

    Solicitud: {user_input}
    Tipo:"""

    intent = client.chat.completions.create(
        model="gpt-4o-mini",  # usar modelo ligero para ahorrar costes
        messages=[{"role": "user", "content": classify_prompt}],
        temperature=0
    ).choices[0].message.content.strip()

    pipelines = {
        "code_review": code_review_pipeline,
        "style_review": style_review_pipeline,
        "fact_check": fact_check_pipeline
    }
    return pipelines.get(intent, default_pipeline)(user_input)

Ejecución en paralelo (Parallel)

Las subtareas que no dependen entre sí se distribuyen simultáneamente y se consolidan al final.

              ┌→ Revisión de código ──┐
Entrada → Split ─┼→ Verificación de hechos ──┼→ Merge → Informe consolidado
              └→ Revisión de estilo ──┘
import asyncio

async def parallel_review(notebook_content: str):
    """Realizar tres revisiones en paralelo sobre el contenido del curso"""
    tasks = [
        asyncio.create_task(check_code(notebook_content)),
        asyncio.create_task(check_facts(notebook_content)),
        asyncio.create_task(check_style(notebook_content))
    ]

    code_result, fact_result, style_result = await asyncio.gather(*tasks)

    # Consolidar
    return generate_summary_report(code_result, fact_result, style_result)

Mezcla de expertos (Mixture-of-Agents, MoA)

Múltiples modelos distintos procesan la misma tarea y un agregador sintetiza el mejor resultado.

             ┌→ Modelo A (experto en razonamiento) ──┐
Pregunta → Split ─┼→ Modelo B (experto en creatividad) ──┼→ Aggregator → Respuesta óptima
             └→ Modelo C (experto en precisión) ──┘

El hallazgo central de MoA es la “colaboratividad” (Collaborativeness) de los modelos: cuando un modelo puede consultar las salidas de otros modelos, a menudo genera respuestas de mayor calidad.

def mixture_of_agents(task: str):
    """Implementación MOA: múltiples modelos + agregación"""
    # Primera capa: los proponentes generan en paralelo
    proposers = ["gpt-4", "claude-3-opus", "gemini-pro"]
    proposals = []

    for model in proposers:
        resp = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": task}]
        )
        proposals.append(resp.choices[0].message.content)

    # Segunda capa: el agregador sintetiza
    aggregator_prompt = f"""
    A continuación hay {len(proposals)} respuestas a la misma pregunta. Sintetiza sus ventajas
    y genera una respuesta óptima.

    {format_proposals(proposals)}

    Respuesta sintetizada:"""

    final = client.chat.completions.create(
        model="gpt-4",  # usar el modelo más potente para agregar
        messages=[{"role": "user", "content": aggregator_prompt}]
    )
    return final.choices[0].message.content

Colaboración humano-máquina (Human-in-the-Loop, HITL)

Introducir revisión humana en nodos clave, formando un ciclo de “IA ejecuta → humano aprueba → IA continúa”.

def hitl_workflow(task: str):
    """Flujo de trabajo con humano en el bucle"""
    plan = generate_plan(task)
    print(f"Plan de ejecución:\n{format_plan(plan)}")

    approval = input("¿Aprueba este plan? (s/n): ")
    if approval.lower() != 's':
        return "Tarea cancelada"

    for step in plan.steps:
        result = execute_step(step)
        print(f"Paso {step.step_id} completado: {result['summary']}")

        if step.get("requires_review"):
            review = input(f"Revise el resultado del paso (aprobar/modificar/rechazar): ")
            if review == "rechazar":
                print("Paso rechazado, re-ejecutando...")
                result = execute_step(step, feedback=review)

    return generate_final_output()

Metodología de selección de patrones

PatrónCaso de uso adecuadoCaso de uso no adecuado
PipelineFlujo fijo, pasos linealesTareas que requieren decisiones dinámicas
BranchingMúltiples tipos de entrada necesitan distinto procesamientoCuando se necesita procesar múltiples aspectos simultáneamente
ParallelSubtareas independientes entre sí, búsqueda de eficienciaTareas con cadenas de dependencias
MoARequisitos de alta calidad, tareas creativasTareas rutinarias sensibles al coste
HITLDecisiones de alto riesgo, requisitos de cumplimientoSistemas en tiempo real con requisitos de baja latencia
Plan & ExecuteFlujo variable, tareas nuevas que requieren exploraciónTareas deterministas altamente repetitivas

La mejor práctica es el modo mixto “explorar-consolidar”: primero usar Plan & Execute para explorar y encontrar la solución óptima, luego consolidarla en un Pipeline para producción a gran escala.


Patrón de colaboración jerárquica

Arquitectura Leader-Worker

La colaboración jerárquica (Hierarchical/Team Leader Pattern) es el patrón de colaboración multi-agente más intuitivo. Simula la estructura organizativa de “jefe de proyecto + miembros del equipo”:

Patrón de colaboración jerárquica

El Leader Agent se encarga de:

  1. Recibir y comprender la tarea de alto nivel
  2. Descomponerla en subtareas y asignarlas al Worker adecuado
  3. Hacer seguimiento del progreso global
  4. Consolidar los resultados de los Workers

Cada Worker Agent posee experiencia en un dominio específico y se concentra en ejecutar las subtareas asignadas.

Implementación de colaboración jerárquica mediante Handoff

class LeaderWorkerSystem:
    """Implementación del sistema de colaboración jerárquica"""

    def __init__(self):
        self.workers = {
            "instructional_designer": self._create_worker(
                "Eres diseñador instruccional, experto en diseñar esquemas de cursos y rutas de aprendizaje."
            ),
            "data_scientist": self._create_worker(
                "Eres científico de datos, experto en escribir código Python de análisis de datos y casos prácticos."
            ),
            "content_writer": self._create_worker(
                "Eres redactor de contenido, experto en transformar contenido técnico en guiones de curso atractivos."
            )
        }
        self.leader = self._create_leader()

    def _create_leader(self):
        return {
            "system_prompt": """Eres el director del proyecto del curso. Tus responsabilidades son:
1. Analizar los requisitos y descomponerlos en subtareas
2. Asignar tareas al experto adecuado
3. Integrar los resultados de cada experto en un curso completo
Expertos disponibles: instructional_designer, data_scientist, content_writer""",
            "tools": [
                {
                    "type": "function",
                    "function": {
                        "name": "delegate_to_worker",
                        "description": "Asignar una subtarea al experto especificado",
                        "parameters": {
                            "type": "object",
                            "properties": {
                                "worker": {
                                    "type": "string",
                                    "enum": list(self.workers.keys()),
                                    "description": "Experto que recibe la tarea"
                                },
                                "task": {
                                    "type": "string",
                                    "description": "Descripción concreta de la subtarea"
                                }
                            },
                            "required": ["worker", "task"]
                        }
                    }
                }
            ]
        }

    def run(self, project_brief: str) -> str:
        """Ejecutar un proyecto completo de desarrollo de curso"""
        messages = [
            {"role": "system", "content": self.leader["system_prompt"]},
            {"role": "user", "content": project_brief}
        ]

        # Bucle del Leader
        while True:
            response = client.chat.completions.create(
                model="gpt-4",
                messages=messages,
                tools=self.leader["tools"]
            )
            msg = response.choices[0].message

            if msg.content and not msg.tool_calls:
                return msg.content  # Salida final consolidada

            if msg.tool_calls:
                messages.append(msg)
                for tc in msg.tool_calls:
                    if tc.function.name == "delegate_to_worker":
                        args = json.loads(tc.function.arguments)
                        # Llamar al Worker para ejecutar la subtarea
                        worker_result = self._run_worker(
                            args["worker"], args["task"]
                        )
                        messages.append({
                            "role": "tool",
                            "tool_call_id": tc.id,
                            "content": worker_result
                        })

    def _run_worker(self, worker_name: str, task: str) -> str:
        worker = self.workers[worker_name]
        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": worker},
                {"role": "user", "content": task}
            ]
        )
        return resp.choices[0].message.content

Ventajas y desventajas de la colaboración jerárquica

Ventajas:

  • Estructura clara, cada Agent tiene responsabilidades bien definidas
  • El Leader controla la visión global, sin desviarse del objetivo
  • Cada Worker posee una ventana de contexto independiente, más enfocado
  • Soporta asignación de tareas en paralelo

Desventajas:

  • Los Workers no se comunican directamente entre sí, hay retraso/distorsión en la transmisión de información
  • El Leader se convierte en un punto único de cuello de botella
  • La combinación de módulos puede carecer de fluidez global

Patrón de colaboración por pizarra

Co-creación descentralizada

El patrón de pizarra (Blackboard/Co-creation Pattern) simula la forma de trabajar de “expertos reunidos alrededor de una pizarra haciendo tormenta de ideas”. No tiene un coordinador centralizado; todos los Agents leen y escriben de forma equitativa en un espacio compartido:

Patrón de colaboración blackboard

Implementación del patrón de pizarra

class BlackboardSystem:
    """Sistema de colaboración por pizarra"""

    def __init__(self, agents: dict, max_rounds: int = 3):
        self.agents = agents
        self.max_rounds = max_rounds
        self.blackboard = []  # espacio compartido

    def run(self, problem: str) -> str:
        # Escribir el problema en la pizarra
        self.blackboard.append({"source": "user", "content": problem})

        for round_num in range(self.max_rounds):
            print(f"\n=== Ronda {round_num + 1} ===")
            new_contributions = []

            # Todos los Agents leen la pizarra en paralelo y contribuyen
            for name, agent_config in self.agents.items():
                contribution = self._agent_contribute(
                    name, agent_config, self.blackboard
                )
                if contribution:
                    new_contributions.append({
                        "source": name,
                        "content": contribution
                    })

            # Escribir las nuevas contribuciones en la pizarra
            self.blackboard.extend(new_contributions)

            # Verificar si se ha alcanzado consenso
            if self._check_consensus():
                break

        return self._synthesize_final_answer()

    def _agent_contribute(self, name, config, blackboard):
        """Cada Agent lee la pizarra y aporta sus propias ideas"""
        board_text = self._format_blackboard(blackboard)

        prompt = f"""Eres un {config['role']}.

Contenido actual en la pizarra compartida:
{board_text}

Basándote en la discusión existente, aporta tus ideas, complementos, cuestionamientos o nuevas propuestas.
Si la solución existente ya es completa, puedes expresar tu acuerdo y explicar por qué."""

        resp = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": config["system_prompt"]},
                {"role": "user", "content": prompt}
            ]
        )
        return resp.choices[0].message.content

    def _format_blackboard(self, blackboard):
        return "\n\n".join([
            f"[{entry['source']}]: {entry['content']}"
            for entry in blackboard
        ])

    def _check_consensus(self):
        """Verificar si las últimas contribuciones en la pizarra han formado consenso"""
        # Implementar lógica de detección de consenso
        pass

    def _synthesize_final_answer(self):
        """Sintetizar la solución final a partir del contenido de la pizarra"""
        pass

Patrón de pizarra vs patrón jerárquico

DimensiónPatrón jerárquicoPatrón de pizarra
Modo de controlCentralizado (el Leader controla)Descentralizado (participación equitativa)
Modo de comunicaciónEstrella (Leader↔Worker)Totalmente conectado (todos los Agents↔pizarra)
Mecanismo de decisiónEl Leader decideConsenso emergente
Tareas adecuadasObjetivo claro, descomponibleExploración abierta, requiere inteligencia colectiva
EficienciaAlta (paralelo + controlable)Más baja (múltiples rondas de discusión)
CreatividadLimitada (restringida por la perspectiva del Leader)Alta (la colisión de ideas genera nuevas propuestas)
CosteMedioAlto (todos los Agents participan en cada ronda)

Metodología de selección de patrones de colaboración

Aprender del mundo real

El diseño de sistemas multi-agente excelentes surge de la observación y destilación del trabajo en equipo en el mundo real. En lugar de memorizar nombres abstractos de patrones, es mejor adentrarse en el negocio y observar cómo los equipos de expertos humanos realizan tareas similares.

Tres dimensiones de observación:

Metodología de selección de patrones de colaboración

Diseño de patrones híbridos

En proyectos reales es raro usar un solo patrón. Lo común es el diseño híbrido:

                    ┌─────────────┐
                    │   Leader    │  ← patrón jerárquico
                    └──────┬──────┘
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │ Diseñador │    │ Redactor  │    │ Revisor   │
    │instrucc.  │    │de contenido│    │  Leader   │
    │  Worker   │    │  Worker   │    └─────┬────┘
    └──────────┘    └──────────┘           │
                                  ┌────────┼────────┐
                                  ▼        ▼        ▼
                             ┌──────┐ ┌──────┐ ┌──────┐
                             │Rev.  │ │Rev.  │ │Rev.  │  ← patrón paralelo
                             │código│ │hechos│ │estilo│
                             └──────┘ └──────┘ └──────┘
                                 │        │        │
                                 └────────┼────────┘

                                    ┌──────────┐
                                    │ Informe  │
                                    │consolidado│
                                    └──────────┘

Conciencia del coste

El consumo de tokens de un sistema multi-agente suele ser de 3 a 5 veces el de un solo Agent. Al diseñar, hay que sopesar:

def estimate_cost(num_agents: int, avg_tokens_per_agent: int,
                  rounds: int = 1, price_per_1k: float = 0.01):
    """Estimar el coste en tokens de un sistema multi-agente"""
    total_tokens = num_agents * avg_tokens_per_agent * rounds
    return total_tokens * price_per_1k / 1000

# Ejemplo: 5 Agents, 2000 tokens cada uno, 3 rondas de discusión en pizarra
cost = estimate_cost(5, 2000, 3)
print(f"Coste estimado: ${cost:.2f}")
# La cifra real puede ser mayor, porque además hay que incluir
# la retransmisión del contenido de la pizarra

Gestión de memoria a corto plazo

Sin estado: la raíz del problema

Los modelos de lenguaje grandes son inherentemente sin estado (Stateless). Cada llamada a la API es independiente: no recuerda el contenido de la ronda anterior de conversación, tus preferencias ni los consensos alcanzados previamente.

# Estas dos llamadas son completamente independientes entre sí
response1 = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Me llamo Zhang San"}]
)
# Respuesta: "¡Hola Zhang San! ¿En qué puedo ayudarte?"

response2 = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "¿Cómo me llamo?"}]
)
# Respuesta: "Lo siento, no sé cómo te llamas, porque no hemos conversado antes."

Solución: mantener una lista de historial de conversación y enviar el historial completo en cada llamada.

class ConversationBuffer:
    """La memoria a corto plazo más simple: guardar el historial completo de conversación"""

    def __init__(self, system_prompt: str = ""):
        self.messages = []
        if system_prompt:
            self.messages.append({"role": "system", "content": system_prompt})

    def chat(self, user_input: str) -> str:
        self.messages.append({"role": "user", "content": user_input})
        response = client.chat.completions.create(
            model="gpt-4",
            messages=self.messages
        )
        reply = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": reply})
        return reply

La presión de la ventana de contexto

A medida que aumentan las rondas de conversación, el enfoque de historial completo enfrenta tres problemas fatales:

  1. Exceder la ventana de contexto: la longitud del historial supera el límite del modelo → error del programa
  2. Coste fuera de control: cada llamada reenvía todo el historial → consumo de tokens crece linealmente
  3. Dilución de la atención: en contextos largos, la capacidad del modelo para procesar información de la parte central disminuye significativamente

Tres estrategias de gestión de memoria

Estrategia uno: truncamiento por ventana fija (Context Truncation)

Conservar solo las últimas N rondas de conversación o N tokens.

class TruncationMemory:
    def __init__(self, max_tokens: int = 4000):
        self.max_tokens = max_tokens
        self.messages = []

    def add_and_truncate(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})

        # Eliminar desde los mensajes más antiguos hasta que el total de tokens esté dentro del límite
        while self._total_tokens() > self.max_tokens:
            self.messages.pop(0)  # eliminar el mensaje no-system más antiguo

    def _total_tokens(self):
        return sum(count_tokens(m["content"]) for m in self.messages)

Ventajas: implementación extremadamente simple, bajo coste computacional. Desventajas: si la información clave está en las primeras conversaciones, al ser truncada el Agent “pierde la memoria”.

Estrategia dos: resumen progresivo (Rolling Summary)

Antes de olvidar, extraer primero los puntos clave.

Historial de conversación: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8]
                          ↓ comprimir la primera mitad
         [Resumen(m1-m4), msg5, msg6, msg7, msg8]
                          ↓ seguir comprimiendo
         [Resumen(m1-m6), msg7, msg8]
class RollingSummaryMemory:
    def __init__(self, summary_trigger_tokens: int = 3000):
        self.summary_trigger = summary_trigger_tokens
        self.messages = []
        self.summary = ""

    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})

        if self._total_tokens() > self.summary_trigger:
            self._compress()

    def _compress(self):
        """Comprimir la primera mitad de la conversación en un resumen"""
        split_point = len(self.messages) // 2
        to_compress = self.messages[:split_point]
        remaining = self.messages[split_point:]

        compress_prompt = f"""
        Resume el siguiente historial de conversación en un párrafo conciso,
        conservando la información clave:

        Conversación:
        {format_messages(to_compress)}

        Resumen:"""

        resp = client.chat.completions.create(
            model="gpt-4o-mini",  # usar modelo ligero para resumir
            messages=[{"role": "user", "content": compress_prompt}]
        )
        self.summary = resp.choices[0].message.content

        # Reemplazar los mensajes comprimidos con el resumen
        self.messages = [
            {"role": "system", "content": f"Resumen del historial de conversación:\n{self.summary}"}
        ] + remaining

    def _total_tokens(self):
        return sum(count_tokens(m["content"]) for m in self.messages)

Ventajas: conserva la información central mientras comprime la longitud, manteniendo coherencia a largo plazo. Desventajas: coste adicional de llamadas API; la calidad del resumen afecta directamente las conversaciones posteriores.

Estrategia tres: recuperación vectorizada (Vector-based Retrieval)

La forma más inteligente: almacenar el historial de conversación en una base de datos vectorial y recuperar los recuerdos más relevantes bajo demanda.

class VectorBasedMemory:
    def __init__(self):
        self.conversations = []  # registro completo de conversaciones
        self.embeddings = []     # vector de cada ronda de conversación
        self.embed_model = "text-embedding-3-small"

    def store_conversation(self, user_msg: str, assistant_msg: str):
        """Almacenar una ronda de conversación y vectorizarla"""
        conversation_text = f"User: {user_msg}\nAssistant: {assistant_msg}"
        self.conversations.append(conversation_text)

        vec = client.embeddings.create(
            model=self.embed_model,
            input=conversation_text
        )
        self.embeddings.append(vec.data[0].embedding)

    def retrieve_relevant(self, current_query: str, top_k: int = 5):
        """Recuperar las conversaciones históricas más relevantes para la pregunta actual"""
        query_vec = client.embeddings.create(
            model=self.embed_model,
            input=current_query
        ).data[0].embedding

        # Calcular similitud
        similarities = [
            np.dot(query_vec, mem_vec) /
            (np.linalg.norm(query_vec) * np.linalg.norm(mem_vec))
            for mem_vec in self.embeddings
        ]

        top_indices = np.argsort(similarities)[-top_k:][::-1]
        return [self.conversations[i] for i in top_indices]

Ventajas: se libera fundamentalmente de la limitación de longitud de la ventana de contexto, coincidencia semántica precisa. Desventajas: mayor complejidad del sistema, introduce modelo de Embedding y base de datos vectorial.

Guía de selección de estrategias

EscenarioEstrategia recomendada
Chatbot de entretenimientoTruncamiento por ventana fija (simple y efectivo)
Atención al cliente (valor de la información decae rápidamente con el tiempo)Truncamiento por ventana fija
Creación de contenido largo / planificación de proyectosResumen progresivo
Asistente personalizado / interacción a largo plazoRecuperación vectorizada
Mejor prácticaUso híbrido: resumen + recuperación vectorial

Memoria a largo plazo y almacenamiento vectorial

De contexto pasivo a gestión activa de memoria

Un Agent verdaderamente inteligente no debería limitarse a recibir pasivamente un contexto procesado, sino que debería poder gestionar activamente su propia memoria: decidir por sí mismo cuándo recordar algo y cuándo recuperarlo.

Esto requiere proporcionar al Agent dos herramientas fundamentales:

# Herramientas de gestión de memoria disponibles para el Agent
memory_tools = [
    {
        "type": "function",
        "function": {
            "name": "record_to_memory",
            "description": "Almacenar información importante en la memoria a largo plazo. Usar cuando el usuario exprese preferencias explícitas, proporcione información clave o tome decisiones importantes.",
            "parameters": {
                "type": "object",
                "properties": {
                    "content": {
                        "type": "string",
                        "description": "Contenido a recordar"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["preference", "fact", "decision", "context"],
                        "description": "Categoría del recuerdo"
                    }
                },
                "required": ["content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "retrieve_from_memory",
            "description": "Recuperar información relevante de la memoria a largo plazo. Usar cuando sea necesario recordar preferencias del usuario, decisiones históricas o contenido discutido anteriormente.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Consulta de recuperación"
                    },
                    "category": {
                        "type": "string",
                        "description": "Opcional, limitar la categoría de recuerdos a recuperar"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

Memoria a corto plazo vs memoria a largo plazo

┌─────────────────────────────────────────────────────┐
│                Arquitectura del sistema de memoria    │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Memoria a corto plazo (Short-term Memory)            │
│  ┌───────────────────────────────────────────────┐  │
│  │ Almacenamiento: buffer de diálogo (lista de    │  │
│  │   messages)                                   │  │
│  │ Gestión: truncamiento / resumen               │  │
│  │ Ciclo de vida: sesión actual                  │  │
│  │ Función: mantener coherencia de sesión,        │  │
│  │   recordar "de qué se estaba hablando"         │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  Memoria a largo plazo (Long-term Memory)             │
│  ┌───────────────────────────────────────────────┐  │
│  │ Almacenamiento: BD vectorial + metadatos       │  │
│  │ Gestión: recuperación vectorizada + invocación │  │
│  │   como herramienta                            │  │
│  │ Ciclo de vida: entre sesiones                 │  │
│  │ Función: persistir información clave, soportar │  │
│  │   recuperación inteligente entre sesiones      │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

Mejores prácticas de gestión de memoria

  1. Recordar con criterio: recordar más no siempre es mejor. La información de bajo valor interfiere en las recuperaciones posteriores. Establece un mecanismo de admisión de escritura: solo escribir cuando el usuario lo solicite explícitamente o cuando la importancia de la información supere un umbral.

  2. Gobernanza continua: la memoria es un activo de datos dinámico. Limpia periódicamente la información obsoleta, fusiona entradas duplicadas y verifica la precisión de los hechos. Proporciona interfaces para que el usuario gestione su memoria (consultar, modificar, eliminar).

  3. Aplicación contextualizada: distintos escenarios requieren diferentes necesidades de memoria. En flujos de trabajo de documentación de cursos no se deben registrar preferencias personalizadas; para información factual de productos (parámetros de API, limitaciones de funcionalidad, etc.) sí se debe registrar y revisar periódicamente su vigencia.


Diseño del sistema de Skill

De Prompt a Skill: la evolución

Un Prompt cuidadosamente elaborado tiene un gran valor, pero solo surte efecto en la sesión actual. Al terminar la sesión, el conocimiento se dispersa. Skill supone la evolución del Prompt a un módulo de conocimiento profesional reutilizable, versionable y compartible en equipo.

Ruta evolutiva:

Prompt improvisado ("Revisa este curso para mí")
    ↓ Problema: hay que reescribirlo cada vez, criterios inconsistentes
Prompt fijo (guardado en el historial de chat)
    ↓ Problema: disperso, difícil de encontrar, no colaborativo
Archivo independiente (course-review.md)
    ↓ Problema: los archivos se hinchan, difícil de mantener
Directorio de base de conocimiento (course-review/)
    ↓ Problema: aún hay que decir manualmente al Agent qué hacer
Skill (SKILL.md + archivos de recursos + scripts)

Estructura de un Skill

Un Skill bien formado se compone de frontmatter YAML + cuerpo Markdown:

---
name: course-review
description: |
  Revisar la precisión técnica, corrección del código y calidad pedagógica del contenido del curso.
  Usar esta skill cuando el usuario solicite revisar, auditar o evaluar cursos o materiales de formación existentes.
---

# Skill de revisión de cursos

## Flujo de revisión
1. Extraer la estructura del directorio del Notebook para comprender la organización general de los capítulos
2. Revisar cada capítulo sección por sección, contrastando con las siguientes dimensiones:
   - Ejecutabilidad del código (ver [code-quality.md](code-quality.md))
   - Precisión del contenido (ver [content-accuracy.md](content-accuracy.md))
   - Estilo de explicación (ver [style-guide.md](style-guide.md))
   - API obsoletas (ver [outdated-api.md](outdated-api.md))
3. Consolidar los resultados de la revisión y generar el informe según el formato de salida

## Lista de anti-patrones
- ❌ No modificar nombres de variables ni números de versión de API en el código
- ❌ No omitir ninguna verificación
- ❌ No introducir nuevos conceptos técnicos durante la revisión

## Formato de salida
Para cada verificación, indicar: Aprobado/No aprobado/Requiere revisión humana + ubicación + sugerencia de modificación

Estructura del directorio:

course-review/
├── SKILL.md              # Punto de entrada principal de instrucciones
├── code-quality.md       # Elementos de verificación de ejecutabilidad del código
├── content-accuracy.md   # Elementos de verificación de precisión factual
├── style-guide.md        # Ejemplos positivos y negativos de estilo de explicación
├── outdated-api.md       # Tabla de referencia de API obsoletas
└── scripts/
    ├── extract_toc.py    # Extraer el directorio del Notebook
    └── validate_code.py  # Ejecutar validación automática de código

Skill vs RAG

Mucha gente confunde Skill y RAG. Su diferencia fundamental es:

RAGSkill
Problema que resuelve”El modelo no conoce cierto hecho""El modelo no sabe cómo hacerlo”
Tipo de informaciónConocimiento factual (contenido de documentos, parámetros de producto)Conocimiento procedimental (flujos, criterios, reglas de juicio)
Modo de activaciónSe recupera y se inyecta en el contextoSe despliega tras selección (el Agent decide si activar)
Modo de cargaInyección única de resultados de recuperaciónDivulgación progresiva (carga de subarchivos bajo demanda)
Ciclo de vidaCada consulta recupera de forma independientePersistente entre sesiones, versionable

Método de cinco pasos para escribir Skills de alta calidad

Paso 1: Determinar si vale la pena
  ├─ ¿Esta tarea tiene "intuición experta"? (un experto la hace bien pero un novato omite fácilmente condiciones de contorno)
  ├─ ¿Es suficientemente compleja? (si se puede hacer con GUI en 3 pasos, no)
  └─ ¿Se ejecutará repetidamente? (si es una sola vez, no)

Paso 2: Extraer qué escribir
  ├─ Extraer el árbol de decisión del experto, no solo los pasos
  ├─ Inyectar verificación de anti-patrones ("qué trampas evitar a toda costa")
  ├─ Patrón Template: proporcionar plantillas de salida estandarizadas
  └─ Patrón Examples: usar ejemplos en lugar de descripciones textuales

Paso 3: Redactar bien las instrucciones
  ├─ Concisión: cada frase debe justificar su coste en tokens
  ├─ Graduación de libertad: el grado de restricción debe ajustarse al riesgo de la tarea
  │   ├─ Baja libertad (migración de BD): scripts precisos
  │   ├─ Libertad media (generación de informes): pseudocódigo/parametrizado
  │   └─ Alta libertad (revisión de código): instrucciones textuales
  └─ Divulgación progresiva: el archivo principal se mantiene limpio, los detalles se cargan bajo demanda

Paso 4: Equipar con herramientas
  ├─ Flujo de trabajo: Checklist rastreable
  ├─ Bucle de retroalimentación: ejecutar → verificar → corregir → repetir
  ├─ Operaciones de alto riesgo: validar el plan antes de ejecutar
  └─ Scripts amigables para IA: estado estructurado + pistas de reparación + degradación elegante + seguridad idempotente

Paso 5: Validar e iterar
  ├─ Fase uno: establecer la línea base de evaluación (primero la evaluación, luego escribir el Skill)
  ├─ Fase dos: extraer el Skill (usar IA para resumir la información proporcionada repetidamente)
  └─ Fase tres: prueba iterativa con dos Agents (diseñador vs usuario)

Skill as Code

Tratar los Skills como código, beneficiándose de las metodologías de ingeniería de software:

  • Control de versiones: el directorio del Skill se incluye en Git, cada modificación tiene historial
  • Revisión de código: los Skills nuevos o modificados requieren revisión mediante PR
  • CI/CD: los cambios en Skills disparan pipelines de evaluación para asegurar que no haya regresiones
  • Compartición comunitaria: compartir y reutilizar Skills como librerías open-source

Divulgación progresiva y Skill as Code

Filosofía de diseño de la divulgación progresiva

La contradicción central a la que se enfrenta la ingeniería de prompts tradicional: demasiada información → contexto abarrotado, atención diluida; muy poca información → el Agent carece de conocimiento suficiente para tomar decisiones.

La divulgación progresiva (Progressive Disclosure) es un patrón de diseño que resuelve esta contradicción:

Nivel 1: Lista de Skills (visible al iniciar el Agent)
  ↓ El Agent elige activar un Skill
Nivel 2: SKILL.md (cargado tras la activación del Skill)
  ↓ El Agent ejecuta un paso determinado
Nivel 3: Subarchivos (cargar recursos correspondientes bajo demanda)
  ↓ Necesidad de verificar un detalle técnico concreto
Nivel 4: Ejecución de scripts (proporciona resultados deterministas)

Principios de diseño:

  • Mantener estructura plana, evitar anidamiento profundo de referencias
  • SKILL.md enlaza directamente todos los archivos de recursos, asegurando accesibilidad en “un solo paso”
  • No meter todo el contenido en SKILL.md: es solo el punto de entrada

Diseño de scripts amigables para IA

La calidad de la salida de los scripts de herramientas afecta directamente al rendimiento del Agent. Cuatro principios clave:

1. Retroalimentación de estado estructurada: emitir JSON en lugar de texto libre:

# ❌ Mala salida
print("Error: exit code 1")
print("Could not find module 'openpyxl'")

# ✅ Salida amigable para IA
print(json.dumps({
    "status": "failed",
    "error_code": "MODULE_NOT_FOUND",
    "missing_module": "openpyxl",
    "fix_hint": "Ejecuta pip install openpyxl para instalar la dependencia faltante",
    "fallback_available": True,
    "affected_cells": [42, 43, 44]
}))

2. Los mensajes de error incluyen pistas de reparación: decir al Agent “cómo arreglarlo”, no solo “qué falló”.

3. Degradación elegante en lugar de colapso: cuando sea posible, proporcionar valores por defecto y continuar.

4. Idempotencia y seguridad: soportar ejecución repetida sin efectos secundarios:

EscenarioNo idempotente (peligroso)Idempotente (seguro)
Escritura de archivosAñadir contenido cada vezVaciar primero y luego escribir
Operaciones de BDINSERT cada vezUsar UPSERT
Llamadas APICrear nuevo recurso cada vezUsar clave de idempotencia

Diseño del marco de evaluación

Diseño del framework de evaluación

Por qué “me da buena sensación” no es fiable

Optimizar un Agent basándose solo en sensaciones subjetivas conduce a:

  • Difícil de cuantificar: “se siente mejor” no sirve como base para decisiones de ingeniería
  • Falta de estándar: distintos evaluadores o distintos momentos pueden tener criterios cambiantes
  • Irreproducible: no se pueden hacer pruebas de regresión sistemáticas para asegurar que los nuevos cambios no rompen funcionalidades anteriores

El desarrollo guiado por evaluación (Evaluation-Driven Development) eleva la evaluación desde el final del flujo de desarrollo hasta una posición central:

         ┌──────────────────┐
         │  Evaluación =    │
         │  patrón de calidad│
         └────────┬─────────┘

    ┌─────────────┼─────────────┐
    ▼             ▼             ▼
Lo que se puede   Cuanto más rápida   Determina el
medir se puede    y precisa sea la    límite superior
mejorar           retroalimentación,  de la capacidad
                  más eficiente la    del producto
                  mejora

Evaluación end-to-end (End-to-End Evaluation)

La evaluación end-to-end se centra en la salida final y responde a la pregunta “¿este Agent le funciona bien al usuario?”.

Los indicadores de evaluación se dividen en dos tipos:

TipoDescripciónEjemplo
Indicadores objetivosSe pueden juzgar directamente mediante reglas de códigoSi el código se puede ejecutar, si el formato cumple el Schema, si la longitud está dentro del rango
Indicadores subjetivosImplican juicios semánticos y de calidadPrecisión del contenido, efectividad pedagógica, si el estilo de lenguaje cumple las normas

Evaluación de caja blanca (White-box Evaluation)

Cuando el flujo del Agent se vuelve complejo, la evaluación end-to-end no permite localizar problemas concretos. La evaluación de caja blanca propone profundizar en el interior del sistema, diseñando sistemas de evaluación independientes para los componentes clave.

Evaluación end-to-end (solo mira la salida final):
  Agent completo → puntuación 4.2/5 → pero no se sabe dónde está el problema

Evaluación de caja blanca (revisa los pasos intermedios):
  ┌────────────┐    ┌────────────┐    ┌────────────┐
  │ Explicación │    │ Generación  │    │ Ajuste de  │
  │ conceptual  │    │ de código   │    │ estilo     │
  │ 3.1/5      │    │ 4.8/5      │    │ 4.5/5      │
  └────────────┘    └────────────┘    └────────────┘
       ↑ ¡El cuello de botella está aquí!

Ventajas principales de la evaluación de caja blanca:

  • Señal clara: señal de mejora sin interferencias, enfocada en el verdadero cuello de botella
  • Iteración rápida: solo hay que probar un componente, sin ejecutar todo el flujo
  • Optimización precisa: el efecto de cada cambio se puede medir con precisión

LLM-as-Judge

Usar modelos grandes como evaluadores

Hacer que otro modelo grande actúe como “experto evaluador”, puntuando automáticamente según indicadores y rúbricas definidos.

def llm_judge_evaluate(generated_output: str, criteria: dict):
    """Usar LLM como evaluador"""
    judge_prompt = f"""
    Eres un evaluador profesional de calidad de cursos. Puntúa según los siguientes criterios:

    Criterios de evaluación:
    {json.dumps(criteria, indent=2, ensure_ascii=False)}

    Contenido a evaluar:
    {generated_output}

    Proporciona el resultado de la puntuación en formato JSON:
    {{
        "scores": {{
            "accuracy": <1-5>,
            "clarity": <1-5>,
            "engagement": <1-5>
        }},
        "overall": <1-5>,
        "comments": "Valoración global"
    }}"""

    resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": judge_prompt}],
        response_format={"type": "json_object"}
    )
    return json.loads(resp.choices[0].message.content)

Sesgos del evaluador LLM

Al usar un LLM como evaluador, hay que estar alerta ante sus sesgos inherentes:

Tipo de sesgoManifestaciónMétodo de mitigación
Sesgo de estiloPreferencia por cierto estilo de código/escrituraDefinir criterios de puntuación explícitos, no depender del “gusto”
Sesgo de longitudConsiderar que las respuestas más largas son “más completas”Incluir “concisión” como dimensión de puntuación
Sesgo del “buen chico”Tendencia a dar evaluaciones positivasUsar evaluación comparativa (A vs B, ¿cuál es mejor?)
Sesgo de posiciónTendencia a elegir contenido en posiciones específicas de la listaAleatorizar el orden del contenido evaluado

Mejor práctica: en las fases iniciales, usar expertos humanos para establecer un “conjunto de pruebas dorado” y usarlo para calibrar al evaluador LLM. Verificar periódicamente la consistencia de la evaluación automática mediante muestreo manual.

Desglose de indicadores de evaluación

Descomponer objetivos de evaluación difusos en reglas concretas que se puedan verificar una a una:

# Ejemplo de desglose de evaluación de calidad de contenido
dimensión_de_evaluación: "Calidad del contenido"
reglas:
  - id: "pain_point"
    description: "¿Comienza con un punto de dolor concreto?"
    type: "boolean"
  - id: "theory_depth"
    description: "¿Señala claramente las limitaciones del enfoque inicial e introduce la teoría central?"
    type: "boolean"
  - id: "code_relevance"
    description: "¿El ejemplo de código está estrechamente relacionado con la teoría explicada y es suficientemente simplificado?"
    type: "boolean"
  - id: "anti_pattern_check"
    description: "¿Evita frases cursis como '¡felicidades por desbloquear una nueva habilidad!'?"
    type: "boolean"

Iteración guiada por evaluación

El ciclo cerrado de evaluación

La evaluación no es algo puntual, sino un motor que impulsa la mejora continua:

    ┌──────────────────────────────────┐
    │                                  │
    ▼                                  │
┌─────────┐   ┌──────────┐   ┌─────────┐
│Construir│ → │Detectar  │ → │Refinar  │
│  MVP    │   │problemas │   │indicadores│
└─────────┘   └──────────┘   └─────────┘


┌─────────┐   ┌──────────┐   ┌─────────┐
│Desplegar│ ← │Prueba de │ ← │Optimizar│
│a prod.  │   │regresión │   │y mejorar│
└─────────┘   └──────────┘   └─────────┘

                                  └────→ (ciclo)

Expertos de negocio liderando los criterios de evaluación

Los indicadores de evaluación (especialmente los subjetivos) deben ser definidos por los expertos de negocio más veteranos:

  1. Movilizar con objetivos de negocio: no decir “ayúdanos a definir indicadores de evaluación”, sino “este Agent te va a ayudar a reducir el ciclo de producción de cursos de 2 semanas a 3 días, manteniendo más del 90% de satisfacción de usuarios”.

  2. Proporcionar herramientas estructuradas para bajar la barrera: plantillas de escalas de puntuación, herramientas de anotación de casos, preguntas guía como “si solo pudieras mirar tres indicadores para juzgar la calidad de un curso, ¿cuáles elegirías?”.

  3. Establecer mecanismos de colaboración continua: en las reuniones semanales de revisión, los expertos miran los datos y el equipo técnico ajusta el sistema, tomando decisiones conjuntas.

La palanca de eficiencia de la evaluación

No toda evaluación necesita ser completamente automática y de cobertura total. Empieza por el método más simple:

Nivel 1: Muestreo manual → "Copiar el código y ejecutarlo"
Nivel 2: Scripts automatizados → "Escribir un script para ejecución por lotes"
Nivel 3: Pipeline de evaluación integrado → Integración CI/CD, activación automática en cada PR
Nivel 4: Monitorización continua → Monitorización en tiempo real de indicadores clave en producción

Cada nivel tiene un retorno de inversión diferente. En las fases iniciales, el Nivel 1 tiene el mayor retorno: detecta problemas más rápido y con el menor coste de implementación. A medida que el sistema madura, se evoluciona gradualmente hacia niveles superiores.


Estrategia de despliegue de modelos

Marco de análisis de requisitos de negocio

Al publicar una aplicación de modelo grande en producción, el primer paso no es la selección tecnológica, sino el análisis de requisitos:

┌────────────────────────────────────────────┐
│           Matriz de análisis de requisitos   │
│                de negocio                   │
├────────────────────────────────────────────┤
│                                            │
│  Requisitos funcionales (qué hacer):         │
│  ├─ Procesamiento de lenguaje natural → LLM general │
│  ├─ Generación de código → LLM optimizado para código │
│  ├─ Razonamiento matemático → LLM fine-tuned en mates │
│  ├─ Comprensión visual → Modelo multimodal  │
│  └─ Procesamiento de voz → Modelo de voz    │
│                                            │
│  Requisitos no funcionales (cómo hacerlo):   │
│  ├─ Rendimiento: TTFT < 500ms, TPOT < 50ms │
│  ├─ Coste: llamada individual < $0.01       │
│  ├─ Estabilidad: 99.9% disponibilidad       │
│  ├─ Seguridad: filtrado de contenido,       │
│  │   protección de privacidad               │
│  └─ Cumplimiento: requisitos regulatorios   │
│                                            │
└────────────────────────────────────────────┘

Estrategia de selección de modelos

No todos los escenarios requieren el modelo más grande. La selección de modelos sigue el principio de “mínimo viable”:

Complejidad de la tarea

    │  ┌──────────────────────────┐
    │  │ Modelo grande (GPT-4, Claude)│
    │  │ - Razonamiento complejo   │
    │  │ - Planificación multi-paso│
    │  │ - Generación creativa     │
    │  └──────────────────────────┘
    │  ┌──────────────────────────┐
    │  │ Modelo medio (GPT-4o-mini) │
    │  │ - Reconocimiento de intención│
    │  │ - Extracción estructurada │
    │  │ - Generación de resúmenes │
    │  └──────────────────────────┘
    │  ┌──────────────────────────┐
    │  │ Modelo pequeño / destilado│
    │  │ - Clasificación de texto  │
    │  │ - Coincidencia de keywords│
    │  │ - Validación de formato   │
    │  └──────────────────────────┘
    └─────────────────────────────────→ Frecuencia de invocación

Destilación: dotar a modelos pequeños de capacidad profesional

La idea central de la destilación: “copiar” la capacidad de juicio del modelo grande al modelo pequeño.

Modelo profesor (GPT-4)      Modelo estudiante (0.6B parámetros)
      │                         │
      │  Generar datos anotados   │
      ├─────────────────────────→│
      │  "Comprende la intención  │  Aprender los patrones
      │   de la solicitud"        │  de comportamiento
      │  [pares entrada→salida]   │  del profesor
      │                         │
      │  Resultado: el modelo     │
      │  pequeño se acerca al     │
      │  nivel del profesor en    │
      │  tareas específicas       │

Destilación vs fine-tuning:

Fine-tuningDestilación
Origen de datosAnotación humanaGenerados por el modelo profesor
Coste de datosAltoBajo (coste de llamadas API)
Escala de datosLimitadaSe puede generar a gran escala
Techo de calidadDepende del anotadorDepende del modelo profesor

Tres rutas de destilación:

RutaRecursos necesariosCaso de uso
Destilación por síntesis de datos (caja negra)Solo acceso APITareas estructuradas, profesor API comercial
Destilación de conocimiento KD (caja blanca)Pesos del modelo profesorProfesor open-source, necesita mayor precisión
Compresión de razonamientoTrazas de razonamiento del profesorTareas de razonamiento multi-paso (como DeepSeek-R1)

Optimización de inferencia

Marco de optimización de rendimiento

La optimización de inferencia de LLM se divide en cuatro direcciones:

Procesar solicitudes más rápido

  • Miniaturización del modelo: elegir variantes con menos parámetros
  • Cuantización: cuantización INT4/INT8/FP16 para reducir requisitos de recursos computacionales
  • Poda: eliminar pesos redundantes, reducir complejidad del modelo
  • Destilación de conocimiento: entrenar modelos pequeños con datos del modelo grande
Comparativa de precisión de cuantización:
  FP32 (precisión completa) → rendimiento base, mayor coste computacional
  FP16 (media precisión)   → ~2x aceleración, precisión casi sin pérdida
  INT8                     → ~4x aceleración, pequeña pérdida de precisión
  INT4                     → ~8x aceleración, requiere evaluar cuidadosamente el impacto en precisión

Reducir el número de solicitudes procesadas

  • Caché de contexto (Context Cache): cachear el prefijo común de diálogos multi-turno para reducir cálculos repetidos
  • Procesamiento por lotes (Batching): combinar múltiples solicitudes en un lote para mejorar la utilización del hardware
  • Caché de resultados: devolver directamente resultados cacheados para preguntas frecuentes idénticas
# Aplicación típica de caché de contexto
# En diálogos multi-turno, el System Prompt + documentos de conocimiento histórico son el prefijo común
# Primera ronda: cálculo completo (precio completo)
# Rondas posteriores: la parte cacheada se factura al 20% del precio

Reducir tokens de entrada y salida

  • Lado de entrada: simplificar la entrada, eliminar información redundante, generar primero un resumen de documentos largos
  • Lado de salida: guiar respuestas concisas mediante Prompt, establecer max_tokens razonable

La filosofía de diseño de max_tokens: es una válvula de seguridad, no un mecanismo de control de contenido. Las respuestas breves y semánticamente completas deben guiarse mediante Prompt; max_tokens es más adecuado como última línea de defensa para el control de costes.

Procesamiento en paralelo

La inferencia de modelos grandes es esencialmente cálculo matricial a gran escala. Comprender las diferencias entre CPU y GPU:

CPUGPU
Número de núcleosPocos núcleos potentes (8-64)Gran cantidad de núcleos simples (miles)
Tareas adecuadasLógica compleja, secuencialCálculo matricial masivamente paralelo

Estrategias de paralelización en GPU:

  • Paralelismo de datos: distribuir fragmentos de datos entre múltiples GPUs
  • Paralelismo de modelo: distribuir distintas capas del modelo entre diferentes dispositivos
  • Paralelismo de pipeline: dividir el proceso de cálculo en etapas ejecutadas secuencialmente

No dependas por defecto del modelo grande

En muchos escenarios, enfoques más simples resultan más eficientes:

EscenarioAlternativa
Mensajes de confirmación estándarPlantillas hardcodeadas + selección aleatoria de variantes
Respuestas con opciones limitadasPrecalcular todos los resultados posibles, emparejar por entrada
Presentación de datosUsar gráficos, tablas y otras UI tradicionales en lugar de descripciones generadas por LLM
Coincidencia de palabras claveEn reconocimiento de intención, filtrar primero por palabras clave y solo si es necesario llamar al LLM

Barreras de seguridad

Amenazas de seguridad para los modelos grandes

Las aplicaciones con modelos grandes enfrentan amenazas de seguridad en múltiples niveles, que requieren estrategias de defensa sistemáticas:

Bareras de seguridad

Matriz de estrategias de defensa

Tipo de ataqueMétodo de ataqueMedida defensiva
Inyección de promptsInducir al modelo a sobrescribir instrucciones del sistemaDetección con barreras de seguridad integradas + aislamiento estricto de entrada de usuario e instrucciones del sistema
Inyección de comandosIncrustar código malicioso en la solicitudAuditoría previa a la ejecución + privilegios mínimos
Fuga de promptsInducir al modelo a revelar su propio System PromptLas barreras de seguridad identifican patrones de sondeo
Envenenamiento de base de conocimientoSubir documentos con información erróneaFlujo de aprobación de ingesta de conocimiento + escaneo previo de contenido
Robo de modeloRecopilar datos de entrenamiento mediante gran volumen de llamadas APILimitación de tasa API + identificación de tráfico de bots
Invocación maliciosa de funcionesInducir al Agent a ejecutar operaciones peligrosasAuditoría previa a invocación de herramientas + mecanismo de fusible

Implementación de ingeniería de barreras de seguridad

class SafetyGuard:
    """Implementación de barreras de seguridad multicapa"""

    def __init__(self):
        self.blocked_keywords = set()    # Palabras sensibles personalizadas
        self.rate_limits = {}            # Registro de limitación de tasa
        self.max_tool_calls = 10         # Máximo de invocaciones de herramienta del Agent
        self.dangerous_commands = {      # Lista negra de comandos peligrosos
            "rm -rf", "DROP TABLE", "DELETE FROM",
            "os.system", "subprocess.call", "eval("
        }

    def check_input(self, user_input: str) -> tuple[bool, str]:
        """Verificación de seguridad de entrada"""
        # 1. Detección de palabras sensibles
        for keyword in self.blocked_keywords:
            if keyword in user_input.lower():
                return False, f"La entrada contiene palabra sensible: {keyword}"

        # 2. Detección de inyección de comandos
        for dangerous in self.dangerous_commands:
            if dangerous.lower() in user_input.lower():
                return False, f"Se detectó posible instrucción peligrosa: {dangerous}"

        return True, "Aprobado"

    def check_tool_call(self, tool_name: str, args: dict) -> tuple[bool, str]:
        """Auditoría previa a la invocación de herramienta"""
        # 1. Verificar frecuencia de invocación
        if self.rate_limits.get(tool_name, 0) >= self.max_tool_calls:
            return False, f"La herramienta {tool_name} excedió el límite de invocaciones"

        # 2. Verificar seguridad de parámetros
        args_str = json.dumps(args).lower()
        for dangerous in self.dangerous_commands:
            if dangerous.lower() in args_str:
                return False, f"Los parámetros de la herramienta contienen instrucción peligrosa: {dangerous}"

        self.rate_limits[tool_name] = self.rate_limits.get(tool_name, 0) + 1
        return True, "Aprobado"

    def check_output(self, output: str) -> tuple[bool, str]:
        """Revisión de contenido de salida"""
        # Detectar si contiene patrones de información sensible que no deberían mostrarse
        sensitive_patterns = [
            r'\b\d{17}[\dXx]\b',           # Número de identificación
            r'\b1[3-9]\d{9}\b',            # Número de teléfono móvil
            r'[Pp]assword\s*[:=]\s*\S+',  # Patrón de contraseña
        ]

        for pattern in sensitive_patterns:
            if re.search(pattern, output):
                return False, f"La salida podría contener información sensible: {pattern}"

        return True, "Aprobado"

Mecanismo de fusible (Circuit Breaker)

Establecer límites de recursos explícitos para cada tarea del Agent:

class CircuitBreaker:
    """Fusible del Agent: evita que los bucles descontrolados causen grandes pérdidas"""

    def __init__(self,
                 max_api_calls: int = 10,      # máximo de llamadas API
                 max_wall_time: int = 300,      # tiempo máximo de ejecución (segundos)
                 max_cost: float = 0.50):       # coste máximo (dólares)
        self.max_api_calls = max_api_calls
        self.max_wall_time = max_wall_time
        self.max_cost = max_cost
        self.reset()

    def reset(self):
        self.api_calls = 0
        self.start_time = time.time()
        self.total_cost = 0.0

    def check(self) -> tuple[bool, str]:
        """Verificar si se debe activar el fusible"""
        self.api_calls += 1
        elapsed = time.time() - self.start_time

        if self.api_calls > self.max_api_calls:
            return False, f"Límite de llamadas API excedido ({self.api_calls}/{self.max_api_calls})"
        if elapsed > self.max_wall_time:
            return False, f"Tiempo de ejecución excedido ({elapsed:.0f}s/{self.max_wall_time}s)"
        if self.total_cost > self.max_cost:
            return False, f"Coste excedido (${self.total_cost:.2f}/${self.max_cost:.2f})"

        return True, "Normal"

Harness Engineering

El plano general del entorno de producción

Harness Engineering es el conjunto completo de prácticas de ingeniería para garantizar que el sistema Agent funcione de forma estable desde el desarrollo hasta la producción. Incluye:

Harness Engineering

Observabilidad

Usar el estándar OpenTelemetry para establecer tres tipos de recolección de datos:

  • Metrics (métricas): consumo de tokens, distribución de latencia, tasa de errores
  • Traces (trazas): cada etapa por la que pasa una solicitud y su duración
  • Logs (registros): entrada y salida de cada etapa, pilas de errores, información de auditoría
# Usar OpenTelemetry para instrumentar llamadas del Agent
from opentelemetry import trace
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# Instrumentar automáticamente llamadas a la API de OpenAI
OpenAIInstrumentor().instrument()

tracer = trace.get_tracer(__name__)

@tracer.start_as_current_span("agent_task")
def run_agent_task(task: str):
    # El span registra automáticamente duración y contexto
    with tracer.start_as_current_span("llm_call") as span:
        span.set_attribute("task", task)
        result = agent.run(task)
        span.set_attribute("tokens_used", result.usage.total_tokens)
        return result

Checklist previa al despliegue

Antes de llevar un Agent a producción, completa las siguientes verificaciones:

☐ SLO definidos
  ├─ TTFT (tiempo hasta el primer token) objetivo: _____ ms
  ├─ TPOT (tiempo por token generado) objetivo: _____ ms
  └─ Objetivo de disponibilidad: _____%

☐ Control de costes
  ├─ Presupuesto máximo por llamada: $_____
  ├─ Límite diario de consumo de tokens: _____ tokens
  └─ Umbrales de alerta configurados

☐ Protección de seguridad
  ├─ Verificación de seguridad de entrada activada
  ├─ Revisión de contenido de salida activada
  ├─ Fusible de comportamiento del Agent configurado
  └─ Alertas de monitorización de seguridad configuradas

☐ Plan de recuperación ante desastres
  ├─ Ruta de degradación de modelo definida
  ├─ Lógica de respaldo para rutas críticas validada
  └─ Simulacro de recuperación de fallos completado

☐ Línea base de evaluación
  ├─ Puntuación de evaluación end-to-end: _____
  ├─ Puntuación de evaluación por componentes: _____
  └─ Pipeline de pruebas de regresión integrado

☐ Observabilidad
  ├─ OpenTelemetry integrado
  ├─ Dashboard de indicadores clave creado
  └─ Reglas de alerta configuradas y validadas

Estrategia de despliegue progresivo

No hacer un lanzamiento completo de una sola vez. Adoptar una estrategia progresiva:

Fase 1: Pruebas internas (1-2 semanas)
  └─ Usado por el equipo, recopilar feedback inicial

Fase 2: Canary release a pequeña escala (5% de usuarios)
  └─ Comparar indicadores de evaluación entre el sistema nuevo y el antiguo

Fase 3: Ampliación del alcance (25% → 50% → 100%)
  └─ Permanecer 3-5 días en cada etapa observando indicadores

Fase 4: Lanzamiento completo
  └─ Mantener monitorización, establecer ciclo de mejora continua

Mejores prácticas de operación en producción

Gestión de línea base de evaluación:

  • Establecer la versión actual en producción como línea base; cualquier versión nueva debe superarla
  • Re-evaluar periódicamente (semanalmente) la línea base y las versiones candidatas con los datos más recientes
  • Integrar la verificación de línea base en el pipeline de despliegue; bloquear automáticamente las versiones que no la alcancen

Estrategia de degradación por capas:

  1. Modelo principal no disponible → cambiar al modelo de respaldo
  2. Modelo de respaldo también no disponible → usar respuestas frecuentes cacheadas
  3. Caché no acierta → devolver plantilla de respuesta de degradación predefinida

Gobernanza de costes:

  • Analizar el consumo de tokens por modelo, por usuario y por tipo de tarea
  • Identificar picos anómalos de coste y configurar alertas
  • Revisar periódicamente: ¿hay contextos largos innecesarios, System Prompts redundantes?

Destilación de modelos: enseñar a modelos pequeños conocimiento de dominio

Por qué importa la destilación

Los modelos grandes (GPT-4, Claude, Qwen-72B) ofrecen excelente calidad pero conllevan altos costes de inferencia y latencia. Para sistemas en producción que manejan miles de solicitudes por minuto, el coste en tokens puede llegar a ser prohibitivo. La destilación de modelos ofrece una solución práctica: usar las salidas del modelo grande como datos de entrenamiento para enseñar a un modelo pequeño (7B-14B) a replicar el mismo comportamiento a una fracción del coste.

Modelo profesor (GPT-4, 175B parámetros)

  ├── Generar respuestas de alta calidad para tareas de dominio


Datos de entrenamiento (pares entrada → salida_del_profesor)

  ├── Fine-tuning del modelo estudiante


Modelo estudiante (7B-14B parámetros)

  ├── Misma calidad, coste 10-50x menor
  └── Latencia 5-10x menor

Pipeline de destilación

Paso 1: Definir el alcance de la tarea

La destilación funciona mejor cuando la tarea está bien definida y es repetitiva. Identifica los dominios específicos donde necesitas que el modelo pequeño rinda:

# Ejemplo: clasificación de intenciones de soporte al cliente
task_definitions = [
    {
        "name": "intent_classification",
        "input_schema": {"user_message": "string", "context": "string"},
        "output_schema": {"intent": "string", "confidence": "float", "reasoning": "string"},
    },
    {
        "name": "response_generation",
        "input_schema": {"intent": "string", "knowledge": "string", "tone": "string"},
        "output_schema": {"response": "string", "sources": ["string"]},
    },
]

Paso 2: Generar datos de entrenamiento con el modelo profesor

Usar el modelo grande para generar ejemplos de alta calidad para tu dominio:

from openai import OpenAI

teacher = OpenAI(api_key="...")  # GPT-4 o similar
student = OpenAI(base_url="http://localhost:8000/v1")  # Tu modelo pequeño

def generate_training_examples(task_def: dict, n_examples: int = 1000) -> list:
    examples = []
    for i in range(n_examples):
        # Generar entradas diversas
        input_prompt = f"Genera una entrada realista para la tarea '{task_def['name']}'. Varía la complejidad y los casos extremos."
        input_resp = teacher.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": input_prompt}],
        )
        user_input = input_resp.choices[0].message.content

        # Generar salida del profesor
        teacher_resp = teacher.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": f"Eres un experto en {task_def['name']}. Sigue este esquema de salida: {task_def['output_schema']}"},
                {"role": "user", "content": user_input},
            ],
        )
        teacher_output = teacher_resp.choices[0].message.content

        examples.append({"input": user_input, "output": teacher_output})

    return examples

training_data = generate_training_examples(task_definitions[0], n_examples=2000)

Paso 3: Fine-tuning del modelo estudiante

Usar LoRA o fine-tuning completo para entrenar el modelo pequeño con las salidas del profesor:

# Usando Hugging Face Transformers + PEFT para fine-tuning con LoRA
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model

model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Configurar LoRA para fine-tuning eficiente
lora_config = LoraConfig(
    r=16,  # Rango de LoRA
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Salida: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622

# Entrenar con los datos de destilación
# ... (bucle de entrenamiento estándar)

Paso 4: Evaluar e iterar

Comparar las salidas del modelo estudiante con las del profesor en un conjunto de prueba separado:

def evaluate_student_vs_teacher(test_set: list, teacher_client, student_client) -> dict:
    results = {"exact_match": 0, "semantic_similarity": 0, "total": len(test_set)}

    for example in test_set:
        student_resp = student_client.chat.completions.create(
            model="student-7b",
            messages=[{"role": "user", "content": example["input"]}],
        )
        student_output = student_resp.choices[0].message.content
        teacher_output = example["output"]

        # Verificación de coincidencia exacta
        if student_output.strip() == teacher_output.strip():
            results["exact_match"] += 1

        # Similitud semántica (usando embeddings)
        # ... (calcular similitud de coseno entre las salidas)

    results["exact_match_rate"] = results["exact_match"] / results["total"]
    return results

# Objetivo: >85% de similitud semántica con el profesor

Cuándo destilar

EscenarioRecomendación
Tareas de alto volumen y repetitivas (clasificación, extracción)Destilar — el ahorro de costes es enorme
Generación creativa y abiertaMantener profesor — la calidad importa más que el coste
Aplicaciones sensibles a latencia (chat en tiempo real)Destilar — los modelos pequeños son 5-10x más rápidos
Tareas de razonamiento raras y complejasMantener profesor — los modelos pequeños struggled con razonamiento nuevo
Híbrido: tareas simples + casos extremos complejosEnrutar — el modelo pequeño maneja el 80%, escalar al profesor el 20%

Mejores prácticas de producción: monitoreo, canary releases, A/B testing

Stack de observabilidad

Los sistemas Agent en producción necesitan observabilidad integral. Configurar las siguientes capas de monitoreo:

1. Métricas a nivel de aplicación

import time
from prometheus_client import Counter, Histogram, Gauge

# Métricas de solicitudes
request_counter = Counter('agent_requests_total', 'Total de solicitudes', ['model', 'intent', 'status'])
request_latency = Histogram('agent_request_duration_seconds', 'Latencia de solicitudes', ['model', 'intent'])
active_sessions = Gauge('agent_active_sessions', 'Sesiones de conversación activas')

# Métricas de uso de tokens
token_usage = Counter('agent_tokens_total', 'Uso de tokens', ['model', 'type'])  # type: input/output

# Métricas de calidad
hallucination_rate = Gauge('agent_hallucination_rate', 'Tasa de alucinaciones estimada')
user_satisfaction = Histogram('agent_user_satisfaction', 'Puntuación de satisfacción del usuario', buckets=[1, 2, 3, 4, 5])

def track_request(model: str, intent: str, status: str, latency: float, tokens_in: int, tokens_out: int):
    request_counter.labels(model=model, intent=intent, status=status).inc()
    request_latency.labels(model=model, intent=intent).observe(latency)
    token_usage.labels(model=model, type='input').inc(tokens_in)
    token_usage.labels(model=model, type='output').inc(tokens_out)

2. Trazado distribuido

Usar OpenTelemetry para trazar solicitudes a través del pipeline del Agent:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

def process_user_request(user_input: str):
    with tracer.start_as_current_span("process_request") as span:
        span.set_attribute("user.input_length", len(user_input))

        with tracer.start_as_current_span("retrieve_context") as retrieve_span:
            context = retrieve_relevant_docs(user_input)
            retrieve_span.set_attribute("docs.retrieved", len(context))

        with tracer.start_as_current_span("llm_generate") as llm_span:
            llm_span.set_attribute("model", "gpt-4")
            response = call_llm(user_input, context)
            llm_span.set_attribute("tokens.used", response.usage.total_tokens)

        return response

3. Estrategia de logging

import structlog

logger = structlog.get_logger()

def log_agent_event(event_type: str, **kwargs):
    logger.info(
        event_type,
        session_id=kwargs.get("session_id"),
        user_id=kwargs.get("user_id"),
        model=kwargs.get("model"),
        intent=kwargs.get("intent"),
        latency_ms=kwargs.get("latency_ms"),
        tokens_in=kwargs.get("tokens_in"),
        tokens_out=kwargs.get("tokens_out"),
        error=kwargs.get("error"),
    )

# Ejemplo de uso
log_agent_event(
    "request_completed",
    session_id="abc123",
    user_id="user_456",
    model="gpt-4",
    intent="faq_answer",
    latency_ms=1234,
    tokens_in=500,
    tokens_out=200,
)

Estrategia de canary release

Lanzar cambios gradualmente para minimizar el riesgo:

Fase 1: Canary (5% del tráfico)
  └─ Ejecutar nuevo modelo/configuración en un subconjunto pequeño
  └─ Monitorear tasas de error, latencia, satisfacción
  └─ Duración: 1-3 días

Fase 2: Ampliación (25% → 50%)
  └─ Aumentar el tráfico gradualmente
  └─ Comparar métricas contra la línea base
  └─ Duración: 3-5 días por etapa

Fase 3: Lanzamiento completo (100%)
  └─ Todo el tráfico en la nueva versión
  └─ Continuar monitoreando
  └─ Mantener la versión anterior disponible para rollback rápido
# Enrutamiento canary simple basado en hash de user ID
import hashlib

def route_to_version(user_id: str, canary_percent: int = 5) -> str:
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    bucket = hash_val % 100
    return "canary" if bucket < canary_percent else "stable"

# En el manejador de solicitudes
def handle_request(user_id: str, user_input: str):
    version = route_to_version(user_id, canary_percent=5)

    if version == "canary":
        response = call_new_model(user_input)
    else:
        response = call_stable_model(user_input)

    # Rastrear métricas por versión
    track_request(model=version, ...)
    return response

Framework de A/B testing

Comparar dos versiones directamente para medir el impacto:

from dataclasses import dataclass
from enum import Enum

class Variant(Enum):
    CONTROL = "control"
    TREATMENT = "treatment"

@dataclass
class ABTestResult:
    variant: Variant
    total_requests: int
    avg_latency_ms: float
    success_rate: float
    user_satisfaction: float  # escala 1-5

def run_ab_test(user_id: str, user_input: str) -> tuple[str, str]:
    """Devuelve (variante, respuesta)"""
    # Asignación consistente basada en user ID
    hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    variant = Variant.CONTROL if hash_val % 2 == 0 else Variant.TREATMENT

    if variant == Variant.CONTROL:
        response = call_control_model(user_input)
    else:
        response = call_treatment_model(user_input)

    return variant.value, response

def analyze_ab_results(results_a: list, results_b: list) -> dict:
    """Comparar métricas entre control y tratamiento"""
    import statistics

    def compute_metrics(results):
        return {
            "count": len(results),
            "avg_latency": statistics.mean([r["latency_ms"] for r in results]),
            "success_rate": sum(1 for r in results if r["success"]) / len(results),
        }

    metrics_a = compute_metrics(results_a)
    metrics_b = compute_metrics(results_b)

    return {
        "control": metrics_a,
        "treatment": metrics_b,
        "latency_improvement": (metrics_a["avg_latency"] - metrics_b["avg_latency"]) / metrics_a["avg_latency"] * 100,
        "success_rate_delta": (metrics_b["success_rate"] - metrics_a["success_rate"]) * 100,
    }

Reglas de alertas

Configurar alertas para condiciones críticas:

# Reglas de alertas de Prometheus
groups:
  - name: agent_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(agent_requests_total{status="error"}[5m]) / rate(agent_requests_total[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Tasa de error del Agent superior al 5%"

      - alert: HighLatency
        expr: histogram_quantile(0.95, rate(agent_request_duration_seconds_bucket[5m])) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Latencia P95 superior a 10 segundos"

      - alert: TokenCostSpike
        expr: rate(agent_tokens_total[1h]) > 100000
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Pico de uso de tokens detectado"

Metodología RIDE: un marco para el impacto empresarial impulsado por IA

El desafío

Las organizaciones que se apresuran a adoptar IA a menudo caen en una de dos trampas:

  1. Solución buscando problema: construir demos de IA impresionantes que no abordan necesidades de negocio reales
  2. Parálisis por análisis: evaluación interminable de herramientas de IA sin lanzar nada

La metodología RIDE proporciona un enfoque estructurado para seleccionar, implementar y medir iniciativas de IA que entregan valor de negocio real.

RIDE: Research → Implement → Deliver → Enhance

┌─────────────────────────────────────────────────────────────┐
│                      Ciclo RIDE                              │
│                                                              │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────┐ │
│   │ Research │───▶│Implement │───▶│ Deliver  │───▶│Enhance│ │
│   │          │    │          │    │          │    │      │ │
│   └──────────┘    └──────────┘    └──────────┘    └──────┘ │
│        ▲                                              │     │
│        └──────────────────────────────────────────────┘     │
│                       (iterar)                               │
└─────────────────────────────────────────────────────────────┘

Fase 1: Research — Seleccionar el problema correcto

Objetivo: identificar casos de uso de IA de alto impacto y factibles, alineados con las prioridades del negocio.

Actividades clave:

  1. Mapear puntos de dolor del negocio: entrevistar stakeholders, analizar tickets de soporte, revisar cuellos de botella de procesos
  2. Puntuar oportunidades usando la matriz Impacto-Factibilidad:
@dataclass
class UseCase:
    name: str
    description: str
    business_impact: int      # 1-10: impacto en ingresos, ahorro de costes, satisfacción del cliente
    technical_feasibility: int # 1-10: disponibilidad de datos, capacidad del modelo, complejidad de integración
    time_to_value: int         # semanas hasta el MVP
    stakeholders: list[str]

def score_use_case(uc: UseCase) -> float:
    """Mayor puntuación = mejor candidato para iniciativa de IA"""
    return (uc.business_impact * 0.4 + uc.technical_feasibility * 0.3 +
            (10 - uc.time_to_value / 4) * 0.3)  # Normalizar tiempo a escala 1-10

# Ejemplo de puntuación
use_cases = [
    UseCase("FAQ Bot", "Automatizar respuestas a FAQs de clientes", 7, 9, 4, ["Soporte", "Ingeniería"]),
    UseCase("Code Review", "Revisión de código asistida por IA", 6, 7, 8, ["Ingeniería"]),
    UseCase("Demand Forecast", "Predecir demanda de productos", 9, 5, 12, ["Producto", "Cadena de suministro"]),
]

for uc in sorted(use_cases, key=score_use_case, reverse=True):
    print(f"{uc.name}: score={score_use_case(uc):.1f}, impact={uc.business_impact}, feasibility={uc.technical_feasibility}")
# Salida:
# FAQ Bot: score=8.2, impact=7, feasibility=9
# Code Review: score=6.7, impact=6, feasibility=7
# Demand Forecast: score=6.1, impact=9, feasibility=5
  1. Validar con stakeholders: presentar los mejores candidatos, obtener aprobación, definir criterios de éxito

Entregable: lista priorizada de 2-3 casos de uso con métricas de éxito claras.

Fase 2: Implement — Construir el MVP

Objetivo: lanzar un prototipo funcional rápidamente, enfocándose en la funcionalidad central.

Principios clave:

  • Empezar con el enfoque más simple: basado en reglas → RAG → modelo fine-tuned (solo escalar si es necesario)
  • Usar herramientas existentes: no construir infraestructura a menos que sea necesario
  • Medir desde el día uno: instrumentar el MVP con el stack de observabilidad de la sección anterior

Checklist de implementación:

Semanas 1-2: Cimientos
  □ Configurar entorno de desarrollo
  □ Definir fuentes de datos y acceso
  □ Crear plantillas básicas de prompts
  □ Crear dataset de evaluación (50-100 ejemplos)

Semanas 3-4: Funcionalidad central
  □ Implementar pipeline de retrieval (si RAG)
  □ Construir bucle del Agent (si usa herramientas)
  □ Integrar con sistemas existentes (API, base de datos)
  □ Añadir manejo básico de errores y fallbacks

Semanas 5-6: Calidad y pruebas
  □ Ejecutar suite de evaluación, iterar sobre prompts
  □ Añadir barreras de seguridad (filtrado de contenido, detección de PII)
  □ Realizar pruebas con 5-10 usuarios internos
  □ Corregir problemas críticos

Semanas 7-8: Preparación del despliegue
  □ Configurar monitoreo y alertas
  □ Documentar runbooks para problemas comunes
  □ Preparar plan de rollback
  □ Desplegar en staging, ejecutar pruebas de carga

Fase 3: Deliver — Medir el impacto en el negocio

Objetivo: cuantificar el impacto real contra los criterios de éxito definidos en Research.

Métricas clave por tipo de caso de uso:

Tipo de caso de usoMétricas primariasMétricas secundarias
Bot de soporte al clienteTasa de desvío de tickets, tiempo de resoluciónSatisfacción del cliente (CSAT), coste por ticket
Asistente de revisión de códigoTiempo de revisión, tasa de defectos escapadosSatisfacción del desarrollador, puntuaciones de calidad de código
Generación de contenidoTiempo de producción de contenido, métricas de engagementPuntuaciones de consistencia de marca, tasa de aprobación editorial
Agent de análisis de datosTiempo de análisis, calidad de insightsSatisfacción de stakeholders, velocidad de decisión

Ejemplo de cálculo de impacto:

# Comparación antes/después para FAQ Bot
before = {
    "monthly_tickets": 5000,
    "avg_resolution_time_hours": 24,
    "cost_per_ticket": 15,  # coste del agente humano
    "csat_score": 3.2,
}

after = {
    "monthly_tickets": 5000,
    "bot_deflection_rate": 0.65,  # 65% manejado por el bot
    "avg_resolution_time_hours": 0.5,  # para los manejados por el bot
    "cost_per_ticket_bot": 0.50,  # coste de API
    "cost_per_ticket_human": 15,  # para escalados
    "csat_score": 4.1,
}

# Calcular impacto
bot_handled = after["monthly_tickets"] * after["bot_deflection_rate"]
human_handled = after["monthly_tickets"] - bot_handled

monthly_cost_before = before["monthly_tickets"] * before["cost_per_ticket"]
monthly_cost_after = (bot_handled * after["cost_per_ticket_bot"] +
                      human_handled * after["cost_per_ticket_human"])

monthly_savings = monthly_cost_before - monthly_cost_after
annual_savings = monthly_savings * 12

print(f"Coste mensual antes: ${monthly_cost_before:,.0f}")
print(f"Coste mensual después: ${monthly_cost_after:,.0f}")
print(f"Ahorro anual: ${annual_savings:,.0f}")
print(f"Mejora de CSAT: {after['csat_score'] - before['csat_score']:.1f} puntos")
# Salida:
# Coste mensual antes: $75,000
# Coste mensual después: $29,875
# Ahorro anual: $541,500
# Mejora de CSAT: 0.9 puntos

Fase 4: Enhance — Iterar y expandir

Objetivo: mejorar continuamente el sistema basándose en datos y feedback.

Estrategias de mejora:

  1. Optimización de prompts: usar datos de evaluación para refinar prompts (ver capítulo de Evaluación)
  2. Mejoras de RAG: añadir más documentos, mejorar chunking, añadir reranking
  3. Actualizaciones de modelo: destilar a modelos más pequeños para ahorro de costes (ver sección de Destilación)
  4. Expansión de funcionalidades: añadir nuevas capacidades basándose en feedback de usuarios
  5. Integración de procesos: profundizar la integración con flujos de trabajo existentes

Cadencia de iteración:

Semanal:
  - Revisar logs de errores y feedback de usuarios
  - Actualizar dataset de evaluación con nuevos ejemplos
  - Corregir bugs críticos

Mensual:
  - Ejecutar suite de evaluación completa
  - Analizar tendencias en métricas
  - Planificar siguiente iteración

Trimestral:
  - Revisar impacto en negocio contra objetivos
  - Evaluar panorama de modelos/proveedores para actualizaciones
  - Planificar mejoras estratégicas

RIDE en la práctica: errores comunes

ErrorCómo evitarlo
Saltarse Research y lanzarse a implementarEmpezar siempre con entrevistas a stakeholders y puntuación de casos de uso
Construir durante meses antes de entregarFijar deadline de 8 semanas para MVP; lanzar algo medible
Medir solo métricas técnicas (latencia, precisión)Definir métricas de negocio desde el inicio; rastrear ahorro de costes, tiempo ahorrado
Tratar la IA como un proyecto puntualPlanificar iteración continua; los sistemas de IA necesitan mantenimiento continuo
Ignorar seguridad y cumplimientoConstruir barreras desde el día uno; no añadirlas después

Apéndice: Consulta rápida de conceptos clave

ConceptoExplicación en una frase
TokenizationConvertir texto en una secuencia de IDs numéricos procesables por el modelo
EmbeddingMapear IDs discretos a vectores densos que contienen información semántica
AttentionPermitir que el modelo, al procesar cada token, preste atención a todas las posiciones relevantes de la secuencia
RAGPrimero recuperar conocimiento relevante, luego hacer que el modelo genere la respuesta basándose en ese conocimiento
ReActHacer que el modelo alterne entre razonamiento (Reasoning) y acción (Action) en un ciclo
Function CallingEl modelo emite instrucciones estructuradas de invocación de herramientas en lugar de respuestas de texto plano
MCPProtocolo de estandarización de herramientas propuesto por Anthropic, que desacopla la definición y el uso de herramientas
Plan & ExecutePrimero elaborar un plan de acción completo y, tras revisarlo y aprobarlo, ejecutarlo paso a paso
HyDEPrimero generar una respuesta hipotética y usar esa respuesta para recuperar, en lugar de la pregunta original
Lost in the MiddleLa capacidad del modelo para procesar información en la parte central de contextos largos disminuye significativamente
Mixture-of-AgentsMúltiples modelos distintos procesan la misma tarea y un agregador sintetiza el mejor resultado
LoRAConseguir fine-tuning eficiente entrenando matrices adaptadoras de bajo rango
DestilaciónUsar la salida del modelo grande como datos de entrenamiento para enseñar a un modelo pequeño
SkillEncapsular conocimiento especializado en unidades funcionales modulares reutilizables
LLM-as-JudgeUsar un modelo grande como evaluador para puntuar automáticamente las salidas
SLOObjetivos de nivel de servicio, como TTFT, TPOT, disponibilidad, etc.

Esta guía ha sido elaborada a partir de las mejores prácticas de ingeniería de agentes, abarcando la ruta completa desde los fundamentos de LLM hasta la puesta en producción. El ámbito tecnológico evoluciona rápidamente; se recomienda seguir de cerca los avances de la comunidad y combinar las metodologías de esta guía con las herramientas más recientes.

Proyecto Integral: Construyendo un Agent de Preguntas y Respuestas Inteligente

Narrativa del proyecto: En las Partes 1-5, aprendiste los componentes de la ingeniería de Agents de forma aislada. Ahora es momento de unirlos todos. Construirás un Agent de Preguntas y Respuestas para Nuevos Empleados — un sistema que comienza como una simple llamada a una API y evoluciona hasta convertirse en un Agent de grado productivo con RAG, uso de herramientas, memoria, habilidades, evaluación y despliegue. Cada etapa corresponde a un hito real de ingeniería.


6.1 Configuración del Entorno y Conversación Básica

El Punto de Partida

Tu empresa tiene un problema: los nuevos empleados siguen haciendo las mismas preguntas sobre incorporación, beneficios, herramientas y procesos. RRHH pasa horas repitiendo respuestas. Tu objetivo: construir un agente de IA que pueda responder estas preguntas con precisión.

Objetivo de la Etapa 1: Hacer funcionar una conversación básica con LLM.

Configuración del Proyecto

# Crear directorio del proyecto
mkdir qa-agent && cd qa-agent

# Configurar entorno Python
python -m venv .venv
source .venv/bin/activate

# Instalar dependencias
pip install openai python-dotenv
# .env
OPENAI_API_KEY=sk-your-key-here

Primera Conversación

# chat.py
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def chat(user_message: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "You are a helpful onboarding assistant for new employees."},
            {"role": "user", "content": user_message},
        ],
    )
    return response.choices[0].message.content

# Probar
print(chat("What's the dress code?"))
# Salida: "Our dress code is business casual..."

Conversación Multi-Turno

Un chat de un solo turno no es suficiente. Los empleados hacen preguntas de seguimiento. Añadamos historial de conversación:

# multi_turn_chat.py
conversation_history = [
    {"role": "system", "content": "You are a helpful onboarding assistant for new employees at Acme Corp."}
]

def chat_with_history(user_message: str) -> str:
    conversation_history.append({"role": "user", "content": user_message})

    response = client.chat.completions.create(
        model="gpt-4",
        messages=conversation_history,
    )
    assistant_reply = response.choices[0].message.content
    conversation_history.append({"role": "assistant", "content": assistant_reply})

    return assistant_reply

# Probar multi-turno
print(chat_with_history("What's the dress code?"))
print(chat_with_history("What about on Fridays?"))  # Pregunta de seguimiento
print(chat_with_history("And for client meetings?"))  # Otro seguimiento

Control del Presupuesto de Tokens

# token_tracker.py
import tiktoken

def count_tokens(messages: list, model: str = "gpt-4") -> int:
    encoding = tiktoken.encoding_for_model(model)
    total = 0
    for msg in messages:
        total += len(encoding.encode(msg["content"])) + 4  # overhead por mensaje
    return total

def chat_within_budget(user_message: str, max_tokens: int = 4000) -> str:
    conversation_history.append({"role": "user", "content": user_message})

    # Recortar historial si se excede el presupuesto
    while count_tokens(conversation_history) > max_tokens:
        # Eliminar el par de mensajes no-system más antiguo
        non_system = [m for m in conversation_history if m["role"] != "system"]
        if len(non_system) >= 2:
            conversation_history.remove(non_system[0])
            conversation_history.remove(non_system[1])
        else:
            break

    response = client.chat.completions.create(
        model="gpt-4",
        messages=conversation_history,
    )
    reply = response.choices[0].message.content
    conversation_history.append({"role": "assistant", "content": reply})
    return reply

Etapa 1 Completada: Tienes un chatbot básico multi-turno. Pero solo sabe lo que el LLM aprendió en su entrenamiento — no conoce las políticas específicas de tu empresa.


6.2 RAG: Conectando el Conocimiento Empresarial

El Problema

print(chat_with_history("What's the parental leave policy?"))
# Salida: "I don't have specific information about Acme Corp's parental leave policy..."

El LLM no conoce los documentos internos de tu empresa. Necesitas Generación Aumentada por Recuperación (RAG).

Paso 1: Preparar tu Base de Conocimiento

# knowledge_base.py
import os
from pathlib import Path

# Documentos de ejemplo de la empresa (en la práctica, cargar desde tu almacén de documentos)
documents = [
    {
        "id": "doc_001",
        "title": "Employee Handbook - Leave Policies",
        "content": """Acme Corp provides the following leave benefits:
- Annual Leave: 20 days per year, prorated for partial years
- Sick Leave: 10 days per year
- Parental Leave: 16 weeks paid leave for primary caregivers, 8 weeks for secondary caregivers
- Bereavement Leave: 5 days for immediate family members
All leave requests must be submitted through the HR portal at least 2 weeks in advance, except for sick leave which can be reported same-day."""
    },
    {
        "id": "doc_002",
        "title": "Employee Handbook - Dress Code",
        "content": """Acme Corp Dress Code:
- Regular days: Business casual (collared shirts, slacks, closed-toe shoes)
- Casual Fridays: Jeans and casual wear allowed, but no flip-flops or gym clothes
- Client meetings: Business formal (suit and tie for men, business suit or dress for women)
- Remote work days: No dress code, but camera-on for meetings
When in doubt, err on the side of being more formal."""
    },
    # ... más documentos
]

Paso 2: Construir el Pipeline de Recuperación

# rag_pipeline.py
from openai import OpenAI
import numpy as np

client = OpenAI()

def get_embedding(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return response.data[0].embedding

def build_vector_store(docs: list) -> dict:
    """Insertar todos los documentos y almacenar como un índice vectorial simple"""
    vector_store = {}
    for doc in docs:
        embedding = get_embedding(doc["content"])
        vector_store[doc["id"]] = {
            "embedding": embedding,
            "content": doc["content"],
            "title": doc["title"],
        }
    return vector_store

def search(query: str, vector_store: dict, top_k: int = 3) -> list[dict]:
    """Recuperar los top-k documentos más relevantes"""
    query_embedding = get_embedding(query)

    results = []
    for doc_id, doc_data in vector_store.items():
        similarity = cosine_similarity(query_embedding, doc_data["embedding"])
        results.append({
            "doc_id": doc_id,
            "title": doc_data["title"],
            "content": doc_data["content"],
            "score": similarity,
        })

    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:top_k]

def cosine_similarity(a: list, b: list) -> float:
    a_arr = np.array(a)
    b_arr = np.array(b)
    return np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr))

Paso 3: Aumentar la Generación con Contexto Recuperado

# rag_chat.py
def rag_chat(user_message: str, vector_store: dict) -> str:
    # Paso 1: Recuperar documentos relevantes
    relevant_docs = search(user_message, vector_store, top_k=3)

    # Paso 2: Construir contexto a partir de los documentos recuperados
    context = "\n\n".join([
        f"[Source: {doc['title']}]\n{doc['content']}"
        for doc in relevant_docs
    ])

    # Paso 3: Generar respuesta con contexto
    system_prompt = f"""You are a helpful onboarding assistant for Acme Corp.
Use the following company documents to answer questions. If the answer is not in the documents, say so honestly.
Always cite which document you're referencing.

--- Company Documents ---
{context}
--- End Documents ---"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
    )
    return response.choices[0].message.content

# Probar
vector_store = build_vector_store(documents)
print(rag_chat("What's the parental leave policy?", vector_store))
# Salida: "According to the Employee Handbook - Leave Policies, Acme Corp provides:
# - 16 weeks paid leave for primary caregivers
# - 8 weeks for secondary caregivers
# All requests must be submitted through the HR portal at least 2 weeks in advance."

Etapa 2 Completada: Tu bot ahora responde a partir de documentos de la empresa. Pero solo puede hablar — no puede hacer cosas como enviar una solicitud de permiso o verificar disponibilidad en el calendario.


6.3 Llamada a Herramientas del Agent y Planificación

El Problema

Un empleado pregunta: “¿Puedes enviar una solicitud de permiso para mí del lunes al miércoles de la próxima semana?”

Tu bot puede explicar la política, pero no puede enviar realmente la solicitud. Necesitas llamada a herramientas.

Paso 1: Definir Herramientas

# tools.py
import json

tools = [
    {
        "type": "function",
        "function": {
            "name": "submit_leave_request",
            "description": "Submit a leave request to the HR system",
            "parameters": {
                "type": "object",
                "properties": {
                    "leave_type": {
                        "type": "string",
                        "enum": ["annual", "sick", "parental", "bereavement"],
                        "description": "Type of leave"
                    },
                    "start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)"},
                    "end_date": {"type": "string", "description": "End date (YYYY-MM-DD)"},
                    "reason": {"type": "string", "description": "Reason for leave"},
                },
                "required": ["leave_type", "start_date", "end_date"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "check_leave_balance",
            "description": "Check remaining leave balance for an employee",
            "parameters": {
                "type": "object",
                "properties": {
                    "employee_id": {"type": "string", "description": "Employee ID"},
                    "leave_type": {"type": "string", "description": "Type of leave to check"},
                },
                "required": ["employee_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge_base",
            "description": "Search the company knowledge base for policies and procedures",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"},
                },
                "required": ["query"],
            },
        },
    },
]

# Implementaciones mock
def submit_leave_request(leave_type: str, start_date: str, end_date: str, reason: str = "") -> dict:
    # En producción, esto llama a tu API de RRHH
    return {"status": "submitted", "request_id": "LR-2024-001", "leave_type": leave_type, "dates": f"{start_date} to {end_date}"}

def check_leave_balance(employee_id: str, leave_type: str = None) -> dict:
    # Datos mock
    balances = {"annual": 15, "sick": 8, "parental": 0, "bereavement": 5}
    if leave_type:
        return {"employee_id": employee_id, "leave_type": leave_type, "remaining_days": balances.get(leave_type, 0)}
    return {"employee_id": employee_id, "balances": balances}

def search_knowledge_base(query: str) -> str:
    # Reutilizar la búsqueda RAG de la sección anterior
    results = search(query, vector_store, top_k=2)
    return "\n\n".join([f"[{r['title']}]: {r['content'][:200]}..." for r in results])

TOOL_IMPLEMENTATIONS = {
    "submit_leave_request": submit_leave_request,
    "check_leave_balance": check_leave_balance,
    "search_knowledge_base": search_knowledge_base,
}

Paso 2: Implementar el Bucle ReAct

# agent.py
def agent_chat(user_message: str, max_iterations: int = 5) -> str:
    messages = [
        {"role": "system", "content": """You are an onboarding assistant for Acme Corp.
You can use tools to help answer questions and perform actions.
Always think step by step. If you need information, use search_knowledge_base.
If the user wants to perform an action, use the appropriate tool.
After getting tool results, provide a clear summary to the user."""},
        {"role": "user", "content": user_message},
    ]

    for i in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )

        message = response.choices[0].message

        # Si no hay llamadas a herramientas, devolver la respuesta final
        if not message.tool_calls:
            return message.content

        # Procesar llamadas a herramientas
        messages.append(message)  # Añadir mensaje del asistente con llamadas a herramientas

        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            print(f"  [Tool Call] {func_name}({func_args})")

            # Ejecutar la herramienta
            result = TOOL_IMPLEMENTATIONS[func_name](**func_args)

            # Añadir resultado de la herramienta a los mensajes
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result),
            })

    return "I wasn't able to complete the task within the allowed steps."

# Probar
print(agent_chat("How many annual leave days do I have left? My employee ID is EMP-042."))
# Salida:
#   [Tool Call] check_leave_balance({'employee_id': 'EMP-042', 'leave_type': 'annual'})
# "You have 15 annual leave days remaining."

print(agent_chat("Can you submit annual leave for me from Dec 23 to Dec 27?"))
# Salida:
#   [Tool Call] submit_leave_request({'leave_type': 'annual', 'start_date': '2024-12-23', 'end_date': '2024-12-27'})
# "Your leave request has been submitted! Request ID: LR-2024-001, covering Dec 23-27, 2024."

Paso 3: Añadir Planificación para Tareas Complejas

Para tareas de múltiples pasos, el Agent necesita planificar antes de ejecutar:

# planner.py
def plan_and_execute(user_request: str) -> str:
    # Paso 1: Generar un plan
    plan_response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": """You are a task planner. Break down the user's request into concrete steps.
For each step, specify which tool to use and what arguments to pass.
Output a JSON array of steps."""},
            {"role": "user", "content": user_request},
        ],
    )

    plan_text = plan_response.choices[0].message.content
    print(f"Plan: {plan_text}")

    # Paso 2: Ejecutar cada paso usando el agent
    # (En producción, parsearías el plan y ejecutarías paso a paso con validación)
    return agent_chat(user_request)

Etapa 3 Completada: Tu bot ahora puede llamar herramientas y realizar acciones. Pero cada conversación empieza desde cero — no recuerda interacciones previas.


6.4 Memoria y Habilidades: Hacer que el Agent sea Más Inteligente con el Tiempo

El Problema

Un empleado tiene tres conversaciones separadas:

  1. “Empiezo el próximo lunes, ¿qué debo traer?”
  2. “¡Gracias! Por cierto, mi ID de empleado es EMP-042.”
  3. “¿Puedes verificar mi saldo de permisos?”

El Agent no tiene idea de cuál es su ID de empleado. Cada conversación está aislada.

Paso 1: Memoria a Corto Plazo (Buffer de Conversación)

# memory.py
from dataclasses import dataclass, field

@dataclass
class ConversationBuffer:
    max_tokens: int = 4000
    messages: list = field(default_factory=list)
    summary: str = ""

    def add_message(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        self._trim_if_needed()

    def _trim_if_needed(self):
        token_count = count_tokens(self.messages)
        if token_count > self.max_tokens:
            # Resumir mensajes antiguos y mantener los recientes
            old_messages = self.messages[:len(self.messages)//2]
            self.summary = self._summarize(old_messages)
            self.messages = self.messages[len(self.messages)//2:]

    def _summarize(self, messages: list) -> str:
        response = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "system", "content": "Summarize this conversation in 2-3 sentences, focusing on key facts and decisions."},
                *messages,
            ],
        )
        return response.choices[0].message.content

    def get_context(self) -> list:
        context = []
        if self.summary:
            context.append({"role": "system", "content": f"Previous conversation summary: {self.summary}"})
        context.extend(self.messages)
        return context

Paso 2: Memoria a Largo Plazo (Almacén de Perfiles de Usuario)

# long_term_memory.py
import json
from pathlib import Path

USER_PROFILES_DIR = Path("user_profiles")
USER_PROFILES_DIR.mkdir(exist_ok=True)

def save_user_fact(user_id: str, fact: str):
    """Guardar un hecho sobre un usuario en su perfil a largo plazo"""
    profile_path = USER_PROFILES_DIR / f"{user_id}.json"
    profile = {}
    if profile_path.exists():
        profile = json.loads(profile_path.read_text())

    if "facts" not in profile:
        profile["facts"] = []
    profile["facts"].append(fact)
    profile_path.write_text(json.dumps(profile, indent=2))

def get_user_facts(user_id: str) -> list[str]:
    """Recuperar todos los hechos conocidos sobre un usuario"""
    profile_path = USER_PROFILES_DIR / f"{user_id}.json"
    if not profile_path.exists():
        return []
    profile = json.loads(profile_path.read_text())
    return profile.get("facts", [])

def extract_and_save_facts(user_id: str, messages: list):
    """Usar LLM para extraer hechos importantes de la conversación y guardarlos"""
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": """Extract important facts about the user from this conversation.
Focus on: name, employee ID, department, preferences, upcoming events, action items.
Output a JSON array of fact strings. If no important facts, output []."""},
            *messages,
        ],
    )
    facts = json.loads(response.choices[0].message.content)
    for fact in facts:
        save_user_fact(user_id, fact)

Paso 3: Habilidades — Flujos de Trabajo Reutilizables

# skills/onboarding_guide.md
"""
---
name: onboarding_guide
description: Guide new employees through their first week
triggers: new employee, first day, onboarding, getting started
---

# Onboarding Guide Skill

## Day 1 Checklist
1. Verify IT setup (laptop, accounts, VPN)
2. Introduce to team via Slack
3. Share key documents: handbook, org chart, tools guide
4. Schedule 1:1 with manager for week overview

## Week 1 Priorities
- Complete mandatory training modules (compliance, security)
- Set up development environment (if engineer)
- Attend team standup meetings
- Read team's project documentation

## Common First-Week Questions
- "How do I submit expenses?" → Use Concur, submit within 30 days
- "What's the wifi password?" → Provided on IT setup sheet
- "Who do I talk about benefits?" → HR portal or email [email protected]
"""

# skill_loader.py
from pathlib import Path

def load_skill(skill_name: str) -> str:
    skill_path = Path(f"skills/{skill_name}.md")
    if not skill_path.exists():
        return ""
    return skill_path.read_text()

def find_relevant_skill(query: str, available_skills: list[str]) -> str | None:
    """Determinar qué habilidad activar según la consulta"""
    skills_info = []
    for skill_name in available_skills:
        content = load_skill(skill_name)
        # Extraer descripción del frontmatter
        skills_info.append(f"- {skill_name}: {content[:200]}")

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "Given the user query, which skill should be activated? Output just the skill name, or 'none' if no skill is relevant."},
            {"role": "user", "content": f"Query: {query}\n\nAvailable skills:\n" + "\n".join(skills_info)},
        ],
    )
    result = response.choices[0].message.content.strip()
    return result if result != "none" else None

Integración: Agent con Conciencia de Memoria

# memory_agent.py
def memory_aware_agent_chat(user_id: str, user_message: str) -> str:
    # Cargar hechos a largo plazo del usuario
    user_facts = get_user_facts(user_id)
    facts_context = "\n".join(user_facts) if user_facts else "No previous facts known."

    # Verificar si se debe activar alguna habilidad
    skill_name = find_relevant_skill(user_message, ["onboarding_guide"])
    skill_context = load_skill(skill_name) if skill_name else ""

    system_prompt = f"""You are an onboarding assistant for Acme Corp.

Known facts about this user:
{facts_context}

{f'Active skill: {skill_context}' if skill_context else ''}

Use the user's known facts to personalize responses.
If the user shares new important information, note it for future reference."""

    messages = [{"role": "system", "content": system_prompt}]
    messages.extend(conversation_buffer.get_context())
    messages.append({"role": "user", "content": user_message})

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

    reply = response.choices[0].message.content

    # Actualizar memoria
    conversation_buffer.add_message("user", user_message)
    conversation_buffer.add_message("assistant", reply)
    extract_and_save_facts(user_id, messages)

    return reply

Etapa 4 Completada: Tu Agent ahora recuerda a los usuarios, activa habilidades y proporciona respuestas personalizadas. Pero, ¿cómo sabes si realmente está dando buenas respuestas?


6.5 Evaluación y Optimización Iterativa

El Problema

Has construido mucho, pero no tienes idea de qué tan bien funciona. ¿Está dando respuestas correctas? ¿Está alucinando? ¿Está omitiendo contexto importante?

Paso 1: Construir un Dataset de Evaluación

# eval_dataset.py
eval_cases = [
    {
        "id": "eval_001",
        "input": "What's the parental leave policy?",
        "expected_output": "16 weeks for primary caregivers, 8 weeks for secondary caregivers",
        "required_sources": ["Employee Handbook - Leave Policies"],
        "category": "factual_recall",
    },
    {
        "id": "eval_002",
        "input": "How do I submit a leave request?",
        "expected_output": "Through the HR portal, at least 2 weeks in advance",
        "required_sources": ["Employee Handbook - Leave Policies"],
        "category": "procedural",
    },
    {
        "id": "eval_003",
        "input": "What should I wear to a client meeting?",
        "expected_output": "Business formal: suit and tie for men, business suit or dress for women",
        "required_sources": ["Employee Handbook - Dress Code"],
        "category": "factual_recall",
    },
    {
        "id": "eval_004",
        "input": "Can you submit a sick leave request for me today?",
        "expected_behavior": "Should call submit_leave_request tool with leave_type='sick'",
        "category": "tool_use",
    },
    {
        "id": "eval_005",
        "input": "What's the meaning of life?",
        "expected_behavior": "Should politely decline or redirect to onboarding topics",
        "category": "boundary",
    },
]

Paso 2: Evaluación Automática

# evaluator.py
def evaluate_factual_recall(agent_fn, case: dict) -> dict:
    """Evaluar si el agent recordó correctamente los hechos de los documentos"""
    response = agent_fn(case["input"])

    # Verificación 1: ¿La respuesta contiene la información esperada?
    expected_keywords = case["expected_output"].lower().split()
    response_lower = response.lower()
    keyword_hits = sum(1 for kw in expected_keywords if kw in response_lower)
    recall_score = keyword_hits / len(expected_keywords)

    # Verificación 2: ¿Citó la fuente correcta?
    source_cited = any(src.lower() in response.lower() for src in case["required_sources"])

    return {
        "case_id": case["id"],
        "category": case["category"],
        "recall_score": recall_score,
        "source_cited": source_cited,
        "passed": recall_score > 0.7 and source_cited,
        "response": response[:200],
    }

def evaluate_tool_use(agent_fn, case: dict) -> dict:
    """Evaluar si el agent usó correctamente las herramientas"""
    # Capturar llamadas a herramientas durante la ejecución
    tool_calls_made = []
    original_implementations = {}

    # Envolver herramientas para capturar llamadas
    for name, impl in TOOL_IMPLEMENTATIONS.items():
        original_implementations[name] = impl
        def make_wrapper(n):
            def wrapper(*args, **kwargs):
                tool_calls_made.append({"name": n, "args": kwargs})
                return original_implementations[n](*args, **kwargs)
            return wrapper
        TOOL_IMPLEMENTATIONS[name] = make_wrapper(name)

    try:
        response = agent_fn(case["input"])
        expected_tool = case["expected_behavior"].split("'")[1] if "'" in case["expected_behavior"] else ""
        correct_tool_called = any(tc["name"] == expected_tool for tc in tool_calls_made)

        return {
            "case_id": case["id"],
            "category": case["category"],
            "correct_tool_called": correct_tool_called,
            "tools_called": [tc["name"] for tc in tool_calls_made],
            "passed": correct_tool_called,
        }
    finally:
        # Restaurar implementaciones originales
        for name, impl in original_implementations.items():
            TOOL_IMPLEMENTATIONS[name] = impl

def run_evaluation(agent_fn) -> dict:
    results = []
    for case in eval_cases:
        if case["category"] in ("factual_recall", "procedural"):
            results.append(evaluate_factual_recall(agent_fn, case))
        elif case["category"] == "tool_use":
            results.append(evaluate_tool_use(agent_fn, case))

    total = len(results)
    passed = sum(1 for r in results if r["passed"])

    return {
        "total_cases": total,
        "passed": passed,
        "pass_rate": passed / total if total > 0 else 0,
        "results": results,
    }

Paso 3: LLM como Juez

# llm_judge.py
def llm_judge_evaluation(case: dict, agent_response: str) -> dict:
    """Usar GPT-4 como juez para evaluar la calidad de la respuesta"""
    judge_prompt = f"""You are an expert evaluator for an onboarding assistant.

User Question: {case['input']}
Expected Answer: {case['expected_output']}
Agent Response: {agent_response}

Rate the response on these dimensions (1-5 scale):
1. Accuracy: Is the information correct?
2. Completeness: Does it cover all key points?
3. Helpfulness: Would a new employee find this useful?
4. Tone: Is it professional and friendly?

Output a JSON object with scores and a brief explanation."""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": judge_prompt},
            {"role": "user", "content": "Evaluate the response."},
        ],
    )

    return json.loads(response.choices[0].message.content)

Paso 4: Iterar Según los Resultados

# iteration.py
def run_eval_improve_loop(max_iterations: int = 3):
    for iteration in range(max_iterations):
        print(f"\n=== Iteration {iteration + 1} ===")

        # Ejecutar evaluación
        results = run_evaluation(memory_aware_agent_chat)
        print(f"Pass rate: {results['pass_rate']:.0%}")

        # Analizar fallos
        failures = [r for r in results["results"] if not r["passed"]]
        for f in failures:
            print(f"  FAIL [{f['case_id']}]: {f.get('response', '')[:100]}")

        if results["pass_rate"] >= 0.9:
            print("Target reached!")
            break

        # Iterar: mejorar prompts, añadir documentos, corregir herramientas
        print("  → Improving system prompt and adding more documents...")
        # (En la práctica, modificarías tus prompts, añadirías documentos, etc.)

Etapa 5 Completada: Ahora tienes un sistema de Agents medible y que mejora iterativamente. Último paso: hacerlo listo para producción.


6.6 Destilación y Despliegue

El Problema

Tu Agent funciona genial con GPT-4, pero a escala los costos son insostenibles:

  • 500 empleados × 10 consultas/día × 30 días = 150,000 consultas/mes
  • A ~2000 tokens por consulta (entrada + salida), son 300M tokens/mes
  • Costo de GPT-4: ~$3,000/mes

Necesitas un modelo más económico que funcione igual de bien para tu dominio específico.

Paso 1: Destilar a un Modelo Más Pequeño

# distillation.py
# Generar datos de entrenamiento usando GPT-4 como modelo profesor
def generate_training_data(n_examples: int = 1000) -> list[dict]:
    training_data = []

    for case in eval_cases * (n_examples // len(eval_cases)):
        # Añadir variaciones para crear datos más diversos
        variations = [
            case["input"],
            f"Hey, {case['input'].lower()}",
            f"Quick question: {case['input']}",
        ]

        for variant in variations:
            response = client.chat.completions.create(
                model="gpt-4",
                messages=[
                    {"role": "system", "content": "You are an onboarding assistant for Acme Corp..."},
                    {"role": "user", "content": variant},
                ],
            )
            training_data.append({
                "input": variant,
                "output": response.choices[0].message.content,
            })

    return training_data

# Ajustar fino un modelo pequeño (ej., Llama 2 7B) usando los datos de entrenamiento
# (Ver el capítulo de Production para código detallado de fine-tuning)

Paso 2: Configurar Monitoreo

# monitoring.py
from prometheus_client import Counter, Histogram, start_http_server

request_counter = Counter('qa_agent_requests_total', 'Total requests', ['intent', 'status'])
request_latency = Histogram('qa_agent_latency_seconds', 'Request latency')
token_counter = Counter('qa_agent_tokens_total', 'Token usage', ['type'])

# Iniciar servidor de métricas Prometheus
start_http_server(8000)

def tracked_agent_chat(user_id: str, user_message: str) -> str:
    import time
    start = time.time()

    try:
        response = memory_aware_agent_chat(user_id, user_message)
        request_counter.labels(intent="general", status="success").inc()
        return response
    except Exception as e:
        request_counter.labels(intent="general", status="error").inc()
        raise
    finally:
        request_latency.observe(time.time() - start)

Paso 3: Desplegar con Canary Release

# deployment.py
def route_request(user_id: str, user_message: str) -> str:
    """Enrutar el 5% del tráfico al nuevo modelo destilado"""
    import hashlib
    bucket = int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100

    if bucket < 5:
        # Canary: usar modelo destilado
        return distilled_model_chat(user_id, user_message)
    else:
        # Estable: usar GPT-4
        return memory_aware_agent_chat(user_id, user_message)

Arquitectura Final

┌─────────────────────────────────────────────────────────┐
│                    Q&A Agent System                       │
│                                                          │
│  User ──▶ Router ──▶ Agent Core ──▶ LLM (GPT-4/7B)     │
│              │            │                               │
│              │            ├── RAG Pipeline (Vector DB)    │
│              │            ├── Tool Registry (HR API)      │
│              │            ├── Memory Store (User Profile) │
│              │            └── Skill Loader (Onboarding)   │
│              │                                            │
│              └── Monitoring (Prometheus + Grafana)        │
│                                                          │
│  Canary: 5% → Distilled Model (7B, fine-tuned)          │
│  Stable: 95% → GPT-4                                    │
└─────────────────────────────────────────────────────────┘

Lo Que Has Construido

Comenzando con una sola llamada a una API, has construido progresivamente:

EtapaCapacidadConocimiento Aplicado
6.1Conversación básicaFundamentos de LLM (Tokens, Ventana de Contexto)
6.2Respuestas basadas en documentosRAG (Embedding, Chunking, Retrieval)
6.3Uso de herramientas y planificaciónNúcleo del Agent (Function Calling, ReAct, MCP)
6.4Memoria y respuestas personalizadasSistemas de Memoria y Habilidades
6.5Calidad medibleFramework de Evaluación
6.6Sistema de producción costo-eficienteDestilación, Monitoreo, Canary Release

Este es el camino completo de la ingeniería de Agents — desde “Hello World” hasta producción.