AI Engineer: construye un agente de IA real, módulo a módulo
Cada módulo combina teoría explicada, ejemplos de código comentados y ejercicios prácticos. Al completarlos, todos los entregables se integran en un sistema de agentes listo para producción.
- módulos
- 10
- entregables
- 10
- proyecto final
- 1
- duración estimada
- ~10 sem
Proyecto final: Nexus Support Agent
Sistema multi-agente de soporte al cliente. End-to-end: RAG, memoria episódica, tool calling, human-in-the-loop, model routing, observabilidad y evaluación continua en CI.
Arquitectura
| Entrada | Core Agents | Infraestructura |
|---|---|---|
| Chat API (FastAPI) | Orchestrator | pgvector + RAG |
| Classifier Agent | SupportAgent (ReAct) | Redis (session) |
| MemoryManager | CriticAgent | LangSmith (tracing) |
| Guardrails | EscalationRouter | Eval pipeline (CI) |
Qué componente aporta cada módulo
- M1 →
LLMClient - M2 →
PromptLoader - M3 →
SupportAgent - M4 →
Orchestrator - M5 →
RAG+Memory - M6 →
EscalationRouter - M7 →
ToolRegistry - M8 →
ModelRouter - M9 →
DebugToolkit - M10 →
Observability
Cronograma sugerido
Una semana por módulo — con integración progresiva al proyecto final.
| Semanas | Fase | Qué construyes |
|---|---|---|
| Semanas 1-2 | Fundamentos | LLMClient + PromptLoader |
| Semanas 3-5 | Arquitectura | Agent + Orchestrator + RAG |
| Semanas 6-7 | Producción | HITL + Tool Layer |
| Semanas 8-9 | Avanzado | Optimización + Debug |
| Semana 10 | LLMOps | Observabilidad + Integración |
Módulo 1 · Fase 1 · LLMs & Prompt Engineering
Fundamentos de LLMs
Arquitectura, inferencia y selección de modelos en producción
¿Qué es un LLM y cómo genera texto?
Un Large Language Model es una red neuronal entrenada para predecir el siguiente token dado un contexto. No "entiende" como lo hacemos nosotros — aprendió patrones estadísticos en billones de tokens de texto. Cada vez que genera una palabra, está calculando una distribución de probabilidad sobre el vocabulario y muestreando de ella.
Imagina que completaste millones de ejercicios de "continúa esta oración". Con suficiente práctica, desarrollas intuición sobre qué palabras suelen seguir a cuáles. Un LLM hace algo similar pero a escala masiva y con patrones mucho más complejos.
Arquitectura Transformer — lo que necesitas saber
No necesitas implementar un Transformer, pero sí entender sus implicaciones prácticas:
Conceptos clave y su impacto en producción
- Self-attention: cada token "atiende" a todos los demás del contexto. Implicación: el modelo puede relacionar información que está lejos en el texto.
- Ventana de contexto: límite máximo de tokens activos. GPT-4: 128K, Claude 3.5: 200K, Gemini 1.5 Pro: 1M. Más contexto = más costo y latencia.
- Decoder-only: los modelos modernos (GPT, Claude, Gemini) solo generan — no tienen un encoder separado. Procesan todo el contexto cada vez que generan un token.
- Tokenización (BPE): el texto se divide en sub-palabras. "tokenización" puede ser 3-4 tokens. El costo real de una llamada depende del número de tokens, no de palabras ni caracteres.
Parámetros de inferencia — el dial de control
Cuando haces una llamada al API, estos parámetros determinan cómo el modelo muestrea su respuesta:
temperature
0 = determinista (siempre el token más probable). 1 = más variado. Para agentes en producción: usa 0–0.3. Para generación creativa: 0.7–1.0.
top_p (nucleus sampling)
Solo considera los tokens cuya probabilidad acumulada llega a p. top_p=0.9 ignora el 10% de tokens menos probables. Alternativa a temperature, no se usan juntos.
max_tokens
Límite de tokens en la respuesta. Impacta costo y latencia directamente. Para respuestas estructuradas (JSON), un límite bajo previene respuestas truncadas inesperadamente.
stop_sequences
El modelo para de generar cuando encuentra este string. Útil para delimitar outputs: ["</response>", "###"]. Más confiable que max_tokens para outputs estructurados.
Usar temperature=0 no garantiza outputs idénticos. Los LLMs pueden tener variación incluso con temperature=0 debido a diferencias en hardware y paralelismo. Para reproducibilidad exacta, guarda el input completo y el output.
Selección de modelo — el trade-off más importante
Guía de selección para producción 2025
- Claude 3 Haiku / GPT-4o-mini: clasificación, routing, extracción simple. ~$0.25/M tokens. Latencia: <1s. Usa cuando el error tiene bajo impacto.
- Claude 3.5 Sonnet / GPT-4o: razonamiento, generación, tool calling complejo. ~$3/M tokens. Balance ideal para la mayoría de agentes en producción.
- Claude 3 Opus / GPT-4-turbo: análisis profundo, decisiones de alto impacto. ~$15/M tokens. Solo cuando la calidad es crítica y el costo es secundario.
- Mistral / LLaMA 3 (self-hosted): datos sensibles, cumplimiento regulatorio, costo a escala extrema. Requiere infraestructura propia.
El 70-80% de las consultas en un sistema de soporte son simples. Clasificar automáticamente la complejidad y usar Haiku para casos simples puede reducir el costo total un 60% sin impacto perceptible en calidad.
LLMClient — wrapper base del sistema
El patrón correcto no es llamar al SDK directamente desde cada agente. Se crea un wrapper centralizado que maneja: retry automático, logging estructurado, conteo de tokens y selección de modelo.
from anthropic import Anthropic
from tenacity import retry, stop_after_attempt, wait_exponential
from enum import Enum
import structlog, time
log = structlog.get_logger()
class ModelTier(Enum):
FAST = "claude-3-haiku-20240307" # barato y rápido
STANDARD = "claude-3-5-sonnet-20241022" # balance ideal
POWERFUL = "claude-3-opus-20240229" # máxima calidad
class LLMResponse:
text: str
input_tokens: int
output_tokens: int
cost_usd: float
latency_ms: float
class LLMClient:
def __init__(self):
self.client = Anthropic()
self.cost_per_token = {
ModelTier.FAST: (0.00025, 0.00125), # (input, output) por 1K tokens
ModelTier.STANDARD: (0.003, 0.015),
ModelTier.POWERFUL: (0.015, 0.075),
}
@retry(stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10))
def call(
self,
messages: list[dict],
model: ModelTier = ModelTier.STANDARD,
temperature: float = 0.3,
max_tokens: int = 1024,
trace_id: str = None,
) -> LLMResponse:
start = time.time()
response = self.client.messages.create(
model=model.value,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
latency = (time.time() - start) * 1000
cost = self._calculate_cost(model, response.usage)
# Log estructurado para observabilidad
log.info("llm_call",
trace_id=trace_id,
model=model.value,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cost_usd=round(cost, 6),
latency_ms=round(latency, 1),
)
return LLMResponse(
text=response.content[0].text,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
cost_usd=cost,
latency_ms=latency,
)
def _calculate_cost(self, model, usage) -> float:
inp, out = self.cost_per_token[model]
return (usage.input_tokens/1000*inp) + (usage.output_tokens/1000*out)
El decorador @retry de tenacity maneja automáticamente los rate limits (429) y errores temporales del API con backoff exponencial. Sin esto, cualquier fallo transitorio rompe el flujo del agente.
Recursos
anthropic-sdk docs, tenacity, structlog, tiktoken
Benchmark de temperatura
Entender empíricamente cómo temperature afecta el output antes de elegir el valor para producción.
- Escribe un prompt que pida clasificar el sentimiento de una frase (positivo/negativo/neutro)
- Ejecuta la misma llamada 10 veces con temperature=0 — ¿siempre da el mismo resultado?
- Repite con temperature=0.5 y temperature=1.0 — ¿qué cambia?
- Registra el costo y la latencia de cada llamada — ¿la temperatura afecta el costo?
- Conclusión: ¿qué temperature elegirías para el clasificador de intent del proyecto?
Conteo de tokens y estimación de costo
Antes de diseñar el sistema, saber cuánto costará cada llamada.
- Escribe el system prompt del agente de soporte (borrador inicial, ~200 palabras)
- Usa
tiktokenpara contar cuántos tokens ocupa - Simula 1000 conversaciones de 5 turnos: calcula el costo total con Haiku vs Sonnet
- ¿Qué porcentaje del costo viene del system prompt vs el historial?
- Documenta cuál modelo elegirías y por qué para el clasificador de intent
Entregable del módulo
Va al proyecto final: LLMClient wrapper. Clase Python lista para producción que encapsula todas las llamadas al API. Todos los agentes del proyecto usarán este wrapper — nunca el SDK directamente. El LLMClient es la capa base del sistema. En M8 (Model Router) se extenderá para seleccionar el modelo dinámicamente por consulta, en vez de recibirlo como parámetro fijo.
Módulo 2 · Fase 1 · LLMs & Prompt Engineering
Prompt Engineering Avanzado
System prompts, constraints, CoT controlado y versionado
El system prompt es la constitución del agente
El system prompt no es una "instrucción inicial" — es el documento que define completamente quién es el agente, qué puede hacer, cómo debe comportarse y cuándo debe pedir ayuda. Un agente sin system prompt bien estructurado es un agente impredecible.
Estructura en 6 secciones (todas obligatorias)
- IDENTITY: nombre, propósito, personalidad. Define el "quién" del agente.
- CAPABILITIES: lista explícita de qué puede y NO puede hacer. Los "no puede" son tan importantes como los "puede".
- CONTEXT: variables dinámicas del entorno: usuario, estado, herramientas disponibles.
- BEHAVIOR RULES: cómo actuar ante situaciones específicas — edge cases explícitos.
- OUTPUT FORMAT: estructura exacta, longitud y canal de la respuesta.
- ESCALATION: criterios exactos para transferir a humano.
Lo que no está explícito en el system prompt, el modelo lo inventa. Cada comportamiento esperado debe estar especificado. La ambigüedad en el prompt es el origen del 80% de los bugs en agentes.
Chain-of-Thought controlado — separar razonamiento de respuesta
El CoT (Chain-of-Thought) mejora la calidad del razonamiento, pero en producción no queremos mostrar el proceso interno al usuario. El patrón correcto es separar los dos:
CoT sin control
- El usuario ve el razonamiento interno
- Expone lógica que puede ser manipulada
- Aumenta tokens de output innecesariamente
- Dificulta el parsing de la respuesta final
CoT con <thinking> separado
- El razonamiento queda en logs internos
- Permite debugging sin exposición al usuario
- El output final es limpio y parseable
- Puedes monitorear la calidad del razonamiento
Few-shot con ejemplos negativos
Los ejemplos positivos enseñan el comportamiento esperado. Los ejemplos negativos son igual de críticos — le muestran al modelo exactamente qué evitar. Sin ellos, el modelo puede caer en respuestas "razonables pero incorrectas".
Incluir solo ejemplos positivos en el few-shot. El modelo aprende "qué hacer" pero no "qué NO hacer". Los edge cases y fallos más frecuentes deben aparecer como ejemplos negativos explícitos.
Prompts como código — versionado y testing
Un prompt que cambia sin control es una regresión silenciosa. La misma disciplina que aplicamos al código aplica a los prompts:
Pipeline de deployment de prompts
- PR en git con el cambio de prompt + justificación en descripción
- Evaluación automática en CI contra el test set base
- Deploy a staging → 10% de tráfico → 48h de monitoreo
- Si métricas OK → promover a 100%. Si degradan → rollback automático
System prompt completo con CoT controlado
IDENTITY:
Eres SupportBot, asistente de atención al cliente de Nexus.
Objetivo: resolver consultas de soporte con empatía y precisión.
Tono: cercano, claro, sin jerga técnica innecesaria.
CAPABILITIES:
✓ Puedes: get_order_status, create_ticket, send_notification, schedule_callback
✗ NO puedes: modificar precios, eliminar cuentas, acceder a datos de pago
CONTEXT:
Usuario: {{user_name}} | Plan: {{plan_name}} | Estado: {{account_status}}
Canal: {{channel}} | Herramientas: {{available_tools}}
BEHAVIOR RULES:
- Lenguaje agresivo → desescalar sin confrontar: "Entiendo tu frustración,
mi objetivo es encontrar una solución que funcione para ti."
- Solicitud fuera de alcance → explicar límite + ofrecer alternativa real
- Input ambiguo → preguntar UNA cosa antes de actuar
- Señal de crisis o urgencia alta → escalar a humano inmediatamente
ESCALATION:
Transferir SIEMPRE cuando: ticket_priority="critical" OR usuario solicita
hablar con persona OR confidence_score < 0.70
REASONING FORMAT:
Antes de responder, razona en <thinking>:
1. ¿Qué pide exactamente el usuario?
2. ¿Qué información tengo vs qué me falta?
3. ¿Qué regla de comportamiento aplica?
4. ¿Debo escalar o puedo resolver?
El contenido de <thinking> NO se muestra al usuario.
OUTPUT FORMAT:
- Máx 3 oraciones por turno (canal: chat/WhatsApp)
- Cuando uses herramienta: responde SOLO JSON válido sin texto adicional:
{"action": "<tool>", "params": {...}, "reason": "<1 oración>", "confidence": 0.0-1.0}
PromptLoader — gestión centralizada de versiones
import os
from pathlib import Path
from jinja2 import Template
PROMPTS_DIR = Path("prompts")
class PromptLoader:
def get(self, agent: str, version: str, context: dict) -> str:
"""Carga un prompt versionado e inyecta variables de contexto."""
path = PROMPTS_DIR / agent / f"v{version}_system.txt"
template_str = path.read_text(encoding="utf-8")
return Template(template_str).render(**context)
def latest(self, agent: str) -> str:
"""Lee la versión actual desde el archivo VERSION del agente."""
version_file = PROMPTS_DIR / agent / "VERSION"
return version_file.read_text().strip()
def load_latest(self, agent: str, context: dict) -> str:
"""Atajo: carga siempre la versión más reciente."""
return self.get(agent, self.latest(agent), context)
# Uso en un agente:
loader = PromptLoader()
system_prompt = loader.load_latest("support_agent", {
"user_name": "Ana López",
"plan_name": "Pro",
"account_status": "active",
"channel": "whatsapp",
"available_tools": "[get_order_status, create_ticket]",
})Recursos
jinja2, jsonlines, Anthropic prompting guide, Learn Prompting
Construcción guiada del system prompt
Escribe el system prompt completo para el SupportBot siguiendo la estructura de 6 secciones.
- Escribe una primera versión sin estructura — solo lo que se te ocurra naturalmente
- Evalúa: ¿qué sección falta? ¿hay ambigüedad en alguna regla?
- Reescribe usando las 6 secciones. Agrega al menos 3 BEHAVIOR RULES específicas
- Prueba el prompt enviando 5 mensajes edge-case: input agresivo, solicitud imposible, input ambiguo, solicitud de datos sensibles, y una consulta válida normal
- Ajusta las reglas según los resultados y documenta qué cambió en CHANGELOG.md
Few-shot con casos negativos
El few-shot más valioso incluye ejemplos de qué NO hacer, no solo de qué hacer.
- Identifica los 3 tipos de errores más comunes que podría cometer el SupportBot (ej: asumir antes de preguntar, responder fuera de scope, usar tono incorrecto)
- Por cada error, escribe un par (mensaje usuario → respuesta INCORRECTA del bot)
- Luego escribe la respuesta CORRECTA para el mismo mensaje
- Agrega estos 3 pares negativos al system prompt y vuelve a testear con los 5 mensajes del EJ1
- ¿Mejoró el comportamiento? ¿En qué casos?
Entregable del módulo
Va al proyecto final: Prompt Library v1.0 + PromptLoader. System prompts versionados para los 3 agentes del proyecto (support, classifier, critic), con variables dinámicas, test set base y un loader que inyecta contexto en tiempo de ejecución. El PromptLoader es usado por todos los agentes. En M10, el eval pipeline se conectará con él para ejecutar el test set automáticamente en cada PR que modifique un prompt.
Módulo 3 · Fase 2 · Agentes & Memoria
Patrones de Agentes
Planner/Executor, ReAct loop, Tool-using y Critic
¿Qué es un agente LLM?
Un agente es un sistema donde el LLM no solo genera texto — también toma decisiones sobre qué acciones ejecutar, observa los resultados de esas acciones, y decide qué hacer a continuación. La diferencia con un simple chatbot es que el agente tiene agencia: puede actuar sobre el mundo.
Un chatbot es como un empleado que solo puede dar respuestas verbales. Un agente es como un empleado que puede también abrir sistemas, enviar emails, crear tickets y buscar información — todo en respuesta a lo que el cliente necesita.
Patrón ReAct — Reason + Act
ReAct es el patrón más usado en producción. En cada turno, el agente: (1) razona sobre el estado actual, (2) decide una acción, (3) observa el resultado, y repite hasta tener suficiente información para responder al usuario.
Ciclo ReAct paso a paso
- Thought: "El usuario pregunta por su pedido. Necesito llamar a get_order_status con su ID."
- Action:
get_order_status(order_id="ORD-123") - Observation:
{"status": "en camino", "eta": "mañana 14:00"} - Thought: "Tengo la información necesaria. Puedo responder al usuario."
- FINISH: "Tu pedido está en camino y llegará mañana antes de las 14:00."
Sin un MAX_ITERATIONS hard limit, un agente puede quedarse en ciclo indefinidamente si una herramienta falla repetidamente o si el razonamiento no converge. Este límite debe ser en el orquestador, no en el prompt.
Critic loop — el agente evalúa su propio output
El Critic es un segundo agente (o una segunda llamada al LLM) que evalúa la respuesta del agente principal antes de enviarla al usuario. Responde PASS/FAIL + razón. Duplica el costo pero aumenta significativamente la calidad en casos de alto impacto.
Úsalo cuando el costo de una respuesta incorrecta es mayor que el costo de la llamada extra. Para soporte: cuando el agente va a crear un ticket o enviar una notificación. No lo uses en cada respuesta — solo en acciones con efectos secundarios.
from dataclasses import dataclass, field
from enum import Enum
class AgentAction(Enum):
FINISH = "FINISH"
ESCALATE = "ESCALATE"
TOOL = "TOOL"
@dataclass
class AgentThought:
reasoning: str # contenido del <thinking>
action: AgentAction
tool_name: str | None = None
tool_params: dict = field(default_factory=dict)
final_answer: str | None = None
confidence: float = 1.0
class SupportAgent:
max_iterations = 8
def __init__(self, llm_client, tool_registry, prompt_loader):
self.llm = llm_client
self.tools = tool_registry
self.loader = prompt_loader
def run(self, user_message: str, context: dict) -> AgentResult:
system = self.loader.load_latest("support_agent", context)
history = []
for i in range(self.max_iterations):
# Paso 1: REASON — el agente piensa qué hacer
messages = self._build_messages(system, user_message, history)
response = self.llm.call(messages, trace_id=context["trace_id"])
thought = self._parse_thought(response.text)
# Paso 2: verificar stopping criteria
if thought.action == AgentAction.FINISH:
return AgentResult(answer=thought.final_answer, iterations=i+1)
if thought.action == AgentAction.ESCALATE:
return AgentResult(escalate=True, reason=thought.reasoning, iterations=i+1)
# Paso 3: ACT — ejecutar la herramienta
observation = self.tools.execute(thought.tool_name, thought.tool_params)
# Paso 4: OBSERVE — agregar al historial
history.append({"thought": thought, "observation": observation})
# MAX_ITERATIONS alcanzado → siempre escalar, nunca lanzar excepción
return AgentResult(escalate=True, reason="max_iterations_reached", iterations=self.max_iterations)
class CriticAgent:
def evaluate(self, agent_result: AgentResult, original_query: str) -> CriticVerdict:
"""Evalúa si la respuesta del agente es correcta antes de enviarla."""
prompt = f"""
Evalúa esta respuesta de soporte:
Consulta original: {original_query}
Respuesta del agente: {agent_result.answer}
Responde SOLO con JSON:
{{"status": "PASS" o "FAIL", "reason": "...", "suggestion": "..."}}
"""
response = self.llm.call([{"role": "user", "content": prompt}],
model=ModelTier.FAST) # Haiku para el critic = más barato
return CriticVerdict(**json.loads(response.text))Recursos
ReAct paper (Yao 2022), Anthropic tool use, pydantic v2
Implementa el loop ReAct desde cero
Antes de usar el código base, entiende el patrón implementándolo tú mismo con un caso simple.
- Crea una herramienta falsa
get_weather(city)que retorna JSON hardcodeado - Implementa el loop ReAct en ~30 líneas: reason → parse action → execute → observe → repeat
- Prueba con: "¿Qué temperatura hace en Madrid?" — el agente debería llamar a la herramienta
- Ahora prueba con: "Cuéntame un chiste" — el agente debería terminar en 1 iteración sin usar herramienta
- Fuerza el loop infinito: haz que
get_weathersiempre retorne error. ¿El MAX_ITERATIONS funciona?
Construye el CriticAgent y prueba su efectividad
Evaluar si el Critic realmente mejora la calidad del sistema.
- Implementa el CriticAgent con el prompt del código base
- Genera 10 respuestas del SupportAgent a consultas variadas
- Evalúa cada una con el Critic — ¿cuántas pasan? ¿cuántas fallan y por qué?
- Para las que fallan: ¿el Critic tiene razón? ¿hay falsos positivos?
- Mide el costo adicional del Critic: ¿cuánto añade por consulta? ¿vale la pena?
Entregable del módulo
Va al proyecto final: SupportAgent Core + CriticAgent. Agente principal con ReAct loop, MAX_ITERATIONS, stopping criteria y escalación. Más un CriticAgent que evalúa respuestas de alto impacto antes de enviarlas. El SupportAgent es el motor del sistema. En M4 será envuelto por el Orchestrator. El CriticAgent se conectará al pipeline de evaluación del M10 para medir calidad en producción.
Módulo 4 · Fase 2 · Agentes & Memoria
Multi-Agent Orchestration
Orquestador, classifier, handoffs tipados y timeout global
Hub-and-spoke: el patrón más robusto para producción
Un orquestador central recibe todos los mensajes, los clasifica con un agente ligero (barato y rápido), y delega al agente especializado correcto con el contexto completo.
Como una recepcionista en un hospital: no hace el diagnóstico, pero sabe exactamente a qué especialista enviarte. El clasificador es la recepcionista — rápido, barato, y con criterio de routing.
El clasificador es la pieza más crítica del sistema
- Usa el modelo más barato: Haiku con un prompt de 5 líneas clasifica mejor que Sonnet con un prompt ambiguo
- Categorías exhaustivas: toda consulta debe caer en alguna categoría — incluye "GENERAL/OTHER"
- Output tipado: el classifier nunca retorna texto libre — retorna un enum con la categoría
- Fallback seguro: si el classifier falla, el sistema enruta al agente general — nunca rompe
El error más frecuente en multi-agent: el agente B recibe el mensaje del usuario pero no sabe qué hizo el agente A. El handoff debe incluir: historial completo, acción ya tomada y razón de la transferencia.
class Intent(Enum):
ORDER_STATUS = "order_status"
CREATE_TICKET = "create_ticket"
ESCALATE = "escalate"
GENERAL = "general"
@dataclass
class AgentHandoff:
"""Contexto completo que pasa entre agentes en un handoff."""
user_id: str
user_message: str
intent: Intent
conversation_history: list[dict]
previous_actions: list[str] # qué ya intentó el agente anterior
context: dict # datos del usuario (plan, status, etc.)
trace_id: str
class Orchestrator:
global_timeout = 30 # segundos — nunca un workflow dura más
def handle(self, user_id: str, message: str) -> OrchestratorResponse:
context = self.context_builder.build(user_id)
trace_id = self._new_trace_id()
# 1. Clasificar intención con modelo barato (Haiku)
intent = self.classifier.classify(message, context)
# 2. Construir handoff con contexto completo
handoff = AgentHandoff(
user_id=user_id, user_message=message, intent=intent,
conversation_history=self.session.get_history(user_id),
previous_actions=[], context=context, trace_id=trace_id
)
# 3. Routing al agente correcto con timeout global
with timeout(self.global_timeout):
agent = self.router[intent]
result = agent.run(handoff)
# 4. Evaluar si escalar antes de responder al usuario
verdict = self.escalation_router.evaluate(result, context)
if verdict.should_escalate:
return self._escalate(handoff, verdict.reason)
return OrchestratorResponse(message=result.answer, trace_id=trace_id)Recursos
signal (timeout), claude-3-haiku, pydantic
Diseña el esquema de routing
Antes de implementar, diseña el mapa completo de intenciones y agentes.
- Lista todas las consultas posibles de un usuario de soporte (al menos 15)
- Agrupa en categorías — ¿cuántos agentes necesitas realmente?
- Escribe el prompt del clasificador con todas las categorías
- Prueba el classifier con las 15 consultas — ¿clasifica correctamente?
- Ajusta hasta lograr >90% de accuracy en las 15 consultas
Simula un handoff fallido
Entender qué pasa cuando el contexto del handoff es incompleto.
- Implementa un handoff mínimo: solo pasa el mensaje del usuario, sin historial ni contexto
- Prueba con: un usuario que retoma una conversación anterior
- ¿El agente B "sabe" qué hizo el agente A? ¿Responde correctamente?
- Agrega el historial completo al handoff y repite. ¿Mejora?
- Documenta qué campos del AgentHandoff son indispensables
Entregable del módulo
Va al proyecto final: Orchestrator + ClassifierAgent. Orquestador con routing, timeout global y handoffs tipados. ClassifierAgent con Haiku que enruta correctamente las consultas. El Orchestrator es el punto de entrada de la API. En M6 se le agrega el EscalationRouter en la salida, y en M7 el ToolRegistry se inyecta en el SupportAgent que el Orchestrator coordina.
Módulo 5 · Fase 2 · Agentes & Memoria
Memoria, Contexto y RAG
Embeddings, vector store, retrieval y estrategias de memoria
El problema de la memoria en LLMs
Por defecto, un LLM no recuerda nada entre sesiones. Cada llamada al API es stateless. Para un agente de soporte, esto es un problema: el usuario no debería tener que repetir su problema en cada interacción.
Los 4 tipos de memoria y cuándo usar cada uno
- Short-term (ventana activa): historial del turno actual en el contexto del LLM. Gratis en costo, se pierde al cerrar sesión.
- Long-term (vector store): knowledge base del dominio. Búsqueda semántica por similitud de embeddings. Para documentación, FAQs, políticas.
- Episodic (historial de interacciones): qué dijo el usuario en sesiones anteriores. Base de datos estructurada con timestamp.
- Semantic (entidades del usuario): datos persistentes: plan, preferencias, historial de tickets. Structured DB.
Short-term = lo que recuerda de esta llamada. Long-term = el manual de soporte que consultó. Episodic = notas de llamadas anteriores con este cliente. Semantic = ficha del cliente con sus datos y plan.
Pipeline RAG — cómo funciona en producción
Los 4 pasos del pipeline
- Ingestion: documento → chunking (512 tokens, 10% overlap) → embedding → vector store + metadata
- Retrieval: query → embed → ANN search (top-10) → filtro por metadata → reranking → top-3
- Augmentation: chunks recuperados → inyectar en el contexto del LLM
- Evaluation: ¿la respuesta usa los chunks? ¿los chunks eran relevantes?
Chunks muy pequeños (< 200 tokens) pierden contexto. Chunks muy grandes (> 1500 tokens) introducen ruido. Experimenta con tu dominio específico — no existe un tamaño universal óptimo.
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.vector_stores.postgres import PGVectorStore
import redis
class MemoryManager:
def __init__(self, pg_conn_str: str, redis_url: str):
self.vector_store = PGVectorStore.from_params(pg_conn_str, embed_dim=1536)
self.session = redis.from_url(redis_url)
self.index = VectorStoreIndex.from_vector_store(self.vector_store)
def get_context(self, query: str, user_id: str) -> MemoryContext:
return MemoryContext(
# Short-term: historial de la sesión actual
short_term = self._get_session(user_id),
# Long-term: knowledge base del dominio (RAG)
long_term = self._retrieve_relevant(query, k=3),
# Episodic: últimas 3 interacciones del usuario
episodic = self._get_recent_episodes(user_id, n=3),
)
def _retrieve_relevant(self, query: str, k: int) -> list[str]:
retriever = self.index.as_retriever(similarity_top_k=k*3) # más para re-rankear
nodes = retriever.retrieve(query)
# Reranking: ordenar por relevancia real, no solo similitud vectorial
reranked = sorted(nodes, key=lambda n: n.score, reverse=True)[:k]
return [n.text for n in reranked]
def _get_session(self, user_id: str) -> list[dict]:
raw = self.session.get(f"session:{user_id}")
return json.loads(raw) if raw else []
def save_turn(self, user_id: str, user_msg: str, agent_response: str):
history = self._get_session(user_id)
history.append({"user": user_msg, "agent": agent_response})
self.session.setex(f"session:{user_id}", 3600, json.dumps(history)) # TTL 1hRecursos
llama-index, pgvector, redis-py, Cohere Rerank, RAGAS (eval)
Experimenta con chunking
El tamaño del chunk es la variable más impactante en la calidad del RAG.
- Toma 5 documentos de soporte (FAQs, guías, políticas)
- Indexa con chunk_size=256 tokens
- Haz 10 preguntas sobre el contenido — ¿qué % responde correctamente?
- Re-indexa con chunk_size=512 y chunk_size=1024. Repite las preguntas
- ¿Qué tamaño da mejores resultados para tu dominio? ¿Por qué?
Mide el impacto de la memoria episódica
Cuantificar si la memoria episódica mejora la experiencia real.
- Simula una conversación de 2 sesiones: en la primera, el usuario reporta un problema. En la segunda, vuelve con el mismo problema
- Prueba sin memoria episódica: ¿el agente recuerda el contexto anterior?
- Activa la memoria episódica y repite. ¿El agente responde diferente?
- Mide el costo extra de incluir el historial episódico en el contexto
- ¿Vale la pena? Documenta la decisión en un ADR
Entregable del módulo
Va al proyecto final: MemoryManager + RAG Pipeline. Pipeline completo de ingestion y retrieval, más clase MemoryManager que gestiona los 3 tipos de memoria del agente. El MemoryManager se inyecta en el Orchestrator. Antes de cada llamada al agente, el sistema recupera contexto relevante (RAG + episódico) y lo añade al prompt dinámicamente.
Módulo 6 · Fase 3 · Producción & Integración
Human-in-the-Loop
Criterios de escalamiento, fallbacks en cascada y circuit breaker
Human-in-the-loop no es un edge case — es diseño
El error más común es tratar la escalación como algo excepcional. En producción, entre el 10-30% de las interacciones terminarán en un humano. El sistema debe estar diseñado para esto desde el principio, no añadirlo después.
Define los criterios de escalamiento ANTES de salir a producción. Si los defines cuando ya hay incidentes, los estás eligiendo bajo presión y sin datos. Los criterios deben ser configurables por entorno y medibles en el dashboard.
5 tipos de criterios de escalamiento
- Umbral de negocio: ticket_priority="critical", account_type="enterprise"
- Confianza baja: confidence_score < 0.70 en la acción a tomar
- Fuera de scope: el agente no puede resolver la solicitud
- Solicitud explícita: el usuario pide hablar con una persona
- Señal emocional: crisis, urgencia extrema, frustración acumulada
Fallback en cascada — el sistema nunca muere
Un sistema de producción debe responder siempre, incluso cuando todo falla. El patrón de fallback en cascada define una cadena de degradación gradual:
Cadena de fallback
- Nivel 1: SupportAgent con Sonnet (normal)
- Nivel 2: SupportAgent con Haiku (más rápido y barato si hay latencia)
- Nivel 3: Respuesta genérica hardcodeada + escalación automática a humano
- Nivel 4: Mensaje de error amigable con número de ticket creado automáticamente
from pybreaker import CircuitBreaker, CircuitBreakerError
# Circuit breaker por herramienta — evita cascada de fallos
ticket_breaker = CircuitBreaker(fail_max=5, reset_timeout=60)
class EscalationRule:
name: str
check: callable # función que recibe (result, context) → bool
reason: str
class EscalationRouter:
rules: list[EscalationRule] = [
EscalationRule("critical_ticket",
lambda r, ctx: ctx.get("ticket_priority") == "critical", "ticket_critico"),
EscalationRule("low_confidence",
lambda r, ctx: r.confidence < 0.70, "confianza_baja"),
EscalationRule("user_requested",
lambda r, ctx: ctx.get("user_requested_human", False), "usuario_solicito"),
]
def evaluate(self, result, context) -> Verdict:
for rule in self.rules:
if rule.check(result, context):
audit_log.record("escalation", rule=rule.name)
return Verdict(should_escalate=True, reason=rule.reason)
return Verdict(should_escalate=False)
class FallbackChain:
def run(self, handoff: AgentHandoff) -> AgentResult:
try:
return self.support_agent.run(handoff) # Nivel 1: normal
except (TimeoutError, RateLimitError):
try:
return self.support_agent_fast.run(handoff) # Nivel 2: modelo barato
except Exception:
return self._static_fallback(handoff) # Nivel 3: respuesta fija
def _static_fallback(self, handoff) -> AgentResult:
ticket_id = self._create_fallback_ticket(handoff)
return AgentResult(
answer=f"Estamos experimentando problemas técnicos. Creamos el ticket #{ticket_id} y un agente te contactará pronto.",
escalate=True, reason="system_fallback"
)Recursos
pybreaker, structlog, pydantic
Define y prueba los criterios de escalamiento
Los criterios mal definidos generan demasiados o muy pocos escalamientos — ambos son costosos.
- Define 5 criterios de escalamiento para el SupportBot. Escríbelos como condiciones exactas
- Crea 10 escenarios de prueba: 5 que deben escalar, 5 que no deben
- Implementa el EscalationRouter y ejecuta los 10 escenarios
- ¿Cuántos falsos positivos (escala cuando no debería)? ¿Falsos negativos?
- Ajusta los umbrales hasta lograr 0 falsos negativos (prioridad) y menos del 10% de falsos positivos
Entregable del módulo
Va al proyecto final: EscalationRouter + FallbackChain. Módulo completo de seguridad: 5 criterios de escalamiento configurables, fallback en cascada de 3 niveles, circuit breaker y audit log. El EscalationRouter se conecta a la salida del Orchestrator. En M10, la tasa de escalamiento se convierte en una métrica de negocio en el dashboard de observabilidad.
Módulo 7 · Fase 3 · Producción & Integración
Tool Layer & APIs Externas
Wrappers tipados, idempotencia, state management y tool registry
El agente nunca toca la infraestructura directamente
La regla más importante del tool layer: el agente llama a contratos (schemas tipados), no a implementaciones. Esto permite cambiar la implementación subyacente sin tocar el agente, y testear el agente con mocks sin infraestructura real.
Anatomía de una herramienta bien diseñada
- Schema tipado: Pydantic model con validación, descriptions y constraints
- Dry-run mode: validar sin ejecutar efectos — permite verificar antes de actuar
- Timeout por herramienta: cada tool tiene su propio SLA — no el global del workflow
- Idempotencia: ejecutar la misma herramienta 2 veces con los mismos params = mismo resultado
- Audit log: toda ejecución queda registrada — exitosa y fallida
El modelo puede generar params fuera de rango, tipos incorrectos, o campos obligatorios vacíos. Nunca confíes en el output del LLM sin validación. Pydantic lanza ValidationError antes de que la acción llegue al servicio.
from pydantic import BaseModel, Field
from typing import Literal
# 1. Schema tipado — lo que el LLM ve y debe rellenar
class CreateTicketParams(BaseModel):
user_id: str = Field(description="ID único del usuario")
subject: str = Field(min_length=5, description="Asunto del ticket")
priority: Literal["low","medium","high","critical"]
category: str = Field(description="Categoría: billing, technical, general")
notes: str | None = None
# 2. Implementación con todas las capas de seguridad
class CreateTicketTool:
name = "create_ticket"
timeout = 5 # segundos
def execute(self, raw_params: dict) -> dict:
# Validación — lanza ValidationError si algo está mal
params = CreateTicketParams(**raw_params)
# Dry-run check — ¿hay conflicto con un ticket abierto?
existing = self.ticket_service.get_open(params.user_id)
if existing and existing.subject.lower() == params.subject.lower():
return {"warning": "duplicate_ticket", "existing_id": existing.id}
# Ejecución con timeout
with timeout(self.timeout):
result = self.ticket_service.create(params)
# Audit log — inmutable
audit_log.record(tool=self.name, params=params.dict(),
result={"ticket_id": result.id}, user_id=params.user_id)
return {"ticket_id": result.id, "status": "created"}
# 3. Registry — el agente solo conoce el registry, no las implementaciones
class ToolRegistry:
def __init__(self):
self._tools = {
"create_ticket": CreateTicketTool(),
"get_order_status": GetOrderStatusTool(),
"send_notification": SendNotificationTool(),
"schedule_callback": ScheduleCallbackTool(),
}
def execute(self, name: str, params: dict) -> dict:
if name not in self._tools:
raise ValueError(f"Herramienta desconocida: {name}")
return self._tools[name].execute(params)
def get_schemas(self) -> list[dict]:
# Genera los schemas para el API de Anthropic automáticamente
return [t.get_anthropic_schema() for t in self._tools.values()]Recursos
pydantic v2, redis-py, httpx (async)
Implementa las 4 herramientas con sus tests
Cada herramienta debe tener al menos 3 tests: happy path, parámetros inválidos y timeout.
- Implementa
create_ticketcon mock del servicio de tickets - Escribe test: ¿qué pasa si user_id está vacío?
- Escribe test: ¿qué pasa si el servicio tarda más de 5 segundos?
- Implementa
get_order_status,send_notificationyschedule_callbackcon la misma estructura - Verifica que el ToolRegistry genera correctamente los schemas para el API de Anthropic
Entregable del módulo
Va al proyecto final: ToolRegistry + SessionManager. 4 herramientas tipadas con validación, timeout y audit log. Registry centralizado. SessionManager para persistencia entre turnos. El ToolRegistry se inyecta en el SupportAgent. Cuando el agente decide usar una herramienta en su loop ReAct, pasa por el registry — nunca llama directamente al servicio.
Módulo 8 · Fase 4 · Trade-offs & Debugging
Trade-offs & Optimización
Model routing, prompt caching, context compression y decisiones de arquitectura
Los 4 trade-offs que todo senior debe dominar
Latencia vs Calidad
Haiku responde en <500ms. Sonnet tarda 1-3s. Opus puede tardar 5-10s. La pregunta no es "cuál es mejor" sino "cuál necesita el usuario en este contexto".
Costo vs Profundidad
Sonnet cuesta 12x más que Haiku. Para clasificación (simple), Haiku es suficiente. Para razonamiento complejo con herramientas, Sonnet vale cada centavo.
Autonomía vs Control
Más autonomía = mejor experiencia de usuario. Más control = menos riesgo de errores costosos. La respuesta depende de la reversibilidad de la acción.
Agente vs Pipeline
Si el flujo siempre sigue los mismos pasos, un DAG determinista es más rápido, barato y predecible. El agente añade valor cuando el input es ambiguo.
El ingeniero que sabe cuándo NO usar un agente es más valioso que el que los usa en todo. Preguntar "¿realmente necesito un agente aquí?" es la diferencia entre soluciones elegantes y sistemas sobrecomplejos.
Model Routing — la optimización de mayor impacto
El principio: usar el modelo más barato que resuelve correctamente el caso. Un clasificador ligero (Haiku) decide qué modelo necesita cada consulta. El 70-80% de las consultas de soporte son simples y pueden resolverse con Haiku.
Estrategias de reducción de costo
- Prompt caching: la parte estática del system prompt se cachea. Anthropic ofrece 90% de descuento en tokens cacheados. Pon la parte estática siempre primero.
- Context compression: resumir el historial largo en vez de enviarlo completo. Ahorro de 40-60% en conversaciones de muchos turnos.
- Batch API: 50% de descuento para tareas no urgentes (evaluaciones, generación offline).
- El 80/20 del costo: el 80% del gasto viene del 20% de las solicitudes más largas. Optimiza la cola, no el promedio.
class ModelRouter:
def select(self, query: str, context: dict) -> ModelTier:
# Regla 1: casos críticos siempre al modelo estándar
if context.get("ticket_priority") == "critical":
return ModelTier.STANDARD
# Regla 2: clasificar complejidad con el modelo más barato posible
complexity_prompt = f"""Clasifica esta consulta: '{query}'
Responde SOLO con: SIMPLE o COMPLEX
SIMPLE: saludos, estado de pedido, preguntas de FAQ
COMPLEX: problemas técnicos, disputas, múltiples pasos"""
response = self.llm.call(
[{"role": "user", "content": complexity_prompt}],
model=ModelTier.FAST, # Haiku para clasificar
max_tokens=5
)
if "SIMPLE" in response.text:
return ModelTier.FAST # Haiku: 10x más barato
return ModelTier.STANDARD # Sonnet: balance ideal
class ContextCompressor:
max_history_tokens = 3000
def compress(self, history: list[dict]) -> list[dict]:
if self._count_tokens(history) <= self.max_history_tokens:
return history # No necesita compresión
# Mantener los últimos 3 turnos intactos (más relevantes)
recent = history[-3:]
older = history[:-3]
# Resumir los turnos más antiguos
summary_prompt = f"Resume en 2 oraciones los puntos clave de esta conversación: {older}"
summary = self.llm.call([{"role": "user", "content": summary_prompt}],
model=ModelTier.FAST)
return [{"role": "system",
"content": f"Contexto previo (resumido): {summary.text}"}] + recentRecursos
litellm, time.perf_counter, Anthropic prompt caching
Benchmark de model routing
Medir empíricamente cuánto ahorra el model routing sin sacrificar calidad.
- Toma 50 consultas reales de soporte (o simuladas)
- Ejecuta todas con Sonnet. Registra costo total y tasa de resolución correcta
- Implementa el ModelRouter y ejecuta las mismas 50 consultas
- Compara: ¿cuánto ahorraste? ¿cayó la tasa de resolución?
- Ajusta el threshold del classifier hasta lograr el mejor balance costo/calidad
Entregable del módulo
Va al proyecto final: ModelRouter + ContextCompressor + ADR. Módulo de optimización funcional más documento ADR con los trade-offs medidos del sistema. El ModelRouter reemplaza el model fijo del LLMClient del M1. Ahora el sistema selecciona el modelo dinámicamente. El ContextCompressor se activa automáticamente en el MemoryManager del M5.
Módulo 9 · Fase 4 · Trade-offs & Debugging
Debugging Probabilístico
Framework de análisis, loop detection y reproducibilidad de fallos
Por qué el debugging en sistemas probabilísticos es diferente
En sistemas deterministas, el mismo input produce el mismo output — siempre. En sistemas con LLMs, el mismo input puede producir outputs ligeramente diferentes en cada llamada. Esto cambia completamente la estrategia de debugging.
Los 3 tipos de fallo más frecuentes
- Alucinaciones: el modelo genera información incorrecta con confianza alta. Causa: contexto insuficiente o constraints débiles en el prompt. Mitigación: RAG + constraints explícitos + grounding checks.
- Loops: el agente repite la misma acción indefinidamente. Causa: la herramienta falla pero el modelo no lo reconoce como error. Mitigación: MAX_ITERATIONS + loop detector + circuit breaker.
- Degradación silenciosa: la calidad cae gradualmente sin alerta visible. Causa: model drift del proveedor o prompt drift por ediciones acumuladas. Mitigación: evaluación continua + alertas en dashboard.
Un fallo aislado es ruido. Un patrón de fallos es una señal. Antes de cambiar el código, cuantifica: ¿cuántas veces ocurre el mismo fallo en 100 llamadas? Si es menos del 1%, documenta y monitorea. Si supera el 5%, actúa.
Framework de debugging en 5 pasos
El proceso correcto
- 1. Reproducir: guardar el input completo (prompt, historial, tool results, modelo, versión). Sin reproducibilidad, el debugging es imposible.
- 2. Aislar: ¿falla en planning, execution o evaluation? Testear cada componente por separado con inputs sintéticos.
- 3. Trazar: revisar el
<thinking>del agente. ¿El razonamiento fue correcto? ¿Los datos estaban bien? - 4. Cuantificar: ¿es un caso aislado o sistémico? Ejecuta 20+ veces antes de concluir.
- 5. Iterar: cambiar UNA variable a la vez. Sin A/B test no hay conclusiones válidas.
import hashlib, json
from datetime import datetime
class LoopDetector:
def __init__(self, window: int = 3):
self.window = window # comparar los últimos N estados
def check(self, history: list) -> bool:
if len(history) < self.window:
return False
# Si los últimos N thoughts son iguales → loop detectado
last_n = history[-self.window:]
hashes = [hashlib.md5(json.dumps(h["thought"].tool_name).encode()).hexdigest()
for h in last_n]
return len(set(hashes)) == 1 # todos iguales = loop
class FailureStore:
def capture(self, context: dict, error: Exception, agent_history: list) -> str:
failure_id = f"fail-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
record = {
"id": failure_id,
"timestamp": datetime.utcnow().isoformat(),
"error_type": type(error).__name__,
"error_message": str(error),
"prompt_version": context.get("prompt_version"),
"model": context.get("model"),
"full_context": context, # TODO: redactar PII antes de guardar
"agent_history": agent_history,
}
self.db.save(failure_id, json.dumps(record))
return failure_id
def replay(self, failure_id: str) -> dict:
"""Recupera el contexto completo para reproducir el fallo exactamente."""
return json.loads(self.db.get(failure_id))Recursos
sqlite3, pytest fixtures, hashlib
Analiza 2 fallos reales del sistema
La mejor forma de aprender debugging es analizar fallos reales, no simulados.
- Ejecuta el SupportAgent con 20 consultas variadas. El FailureStore captura todo lo que falle
- Elige los 2 fallos más interesantes del store
- Para cada uno: usa el ReplayRunner para reproducir el fallo exactamente
- Inspecciona el
<thinking>del agente: ¿dónde se equivocó el razonamiento? - Escribe el análisis en
docs/failure-analysis-report.md: causa raíz, fix propuesto, test de regresión
Entregable del módulo
Va al proyecto final: Debug Toolkit + Failure Analysis Report. FailureStore, LoopDetector, ReplayRunner y un reporte de análisis de al menos 2 fallos reales con root cause y fix propuesto. El FailureStore se conecta al Orchestrator. El LoopDetector envuelve el ReAct loop del SupportAgent. Cualquier excepción no manejada queda capturada automáticamente con contexto completo.
Módulo 10 · Fase 5 · Observabilidad & Evaluación
LLMOps: Observabilidad & Evaluación Continua
Tracing, métricas de negocio, A/B testing de prompts y guardrails
No puedes mejorar lo que no mides
El módulo de LLMOps es el que cierra el ciclo. Sin observabilidad, el sistema es una caja negra que funciona (o no) sin que nadie sepa por qué. Con observabilidad, cada decisión de mejora está respaldada por datos.
Las métricas que importan — y en qué orden
- Task completion rate: la métrica más importante. ¿Qué % de conversaciones terminaron con el problema del usuario resuelto?
- Escalation rate: % escalado a humano. Si sube → el agente está empeorando. Si baja mucho → puede estar dejando pasar casos que debería escalar.
- Cost per successful interaction: (tokens × precio) / interacciones exitosas. La métrica de eficiencia del sistema.
- Latencia p95: el percentil 95 de latencia — lo que el 95% de los usuarios experimenta. El promedio miente.
- Tool error rate: % de llamadas a herramientas que fallan. Señala problemas de APIs externas.
El dashboard de métricas debe ser visible para todo el equipo — no solo para el área técnica. Un dashboard con business metrics + technical metrics en una sola vista elimina el 80% de las discusiones de priorización.
LLM-as-judge — evaluación automática escalable
Evaluar manualmente la calidad de 1000 respuestas por semana es inviable. El patrón LLM-as-judge usa un segundo LLM para evaluar el output del primero. El evaluador recibe: la consulta original, la respuesta generada y los criterios de evaluación.
El LLM-as-judge tiene sesgos: favorece respuestas más largas, más formales, o que suenan "más seguros". Siempre valida tu judge contra evaluaciones humanas en un sample antes de usarlo como única fuente de verdad.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
tracer = trace.get_tracer("nexus-support-agent")
class AgentTracer:
def trace_request(self, trace_id: str, user_id: str):
return tracer.start_as_current_span("agent_request",
attributes={"trace_id": trace_id, "user_id": user_id})
class MetricsCollector:
def record_interaction(self, result: AgentResult, context: dict):
TASK_COMPLETION.inc(1 if result.success else 0)
ESCALATION_RATE.inc(1 if result.escalated else 0)
COST_COUNTER.inc(result.cost_usd)
LATENCY_HISTOGRAM.observe(result.latency_ms)
TOKEN_COUNTER.inc(result.total_tokens)
class LLMJudge:
judge_prompt = """Evalúa esta respuesta de soporte.
Query: {query}
Respuesta: {response}
Puntúa 1-5 en cada criterio y responde SOLO JSON:
{{"relevance": 1-5, "accuracy": 1-5, "tone": 1-5, "completeness": 1-5,
"overall": 1-5, "reasoning": "explicación breve"}}"""
def evaluate(self, query: str, response: str) -> dict:
result = self.llm.call(
[{"role": "user", "content": self.judge_prompt.format(
query=query, response=response)}],
model=ModelTier.STANDARD # el judge necesita buen criterio
)
return json.loads(result.text)
class EvalPipeline:
def run(self, prompt_version: str) -> EvalReport:
results = []
for case in self.load_test_set():
output = self.agent.run(case["input"], case["context"])
score = self.judge.evaluate(case["input"], output.answer)
results.append({"case_id": case["id"], "score": score, "passed": score["overall"] >= 3})
pass_rate = sum(1 for r in results if r["passed"]) / len(results)
return EvalReport(results=results, pass_rate=pass_rate,
version=prompt_version, baseline=self.get_baseline())Recursos
opentelemetry, langsmith, prometheus, grafana, presidio (PII)
Implementa el dashboard de métricas completo
El dashboard es la primera cosa que miras cuando algo falla en producción.
- Instrumenta el Orchestrator para que cada request genere las 5 métricas definidas
- Levanta Prometheus + Grafana localmente con Docker Compose
- Crea un dashboard con: task_completion_rate, escalation_rate, cost_per_interaction, p95_latency y tool_error_rate
- Ejecuta 50 consultas simuladas y verifica que las métricas se actualizan correctamente
- Configura una alerta: si escalation_rate sube más del 20% en 1 hora, alerta al canal de Slack
Pipeline de evaluación en CI
El test que previene que un cambio de prompt rompa el sistema en producción.
- Crea un GitHub Action que ejecuta el EvalPipeline en cada PR que modifique un archivo en prompts/
- El Action falla el PR si el pass_rate cae más del 5% respecto al baseline
- Haz un cambio de prompt intencionalmente malo y verifica que el CI lo detecta
- Haz un cambio bueno y verifica que el CI lo aprueba
- Documenta el proceso en el README del repositorio
Entregable del módulo
Va al proyecto final: Observability Stack + Eval Pipeline en CI. Instrumentación completa del sistema: tracing, 5 métricas de negocio/técnicas, LLM-as-judge, pipeline de evaluación automática y guardrails de seguridad. Este módulo instrumenta todos los componentes anteriores. El eval pipeline conecta con el PromptLoader del M2, el CriticAgent del M3, y el FailureStore del M9, formando el ciclo completo de mejora continua.
Proyecto final
Proyecto final: Nexus Support Agent, sistema multi-agente end-to-end
Todos los entregables de los 10 módulos integrados en un sistema de soporte al cliente observable, optimizado y listo para producción.
Core
- Clasificación automática con modelo ligero
- RAG sobre knowledge base con reranking
- Memoria episódica por usuario
- 4 herramientas externas tipadas
- Critic loop pre-respuesta
Seguridad
- Human-in-the-loop con 5 criterios
- Fallback en cascada de 3 niveles
- Circuit breaker por herramienta
- PII detection en inputs/outputs
- Timeout global por workflow
LLMOps
- Tracing completo con trace_id
- Dashboard con 5 KPIs en tiempo real
- Eval pipeline en CI automático
- Model routing dinámico
- ADR documentado con datos
Estructura del repositorio
src/agents/ llm/ memory/ tools/ safety/ observability/ optimization/ debug/prompts/support_agent/ classifier/ critic/ con versionadoevals/test_set.jsonl · eval_pipeline.py · llm_judge.pydocs/ADR-001.md · ADR-002.md · failure-analysis.mdtests/unit/ integration/ coverage >70%.github/workflows/eval_on_pr.yml · ci.yml
Criterios de aprobación
| Criterio |
|---|
Tu progreso queda guardado en este navegador.