Inteligencia Artificial

Graph engineering: cuándo tu agente deja de ser un bucle

Autorangel cruz
Publicado
Lectura10 min de lectura
Graph engineering: cuándo tu agente deja de ser un bucle

Un grafo de agentes son tres cosas: un estado compartido, unos nodos que lo modifican y unas aristas que deciden qué nodo va después. Eso es todo. Lo difícil no es entenderlo, es saber cuándo tu bucle de siempre se te quedó corto.

Y la respuesta que se repite por ahí, que pases a grafo cuando tengas "varias especialidades" o necesites "paralelo y junta", es la menos útil de todas. El motivo real es más concreto y casi nadie lo menciona: la ejecución durable.

Vamos por partes, porque el vocabulario está bastante enturbiado.

Qué es un grafo de agentes

La documentación de LangGraph lo define en tres piezas, y conviene quedarse con ellas antes de nada:

  • State. Una estructura de datos compartida que representa la instantánea de la aplicación. Es lo que todos los nodos leen y escriben.
  • Nodes. Funciones que ejecutan lógica y actualizan el estado.
  • Edges. Funciones que deciden el flujo de ejecución entre nodos.

Lo importante de esa definición es lo que dice a continuación: los nodos y las aristas pueden contener llamadas a LLM o código normal. Un nodo no tiene que ser un agente. Puede ser un if, una consulta a base de datos o un curl.

En código se ve así de aburrido, que es justo la gracia:

from langgraph.graph import StateGraph
from typing import TypedDict
 
class AgentState(TypedDict):
    messages: list
    current_tool: str
    retry_count: int
 
def should_continue(state):
    if state["retry_count"] > 3:
        return "end"
    elif state["current_tool"] == "search":
        return "process_search"
    else:
        return "call_llm"
 
workflow = StateGraph(AgentState)
workflow.add_node("call_llm", call_llm_node)
workflow.add_node("process_search", search_node)
workflow.add_conditional_edges("call_llm", should_continue)

Fíjate en should_continue. Es una función Python normal, sin ninguna IA dentro, y es la que decide el flujo. Ese es el cambio de fondo: el control deja de estar en el modelo y pasa a estar en tu código.

El vocabulario real no es "loop vs graph"

Aquí conviene parar, porque hay un término de moda que no viene de donde parece.

Si buscas "loop engineering vs graph engineering" te van a salir seis o siete artículos que dicen exactamente lo mismo, con la misma tabla comparativa, y ninguno cita una fuente. Es vocabulario de marketing de contenidos, no de ingeniería.

Anthropic, en el artículo que sirve de referencia canónica para esto, ni siquiera usa esas palabras. Distingue otra cosa:

Workflows son sistemas donde los LLM y las herramientas se orquestan mediante rutas de código predefinidas. Agents son sistemas donde los LLM dirigen dinámicamente sus propios procesos y su uso de herramientas, manteniendo el control sobre cómo realizan las tareas.

Esa sí es la distinción que importa, y no va de la forma del diagrama: va de quién decide el siguiente paso. Si lo decide tu código, es un workflow. Si lo decide el modelo, es un agente. Un grafo puede ser cualquiera de los dos, según dónde pongas las aristas condicionales.

Y de ahí salen los cinco patrones que Anthropic nombra, que son mucho más útiles que la etiqueta "graph engineering":

Patrón Qué hace
Prompt chaining Descompone la tarea en pasos donde cada llamada procesa la salida de la anterior
Routing Clasifica la entrada y la dirige a una tarea especializada
Parallelization Divide en subtareas independientes, o corre la misma varias veces para tener diversidad
Orchestrator-workers Un LLM central descompone, delega en trabajadores y sintetiza los resultados
Evaluator-optimizer Un LLM genera y otro evalúa y da feedback, en bucle

Ninguno de esos cinco necesita que digas la palabra "grafo" para implementarlo.

Un bucle ya es un grafo

Esta es la parte que la comparativa de moda se salta, y que hace que la pregunta esté mal planteada.

Un bucle es un grafo de un solo nodo con una arista que apunta a sí mismo. Y al revés: cada nodo de tu grafo puede tener un bucle dentro. La propia documentación de LangGraph dice que la combinación de nodos y aristas permite "workflows con bucles".

Dos diagramas lado a lado. A la izquierda un bucle: un único nodo llamado agente con una arista que sale de él y vuelve a entrar en él mismo. A la derecha un grafo: un nodo plan que se bifurca en dos workers en paralelo, que confluyen en un nodo junta, y uno de los workers conserva su propia arista hacia sí mismo, resaltada.

No son dos paradigmas rivales entre los que elegir. Uno contiene al otro. Lo que cambia no es la topología, es cuánta estructura declaras por adelantado:

  • En un bucle agéntico, declaras el objetivo y dejas que el modelo decida el camino en cada vuelta.
  • En un grafo, declaras el camino, o al menos las bifurcaciones posibles, y el modelo decide dentro de los carriles.

El Ralph loop es el extremo de lo primero: un while de shell, el mismo prompt cada vuelta, y el sistema de archivos como memoria. Cero estructura declarada. Funciona sorprendentemente bien para lo que funciona.

Lo que de verdad compras con un grafo

Aquí está el argumento que no vas a encontrar en las comparativas, y es el que decide en producción.

Cuando aceptas la complejidad de un grafo, lo que obtienes a cambio no es "modularidad" ni "separación de responsabilidades", que son palabras que no pagan facturas. Es esto:

Reanudar en vez de reempezar

Un grafo con checkpointer guarda el estado después de cada nodo. Si la corrida se cae en el minuto 40 de 50, reanuda desde el último checkpoint: los nodos ya completados no se reejecutan, porque sus resultados están guardados.

En un bucle, una caída a mitad significa volver a empezar. Y si cada vuelta cuesta tokens, volver a empezar cuesta dinero.

from langgraph.checkpoint.memory import MemorySaver
 
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

Es literalmente una línea. Ese es el precio de entrada a todo lo demás.

Parar a pedir permiso

La función interrupt() detiene la ejecución dentro de un nodo, expone lo que haga falta y espera a que un humano responda:

from langgraph.types import interrupt
 
def review_node(state: State):
    edited_content = interrupt({
        "instruction": "Review and edit this content",
        "content": state["generated_text"],
    })
    return {"generated_text": edited_content}

La ejecución se reanuda después con Command(resume=...). Sin checkpoints esto no puede existir, porque no habría a dónde volver.

Si tu agente hace algo irreversible, mandar un correo, tocar producción, mover dinero, esta es la razón por la que quieres un grafo y no un bucle.

Reintentos por nodo, no globales

workflow.add_node(
    "search_documentation",
    search_documentation,
    retry_policy=RetryPolicy(max_attempts=3),
)

El nodo que llama a una API inestable reintenta tres veces. El nodo que escribe en base de datos, ninguna. En un bucle, el reintento es todo o nada.

Bifurcar desde el pasado

Los checkpoints permiten reproducir una corrida desde un punto anterior, o bifurcar cambiando el estado y ver qué habría pasado:

history = list(graph.get_state_history(config))
before_joke = next(s for s in history if s.next == ("write_joke",))
 
fork_config = graph.update_state(before_joke.config, values={"topic": "chickens"})
fork_result = graph.invoke(None, fork_config)

Para depurar un agente que se portó raro hace tres días, esto es la diferencia entre investigar y adivinar. Con un aviso: los nodos posteriores al checkpoint sí se reejecutan, incluidas las llamadas al LLM, así que el resultado puede salir distinto.

El patrón que justifica el paso

Si tuviera que elegir uno solo para explicar por qué existen los grafos, sería orchestrator-workers, porque es el que un bucle no sabe hacer.

Un nodo planifica y decide cuántos trabajadores hacen falta, cosa que no se sabe al escribir el código. La API Send crea esas aristas en tiempo de ejecución:

from langgraph.types import Send
 
def assign_workers(state: State):
    """Un trabajador por cada sección del plan"""
    return [Send("llm_call", {"section": s}) for s in state["sections"]]
 
builder.add_conditional_edges("orchestrator", assign_workers, ["llm_call"])

Y como todos escriben a la vez sobre la misma clave del estado, hace falta decirle cómo combinarlas. Eso es un reducer:

import operator
from typing import Annotated
 
class State(TypedDict):
    sections: list[Section]
    completed_sections: Annotated[list, operator.add]  # todos acumulan aquí

Sin el Annotated[list, operator.add], cada trabajador pisaría al anterior, porque el comportamiento por defecto es sobrescribir. Es el detalle que rompe la primera vez que alguien monta esto.

Cuándo NO montar un grafo

Es la parte que más falta en los artículos que venden la arquitectura, así que la digo con la cita delante. Anthropic:

Recomendamos encontrar la solución más simple posible, y aumentar la complejidad solo cuando haga falta.

Y también:

Los sistemas agénticos suelen cambiar latencia y coste por mejor rendimiento en la tarea, y deberías considerar cuándo compensa ese intercambio.

Traducido a decisiones concretas, no montes un grafo si:

  • Tu tarea dura dos minutos. Reanudar no vale nada si reempezar es barato. El argumento entero de la ejecución durable se cae.
  • No tienes un paso irreversible. Sin nada que aprobar, interrupt() sobra.
  • El camino es siempre el mismo. Si no hay bifurcaciones reales, un grafo lineal es un for con ceremonia.
  • Todavía no sabes qué pasos hay. Declarar la estructura por adelantado exige conocerla. Al explorar, el bucle es mejor herramienta precisamente porque no te obliga a decidir.

Hay una cuarta trampa, menos evidente: montar el grafo para "estar preparado". La estructura que declaras hoy es la que tienes que mantener mañana, y la de un agente que todavía cambia cada semana envejece muy rápido.

Sin framework también se puede

Un apunte que ahorra discusiones, y viene del mismo artículo de Anthropic: recomiendan empezar con llamadas directas a la API antes que con un framework, porque muchos de estos patrones se implementan en unas pocas líneas de código.

Un routing es un if. Un prompt chaining son tres llamadas seguidas. Una parallelization es un asyncio.gather. Nada de eso necesita un grafo declarado.

Lo que sí es difícil de hacer a mano es la parte de arriba: checkpoints, reanudar tras una caída, pausar para aprobación humana y bifurcar desde el pasado. Ahí es donde un framework de grafos deja de ser ceremonia y empieza a ser infraestructura.

O sea que la pregunta útil no es "¿bucle o grafo?". Es "¿necesito persistencia entre pasos?". Si la respuesta es no, casi todo lo demás sobra.

Preguntas frecuentes

¿Qué es graph engineering?

Diseñar sistemas de agentes declarando su estructura como un grafo: un estado compartido, nodos que lo modifican y aristas que deciden el flujo. Conviene saber que el término circula sobre todo en blogs y no en la documentación de los proveedores: Anthropic habla de "workflows" frente a "agents", y LangGraph simplemente de grafos con estado.

¿Un bucle y un grafo son cosas distintas?

No son categorías excluyentes. Un bucle es un grafo de un nodo con una arista hacia sí mismo, y cada nodo de un grafo puede contener un bucle. Lo que cambia es cuánta estructura declaras por adelantado y quién decide el siguiente paso, si tu código o el modelo.

¿Cuándo paso de un bucle a un grafo?

Cuando necesites que la ejecución sobreviva a una caída, cuando haya un paso irreversible que requiera aprobación humana, o cuando el número de subtareas se decida en tiempo de ejecución. Si tu tarea dura minutos y no toca nada irreversible, el bucle es suficiente.

¿Qué es la ejecución durable en LangGraph?

Guardar el estado después de cada nodo mediante un checkpointer. Permite reanudar tras un error sin reejecutar los nodos que ya terminaron, pausar con interrupt() para aprobación humana, y reproducir o bifurcar la ejecución desde un checkpoint anterior.

¿Necesito LangGraph para esto?

No. Anthropic recomienda empezar con llamadas directas a la API, porque la mayoría de los patrones son unas pocas líneas. Lo que un framework de grafos aporta de verdad es la persistencia entre pasos: checkpoints, reanudación y aprobación humana. Si no necesitas eso, probablemente no necesites el framework.

¿Qué es un reducer y por qué me rompe el paralelismo?

Es la función que decide cómo se combinan las escrituras de varios nodos sobre la misma clave del estado. Por defecto la nueva escritura sobrescribe a la anterior, así que en ejecución paralela cada trabajador pisa al anterior. Con Annotated[list, operator.add] se acumulan en lugar de pisarse.

Fuentes