volver a escritos
1 de agosto de 2026·5 min·AI

Agentic workflows en producción: dónde fracasan los equipos que copian tutoriales de YouTube

Muchos equipos que implementan agentic workflows fracasan al llegar a producción porque confunden un demo que funciona con un sistema que aguanta el día a día. Acá te cuento dónde se quiebra la realidad y cómo LangGraph ayuda a resolver algunos de esos problemas

Agentic workflows en producción: dónde fracasan los equipos que copian tutoriales de YouTube

Muchos equipos que implementan agentic workflows fracasan al llegar a producción porque confunden un demo que funciona con un sistema que aguanta el día a día. Acá te cuento dónde se quiebra la realidad y cómo LangGraph ayuda a resolver algunos de esos problemas.

Hay un video donde alguien arma un agente en quince minutos. Le da un prompt prolijo, el modelo llama funciones en el orden correcto, toma decisiones coherentes. Sale perfecto.

Lo copiás en tu empresa. La primera semana funciona. La segunda empieza a fallar.

La explicación no es que el modelo sea malo, ni que te falte prompt engineering. Los tutoriales viven en un mundo sin gravedad: sin datos sucios, sin concurrencia, sin plata en juego, sin nadie que tenga que rendir cuentas de por qué el sistema hizo lo que hizo. Cuando aparece la gravedad, se rompen cuatro cosas bastante predecibles.

Las repaso una por una, y en cada caso muestro qué te da LangGraph para contenerlas. Spoiler: no te resuelve todo, y sobre eso también hay una sección.

El orden de los pasos era una sugerencia, no una garantía

Pensá en un agente que decide qué transacciones aprobar. En el lab, el modelo anuncia que va a revisar la política de límites y la revisa. En producción, a veces la saltea y aprueba directo.

No es un bug de código. Es que "revisá los límites antes de aprobar" vive en el prompt, compitiendo con el historial de la conversación, con el resultado de las tools, con el system message. Cuando el contexto crece, la instrucción pierde peso relativo. No siempre. Lo suficiente.

En LangGraph el orden no se pide, se declara:

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END

class EstadoAprobacion(TypedDict):
    transaccion: dict
    limite_ok: bool | None
    decision: str | None

def validar_limites(state: EstadoAprobacion):
    monto = state["transaccion"]["monto"]
    return {"limite_ok": monto <= LIMITE_DIARIO}

def ejecutar(state: EstadoAprobacion):
    return {"decision": "aprobada"}

def rechazar(state: EstadoAprobacion):
    return {"decision": "rechazada_por_limite"}

def ruta(state: EstadoAprobacion) -> Literal["ejecutar", "rechazar"]:
    return "ejecutar" if state["limite_ok"] else "rechazar"

g = StateGraph(EstadoAprobacion)
g.add_node("validar", validar_limites)
g.add_node("ejecutar", ejecutar)
g.add_node("rechazar", rechazar)

g.add_edge(START, "validar")
g.add_conditional_edges("validar", ruta)
g.add_edge("ejecutar", END)
g.add_edge("rechazar", END)

app = g.compile()

No hay forma de llegar a ejecutar sin pasar por validar. No porque el modelo se acuerde, sino porque no existe la arista.

Fijate qué se movió de lugar: la validación de límites ya no es una decisión del LLM, es Python. El modelo sigue haciendo lo que hace bien —interpretar la intención del usuario, redactar la respuesta, elegir entre caminos ambiguos— pero dejó de ser responsable de recordar el procedimiento.

La memoria vivía en un diccionario de Python

Un agente simple mantiene contexto dentro de una conversación. El problema aparece cuando necesitás que recuerde decisiones de sesiones anteriores, o que maneje un flujo de negocio que dura días.

En soporte al cliente esto es el caso base, no el borde: querés que el agente sepa que este cliente ya escaló dos veces esta semana, que hay un reembolso pendiente, que el caso está marcado como sensible. Sin eso, el agente vuelve a empezar de cero en cada conversación y el cliente repite su historia por tercera vez.

Los ejemplos de quince minutos guardan el estado en un diccionario de Python. Escala perfecto para exactamente un usuario y hasta el próximo deploy.

LangGraph separa dos cosas que suelen confundirse: el estado (un objeto tipado que viaja por el grafo y que cada nodo actualiza parcialmente) y el checkpointer (dónde se persiste ese estado entre invocaciones).

from langgraph.checkpoint.postgres import PostgresSaver

with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup()
    app = g.compile(checkpointer=checkpointer)

    config = {"configurable": {"thread_id": "cliente-4821"}}
    app.invoke({"transaccion": tx}, config)

El thread_id es la clave del hilo. Volvés mañana con el mismo id y el grafo retoma desde el último checkpoint, con el estado intacto. En desarrollo usás un saver en memoria; en producción, Postgres o lo que ya tengas. La lógica del grafo no cambia.

El efecto secundario es más valioso que la memoria en sí: como cada paso queda persistido, tenés una traza completa de por qué el sistema hizo lo que hizo. Auditoría gratis, sin escribir logging ad hoc.

El modelo se hacía cargo de recuperar los errores

Los ejemplos de tutorial llaman APIs con datos limpios y respuestas predecibles. En producción los datos son un caos: un timeout que corta la request a mitad de camino, un endpoint que devuelve HTML porque el servidor está caído, un campo que llega null cuando esperabas un objeto.

Hacé la cuenta antes de escalar. Si un flujo típico hace cien llamadas a APIs y falla el 5%, no estás pagando cinco errores: estás pagando cinco reintentos que reenvían el historial completo, más el razonamiento del modelo tratando de entender qué salió mal, más las validaciones que agregaste después del primer incidente. Cinco fallos se convierten en un múltiplo del costo base, y el múltiplo crece con el largo de la conversación.

La raíz del problema es que le estás delegando al modelo la recuperación de errores. Es la tarea más cara posible: mucho contexto, mucha generación, poca certeza.

En un grafo, el error es un camino más:

def llamar_api(state):
    try:
        return {"respuesta": cliente.get(state["endpoint"]), "error": None}
    except (Timeout, ValidationError) as e:
        return {"respuesta": None, "error": str(e)}

def ruta_error(state) -> Literal["procesar", "fallback"]:
    return "fallback" if state["error"] else "procesar"

g.add_conditional_edges("llamar_api", ruta_error)

El fallback es Python determinístico: devolvés el último valor cacheado, escalás a un humano, respondés con un mensaje fijo. Cero tokens. El modelo ni se entera de que hubo un error.

Nadie pidió un agente autónomo

Los demos venden agencia total: el modelo decide qué, cuándo y cómo, sin supervisión. En un negocio real eso rara vez es lo que se necesita. Un agente que cancela pedidos sin confirmación no es inteligente, es un riesgo legal. Uno que recomienda créditos sin validar solvencia es un problema regulatorio.

Los sistemas agénticos que funcionan en producción no suelen ser autónomos. Son sistemas donde el modelo propone, una capa determinística valida y un humano aprueba en los puntos críticos.

LangGraph tiene esto como primitiva, no como parche:

app = g.compile(checkpointer=checkpointer, interrupt_before=["ejecutar"])

# El grafo corre hasta antes de "ejecutar" y se detiene.
estado = app.get_state(config)   # se lo mostrás a un humano
app.invoke(None, config)         # continúa donde quedó, tras la aprobación

Como el estado está persistido, la pausa puede durar minutos u horas. El proceso puede morir y reiniciar. El hilo sigue ahí. Eso es lo que hace viable un flujo de aprobación real y no un input() bloqueante en un notebook.

Qué no te resuelve LangGraph

Vale ser honesto, porque el entusiasmo con la herramienta genera su propia categoría de proyectos fallidos.

LangGraph no mejora la calidad de las respuestas del modelo: si alucina en un nodo, va a alucinar igual dentro de un grafo. Lo que ganás es contención, no exactitud.

Tampoco es gratis. Definir estados tipados, nodos y transiciones es bastante más trabajo que un loop ReAct de treinta líneas, y el debugging de un grafo mal diseñado —con estado que se pisa entre nodos— es su propio dolor de cabeza. Si tu flujo es genuinamente lineal y sin estado, una función con tres llamadas y un try/except es la respuesta correcta y no necesitás nada de esto.

Y adoptarlo es una decisión de arquitectura difícil de revertir a mitad de camino. Conviene tomarla temprano y con los ojos abiertos.

Por dónde empezar

Antes de escribir la primera línea, mapeá los puntos de falla. Qué pasa cuando el modelo se equivoca. Qué pasa cuando la API no responde. Qué pasa cuando el usuario hace algo que no previste. Cuáles son las acciones irreversibles que nadie debería tomar sin confirmación.

Esas respuestas son tu grafo: los nodos son los pasos que tienen que pasar sí o sí, las aristas condicionales son las bifurcaciones, los interrupts son las acciones irreversibles.

El tutorial de quince minutos te da el punto de partida. Lo que separa ese demo de un sistema que aguanta producción no es el modelo ni el framework: es haber decidido de antemano qué cosas no pueden quedar libradas a que el modelo se acuerde.