Guía11 min

LangGraph: guía práctica de grafos con estado para agentes en Python

Resumen

Guía práctica de LangGraph en 2026: instalar langgraph 1.2.11 con Python 3.10+, modelar estado con TypedDict y StateGraph, conectar nodos con add_edge y add_conditional_edges entre START y END, persistir con checkpointer y thread_id, pausar para aprobación humana con interrupt, y abanicar trabajo con Send. Cuándo un grafo le gana a un crew de roles.

OpenAIGitHub
Grafo de nodos conectados con estado compartido para un agente IA

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

LangGraph es el framework Python para agentes como grafos con estado: defines un estado compartido, nodos que lo transforman y aristas que deciden el siguiente paso. No es un equipo de roles con handoffs en lenguaje natural como CrewAI. No es un agente único tipado como Pydantic AI. Es control exacto del flujo: cada rama, cada reintento y cada pausa están en código que puedes leer.

Esta guía cubre el recorte del hub Construir agentes que sí pegas en un repo hoy: versiones, primer grafo, ramas condicionales, persistencia, aprobación humana y fan-out. El curso Instalar un agente cubre el harness de producto; aquí el contrato es el de langgraph. La tabla de decisión entre frameworks vive en LangGraph vs CrewAI vs OpenAI Agents SDK.

Versiones y arranque

PyPI publica langgraph 1.2.11 (verificado 2026-09-07) con requires_python: >=3.10. La documentación oficial migró a docs.langchain.com (la URL vieja langchain-ai.github.io/langgraph/ solo redirige).

pip install langgraph

Si tus nodos llaman a un LLM necesitas además el proveedor, por ejemplo langchain-openai. Mantén langgraph pineado en requirements.txt: la API 1.x evoluciona y un >= flotante en prod es una sorpresa calendarizada.

El modelo mental: estado, nodos, aristas

Todo grafo cuelga de tres piezas:

PiezaQué esEjemplo
EstadoUn TypedDict compartido que cada nodo lee y escribe{"pregunta": str, "borrador": str, "intentos": int}
NodosFunciones f(estado) -> dict con el parche a aplicarinvestigar, redactar, revisar
AristasEl orden: fijo (add_edge) o decidido por código (add_conditional_edges)revisar → aprobar | reintentar

La diferencia con un chain es que el grafo puede volver atrás: un revisor que rechaza manda el flujo de vuelta al redactor con el motivo. Esa es la operación que un crew expresa con palabras y aquí expresas con una arista.

Tu primer grafo en 40 líneas

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

class Estado(TypedDict):
    tema: str
    datos: list
    borrador: str
    intentos: int

def investigar(e: Estado) -> dict:
    filas = buscar_fuentes(e["tema"])  # tu función, tu API
    return {"datos": filas}

def redactar(e: Estado) -> dict:
    texto = llm(f"Resume en 150 palabras: {e['datos']}")
    return {"borrador": texto, "intentos": e["intentos"] + 1}

def revisar(e: Estado) -> dict:
    return {}  # el veredicto lo decide la arista condicional

def veredicto(e: Estado) -> str:
    ok = "fuente" in e["borrador"].lower() and len(e["datos"]) >= 3
    if ok or e["intentos"] >= 2:
        return "aprobar"
    return "reintentar"

g = StateGraph(Estado)
g.add_node("investigar", investigar)
g.add_node("redactar", redactar)
g.add_node("revisar", revisar)
g.add_edge(START, "investigar")
g.add_edge("investigar", "redactar")
g.add_edge("redactar", "revisar")
g.add_conditional_edges("revisar", veredicto, {
    "aprobar": END,
    "reintentar": "redactar",
})
app = g.compile()
print(app.invoke({"tema": "precio del café", "datos": [], "borrador": "", "intentos": 0}))

Tres decisiones ya tomadas aquí y por qué: el contador intentos con tope en 2 evita el loop infinito (todo ciclo necesita fusible), la arista condicional devuelve strings que son las únicas claves del mapa (un typo = error en runtime, revísalo con un test), y revisar como nodo separado aunque no escriba estado deja el trazo legible.

Grafo con ciclo de reintento acotado por contador de intentos

Persistencia: checkpointer + thread_id

Sin checkpointer, cada invoke empieza de cero. Con checkpointer, el grafo guarda el estado por thread_id y puedes continuar conversaciones, reanudar tras un error y auditar qué pasó en cada paso:

from langgraph.checkpoint.memory import MemorySaver

app = g.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "cliente-42"}}
app.invoke({"tema": "precio del café", "datos": [], "borrador": "", "intentos": 0}, config)
# ...más tarde, mismo thread_id: el grafo recuerda dónde iba
app.invoke({"tema": "nuevo dato"}, config)

MemorySaver es para desarrollo: vive en RAM y se pierde al reiniciar. En producción usa un checkpointer persistente (Postgres, Redis) y trata el thread_id como lo que es: la identidad de la conversación. El patrón completo de resume-que-omite-éxitos vive en ejecución durable.

Aprobación humana con interrupt

Para pasos que tocan dinero, datos de clientes o acciones irreversibles, el grafo se pausa antes del nodo y espera tu veredicto:

app = g.compile(
    checkpointer=MemorySaver(),
    interrupt_before=["publicar"],  # se detiene aquí hasta aprobar
)

El flujo: invoke corre hasta publicar y se detiene, un humano revisa el estado, y la ejecución continúa con Command(resume=...). Esto es el default-deny con timeout de aprobaciones humanas implementado en el framework en vez de en tu capa de llamada. Úsalo en exactamente los nodos irreversibles; interrumpir todo convierte al humano en el cuello de botella.

Fan-out con Send

Cuando N subtareas son independientes (investigar 5 fuentes a la vez), un nodo devuelve una lista de Send y el grafo las ejecuta en paralelo, esperando a todas antes de continuar:

from langgraph.types import Send

def abanicar(e: Estado):
    return [Send("investigar_fuente", {"url": u}) for u in e["urls"]]

Regla: el fan-out multiplica costo y latencia del paso más lento. Ponle techo (máximo de URLs, timeout por rama) o un input entusiasta convierte tu grafo en un DDoS con tu API key.

Checklist de producción

  • langgraph pineado (==1.2.11), Python 3.10+.
  • Estado TypedDict mínimo: solo lo que los nodos leen o escriben de verdad.
  • Todo ciclo con fusible (contador de intentos o tope de costo).
  • Checkpointer persistente en prod; thread_id como identidad de conversación.
  • interrupt_before solo en nodos irreversibles, con timeout default-deny.
  • Fan-out con techo de ramas y timeout por rama.
  • Secrets en entorno, nunca en el estado (el estado se persiste y se traza).
  • Tracing activado antes del primer deploy.

Decisión: grafo con estado frente a crew de roles o agente único

FAQ

¿LangGraph o CrewAI? Ramas, ciclos y estado tipado que debes auditar: LangGraph. Colaboración entre roles donde el handoff puede ser lenguaje natural: CrewAI. Tabla completa en la comparativa.

¿Cuándo NO usar un grafo? Si tu flujo es una lista recta sin ramas ni reintentos, un grafo es ceremonia. Un Crew secuencial o una función con un loop bastan; sube a grafo cuando aparezca la primera rama real.

¿Dónde van los prompts del sistema? En los nodos que llaman al LLM, como plantillas con el estado inyectado (ver system prompts para agentes). El grafo decide el flujo; el prompt decide la calidad de cada paso.

¿LangGraph solo sirve con LangChain? No. Los nodos son funciones Python cualquiera: puedes llamar a tu API, tu base de datos o cualquier SDK de modelo. LangChain es conveniencia, no requisito.

¿Cómo pruebo un grafo sin quemar tokens? Nodos puros (funciones sin LLM) con asserts directos; para los nodos con LLM, un stub que devuelva respuestas fijas y verifique las transiciones. El veredicto de cada arista condicional merece su propio test: es donde viven los typos caros.