Inicio / Artículos

Conjunto de herramientas, bucle, grafo: tres capas detrás de la mayoría de los fallos de los agentes en producción.

Las fallas en los agentes de producción rara vez provienen del modelo. Separe el conjunto de herramientas, la condición de detención del bucle y la topología del grafo LangGraph; cada uno tiene un responsable.

Editor: REACTAPP.TOP24 min de lectura5526 palabras

Fuentes: The Missing Layer in Production AI Agent Stacks

Un agente muere durante su funcionamiento, y el equipo culpa al modelo. Lo reemplazan por uno más inteligente, pero el agente muere de nuevo. Se reescribe la instrucción, y sigue muriendo. Se introduce lógica de intentos repetidos, pero nada cambia. Se envía un ticket al proveedor del LLM, quien informa que el modelo se comporta según lo diseñado. Todos están confundidos.

El patrón se repite. Lo que parece un defecto del modelo es en realidad un error en la maquinaria que lo rodea, y esa maquinaria se divide en tres capas que los equipos rara vez mantienen separadas: las herramientas a las que puede acceder el agente (el conjunto de herramientas), el ciclo que determina cuándo termina el trabajo (el bucle) y el mapa de los lugares a los que puede ir el agente (el grafo). Una vez que estas capas se difuminan, un error en el bucle o en las herramientas se culpa al modelo, y los esfuerzos se dirigen a la capa visible mientras la que está dañada permanece oculta.

Dos capas cuando se necesitan tres

La mayoría de las pilas de agentes de producción en 2026 funcionan con dos capas. Cuentan con un grafo, creado con LangGraph, CrewAI o el SDK de OpenAI Agents, y una capa de durabilidad como Temporal, o bien no tienen ninguna capa de este tipo. Lo que se omite es el mecanismo de control: la capa que determina qué herramientas puede usar el agente, qué URLs puede consultar, qué puede devolver y cuándo debe detenerse.

Los proveedores también van por el mismo camino. Claude Code cuenta ahora con una configuración sandbox.network.deniedDomains. GitHub implementó controles de firewall a nivel organizacional para su agente Copilot, y VS Code dispone de claves de política de grupo que filtran qué puede acceder el agente en la red. Un catálogo público de patrones de agentes enumera la política de salida como un patrón por sí mismo. La investigación apunta en la misma dirección: un estudio de arXiv de 2026 mantuvo el modelo constante en cada ejecución, sustituyendo únicamente el entorno de ejecución en seis modelos de vanguardia, y midió un 41% menos de costos por tarea y un 44% menos de latencia. Pocos equipos han invertido en esa capa.

Nada de esto existe de forma aislada. Alrededor de las tres capas principales se encuentran una pasarela que dirige las solicitudes y aplica límites presupuestarios, una memoria encargada de la recuperación de datos y un caché semántico, un sistema de seguimiento de cada paso, una estructura de gobernanza que establece cuotas y límites de velocidad, y un mecanismo de evaluación que permite o impide el despliegue en un conjunto de datos de referencia. Cada uno de ellos es útil. Sin embargo, ninguno es lo que típicamente hace que un agente falle. Lo que realmente provoca el fallo son límites confusos en los harnesses, bucles y grafos, ya que el error resultante se puede confundir fácilmente con un defecto del modelo.

Cómo se descubrieron las tres capas

El desencadenante fue un agente de abastecimiento que se quedó repitiendo una llamada a query_suppliers. El LLM seguía anunciando que necesitaba más datos, el bucle seguía repitiéndose y la factura por tokens seguía aumentando. En lugar de leer tutoriales, lo útil resultó ser realizar análisis posteriores a incidentes, consultar documentos de arquitectura e informes de incidentes, y en proyectos no relacionados seguían apareciendo las mismas tres palabras: harness, loop, graph.

Un enfoque frecuentemente citado en la literatura sobre arquitectura de agentes describe esta división de esta manera. El harness abarca la relación de un agente con su entorno. El loop se refiere a cómo se controla la ejecución de un agente a lo largo del tiempo. El graph muestra cuántos agentes, herramientas y puntos de control están conectados en toda la aplicación. Cada capa se basa en la que está debajo de ella, y cada una solo se manifiesta una vez que la capa inferior es lo suficientemente estable.

Con los nombres en mano, el error original se volvió evidente: la condición de detención se encontraba dentro de la herramienta y no en el bucle. Se habían confundido “Harness” y el bucle. El modelo no estaba roto, sino los límites de las capas. Otros informan del mismo patrón. Alguien lo expresó sin rodeos: un agente que se considera poco fiable debido a un modelo incorrecto suele tener como culpable a la capa equivocada. Una newsletter describió a un fundador que eliminó un modelo, lo reemplazó por uno supuestamente más inteligente y recibió de vuelta al mismo agente defectuoso, porque el fallo residía en la maquinaria que lo rodeaba.

Estas tres capas no fueron inventadas aquí. Fueron nombradas por personas que gestionan agentes a gran escala y se mencionan repetidamente en fuentes independientes. Lo que sigue es lo que ocurre cuando esos nombres se aplican al código real.

Harness: lo que puede tocar el agente

El arnés representa el límite de las capacidades. Determina qué herramientas tiene el agente, cuáles son sus esquemas de entrada y salida, qué permisos posee, qué memoria puede leer, qué URLs puede consultar y qué datos puede ver.

Imagínese al agente como un contratista. El arnés es la placa que cuelga del cordón: decide qué puertas se abren, qué sistemas son visibles y qué se permite una vez dentro. No dice nada sobre inteligencia; define el área de acceso, y la seguridad también reside allí. Sin él, un agente es simplemente una llamada a función que contiene una clave API, con libertad para llamar, leer y devolver cualquier cosa. Con él, el agente solo puede hacer lo que el arnés permite.

La mayoría de los proyectos de agentes nunca crean esta capa. Las tutoriales terminan una vez que el modelo llama a una herramienta y recibe un resultado. Se omiten tres situaciones: una herramienta que devuelve un bloque JSON de 10 MB que desborda la ventana de contexto, una herramienta que consulta una URL arbitraria porque el texto inyectado se lo indica, y un efecto secundario que se vuelve a ejecutar dos veces porque nada contiene una clave de idempotencia. Las tres son problemas relacionados con el framework, y manejarlos es lo que convierte una demostración en un sistema de producción.

El catálogo de patrones detalla el principio para la salida a red. Los dominios a los que pueden acceder las herramientas de los agentes deben limitarse mediante listas de permisos y denegaciones que el framework hace cumplir, lo cual aleja al modelo del límite de confianza de la red. Cuando el modelo decide si conectarse, la inyección de prompts puede anular ese control. Una verificación a nivel de framework convierte el aislamiento en una propiedad estructural y no en una probabilística.

Esa distinción es precisamente lo importante. Los modelos son probabilísticos y pueden ser engañados, manipulados o confundidos. Un arnés, en cambio, es estructural y aplica reglas en el código y no en los prompts. Pedir amablemente al modelo que no acceda a URLs arbitrarias es una solicitud; hacerlo imposible es una garantía.

Bucle: cuando el agente se detiene

El bucle es el motor del sistema. El agente observa el mundo, elige una acción, la ejecuta, verifica el resultado y luego continúa o se detiene.

El ciclo de observar, actuar y verificar es la parte sencilla. La parte difícil es la condición de detención, ya que sin ella un agente sin supervisión consume tokens indefinidamente sin dar señales evidentes de fallo.

El agente de abastecimiento no tenía ninguno. Llamó a query_suppliers, examinó los resultados, decidió que necesitaba más y volvió a llamar, una y otra vez. El modelo de lenguaje continuaba indicando que necesitaba más datos, ya que los modelos de lenguaje tienden a realizar una llamada adicional a la herramienta: son útiles, lo intentan y no quieren rendirse. Esa característica es una ventaja en tareas creativas, pero una desventaja en tareas de producción.

El catálogo de patrones denomina esto bucle ilimitado. La implementación consiste en “iterar mientras el modelo informe que hay trabajo pendiente”, sin ningún contador, temporizador o límite de gasto fuera del modelo que pueda detenerlo. La suposición tácita es que el modelo anunciará la finalización. En tareas difíciles, esto rara vez ocurre: el modelo se desvía hacia preguntas relacionadas, repite acciones fallidas y sigue en un ciclo de errores sin reconocerlo.

Esto no es hipotético:

  • En julio de 2025, una recursión descontrolada en Claude Code consumió entre 16,000 y 50,000 dólares en cinco horas. No se produjo caída ni error alguno; los agentes simplemente siguieron ejecutando instrucciones sin llegar a un punto final.
  • Los informes públicos de errores describen subagentes que generan descendientes a más de 50 niveles de profundidad. Una sesión utilizó 4 millones de tokens en cinco minutos. Otro usuario pagó 27.60 dólares por exceder el límite del plan cuando se activaron los créditos de uso en medio de la sesión, sin posibilidad de detenerla. En el primer caso, la investigación útil finalizó después de tres o cuatro agentes, mientras que los otros 44 no aportaron nada nuevo. En el segundo, los subagentes interpretaron una instrucción sobre cómo manejar varios objetivos independientes como una autorización para generar sus propios descendientes.
  • Un artículo de julio de 2026 denominó el problema “Bucles de agencia infinitos”. Su herramienta de análisis estático, IAL-SCAN, examinó 6,549 repositorios de agentes de LLM y detectó 68 fallos en 47 proyectos. Sesenta y ocho agentes que nunca terminan representan más bien una bomba de tiempo para los costos que un simple número de errores.

Una guía de diseño para bucles de agencia limitados plantea el mismo requisito: el límite debe formar parte del diseño desde el inicio, y no ser una optimización añadida cuando surgen problemas. De lo contrario, la única regla para detenerlos es que el modelo decida que ha terminado, lo cual es más una esperanza que un control real.

Por lo tanto, la condición de parada debe ser mecánica, no semántica. “El LLM cree que el trabajo está terminado” es semántico. “Se han realizado tres intentos” o “Se han gastado 50 centavos” es mecánico. Las condiciones mecánicas son deterministas, verificables y estables al reproducirse. No dependen del juicio del modelo, que es probabilístico y puede variar al intentarlo de nuevo.

El agente de soporte de una empresa de análisis muestra cuánto cuesta esto. Su objetivo era rastrear la causa raíz de un tiempo de espera en la pasarela de pagos en menos de dos minutos. Buscó en los registros, obtuvo 4,200 filas, las comprimió, concluyó que se necesitaba más contexto y volvió a buscar. Después de doce intentos, el ticket aún no tenía resumen, mientras que el proceso había utilizado 380,000 tokens y $47 en gastos de API. Nada en el modelo se negó a continuar y nada en el entorno de ejecución ordenó una interrupción. Tras establecer límites de iteraciones, detectores de estancamiento (que identifican resultados casi idénticos repetidos) y un controlador del presupuesto de tokens, la proporción de bucles descontrolados disminuyó en un 91%, y el tiempo medio para resolver un ticket pasó de 4.1 a 1.6 minutos. Fue un bucle limitado el que realizó el trabajo, no un modelo más potente.

Qué contiene un bucle limitado

El ejemplo de análisis indica los componentes de un bucle que no puede descontrolarse. Cada uno es verificado por código fuera del modelo:

  • Límite de iteraciones. Un recuento estricto de pasos, de modo que una tarea que alcanza el límite termina con un fallo explícito y no con un funcionamiento silencioso.
  • Verificación de estancamiento. Una huella de los resultados recientes, de forma que varios resultados casi idénticos seguidos se consideran como falta de progreso, incluso cuando el número de pasos sigue siendo bajo.
  • Límite de gasto. Un presupuesto en tokens o dinero que la ejecución no puede superar, siendo el regulador parte del tiempo de ejecución y no de la instrucción de entrada.
  • Tiempo de espera máximo. Un límite en el tiempo transcurrido, que detecta bucles lentos que nunca activan los otros contadores.
  • Límites de recursión para subagentes. Un límite de profundidad y un límite total de creación, de modo que una tarea delegada no pueda generar subsubagentes que a su vez creen otros.

Ninguno de estos requiere la cooperación del modelo, y cada uno puede probarse de forma unitaria con un modelo falso que nunca indica que ha terminado. Una ejecución fallida que termina rápidamente después de tres intentos cuesta unos pocos centavos. La misma ejecución, dejada a la discreción del modelo, cuesta lo que permita el sistema de tarifas.

Gráfico: a dónde puede ir el agente

El gráfico representa la topología: los nodos que existen, las conexiones que los unen, los puntos donde el camino se bifurca, los lugares donde una persona da su aprobación y el estado que se transmite de un nodo a otro. Si el gráfico es el mapa, entonces el bucle es el motor que lo recorre y los límites determinan qué puede ocurrir en cada parada.

La elección de diseño central es la explicitud. Un bucle ReAct (pensar, actuar, observar, repetir) es un grafo implícito con un nodo que se alimenta a sí mismo y el LLM que actúa como función de enlace. Para tareas simples, eso es suficiente. Pero en cuanto la estructura se vuelve importante, falla. Una comparación entre ReAct y LangGraph lo resume: en el primero, el modelo elige el siguiente paso a partir del prompt y el historial; en el segundo, el desarrollador lo elige mediante código y enrutamiento explícito.

La diferencia se hace notar en cuanto es necesario un paso de aprobación humana. En un grafo implícito no existe un “entre”: el agente avanza de un paso a otro mediante su propio razonamiento. Agregar un punto de control implica romper el bucle, incorporar un mecanismo de pausa, externalizar el estado y reanudar la ejecución al recibir una señal. Eso equivale a adaptar un grafo a un bucle, y el bucle se resiste porque fue diseñado para funcionar por sí mismo. En un grafo explícito, basta con un nodo nuevo que contenga una llamada a interrupt(). El grafo cambia, mientras que el bucle y los componentes asociados permanecen igual.

Un desarrollador ejecutó la misma política, el mismo modelo determinista y los tres escenarios mediante un bucle simple de llamadas a herramientas y un grafo de estado LangGraph explícito. El comportamiento fue idéntico, pero la versión basada en el grafo requería 97 líneas frente a 50, es decir, un 94% más de código; por lo tanto, en tres pasos el bucle simple resultó superior en todos los aspectos. Luego, el desarrollador señaló lo que tal comparación no puede capturar. En el bucle, el comportamiento se deduce del orden de las instrucciones y debe ser comprendido al leer su contenido. En el grafo, los bordes se declaran como datos, lo que permite inspeccionar, dibujar y reproducir la topología sin tener que abrir ningún nodo. El valor es nil en tres pasos, grande en treinta, y aún mayor cuando otra persona toma el control.

Ese es el argumento de la producción. Los demos tienen tres pasos, y quien los escribió es su propietario. La producción tiene treinta pasos y es propiedad de quien esté de guardia a las 3 de la mañana. A esa escala, una topología explícita es esencial para sobrevivir y no representa una carga adicional.

Una prueba práctica para decidir entre las dos formas es preguntarse quién será el propietario del agente en seis meses. Un agente breve y de un solo propósito, mantenido por su autor, puede seguir siendo un simple bucle. Un agente que requiere aprobaciones humanas, tenga varias ramas o un propietario de guardia diferente, merece una estructura gráfica explícita, ya que la topología se convierte en datos que se pueden dibujar, revisar y reproducir.

Así se ve la confusión entre las capas

Los modos de fallo que se mencionan a continuación provienen de una fase de desarrollo, que es el lugar adecuado para mezclar las capas. Son los mismos que aparecen en la producción.

El mecanismo que controla el bucle

En la versión confusa, la verificación de “¿es esto lo suficientemente bueno?” se encuentra dentro de la herramienta, por lo que es la herramienta la que decide cuándo detener el bucle. Una herramienta debería ser una capacidad, como “poder consultar a proveedores”, y no un tomador de decisiones que diga “ya basta, detente”. Al incluir allí la condición de detención, el bucle se vuelve imposible de probar por separado, hace que sea imposible reemplazar la herramienta sin volver a implementar la lógica de detención y no deja espacio para un límite de intentos, ya que la herramienta no sabe nada sobre los intentos realizados.

La solución es estructural: se debe sacar la condición de detención de la herramienta y colocarla en el nodo del grafo; la herramienta devuelve los datos, el nodo decide si se justifica realizar otra pasada, el conjunto de componentes proporciona la capacidad necesaria, y el bucle permanece en el grafo. La versión confusa se ve así en el código:

# CONFUSED LAYER — Harness owns Loop logic
async def query_suppliers(item: str, budget: float, quantity: int, attempts: int) -> dict:
    """Tool decides when the loop stops. Capability + control flow in one function."""
    results = query_mock_marketplace(item, budget, quantity)

    # The tool decides when to stop — couples execution logic with capability
    if len(results) > 5 or attempts >= 3:
        return {"status": "STOP", "suppliers": results}
    return {"status": "CONTINUE", "suppliers": results}


async def research_node(state: AgentState) -> dict:
    # The node just calls the tool and forwards whatever the tool decided.
    # It has no idea why the loop stopped — that logic is hidden inside the tool.
    result = await query_suppliers(
        state["brief"].item, state["brief"].budget, state["brief"].quantity,
        state.get("attempts", 0) + 1,
    )
    return {"suppliers": result["suppliers"], "status": result["status"].lower()}

# ✅ CLEAN SEPARATION — Harness is pure capability, Loop lives in the Node
MAX_RESEARCH_ATTEMPTS = 3

async def query_suppliers_impl(item: str, budget: float, quantity: int) -> dict:
    """Pure Harness capability: schema validation, circuit breaker, marketplace call.

    The tool has no idea a loop exists. It returns data. It does not decide
    when to stop. Circuit breaker + fault injection live here because they are
    capability concerns (is the downstream reachable?), not control-flow concerns.
    """
    breaker = get_circuit_breaker("query_suppliers")
    if not breaker.allow_call():
        raise CircuitBreakerOpenError("query_suppliers", breaker.state)

    maybe_inject("query_suppliers")  # fault injection for chaos tests
    try:
        results = query_mock_marketplace(item, budget, quantity)
        breaker.record_success()
        return {"suppliers": results}
    except Exception as e:
        breaker.record_failure()
        raise


async def research_node(state: AgentState) -> dict:
    """Graph Node / Loop: mechanical stop conditions and routing decisions.

    The node reads state, calls the tool through the registry, and decides
    whether to stop — based on counters in state, NOT on the LLM's judgment
    and NOT on the tool's opinion.
    """
    brief = state["brief"]
    attempts = state.get("attempts", 0) + 1

    # Harness call — schema validation, timeout, output cap all happen inside registry.call
    query_result = await registry.call("query_suppliers", {
        "item": brief.item,
        "budget": brief.budget,
        "quantity": brief.quantity,
    })
    suppliers = query_result.get("suppliers", [])

    # Mechanical stop condition — enforced OUTSIDE the tool, in code, on state.
    # Deterministic. Survives replay. Testable without the tool being live.
    if not suppliers and attempts >= MAX_RESEARCH_ATTEMPTS:
        logger.info(
            "STOP_CONDITION_FIRED path=no_matches item=%s attempts=%d max=%d",
            brief.item, attempts, MAX_RESEARCH_ATTEMPTS,
        )
        return {"suppliers": [], "attempts": attempts, "status": "no_matches"}

    if not suppliers:
        return {"suppliers": [], "attempts": attempts, "status": "running"}

    # Enrich, then mark completed
    enriched = []
    for s in suppliers:
        quote = await registry.call("get_price_quote", {
            "supplier_name": s["name"], "item": brief.item, "quantity": brief.quantity,
        })
        rating = await registry.call("check_seller_rating", {"supplier_name": s["name"]})
        enriched.append({**s, "quote": quote, "rating_info": rating})

    return {"suppliers": enriched, "attempts": attempts, "status": "completed"}


# The Graph owns WHERE the agent can go — conditional edges, not the node body.
def _should_retry_or_end(state: AgentState) -> str:
    """After Research: loop back if still running, else Score or END."""
    if state["status"] == "running":
        return "research"          # self-loop — the retry
    if state["status"] == "no_matches":
        return END                 # mechanical stop → graph terminates
    return "score"                 # success → next node


# Wiring (in build_sourcing_graph):
#   graph.add_conditional_edges("research", _should_retry_or_end,
#       {"research": "research", "score": "score", END: END})

El bucle que controla el grafo

Muchos frameworks proporcionan un bucle ReAct en el que la topología es implícita: el agente decide el siguiente paso a partir de la salida del modelo y no existe un mapa explícito. Esto sigue siendo válido hasta que es necesario incluir un paso de aprobación humana entre la toma de decisiones y la confirmación; entonces comienza la adaptación, con el bucle oponiéndose a ella.

La solución consiste en diseñar el grafo de forma explícita desde el principio, con nodos que representan funciones, aristas que representan transiciones, algunas de ellas condicionales y otras que actúan como interrupciones. El bucle sigue guiando al agente, pero ahora es el grafo el que define los lugares alcanzables, y no el bucle. La forma monolítica de ReAct, en la que el bucle es el grafo y el LLM es la función de la arista, se ve así:

# ❌ CONFUSED LAYER — Implicit topology inside a monolithic ReAct loop
# The loop IS the graph. The LLM is the edge function. There is no "between."
while not agent.is_done():
    action = llm.decide_next_step(history)
    if action == "query":
        data = query_suppliers()
    elif action == "approve":
        # Retrofitting human approval into an implicit loop requires messy pauses.
        # You have to break the loop, externalize state, park the worker, and
        # resume on a webhook — the loop fights you because it was designed to
        # be self-driving.
        pause_execution_and_wait_for_webhook()
    elif action == "stop":
        break


# ✅ CLEAN SEPARATION — Declarative graph with explicit nodes and human interrupt
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt


def approve_node(state: AgentState) -> dict:
    """Human checkpoint via interrupt() — a STRUCTURAL pause, not a prompt.

    The graph genuinely suspends here. interrupt() pauses execution and waits
    for a human to call Command(resume=approval_data). The worker is freed.
    If the worker crashes while waiting, Temporal replays the activity, the
    graph re-runs from the last checkpoint, and interrupt() fires again —
    the human re-approves. No data loss.
    """
    selected = state.get("selected_supplier", {})
    brief = state.get("brief", {})

    approval_request = {
        "item": brief.item,
        "quantity": brief.quantity,
        "supplier": selected.get("name", ""),
        "price": selected.get("price", 0),
        "total_cost": selected.get("price", 0) * brief.quantity,
    }

    # interrupt() pauses the graph HERE. The human must call /approve
    # with a resume value to continue.
    approval = interrupt(approval_request)

    approved = approval.get("approved", False)
    comment = approval.get("comment", "")

    return {
        "approval_status": "approved" if approved else "rejected",
        "approval_comment": comment,
        "rejection_count": state.get("rejection_count", 0) + (0 if approved else 1),
    }


def _after_approve(state: AgentState) -> str:
    """After Approve: go to Confirm if approved, retry Research if rejected."""
    if state.get("approval_status") == "approved":
        return "confirm"
    if state.get("rejection_count", 0) + 1 >= 3:
        return END
    return "research"


def build_sourcing_graph(checkpointer=None):
    """Topology is DECLARED DATA, not statement order in a loop body.

        START → Research → (retry) → Score → Decide → Approve → Confirm → END
                                              ↓ (rejected, < 3)
                                             Research
                                              ↓ (rejected, = 3)
                                             END
    """
    graph = StateGraph(AgentState)

    graph.add_node("research", research_node)
    graph.add_node("score", score_node)
    graph.add_node("decide", decide_node)
    graph.add_node("approve", approve_node)      # explicit checkpoint node
    graph.add_node("confirm", confirm_node)

    graph.add_edge(START, "research")
    graph.add_conditional_edges("research", _should_retry_or_end,
        {"research": "research", "score": "score", END: END})
    graph.add_edge("score", "decide")
    graph.add_edge("decide", "approve")
    graph.add_conditional_edges("approve", _after_approve,
        {"confirm": "confirm", "research": "research", END: END})
    graph.add_edge("confirm", END)

    return graph.compile(checkpointer=checkpointer)

El grafo que controla el harness

Este caso es más raro, aunque aparece en las revisiones de código. Un nodo del grafo llama directamente a una API externa, como una solicitud HTTP GET a un endpoint del proveedor dentro de la función del nodo, de modo que el grafo controla la llamada a la herramienta.

Esto es incorrecto porque el grafo es una estructura topológica y no debería saber nada sobre HTTP, autenticación, intentos de reintentos ni interruptores de circuito. Al tener la llamada dentro del nodo, agregar un interruptor de circuito implica editar el grafo, cambiar su implementación también implica editar el grafo, y probar el grafo requiere una herramienta en tiempo real.

La solución consiste en enrutar la llamada a través del harness. El nodo solicita call_tool("query_suppliers", {...}) y recibe un resultado validado y de tipo definido. El harness se encarga de la solicitud HTTP, los intentos de reintentos, los interruptores de circuito y la validación del esquema. El grafo no sabe que existe HTTP, y el harness no sabe que existe un grafo:

# ❌ CONFUSED LAYER — Graph node owns the tool call (Harness concern)
import httpx


def supplier_node(state: AgentState) -> dict:
    """The graph node reaches directly into HTTP, auth, retries, and parsing.

    Why that's wrong: the graph is topology. It should not know about HTTP,
    authentication, retries, circuit breakers, or schema validation. Those are
    harness concerns. When the graph owns the tool call:
      - You can't add a circuit breaker without modifying the graph.
      - You can't swap the tool implementation without modifying the graph.
      - You can't test the graph without the tool being live.
      - You can't enforce an egress allowlist — the URL is hardcoded here.
    """
    resp = httpx.get(
        f"https://supplier-api.example.com/suppliers",
        params={"item": state["brief"].item},
        headers={"Authorization": f"Bearer {SUPPLIER_API_KEY}"},
        timeout=10.0,
    )
    resp.raise_for_status()
    suppliers = resp.json()["suppliers"]  # no schema validation, no output cap
    return {"suppliers": suppliers}


# ✅ CLEAN SEPARATION — Graph node calls through the harness; harness owns the call
async def supplier_node(state: AgentState) -> dict:
    """The graph node says 'call this tool with these args' and gets back a
    validated, typed result. The graph doesn't know there's HTTP involved.
    The harness doesn't know there's a graph involved.
    """
    query_result = await registry.call("query_suppliers", {
        "item": state["brief"].item,
        "budget": state["brief"].budget,
        "quantity": state["brief"].quantity,
    })
    suppliers = query_result.get("suppliers", [])

    # Mechanical stop condition — the node's job, not the tool's
    if state.get("attempts", 0) + 1 >= MAX_RESEARCH_ATTEMPTS and not suppliers:
        return {"suppliers": [], "attempts": state.get("attempts", 0) + 1, "status": "no_matches"}

    return {"suppliers": suppliers, "attempts": state.get("attempts", 0) + 1, "status": "completed"}


# What the harness does — the graph never sees this:
#
#   registry.call("query_suppliers", {...})
#     │
#     ├─ 1. validate_input(QuerySuppliersInput, args)        # Pydantic v2
#     ├─ 2. EgressFilter(spec.allowed_egress)                # URL allowlist
#     ├─ 3. SandboxedHTTPClient(egress_filter)               # blocked domains = structural
#     ├─ 4. run_with_timeout(fn, kwargs, spec.timeout_seconds)
#     ├─ 5. enforce_output_cap(result, spec.max_output_bytes)  # 64KB cap
#     └─ 6. validate_output(QuerySuppliersOutput, result)    # garbage caught here
#
# The graph node gets back a validated dict. It never sees the HTTP call,
# the auth header, the retry, the circuit breaker, or the egress filter.
# Swap httpx for aiohttp, swap the supplier API for a new one, add a circuit
# breaker — the graph doesn't change. The harness doesn't know a graph exists.

Quién posee cada capa

Cada capa necesita una infraestructura que sea responsable de ella. Cuando la propiedad no está clara, las capas sufren interferencias.

Temporal se encarga de la seguridad ante fallos en el bucle

Cuando el motor falla a mitad de un ciclo, Temporal se ocupa de la recuperación. Almacena cada paso en un historial de eventos. Después de que un proceso falla, vuelve a ejecutar el flujo de trabajo sin repetir las actividades, sino inyectando sus resultados registrados, de modo que el bucle se reinicia desde el último paso completado y no desde cero.

Esta es la razón por la cual una condición de parada debe ser determinista. Una condición como “el modelo evalúa el trabajo realizado” es no determinista, y una reproducción podría llegar a un veredicto diferente. Un contador de pasos, un límite de gasto o un umbral de calidad derivado de las salidas de las herramientas funcionan bien en casos de reproducción. El modelo puede intervenir, pero la comprobación if attempts >= 3: stop debe estar en el código.

LangGraph posee la topología del grafo

En LangGraph, los nodos son funciones simples, las aristas son transiciones, y las aristas condicionales son funciones que devuelven el nombre del siguiente nodo. LangGraph compila todo esto en un grafo ejecutable. Dado que el grafo es explícito, se puede visualizar, cada nodo puede probarse por separado, y se puede añadir un nodo sin perturbar el resto. Un punto de aprobación humano es simplemente un nodo que llama a interrupt(), lo cual modifica el grafo sin cambiar los bucles ni los mecanismos de conexión.

LangGraph también controla la estructura del estado, que es un TypedDict que se transmite entre los nodos. Cada nodo lee lo que necesita y escribe lo que produce, de modo que el estado funciona como el contrato entre ellos. Un nodo que nunca escribe en suppliers impide que el siguiente nodo pueda leerlo, y el verificador de tipos lo detecta antes de la ejecución.

Un mecanismo de herramientas construido a mano controla todo lo demás

Este es un nivel que ni Temporal ni LangGraph ofrecen, por lo que debe crearse desde cero. Se encarga de:

  • Registro. Cada herramienta declara un nombre, un esquema de entrada de Pydantic, un esquema de salida, un tiempo de espera y si necesita ser idempotente.
  • Validación. Las entradas se validan antes de que ejecute una herramienta y las salidas después, de modo que se detectan las respuestas inválidas antes de que las vea el agente.
  • Filtrado de salida. Las herramientas que realizan llamadas HTTP pasan por un cliente aislado con una lista blanca de URLs, por lo que ninguna herramienta puede llamar a una dirección arbitraria. El mecanismo es quien decide, no el modelo.
  • Límite de salida. Los resultados se acortan a 64KB antes de llegar a la ventana de contexto. Un archivo de 10MB llega truncado con un marcador _truncated: true.
  • Disyuntores. Cada herramienta cuenta con uno, compartido entre todos los flujos de trabajo de los agentes. Cinco fallos seguidos activan el disyuntor, y a partir de ese momento los agentes fallan rápidamente en lugar de seguir intentando comunicarse con una API inoperativa.
  • Límites de concurrencia por herramienta. Los límites establecidos para cada herramienta impiden que una herramienta lenta ocupe todos los espacios de actividad disponibles para los trabajadores.
  • Claves de idempotencia. En el caso de herramientas que generan efectos secundarios, el sistema crea una clave a partir del ID del flujo de trabajo y del nombre de la tarea. Cuando se vuelve a intentar una tarea, se recupera el resultado existente y no se crea ninguno duplicado.

Ninguno de estos elementos proviene de Temporal o LangGraph. Se trata de la capa que garantiza que las herramientas puedan ser llamadas de forma segura, y es la que la mayoría de los proyectos de agentes omiten.

Verificación: “el agente dijo que funcionó” frente a “funcionó”

El tercer paso del bucle es la verificación, y debe realizarse de forma externa. No puede significar que el agente revise su propio resultado, ni que el LLM evalúe su propio trabajo.

El catálogo de patrones denomina a esta solución el registro de verificación: las declaraciones autoinformadas como “la compilación se completó con éxito” dan paso a registros estructurados, donde cada verificación corresponde a un INSERT en la base de datos y cada conjunto de pruebas a un SELECT. Dejarlo sin supervisión puede hacer que el agente imagine que las verificaciones se completaron con éxito, omita pasos sin indicarlo o afirme obtener resultados de una herramienta que nunca ejecutó.

El equipo Honk de Spotify se encontró exactamente con esto. Sus agentes a veces afirmaban “las pruebas son positivas, no se encontraron problemas” y estaban equivocados. Integraron verificadores deterministas para el formato, la compilación y las pruebas dentro del bucle y bloquearon la creación de solicitudes de pull siempre que algún verificador fallaba. El texto escrito por el agente dejó de ser la señal de éxito; fue reemplazado por la salida estructurada del verificador.

Otros proyectos van más allá. Una herramienta de código abierto o bien muestra un comprobante que indica “listo, todas las pruebas pasaron” o marca la afirmación como no verificada, y un comando que indica que las pruebas pasaron por sí solo nunca actualiza una afirmación semántica. Un artículo de investigación formula la regla de la siguiente manera: se debe volver a derivar la afirmación y no confiar en ella, ya que el recuento de palabras del agente no sirve de nada como entrada.

En la pila descrita aquí, la verificación es una actividad temporal separada, como verify_po_exists o verify_payment_initiated, que consulta directamente la base de datos. El agente afirma haber creado la orden de compra. La actividad verifica la base de datos de órdenes de compra, y si la orden no está presente, el flujo de trabajo falla independientemente de lo que haya informado el agente.

El autoinforme es la señal que con mayor probabilidad está equivocada de maneras difíciles de detectar, y por eso la verificación debe ser externa. El agente llamado create_po analizó la respuesta y afirmó que todo funcionó. Sin embargo, la respuesta podría haber sido un 200 OK que contenía un cuerpo con error, o el pedido podría haber sido registrado y luego anulado debido a un tiempo de espera en la transacción, o el agente podría haber leído el campo incorrecto e informado un ID que nunca existió. El agente solo sabe lo que devolvió la herramienta y nada sobre el estado de la base de datos. Por eso se consulta a la base de datos.

La evidencia de la producción lo respalda. Un artículo de julio de 2026 analizó a Leni, un analista de negocios basado en IA, cuyos ciclos de verificación ejecutan, observan, comparan y corrigen utilizando modelos especializados en tareas específicas. El sistema completo superó al modelo base estándar en 11 puntos porcentuales en SpreadsheetBench, con un 91,25 % frente al 80,25 %. La verificación por sí sola añadió un modesto 1,5 puntos, pero estos se concentraron en el rango más alto de las puntuaciones, donde permitieron completar tareas que de otro modo habrían fallado. La mayor parte de la mejora general provino de los marcos de apoyo, las rutas de procesamiento y los modelos especializados. La verificación no resuelve todos los casos; sí salva a aquellos en la zona límite, donde el agente afirma haber completado la tarea mientras la realidad indica lo contrario.

El principio de separación

La regla fundamental: cada capa debe ser verificable, reemplazable y comprensible por separado del resto.

  • Se puede verificar el grafo mediante llamadas a herramientas simuladas, lo que confirma que los nodos correctos se activan en el orden adecuado.
  • Se puede probar el conjunto de herramientas llamando directamente a las herramientas y examinando la validación del esquema, los disyuntores y la idempotencia.
  • Se puede probar el bucle con una actividad trivial dentro de un flujo de trabajo Temporal, lo que confirma que sobrevive a una caída.

Cambiar el grafo no afecta al conjunto de herramientas, intercambiar la implementación de una herramienta no afecta al grafo, y modificar la política de reintentos del bucle no afecta a ninguno de los dos.

La ventaja es práctica. El grafo del agente de abastecimiento cambió tres veces: un nodo de puntuación, un bucle de intentos repetidos con detección de rechazos y un punto de control de aprobación humana, sin necesidad de modificar el código de harness. El proveedor de LLM pasó de una conexión directa con OpenAI a una capa de enrutamiento sin tocar el código del grafo. La política de intentos repetidos pasó de ser agresiva a ser estrictamente no repetible para actividades que generan efectos secundarios, sin cambiar el grafo ni harness. La separación brinda más agilidad que pureza: la libertad de cambiar una cosa sin dañar otras tres.

La misma disciplina da sus frutos cuando algo sale mal. Se puede rastrear un fallo hasta una capa específica haciendo tres preguntas concretas. ¿Devolvió una herramienta algo que nunca debería haber podido devolver, o llegó a un lugar al que nunca debería haber podido llegar? Eso apunta al sistema de control. ¿Sobrepasó el agente algún límite razonable? Eso apunta al bucle de ejecución. ¿Llegó el agente a un nodo al que no debería haber podido acceder, o omitió un punto de control? Eso apunta al grafo. Cambiar el modelo no resuelve ninguno de estos tres problemas.

Tres preguntas para el diseño de cualquier agente

  1. ¿Qué puede tocar? (Sistema de control) ¿Qué herramientas existen, con qué esquemas, qué cantidad de salida está permitida, dónde se encuentran los umbrales de los disyuntores y qué claves de idempotencia se aplican.
  2. ¿Cuándo se detiene? (Bucle) El techo de pasos, el techo de costos, el tiempo de espera y la condición de parada mecánica que no depende del juicio del LLM.
  3. ¿A dónde puede ir? (Gráfico) Los nodos, las aristas, las ramas condicionales, los puntos de control humanos y la forma del estado que fluye entre los nodos.

Cuando cualquier respuesta es “el modelo decide”, existe un problema. El LLM da sugerencias mientras el código toma la decisión, el mecanismo de control la aplica, el gráfico dirige el flujo y los límites del bucle lo limitan.

En resumen, hay tres capas, tres responsables y tres preguntas. Si se mezclan, los errores parecen problemas del modelo cuando en realidad no lo son. Si se mantienen separados, la falta de fiabilidad del modelo se controla, las herramientas pueden usarse sin riesgo y el agente se detiene cuando debe hacerlo.

Leer más

  • Creación de un bucle de mejora de agentes con LangGraph y las evaluaciones de LangSmith — Un proceso paso a paso con LangGraph y LangSmith: recolección de datos, tres capas de puntuación, agrupamiento de fallos, conjuntos de datos de regresión offline y evaluaciones previas al lanzamiento.
  • Antes de que un agente de IA tenga acceso a producción: contratos, gráficos de reclamaciones, pruebas en modo sombra y canarios — Un proceso de lanzamiento basado en evidencias para agentes de IA: contratos definidos con precisión, escenarios con estado, inyección de fallos, límites de incertidumbre, canarios en fases sucesivas y promoción controlada por la máquina.
  • Agregar agentes de IA a microservicios Spring Boot con Spring AI — Una guía práctica para encapsular herramientas en Spring AI, añadir mecanismos de protección y métricas de tokens, y ejecutar bucles de agentes de forma asíncrona dentro de los servicios existentes de Spring Boot.
  • Agentes seguros para reproducción: mantener las llamadas a LLM fuera del flujo de trabajo con Temporal — Por qué un agente que se cae durante su ejecución no debe volver a utilizar su LLM: las actividades como límite del no determinismo, las mecánicas de reproducción, las señales de estado, los intentos de reintentar y los grafos sin estado.
  • Desplegando agentes de IA en producción: trade-offs entre cloud, autohospedado e híbrido — Compare los despliegues de agentes en cloud, autohospedado e híbrido en términos de exposición de datos, costos, dependencia, enrutamiento y capacidad de observabilidad, con cuatro preguntas para tomar decisiones y errores que evitar.