Agentes de IA que realmente funcionan
Desplegué mi primer agente de IA a finales de 2024.
Respondía tickets de soporte, consultaba datos de
pedidos y procesaba reembolsos. La demo fue
impecable. Tres casos de prueba, tres ejecuciones
perfectas. Dirección aprobó. Lo desplegamos.
En 72 horas, el agente había entrado en un loop
infinito con un ID de pedido malformado, quemó $400
en llamadas a la API e intentó reembolsar un pedido
que no existía. Lo bajamos un viernes por la noche.
Ese fallo me enseñó más sobre diseño de agentes que
cualquier tutorial. El problema no era el LLM. Era
la orquestación -- o más bien, la falta total de ella.
Por qué la mayoría de demos de agentes fallan en producción
El tutorial típico de agentes se ve así: dale al
modelo una lista de herramientas, mételo en un while
loop y deja que decida qué hacer. Este es el patrón
ReAct, y funciona perfecto con problemas de juguete.
Producción no es un problema de juguete.
Loops sin límite. Sin un tope de pasos, el
agente va a llamar la misma herramienta 47 veces
intentando parsear una respuesta que no entiende. Lo
he visto. Los logs tenían 12,000 líneas.
Abuso de herramientas. Dale a un agente acceso a
una herramienta de consulta a base de datos y otra
de eliminación, y eventualmente va a decidir que
borrar un registro es la forma más rápida de
"resolver" una discrepancia. El modelo optimiza para
completar la tarea, no para tus reglas de negocio.
Costos disparados. Cada paso del agente es una
llamada al LLM. Una tarea de 5 pasos a $0.03 por
llamada cuesta $0.15. Un loop de 15 reintentos
cuesta $0.45. Multiplica por 10,000 peticiones
diarias y estás quemando $4,500/día en una
funcionalidad que supuestamente iba a ahorrar dinero.
Sin observabilidad. Cuando el agente toma una
mala decisión en el paso 7 de 12, necesitas saber
por qué. La mayoría de frameworks de agentes te dan
una respuesta final y nada más. Suerte depurando eso
en producción.
La arquitectura que funciona
Después de reconstruir ese primer agente (y dos más
después), definí un pipeline de cuatro etapas. Cada
agente de producción que he desplegado desde entonces
sigue este patrón.
Router → Planner → Executor → Validator
Router. Clasifica la petición entrante y decide
qué workflow de agente la maneja. Es una llamada LLM
barata y rápida con salida estructurada. Sin
herramientas, sin loops. Solo clasificación. Si la
petición no coincide con ningún workflow conocido, se
enruta a un humano.
Planner. Toma la petición clasificada y produce
un plan paso a paso. El plan es un array tipado de
acciones -- no texto libre. Cada acción especifica
qué herramienta llamar, qué argumentos pasar y cómo
se ve la salida esperada. El planner nunca ejecuta
nada. Solo planifica.
Executor. Recorre el plan paso a paso. Llama
herramientas, captura resultados, maneja errores. Si
un paso falla, el executor no improvisa. O reintenta
con backoff exponencial o escala al validator.
Validator. Revisa la salida del executor contra
la petición original. ¿Realmente respondimos la
pregunta? ¿El resultado tiene sentido? Si no, el
validator puede devolver al planner para un plan
revisado -- pero solo una vez. Sin loops infinitos
de replanificación.
Esta separación importa porque te da superficies de
control. Puedes cambiar el modelo del planner sin
tocar la ejecución. Puedes agregar un checkpoint
humano entre planificación y ejecución. Puedes
cachear planes para peticiones idénticas.
Tool calling bien hecho
Las herramientas son donde los agentes se vuelven
peligrosos. Cada herramienta es una acción en el
mundo real -- una llamada a una API, una escritura
en base de datos, una notificación enviada.
Trátalas como endpoints de API, no como argumentos
de función.
Validación de schemas
Cada herramienta tiene un schema Zod. Los inputs se
validan antes de ejecutar. Los outputs se validan
después.
import { z } from 'zod'
const lookupOrderSchema = {
input: z.object({
orderId: z.string()
.regex(/^ORD-\d{8}$/, 'Invalid order ID format'),
fields: z.array(
z.enum(['status', 'total', 'items', 'shipping'])
).optional()
}),
output: z.object({
orderId: z.string(),
status: z.enum([
'pending', 'shipped', 'delivered', 'cancelled'
]),
total: z.number(),
items: z.array(z.object({
name: z.string(),
quantity: z.number()
}))
})
}
Cuando el agente intenta llamar lookupOrder con
orderId: "revisa todos los pedidos", el schema lo
rechaza antes de que toque tu base de datos. Sin
ambigüedad. Sin interpretación creativa.
Menor privilegio
No todo workflow de agente necesita todas las
herramientas. El agente de reembolsos tiene
lookupOrder e issueRefund. No tiene
deleteOrder ni modifyInventory. Defino conjuntos
de herramientas por workflow, no por agente.
const workflowTools: Record<string, Tool[]> = {
'refund': [lookupOrder, issueRefund, notifyCustomer],
'status-check': [lookupOrder, getShipmentTracking],
'escalation': [lookupOrder, createTicket, notifyAgent]
}
Es el mismo principio que los roles IAM. No le
darías acceso de escritura a una service account de
solo lectura. No le des a un workflow de consulta de
estado acceso a herramientas de mutación.
Contención de errores
Las herramientas fallan. Las APIs dan timeout. Las
bases de datos devuelven nulls inesperados. Cada
llamada a herramienta se envuelve en un error
boundary que captura el fallo, lo clasifica y
devuelve un error estructurado al agente.
async function executeToolSafe(
tool: Tool,
input: unknown
): Promise<ToolResult> {
const parsed = tool.schema.input.safeParse(input)
if (!parsed.success) {
return {
status: 'validation_error',
error: parsed.error.format(),
retryable: false
}
}
try {
const result = await Promise.race([
tool.execute(parsed.data),
timeout(tool.timeoutMs ?? 5000)
])
const output = tool.schema.output.safeParse(result)
if (!output.success) {
return {
status: 'output_validation_error',
error: output.error.format(),
retryable: true
}
}
return { status: 'success', data: output.data }
} catch (err) {
return {
status: 'execution_error',
error: String(err),
retryable: isRetryable(err)
}
}
}
El agente nunca ve una excepción cruda. Ve un
resultado estructurado con código de estado,
descripción del error y un flag retryable. Esto
evita que el LLM alucine estrategias de recuperación.
Patrones de LangGraph para workflows multi-paso
Uso LangGraph para orquestación de agentes porque
hace explícita la máquina de estados. Puedes ver el
grafo. Puedes probar nodos individuales. Puedes
reproducir desde cualquier checkpoint.
Así se ve el patrón router-planner-executor como un
workflow de LangGraph:
import { StateGraph, Annotation } from '@langchain/langgraph'
const AgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (a, b) => [...a, ...b],
default: () => []
}),
plan: Annotation<ActionStep[]>({
default: () => []
}),
currentStep: Annotation<number>({
default: () => 0
}),
results: Annotation<ToolResult[]>({
reducer: (a, b) => [...a, ...b],
default: () => []
}),
totalTokens: Annotation<number>({
default: () => 0
}),
status: Annotation<
'routing' | 'planning' | 'executing' |
'validating' | 'done' | 'failed'
>({
default: () => 'routing' as const
})
})
const graph = new StateGraph(AgentState)
.addNode('router', routerNode)
.addNode('planner', plannerNode)
.addNode('executor', executorNode)
.addNode('validator', validatorNode)
.addEdge('__start__', 'router')
.addConditionalEdges('router', routeDecision)
.addEdge('planner', 'executor')
.addConditionalEdges('executor', executionDecision)
.addConditionalEdges('validator', validationDecision)
.compile()
Las aristas condicionales son donde vive la lógica.
executionDecision verifica: ¿el paso actual tuvo
éxito? ¿Hay un siguiente paso? ¿Alcanzamos el
límite de pasos? validationDecision verifica: ¿la
salida pasó la validación? ¿Ya replanificamos una
vez?
Cada nodo es una función pura que recibe estado y
devuelve una actualización parcial del estado.
Testear es directo -- pasas un estado, asiertas
sobre la salida. Sin mockear llamadas LLM para tests
unitarios de tu lógica de orquestación.
Manejo de reintentos
Implemento reintentos a nivel de executor, no a
nivel de grafo. El executor rastrea intentos por
paso y aplica backoff exponencial.
async function executorNode(
state: typeof AgentState.State
) {
const step = state.plan[state.currentStep]
const maxRetries = step.maxRetries ?? 3
let attempt = 0
while (attempt < maxRetries) {
const result = await executeToolSafe(
step.tool,
step.args
)
if (result.status === 'success') {
return {
results: [result],
currentStep: state.currentStep + 1,
status: state.currentStep + 1
>= state.plan.length
? 'validating' : 'executing'
}
}
if (!result.retryable) break
attempt++
await sleep(Math.pow(2, attempt) * 1000)
}
return { status: 'failed', results: [{
status: 'max_retries_exceeded',
step: state.currentStep
}] }
}
Controles de costo
Sin controles de costo, los agentes son un cheque en
blanco. Aprendí esto por las malas con el incidente
de $400 del viernes por la noche. Todo agente en
producción necesita tres salvaguardas.
Presupuestos de tokens
Establece un presupuesto máximo de tokens por
ejecución del agente. Rastrea el uso acumulado en
todas las llamadas LLM de la ejecución. Mata la
ejecución cuando toca el techo.
const COST_LIMITS = {
maxTokensPerRun: 50_000,
maxStepsPerRun: 10,
maxCostPerRun: 0.50 // USD
}
function checkBudget(
state: typeof AgentState.State
): 'continue' | 'budget_exceeded' {
if (state.totalTokens > COST_LIMITS.maxTokensPerRun)
return 'budget_exceeded'
if (state.currentStep > COST_LIMITS.maxStepsPerRun)
return 'budget_exceeded'
return 'continue'
}
En producción, mis agentes promedian 3-5 pasos y
8,000-15,000 tokens por ejecución. Eso da
aproximadamente $0.04-0.12 por ejecución con Claude
Sonnet. El techo de 50,000 tokens atrapa loops
desbocados sin cortar tareas legítimamente complejas.
Límites de pasos
Los presupuestos de tokens atrapan sobrecostos. Los
límites de pasos atrapan bugs de lógica. Si tu
agente necesita más de 10 pasos para una tarea que
debería tomar 4, algo está mal. No lo dejes seguir
intentando. Falla rápido, registra el estado y enruta
a un humano.
Circuit breakers
Monitorea tasas de error en todas las ejecuciones del
agente. Si más del 20% de las ejecuciones fallan en
una ventana de 5 minutos, activa el circuit breaker
y enruta todas las peticiones a un fallback
(generalmente una cola humana o un workflow
determinístico más simple).
class CircuitBreaker {
private failures = 0
private total = 0
private lastReset = Date.now()
record(success: boolean) {
this.total++
if (!success) this.failures++
if (Date.now() - this.lastReset > 5 * 60_000) {
this.failures = 0
this.total = 0
this.lastReset = Date.now()
}
}
isOpen(): boolean {
if (this.total < 10) return false
return this.failures / this.total > 0.2
}
}
Esto nos salvó durante una caída de una API de
terceros. El agente empezó a fallar en cada consulta
de pedido, el circuit breaker se activó después de
10 ejecuciones y las peticiones se enrutaron a una
cola humana en 90 segundos. Sin esto, habríamos
quemado 10,000 ejecuciones fallidas a $0.08 cada
una -- $800 en llamadas desperdiciadas antes de que
alguien se diera cuenta.
Human-in-the-loop
No toda decisión debe automatizarse. Trazo la línea
basándome en dos factores: reversibilidad y costo.
Las acciones irreversibles requieren aprobación
humana. Reembolsos mayores a $100, eliminación de
cuentas, exportaciones de datos. El agente prepara
la acción, la presenta a un operador humano con
contexto y espera aprobación.
Las decisiones de alta incertidumbre pasan por
revisión humana. Cuando el score de confianza del
agente (extraído del paso de validación) cae por
debajo de un umbral, se enruta a un humano en lugar
de adivinar.
En LangGraph, human-in-the-loop es un checkpoint. El
grafo se pausa en un nodo específico y se reanuda
cuando el humano responde.
const graph = new StateGraph(AgentState)
.addNode('planner', plannerNode)
.addNode('human_review', humanReviewNode)
.addNode('executor', executorNode)
.addConditionalEdges('planner', (state) => {
const needsApproval = state.plan.some(
step => step.tool.requiresApproval
)
return needsApproval ? 'human_review' : 'executor'
})
.compile({ checkpointer })
En la práctica, alrededor del 15% de nuestras
ejecuciones llegan a un checkpoint humano. Suena
como mucho, pero son el 15% con mayor probabilidad
de causar daño si salen mal. El otro 85% corre de
forma autónoma con una latencia promedio de 2.3
segundos.
Medir lo que importa
No puedes mejorar lo que no mides. Estas son las
métricas que rastro en cada agente en producción.
Tasa de completado. El porcentaje de ejecuciones
que producen un resultado válido y verificado.
Nuestro objetivo es 92%. Actualmente estamos en 89%
y subiendo. El 11% restante se enruta a humanos, y
está bien -- el sistema está funcionando como fue
diseñado.
Pasos promedio por tarea. Te dice si el agente
es eficiente o está dando vueltas. Apuntamos a 3-5
pasos. Si el promedio sube de 6, revisamos los
prompts del planner y los schemas de herramientas.
Generalmente una descripción vaga de herramienta
hace que el agente pruebe varias antes de encontrar
la correcta.
Costo por tarea. Lo rastreo desglosado por
llamadas LLM, llamadas a APIs de herramientas e
infraestructura. Números actuales: $0.08 promedio,
$0.42 P99. El P99 es alto porque algunas tareas
legítimamente requieren más pasos.
Latencia (P50 y P99). P50 es 2.3 segundos, P99
es 8.1 segundos. Los usuarios toleran hasta 10
segundos para tareas complejas si muestras
indicadores de progreso. Más allá de eso, necesitas
ir async.
Tasa de fallback. Qué tan seguido el agente
delega a un humano. Apuntamos a menos del 20%. Si
sube más, el agente no está rindiendo lo suficiente.
Si baja del 5%, probablemente estamos auto-aprobando
cosas que no deberíamos.
interface AgentMetrics {
runId: string
workflow: string
status: 'completed' | 'failed' | 'escalated'
steps: number
totalTokens: number
costUsd: number
latencyMs: number
humanReviewRequired: boolean
toolErrors: number
}
Envío estas métricas a nuestro stack de
observabilidad (Datadog) después de cada ejecución.
Tenemos dashboards, alertas por picos de costo y
revisiones semanales de las peores ejecuciones.
El manual, resumido
- Separa responsabilidades. Router, planner,
executor, validator. Cada etapa tiene un trabajo.
- Valida todo. Inputs de herramientas, outputs
de herramientas, resultados finales. Los schemas
Zod son tus aliados.
- Limita el radio de explosión. Conjuntos de
herramientas con menor privilegio, límites de
pasos, presupuestos de tokens, circuit breakers.
- Pausa ante la incertidumbre. Human-in-the-loop
para acciones irreversibles y decisiones de baja
confianza.
- Mide sin descanso. Tasa de completado, costo
por tarea, latencia, tasa de fallback. Cada
ejecución.
El agente que falló aquel viernes por la noche era
la versión 1. Estamos en la versión 4 ahora. Maneja
8,000 peticiones diarias a $0.08 de costo promedio
con un 91% de tasa de completado autónomo. La
diferencia no es un mejor modelo. Es mejor ingeniería
alrededor del modelo.
Los agentes en producción no se tratan de hacer al
LLM más inteligente. Se tratan de hacer que el
sistema alrededor del LLM sea predecible, observable
y seguro. El modelo es el motor. La orquestación es
el auto.
Construye el auto.