recursion_limitrecursion_limit es el airbag, no el cinturón.
def router_validacion(state) -> str:
if state["es_valido"]:
return "exito"
elif state["intentos"] >= 3:
return "max_reintentos"
return "reintentar"
builder.add_conditional_edges(
"validar", router_validacion,
{
"exito": "fin_exito",
"max_reintentos": "fin_error",
"reintentar": "validar", # <- el ciclo
},
)
No hay ninguna API especial de bucles. "reintentar": "validar" es una
entrada del mapa como cualquier otra; que apunte al nodo del que sales es lo
único que la convierte en un ciclo.
def nodo_validar(state):
intento_actual = state.get("intentos", 0) + 1
...
return {"es_valido": ok, "intentos": intento_actual}
Dos reglas que no se negocian. Nada de variables globales: el estado es
lo único que LangGraph serializa, restaura y enseña al depurar; un contador
global no aparece en ningún checkpoint y miente en cuanto ejecutas dos hilos.
Y intentos no lleva reducer: es un valor, no una traza.
| Freno | Quién lo pone | Qué hace |
|---|---|---|
| Condición de salida | Tú, en el router | Termina en un nodo de tu grafo, con un mensaje que controlas |
recursion_limit | LangGraph, por defecto 25 | Lanza GraphRecursionError y aborta |
El límite cuenta super-pasos del grafo, no visitas a un nodo concreto: un ciclo de tres nodos consume el presupuesto tres veces más rápido que uno de un solo nodo. Por eso 25 no se traduce a «25 reintentos».
Proporciones ilustrativas con el mismo presupuesto de 25 super-pasos. Cuenta las tuyas en el ejercicio del final en vez de fiarte del dibujo.
def nodo_fallo_critico(state):
return {"mensaje_final": "Se excedió el número máximo de reintentos."}
La rama max_reintentos no lanza: va a un nodo que escribe un mensaje en el
estado y termina en END. Así el estado final tiene la misma forma
tanto si el proceso salió bien como si no, y quien llama al grafo lee
mensaje_final en lugar de envolver el invoke en un
try. Llegar a GraphRecursionError en producción es un
fallo de diseño, no una protección que funcionó.
El troubleshooting de la guía dice: «si necesitas ciclos muy largos,
aumenta recursion_limit en compile()». Antes de
repetirlo, abre la firma de compile() en tu versión instalada
(help(builder.compile)) y busca ese parámetro.
¿Está ahí, o es un campo de la configuración que se pasa en
invoke? Comprueba de paso el valor por defecto en tu versión, en
vez de fiarte del «ej. 25» con el que la guía se cubre.
app.invoke({"input_usuario": "Dato123", "intentos": 0, "es_valido": False,
"mensaje_final": None},
config={"recursion_limit": 2})
Copia el nombre exacto de la excepción. Luego sube el límite a 25, quita el
elif state["intentos"] >= 3 del router y cuenta cuántas veces se
ejecuta nodo_validar antes del error. ¿Coincide con 25? Si no, ya
sabes qué está contando el límite.