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:
- Fundamentos de LLM: comprender Tokenization, la arquitectura Transformer, la ventana de contexto y la ingeniería de prompts
- Principios y práctica de RAG: dominar la generación aumentada por recuperación para que el modelo acceda a conocimiento privado
- Uso de herramientas por el Agente: Function Calling, ciclo ReAct, protocolo MCP
- Planificación y ejecución del Agente: mecanismos de reflexión, Plan & Execute, orquestación de flujos de trabajo
- Colaboración multi-agente: colaboración jerárquica, patrón de pizarra, metodología de colaboración
- Memory y Skill: gestión de memoria a corto/largo plazo, diseño de sistemas de Skill
- Evaluación de Agentes: evaluación end-to-end, evaluación de caja blanca, iteración guiada por evaluación
- 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:
| Estrategia | Principio | Caso de uso |
|---|---|---|
| Decodificación voraz (Greedy) | En cada paso elige el token con mayor probabilidad | Tareas que requieren salida determinista (generación de código, extracción estructurada) |
| Beam Search | Mantiene múltiples rutas candidatas y elige la secuencia con mayor probabilidad global | Traducción, resumen y otras tareas que requieren óptimo global |
| Top-k Sampling | Muestrea aleatoriamente entre los k tokens de mayor probabilidad | Escritura creativa, generación de diálogos |
| Top-p (Nucleus) | Muestrea del conjunto mínimo de tokens cuya probabilidad acumulada supera p | Diá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 aleatoriaTop_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 detieneLas 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:
| Modelo | Token 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).
Atención causal (Causal Self-Attention)
La fórmula central del mecanismo de Attention:
Attention(Q, K, V) = softmax(QK^T / √d_k) · VDonde 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 cabezasRed 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.
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écnica | Problema que resuelve | Método principal |
|---|---|---|
| RAG | Conocimiento privado insuficiente | Recuperar información relevante de bases de conocimiento externas e inyectarla en el contexto |
| Prompt Engineering | Instrucciones poco precisas | Guiar el comportamiento del modelo mediante instrucciones cuidadosamente diseñadas |
| Tool Use | El modelo no puede ejecutar acciones | Dotar al modelo de la capacidad de invocar herramientas externas |
| Memory | Olvido entre sesiones | Establecer 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 necesariasFew-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 → regenerarUn flujo completo de Meta Prompting también puede incorporar “respuesta de referencia” y puntuación cuantitativa:
- Establecer respuesta de referencia: definir la salida ideal
- Analizar la brecha: hacer que el modelo “evaluador” compare el resultado generado con la respuesta de referencia
- Optimizar el prompt: reescribir el prompt basándose en el informe de análisis de brecha
- 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 lejanosEl 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 relevanteSelecció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ón | Productos representativos | Ventajas | Desventajas | Caso de uso |
|---|---|---|---|---|
| Almacenamiento en memoria | Integrado en LlamaIndex | Cero configuración, prototipado rápido | Datos no persistentes, limitado por memoria | Desarrollo y pruebas |
| Base vectorial local | Milvus, Qdrant, Chroma | Funcionalidad completa, datos bajo control | Requiere despliegue y mantenimiento propios | Aplicaciones pequeñas/medianas |
| Servicio gestionado | Pinecone, Weaviate Cloud | Sin operaciones, escalado automático | Mayor coste, datos en externo | Producción, necesidades elásticas |
| Extensión de BD existente | PostgreSQL + pgvector, Elasticsearch | Aprovecha infraestructura existente | Rendimiento vectorial inferior a BD dedicadas | Equipos 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 contextoNo 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ónMejor 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 documento | Estrategia recomendada | Motivo |
|---|---|---|
| Manual técnico (estructura clara) | Segmentación Markdown | Aprovecha la jerarquía de encabezados para mantener la estructura |
| Contrato legal (lógica rigurosa) | Segmentación semántica | Mantiene la integridad semántica de las cláusulas |
| Registros de conversación | Segmentación con ventana de oración | Necesita contexto anterior y posterior para entender la semántica |
| Documentación de código | Segmentación por Token + semántica | Necesita control preciso de longitud |
| Noticias/blogs | Segmentación por oración | Asociaciones 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
- Parseo de documentos: convertir PDF, Word, Markdown y otros formatos a texto plano
- Segmentación de texto: dividir el documento en párrafos según la estrategia elegida
- Vectorización: transformar cada segmento en vector usando el modelo de Embedding
- 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 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘- Pregunta del usuario: recibir la pregunta
- Recuperación vectorial: vectorizar la pregunta y buscar los segmentos más similares en la base de datos vectorial
- Ensamblado del Prompt: combinar los fragmentos de conocimiento recuperados + la pregunta original + instrucciones en un Prompt completo
- 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.contentReescritura 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
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 resultsHyDE 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
| Momento | Estrategia de mejora | Descripción |
|---|---|---|
| Antes de la recuperación | Reescritura de pregunta | Transformar pregunta dependiente del contexto en independiente |
| Antes de la recuperación | Expansión de pregunta | Añadir más información semántica para mejorar recall |
| Antes de la recuperación | Extracción de etiquetas | Filtrar primero por etiquetas, luego recuperación vectorial |
| Antes de la recuperación | Descomposición de consulta en múltiples pasos | Dividir pregunta compleja en varias subconsultas |
| Después de la recuperación | ReRank | Reordenar con un modelo más preciso |
| Después de la recuperación | Ventana deslizante | Al 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 recallPrincipios 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 — el constructor no-code de flujos multi-agente de Microsoft — vía microsoft/autogen
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:
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.contentMejores prácticas para la definición de herramientas
- Descripciones precisas: el modelo decide cuándo invocar una herramienta basándose en
description; una descripción ambigua provoca invocaciones erróneas - Parámetros con restricciones: usa
enum,requiredy restricciones de tipo para reducir errores en los parámetros - Funciones de responsabilidad única: no agrupes múltiples operaciones en una sola función
- 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…”.
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
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 SchemaLa 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
| Rol | Responsabilidad | Analogía |
|---|---|---|
| MCP Server | Declarar herramientas (nombre, descripción, parámetros), ejecutar la lógica | Dispositivo USB |
| MCP Client | Conectarse al MCP Server, obtener definiciones de herramientas, enviar solicitudes de invocación | Controlador host USB |
| Agent | Usar el MCP Client para obtener la lista de herramientas, decidir las invocaciones | Aplicació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 modeloValor 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 draftVentaja 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 codeEscenarios 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 → ... → Outputclass 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.
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.contentColaboració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ón | Caso de uso adecuado | Caso de uso no adecuado |
|---|---|---|
| Pipeline | Flujo fijo, pasos lineales | Tareas que requieren decisiones dinámicas |
| Branching | Múltiples tipos de entrada necesitan distinto procesamiento | Cuando se necesita procesar múltiples aspectos simultáneamente |
| Parallel | Subtareas independientes entre sí, búsqueda de eficiencia | Tareas con cadenas de dependencias |
| MoA | Requisitos de alta calidad, tareas creativas | Tareas rutinarias sensibles al coste |
| HITL | Decisiones de alto riesgo, requisitos de cumplimiento | Sistemas en tiempo real con requisitos de baja latencia |
| Plan & Execute | Flujo variable, tareas nuevas que requieren exploración | Tareas 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”:
El Leader Agent se encarga de:
- Recibir y comprender la tarea de alto nivel
- Descomponerla en subtareas y asignarlas al Worker adecuado
- Hacer seguimiento del progreso global
- 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.contentVentajas 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:
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"""
passPatrón de pizarra vs patrón jerárquico
| Dimensión | Patrón jerárquico | Patrón de pizarra |
|---|---|---|
| Modo de control | Centralizado (el Leader controla) | Descentralizado (participación equitativa) |
| Modo de comunicación | Estrella (Leader↔Worker) | Totalmente conectado (todos los Agents↔pizarra) |
| Mecanismo de decisión | El Leader decide | Consenso emergente |
| Tareas adecuadas | Objetivo claro, descomponible | Exploración abierta, requiere inteligencia colectiva |
| Eficiencia | Alta (paralelo + controlable) | Más baja (múltiples rondas de discusión) |
| Creatividad | Limitada (restringida por la perspectiva del Leader) | Alta (la colisión de ideas genera nuevas propuestas) |
| Coste | Medio | Alto (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:
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 pizarraGestió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 replyLa 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:
- Exceder la ventana de contexto: la longitud del historial supera el límite del modelo → error del programa
- Coste fuera de control: cada llamada reenvía todo el historial → consumo de tokens crece linealmente
- 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
| Escenario | Estrategia recomendada |
|---|---|
| Chatbot de entretenimiento | Truncamiento 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 proyectos | Resumen progresivo |
| Asistente personalizado / interacción a largo plazo | Recuperación vectorizada |
| Mejor práctica | Uso 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
-
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.
-
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).
-
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ónEstructura 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ódigoSkill vs RAG
Mucha gente confunde Skill y RAG. Su diferencia fundamental es:
| RAG | Skill | |
|---|---|---|
| Problema que resuelve | ”El modelo no conoce cierto hecho" | "El modelo no sabe cómo hacerlo” |
| Tipo de información | Conocimiento factual (contenido de documentos, parámetros de producto) | Conocimiento procedimental (flujos, criterios, reglas de juicio) |
| Modo de activación | Se recupera y se inyecta en el contexto | Se despliega tras selección (el Agent decide si activar) |
| Modo de carga | Inyección única de resultados de recuperación | Divulgación progresiva (carga de subarchivos bajo demanda) |
| Ciclo de vida | Cada consulta recupera de forma independiente | Persistente 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:
| Escenario | No idempotente (peligroso) | Idempotente (seguro) |
|---|---|---|
| Escritura de archivos | Añadir contenido cada vez | Vaciar primero y luego escribir |
| Operaciones de BD | INSERT cada vez | Usar UPSERT |
| Llamadas API | Crear nuevo recurso cada vez | Usar clave de idempotencia |
Diseño del marco 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
mejoraEvaluació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:
| Tipo | Descripción | Ejemplo |
|---|---|---|
| Indicadores objetivos | Se pueden juzgar directamente mediante reglas de código | Si el código se puede ejecutar, si el formato cumple el Schema, si la longitud está dentro del rango |
| Indicadores subjetivos | Implican juicios semánticos y de calidad | Precisió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 sesgo | Manifestación | Método de mitigación |
|---|---|---|
| Sesgo de estilo | Preferencia por cierto estilo de código/escritura | Definir criterios de puntuación explícitos, no depender del “gusto” |
| Sesgo de longitud | Considerar 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 positivas | Usar evaluación comparativa (A vs B, ¿cuál es mejor?) |
| Sesgo de posición | Tendencia a elegir contenido en posiciones específicas de la lista | Aleatorizar 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:
-
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”.
-
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?”.
-
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ónCada 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ónDestilació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-tuning | Destilación | |
|---|---|---|
| Origen de datos | Anotación humana | Generados por el modelo profesor |
| Coste de datos | Alto | Bajo (coste de llamadas API) |
| Escala de datos | Limitada | Se puede generar a gran escala |
| Techo de calidad | Depende del anotador | Depende del modelo profesor |
Tres rutas de destilación:
| Ruta | Recursos necesarios | Caso de uso |
|---|---|---|
| Destilación por síntesis de datos (caja negra) | Solo acceso API | Tareas estructuradas, profesor API comercial |
| Destilación de conocimiento KD (caja blanca) | Pesos del modelo profesor | Profesor open-source, necesita mayor precisión |
| Compresión de razonamiento | Trazas de razonamiento del profesor | Tareas 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ónReducir 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 precioReducir 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_tokensrazonable
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:
| CPU | GPU | |
|---|---|---|
| Número de núcleos | Pocos núcleos potentes (8-64) | Gran cantidad de núcleos simples (miles) |
| Tareas adecuadas | Lógica compleja, secuencial | Cá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:
| Escenario | Alternativa |
|---|---|
| Mensajes de confirmación estándar | Plantillas hardcodeadas + selección aleatoria de variantes |
| Respuestas con opciones limitadas | Precalcular todos los resultados posibles, emparejar por entrada |
| Presentación de datos | Usar gráficos, tablas y otras UI tradicionales en lugar de descripciones generadas por LLM |
| Coincidencia de palabras clave | En 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:
Matriz de estrategias de defensa
| Tipo de ataque | Método de ataque | Medida defensiva |
|---|---|---|
| Inyección de prompts | Inducir al modelo a sobrescribir instrucciones del sistema | Detección con barreras de seguridad integradas + aislamiento estricto de entrada de usuario e instrucciones del sistema |
| Inyección de comandos | Incrustar código malicioso en la solicitud | Auditoría previa a la ejecución + privilegios mínimos |
| Fuga de prompts | Inducir al modelo a revelar su propio System Prompt | Las barreras de seguridad identifican patrones de sondeo |
| Envenenamiento de base de conocimiento | Subir documentos con información errónea | Flujo de aprobación de ingesta de conocimiento + escaneo previo de contenido |
| Robo de modelo | Recopilar datos de entrenamiento mediante gran volumen de llamadas API | Limitación de tasa API + identificación de tráfico de bots |
| Invocación maliciosa de funciones | Inducir al Agent a ejecutar operaciones peligrosas | Auditorí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:
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 resultChecklist 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 validadasEstrategia 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 continuaMejores 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:
- Modelo principal no disponible → cambiar al modelo de respaldo
- Modelo de respaldo también no disponible → usar respuestas frecuentes cacheadas
- 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 menorPipeline 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 profesorCuándo destilar
| Escenario | Recomendación |
|---|---|
| Tareas de alto volumen y repetitivas (clasificación, extracción) | Destilar — el ahorro de costes es enorme |
| Generación creativa y abierta | Mantener 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 complejas | Mantener profesor — los modelos pequeños struggled con razonamiento nuevo |
| Híbrido: tareas simples + casos extremos complejos | Enrutar — 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 response3. 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 responseFramework 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:
- Solución buscando problema: construir demos de IA impresionantes que no abordan necesidades de negocio reales
- 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:
- Mapear puntos de dolor del negocio: entrevistar stakeholders, analizar tickets de soporte, revisar cuellos de botella de procesos
- 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- 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 cargaFase 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 uso | Métricas primarias | Métricas secundarias |
|---|---|---|
| Bot de soporte al cliente | Tasa de desvío de tickets, tiempo de resolución | Satisfacción del cliente (CSAT), coste por ticket |
| Asistente de revisión de código | Tiempo de revisión, tasa de defectos escapados | Satisfacción del desarrollador, puntuaciones de calidad de código |
| Generación de contenido | Tiempo de producción de contenido, métricas de engagement | Puntuaciones de consistencia de marca, tasa de aprobación editorial |
| Agent de análisis de datos | Tiempo de análisis, calidad de insights | Satisfacció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 puntosFase 4: Enhance — Iterar y expandir
Objetivo: mejorar continuamente el sistema basándose en datos y feedback.
Estrategias de mejora:
- Optimización de prompts: usar datos de evaluación para refinar prompts (ver capítulo de Evaluación)
- Mejoras de RAG: añadir más documentos, mejorar chunking, añadir reranking
- Actualizaciones de modelo: destilar a modelos más pequeños para ahorro de costes (ver sección de Destilación)
- Expansión de funcionalidades: añadir nuevas capacidades basándose en feedback de usuarios
- 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égicasRIDE en la práctica: errores comunes
| Error | Cómo evitarlo |
|---|---|
| Saltarse Research y lanzarse a implementar | Empezar siempre con entrevistas a stakeholders y puntuación de casos de uso |
| Construir durante meses antes de entregar | Fijar 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 puntual | Planificar iteración continua; los sistemas de IA necesitan mantenimiento continuo |
| Ignorar seguridad y cumplimiento | Construir barreras desde el día uno; no añadirlas después |
Apéndice: Consulta rápida de conceptos clave
| Concepto | Explicación en una frase |
|---|---|
| Tokenization | Convertir texto en una secuencia de IDs numéricos procesables por el modelo |
| Embedding | Mapear IDs discretos a vectores densos que contienen información semántica |
| Attention | Permitir que el modelo, al procesar cada token, preste atención a todas las posiciones relevantes de la secuencia |
| RAG | Primero recuperar conocimiento relevante, luego hacer que el modelo genere la respuesta basándose en ese conocimiento |
| ReAct | Hacer que el modelo alterne entre razonamiento (Reasoning) y acción (Action) en un ciclo |
| Function Calling | El modelo emite instrucciones estructuradas de invocación de herramientas en lugar de respuestas de texto plano |
| MCP | Protocolo de estandarización de herramientas propuesto por Anthropic, que desacopla la definición y el uso de herramientas |
| Plan & Execute | Primero elaborar un plan de acción completo y, tras revisarlo y aprobarlo, ejecutarlo paso a paso |
| HyDE | Primero generar una respuesta hipotética y usar esa respuesta para recuperar, en lugar de la pregunta original |
| Lost in the Middle | La capacidad del modelo para procesar información en la parte central de contextos largos disminuye significativamente |
| Mixture-of-Agents | Múltiples modelos distintos procesan la misma tarea y un agregador sintetiza el mejor resultado |
| LoRA | Conseguir fine-tuning eficiente entrenando matrices adaptadoras de bajo rango |
| Destilación | Usar la salida del modelo grande como datos de entrenamiento para enseñar a un modelo pequeño |
| Skill | Encapsular conocimiento especializado en unidades funcionales modulares reutilizables |
| LLM-as-Judge | Usar un modelo grande como evaluador para puntuar automáticamente las salidas |
| SLO | Objetivos 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-herePrimera 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 seguimientoControl 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 replyEtapa 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:
- “Empiezo el próximo lunes, ¿qué debo traer?”
- “¡Gracias! Por cierto, mi ID de empleado es EMP-042.”
- “¿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 contextPaso 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 NoneIntegració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 replyEtapa 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:
| Etapa | Capacidad | Conocimiento Aplicado |
|---|---|---|
| 6.1 | Conversación básica | Fundamentos de LLM (Tokens, Ventana de Contexto) |
| 6.2 | Respuestas basadas en documentos | RAG (Embedding, Chunking, Retrieval) |
| 6.3 | Uso de herramientas y planificación | Núcleo del Agent (Function Calling, ReAct, MCP) |
| 6.4 | Memoria y respuestas personalizadas | Sistemas de Memoria y Habilidades |
| 6.5 | Calidad medible | Framework de Evaluación |
| 6.6 | Sistema de producción costo-eficiente | Destilación, Monitoreo, Canary Release |
Este es el camino completo de la ingeniería de Agents — desde “Hello World” hasta producción.