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.

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:
| Pieza | Qué es | Ejemplo |
|---|---|---|
| Estado | Un TypedDict compartido que cada nodo lee y escribe | {"pregunta": str, "borrador": str, "intentos": int} |
| Nodos | Funciones f(estado) -> dict con el parche a aplicar | investigar, redactar, revisar |
| Aristas | El 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.

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
-
langgraphpineado (==1.2.11), Python 3.10+. - Estado
TypedDictmí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_idcomo identidad de conversación. -
interrupt_beforesolo 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.

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.
Lecturas relacionadas
Sigue explorando Frameworks de Agentes y otras piezas para builders.

CrewAI: guía práctica de crews multi-agente en Python con roles

Mastra: guía práctica de agentes TypeScript con Studio

Pydantic AI: guía práctica para agentes tipados en Python
