Inicio Servicios Proceso Proyectos Open Source Blog en Reservar llamada
AI Agents Architecture Production

Cómo diseñamos agentes de IA autónomos para procesos de negocio

Arquitectura, patrones de diseño y lecciones aprendidas construyendo agentes que operan 24/7 sin intervención humana.

marzo 2026 9 min
Cómo diseñamos agentes de IA autónomos para procesos de negocio

Un agente de IA no es un chatbot con más contexto. Es un sistema autónomo que recibe un objetivo, planifica los pasos necesarios, ejecuta acciones a través de herramientas externas y se adapta cuando algo falla. La diferencia es operativa: un chatbot responde preguntas, un agente completa tareas.

En Cloudstudio llevamos más de un año diseñando y desplegando agentes de IA para clientes que necesitan automatizar procesos complejos. En este artículo compartimos la arquitectura que utilizamos, los patrones que funcionan y los errores que hemos aprendido a evitar.

El bucle fundamental del agente

Cada agente sigue un ciclo: observar el estado actual, decidir la siguiente acción, ejecutarla y evaluar el resultado. Este bucle se repite hasta que se cumple el objetivo o se escala a un humano. La clave es que cada paso es una llamada al modelo con contexto actualizado; el agente no memoriza un plan fijo, sino que reevalúa en cada iteración.

Utilizamos a Claude como el cerebro de nuestros agentes. Su capacidad nativa de uso de herramientas permite definir herramientas como funciones que el modelo puede invocar directamente: consultar bases de datos, enviar correos electrónicos, actualizar CRMs, generar documentos. El modelo decide cuándo y cómo usar cada herramienta basándose en el contexto de la tarea.

Aquí está el bucle central del agente que utilizamos en producción. Es engañosamente simple: la complejidad reside en las herramientas y en el prompt del sistema:

import anthropic
import json
import time
from dataclasses import dataclass, field
from typing import Any

@dataclass
class AgentState:
    goal: str
    messages: list = field(default_factory=list)
    steps_taken: int = 0
    total_tokens: int = 0
    total_cost: float = 0.0
    max_steps: int = 25
    max_cost: float = 5.00  # USD budget limit

class Agent:
    def __init__(self, system_prompt: str, tools: list, tool_handlers: dict):
        self.client = anthropic.Anthropic()
        self.system_prompt = system_prompt
        self.tools = tools
        self.tool_handlers = tool_handlers

    def run(self, goal: str) -> AgentState:
        state = AgentState(goal=goal)
        state.messages = [{"role": "user", "content": goal}]

        while state.steps_taken < state.max_steps:
            # Check budget before each step
            if state.total_cost >= state.max_cost:
                state.messages.append({
                    "role": "user",
                    "content": "Budget limit reached. Summarize what you have accomplished so far."
                })

            response = self.client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=4096,
                system=self.system_prompt,
                tools=self.tools,
                messages=state.messages,
            )

            # Track usage
            state.total_tokens += response.usage.input_tokens + response.usage.output_tokens
            state.total_cost += self._calculate_cost(response.usage)
            state.steps_taken += 1

            # If Claude is done (no more tool calls), return
            if response.stop_reason == "end_turn":
                state.messages.append({"role": "assistant", "content": response.content})
                return state

            # Process tool calls
            if response.stop_reason == "tool_use":
                state.messages.append({"role": "assistant", "content": response.content})
                tool_results = self._execute_tools(response.content)
                state.messages.append({"role": "user", "content": tool_results})

        return state  # Max steps reached

    def _execute_tools(self, content) -> list:
        results = []
        for block in content:
            if block.type == "tool_use":
                result = self._safe_execute(block.name, block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                })
        return results

    def _safe_execute(self, tool_name: str, params: dict) -> dict:
        handler = self.tool_handlers.get(tool_name)
        if not handler:
            return {"error": f"Unknown tool: {tool_name}"}
        try:
            return handler(**params)
        except Exception as e:
            return {"error": f"{type(e).__name__}: {str(e)}"}

    def _calculate_cost(self, usage) -> float:
        # Sonnet pricing per million tokens
        input_cost = (usage.input_tokens / 1_000_000) * 3.0
        output_cost = (usage.output_tokens / 1_000_000) * 15.0
        return input_cost + output_cost

Los límites max_steps y max_cost no son opcionales: son la diferencia entre un agente controlado y un proceso desbocado que agota tu presupuesto de API a las 3 de la mañana.

Definiciones de herramientas: diseño para la confiabilidad

La calidad de tus herramientas determina la calidad de tu agente. Cada herramienta debe tener una descripción precisa, un esquema de entrada estricto y un comportamiento de error predecible. Aquí tienes un conjunto de herramientas de ejemplo para un agente de atención al cliente:

SUPPORT_TOOLS = [
    {
        "name": "search_knowledge_base",
        "description": "Busca en la base de conocimientos interna artículos relevantes para el problema de un cliente. Devuelve los 5 artículos más coincidentes con títulos y contenido. Usa esto PRIMERO antes de intentar responder cualquier pregunta técnica.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Consulta de búsqueda en lenguaje natural que describe el problema del cliente"
                },
                "category": {
                    "type": "string",
                    "enum": ["billing", "technical", "account", "product"],
                    "description": "Categoría para limitar la búsqueda"
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "lookup_customer",
        "description": "Busca los detalles de la cuenta de un cliente por correo electrónico. Devuelve el plan de suscripción, el estado de la cuenta, los tickets recientes y el historial de facturación. Usa esto para entender el contexto del cliente.",
        "input_schema": {
            "type": "object",
            "properties": {
                "email": {
                    "type": "string",
                    "description": "Dirección de correo electrónico del cliente"
                }
            },
            "required": ["email"]
        }
    },
    {
        "name": "create_ticket",
        "description": "Crea un ticket de soporte para problemas que requieren seguimiento humano. Usa esto cuando el problema no se pueda resolver automáticamente o requiera permisos elevados.",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_email": {"type": "string"},
                "subject": {"type": "string", "description": "Resumen breve del problema"},
                "body": {"type": "string", "description": "Descripción detallada que incluye los pasos seguidos"},
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "urgent"]
                },
                "category": {
                    "type": "string",
                    "enum": ["billing", "technical", "account", "product"]
                }
            },
            "required": ["customer_email", "subject", "body", "priority", "category"]
        }
    },
    {
        "name": "escalate_to_human",
        "description": "Transfiere inmediatamente la conversación a un agente humano. Usa esto cuando: el cliente solicite explícitamente un humano, el problema involucre reembolsos de más de $100, o no estés seguro de tu resolución.",
        "input_schema": {
            "type": "object",
            "properties": {
                "reason": {"type": "string", "description": "Por qué es necesaria la escalación"},
                "summary": {"type": "string", "description": "Resumen de la conversación hasta el momento"},
                "suggested_team": {
                    "type": "string",
                    "enum": ["billing", "engineering", "account_management"]
                }
            },
            "required": ["reason", "summary"]
        }
    }
]

Dos principios de diseño que hemos aprendido por las malas: primero, incluye siempre una herramienta de escalación. Un agente que no puede escalar alucinará soluciones cuando esté atascado. Segundo, escribe las descripciones de las herramientas como directivas: "Usa esto PRIMERO antes de..." le da al modelo un marco de decisión claro.

Patrones que funcionan en producción

Intervención humana. Ningún agente debe operar sin un mecanismo de escalada. Definimos umbrales de confianza: si el agente no está seguro de su decisión, pausa la ejecución y notifica a un humano. Esto es crítico en procesos con impacto financiero o legal.

Recuperación de errores. Los agentes en producción encuentran errores constantemente: APIs que fallan, datos inesperados, tiempos de espera agotados. Diseñamos cada herramienta con reintentos automáticos, alternativas y disyuntores. El agente debe ser capaz de diagnosticar el error e intentar una ruta alternativa.

Aquí está nuestro envoltorio de recuperación de errores que envuelve cada controlador de herramientas:

import time
import random
import logging
from functools import wraps

logger = logging.getLogger(__name__)

def resilient_tool(max_retries=3, timeout=30, fallback=None):
    """Decorator that adds retry logic and circuit breaking to tool handlers."""
    def decorator(func):
        _failure_count = {"value": 0}
        _circuit_open_until = {"value": 0}

        @wraps(func)
        def wrapper(**kwargs):
            # Circuit breaker: if too many recent failures, fail fast
            if time.time() < _circuit_open_until["value"]:
                if fallback:
                    logger.warning(f"Circuit open for {func.__name__}, using fallback")
                    return fallback(**kwargs)
                return {"error": "Service temporarily unavailable", "retry_after": 60}

            for attempt in range(max_retries):
                try:
                    result = func(**kwargs)
                    _failure_count["value"] = 0  # Reset on success
                    return result
                except TimeoutError:
                    if attempt < max_retries - 1:
                        wait = (2 ** attempt) + random.uniform(0, 1)
                        time.sleep(wait)
                    else:
                        _failure_count["value"] += 1
                        if _failure_count["value"] >= 5:
                            _circuit_open_until["value"] = time.time() + 60
                        return {"error": f"Timeout after {max_retries} retries"}
                except Exception as e:
                    logger.error(f"Tool {func.__name__} failed: {e}")
                    _failure_count["value"] += 1
                    if _failure_count["value"] >= 5:
                        _circuit_open_until["value"] = time.time() + 60
                    return {"error": str(e)}

        return wrapper
    return decorator


@resilient_tool(max_retries=3, timeout=10)
def lookup_customer(email: str) -> dict:
    """Look up customer from CRM."""
    response = crm_client.get(f"/customers?email={email}", timeout=10)
    response.raise_for_status()
    return response.json()

El patrón de disyuntor es esencial. Sin él, una interrupción del servicio descendente hace que su agente gaste todo su presupuesto reintentando una herramienta que nunca tendrá éxito. Con el disyuntor, después de 5 fallos consecutivos, la herramienta falla rápidamente durante 60 segundos, dándole al agente una señal de error clara con la que trabajar.

Observabilidad. Cada acción del agente se registra con marcas de tiempo, costes de tokens, duración y resultado. Sin una observabilidad completa, depurar un agente en producción es imposible. Utilizamos trazas estructuradas que permiten reconstruir cada decisión del agente paso a paso.

Configuración de monitoreo y observabilidad

No se puede operar un agente que no se puede observar. Registramos cada paso en un formato estructurado que nos permite reconstruir la cadena de decisión completa:

import time
import json
import logging
from contextlib import contextmanager
from dataclasses import dataclass, asdict

logger = logging.getLogger("agent.trace")

@dataclass
class AgentTrace:
    agent_id: str
    session_id: str
    step: int
    action: str  # "llm_call", "tool_call", "tool_result", "error", "escalation"
    tool_name: str | None = None
    input_summary: str | None = None
    output_summary: str | None = None
    input_tokens: int = 0
    output_tokens: int = 0
    cost_usd: float = 0.0
    duration_ms: int = 0
    success: bool = True
    error: str | None = None

class AgentTracer:
    def __init__(self, agent_id: str, session_id: str):
        self.agent_id = agent_id
        self.session_id = session_id
        self.step = 0
        self.traces: list[AgentTrace] = []

    @contextmanager
    def trace_step(self, action: str, tool_name: str = None):
        self.step += 1
        trace = AgentTrace(
            agent_id=self.agent_id,
            session_id=self.session_id,
            step=self.step,
            action=action,
            tool_name=tool_name,
        )
        start = time.monotonic()
        try:
            yield trace
            trace.success = True
        except Exception as e:
            trace.success = False
            trace.error = str(e)
            raise
        finally:
            trace.duration_ms = int((time.monotonic() - start) * 1000)
            self.traces.append(trace)
            # Emit as structured JSON log
            logger.info(json.dumps(asdict(trace)))

    def summary(self) -> dict:
        return {
            "total_steps": self.step,
            "total_tokens": sum(t.input_tokens + t.output_tokens for t in self.traces),
            "total_cost": sum(t.cost_usd for t in self.traces),
            "total_duration_ms": sum(t.duration_ms for t in self.traces),
            "errors": [t.error for t in self.traces if not t.success],
            "tools_used": [t.tool_name for t in self.traces if t.tool_name],
        }

Enviamos estos registros JSON a nuestra pila de observabilidad (Datadog o similar) y creamos paneles que muestran: tasa de éxito del agente, promedio de pasos por tarea, costo por tarea, herramientas más utilizadas y frecuencia de errores por herramienta. El panel ha sido la herramienta de depuración más valiosa; cuando un agente comienza a comportarse de manera inesperada, las trazas indican exactamente dónde falló el razonamiento.

Control de costos: límites de presupuesto y seguimiento de tokens

Los agentes pueden ser costosos porque realizan múltiples llamadas a LLM por tarea. Sin controles de costos, una sola entrada patológica puede desencadenar docenas de iteraciones. Así es como aplicamos los presupuestos:

@dataclass
class BudgetConfig:
    max_cost_per_session: float = 2.00     # USD
    max_cost_per_step: float = 0.50        # USD
    max_tokens_per_session: int = 100_000
    max_steps: int = 25
    alert_threshold: float = 0.75          # Alert at 75% of budget

class BudgetManager:
    def __init__(self, config: BudgetConfig):
        self.config = config
        self.total_cost = 0.0
        self.total_tokens = 0
        self.steps = 0

    def check_budget(self, estimated_input_tokens: int = 0) -> dict:
        """Check if we can afford another step. Returns status and remaining budget."""
        if self.steps >= self.config.max_steps:
            return {"allowed": False, "reason": "max_steps_reached"}
        if self.total_cost >= self.config.max_cost_per_session:
            return {"allowed": False, "reason": "

## Lo que aprendimos por las malas

El mayor error es asumir que el agente siempre tomará la decisión correcta. En producción, los casos extremos son la norma. Un agente de soporte que clasifica tickets funcionará perfectamente el 95% de las veces, pero ese 5% restante puede generar respuestas incorrectas para clientes importantes.

La solución no es más ingeniería de prompts. Es diseñar el sistema para que los fallos sean detectables, reversibles y escalables. Limitar el radio de impacto de cada acción del agente. Y medir continuamente la calidad de las decisiones frente a un conjunto de evaluación.

Tres lecciones más tras ejecutar agentes en producción durante un año:

Prueba con entradas adversarias. Los usuarios enviarán a tu agente cosas que nunca imaginaste: mensajes vacíos, mensajes en el idioma equivocado, capturas de pantalla cuando espera texto o instrucciones deliberadamente engañosas. Crea un conjunto de pruebas de más de 50 casos adversarios y ejecútalo en cada cambio de prompt.

Mantén el prompt del sistema por debajo de los 2,000 tokens. Los prompts más largos le dan al modelo más información, pero también aumentan la latencia y el coste por paso. El bucle del agente multiplica este coste entre 5 y 25 veces. Hemos descubierto que los prompts de sistema concisos y bien estructurados superan a los prolijos.

Registra cada llamada a herramientas, no solo los errores. Cuando algo sale mal en el paso 15 de la ejecución de un agente, necesitas el rastro completo para entender cómo llegó allí. Registrar solo los errores te da el síntoma. Registrar cada paso te da el diagnóstico.

Toni Soriano
Toni Soriano
Principal AI Engineer en Cloudstudio. +18 años construyendo sistemas en producción. Creador de Ollama Laravel (300K+ descargas).
LinkedIn →

¿Necesitas un agente IA?

Diseñamos y construimos agentes autónomos para procesos de negocio complejos. Hablemos de tu caso de uso.

Recurso gratuito

Obtén el checklist de implementación de IA

10 preguntas que todo equipo debería responder antes de construir sistemas de IA. Evita los errores más comunes que vemos en proyectos de producción.

¡Revisa tu bandeja de entrada!

Te hemos enviado el checklist de implementación de IA.

Sin spam. Cancela cuando quieras.