La mayoría de los tutoriales de agentes de IA terminan donde empieza producción: la demo responde una pregunta y olvida todo. Un agente de soporte real necesita memoria entre turnos, herramientas que tocan sistemas reales y — lo crítico — el criterio para frenar y pedirle a un humano antes de devolver dinero.
En este tutorial vas a construir exactamente eso con LangGraph: un agente de soporte con grafo de estado tipado, llamadas a herramientas, memoria con checkpointer, un paso de aprobación humana y tracing en LangSmith. Cada API está verificada contra la documentación oficial actual.
1. El Problema: Las Demos Olvidan, los Reembolsos No
Un chatbot de soporte sobre una simple llamada al LLM tiene tres fallas fatales. No tiene memoria entre mensajes, así que cada turno empieza de cero. No puede actuar, así que solo habla de reembolsos en vez de procesarlos. Y no sabe escalar, así que la decisión riesgosa — devolver dinero — ocurre sin supervisión o se bloquea por completo.
El Error Más Común
Darle a un agente una herramienta de reembolso sin compuerta de aprobación. Vi este patrón en proyectos de rescate más de una vez: el agente funciona bárbaro en la demo y en la primera semana devuelve un reembolso real a la orden equivocada. Autonomía sin botón de pausa es un riesgo, no una feature.
LangGraph lo resuelve con un grafo explícito: los nodos hacen el trabajo, las aristas deciden qué sigue, un checkpointer recuerda el estado por hilo de conversación y un interrupt pausa la ejecución hasta que un humano aprueba. Esa es la arquitectura que vamos a construir.
2. Conceptos Mínimos: Estado, Nodos, Aristas, Checkpoints
LangGraph es un framework de orquestación de bajo nivel: definís un agente como un grafo en vez de una cadena. Cuatro ideas explican el 90% del framework. Aprendelas y el tutorial se va a leer como Python común.
StateGraph + TypedDict
langgraph.graph · la memoria compartida
Un único objeto de estado tipado fluye por cada nodo. Las listas de mensajes usan un reductor (operator.add o add_messages) para acumular en vez de sobrescribir. Los nodos devuelven actualizaciones parciales, nunca todo el estado.
ToolNode
langgraph.prebuilt · las manos
Un nodo prearmado que ejecuta las llamadas a herramientas del modelo y devuelve ToolMessages. Definís herramientas con el decorador @tool y las conectás con model.bind_tools(tools). Sin ejecutor custom.
Checkpointer + thread_id
persistencia · la memoria
Compilá con checkpointer=InMemorySaver() en local (PostgresSaver o SqliteSaver en producción) y pasá config={“configurable”: {“thread_id”: …}} para que cada conversación retome su propio estado.
interrupt + Command(resume=…)
langgraph.types · el botón de pausa
Llamá interrupt(payload) dentro de un nodo para congelar la ejecución y mostrar una pregunta. Retomá con graph.invoke(Command(resume=respuesta), config). El nodo se re-ejecuta desde su inicio: el código previo al interrupt debe ser idempotente.
Aristas condicionales
ruteo · el criterio
add_conditional_edges rutea según el estado: si el último mensaje tiene tool_calls va al nodo de herramientas, si no a END. Esta función mínima es todo el loop ReAct: el modelo actúa, las herramientas responden, el modelo continúa.
Tracing con LangSmith
observabilidad · la caja negra
Con LANGSMITH_TRACING=true cada nodo, herramienta y token queda registrado como trace. Cuando tu agente falla en producción, leés el trace de arriba a abajo en vez de adivinar.
Lo Que Vamos a Construir
Un agente de soporte con 4 nodos: agent (el LLM), tools (búsqueda de órdenes + reembolso), refund_approval (compuerta humana con interrupt) y END. Memoria con InMemorySaver, ruteo con should_continue y tracing completo en LangSmith.
3. Pasos 1–2: Herramientas y Estado
Instalá los paquetes, definí dos herramientas de soporte y atalas al modelo. La herramienta de reembolso no se ejecuta directo — la va a controlar el nodo de aprobación. El estado es un TypedDict con lista de mensajes acumulativa más un campo de dominio.
pip install langgraph langchain-anthropic
export ANTHROPIC_API_KEY="sk-..."
export LANGSMITH_TRACING="true" # caja negra activada
from langchain.tools import tool
from langchain.chat_models import init_chat_model
model = init_chat_model("claude-sonnet-4-6", temperature=0)
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order by ID. Returns status and total."""
return f"Order {order_id}: shipped, total $84.20"
@tool
def issue_refund(order_id: str, amount: float) -> str:
"""Issue a refund. Only runs after human approval."""
return f"Refunded ${amount:.2f} to order {order_id}"
tools = [lookup_order, issue_refund]
model_with_tools = model.bind_tools(tools)
from typing_extensions import TypedDict, Annotated
from langchain_core.messages import AnyMessage
import operator
class SupportState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
customer_tier: strPor Qué operator.add Importa
Sin el reductor Annotated, cada nodo sobrescribiría el historial de mensajes. Con operator.add, cada nodo acumula y la conversación completa sobrevive todo el run — incluso a través de interrupts.
4. Pasos 3–4: Nodos, Aristas y el Grafo
Dos nodos (agent + ToolNode prearmado), una función de ruteo, cuatro aristas. El nodo agent llama al modelo atado a herramientas; should_continue manda las llamadas a ejecución y las respuestas planas a END. Compilá primero sin checkpointer para probar el loop, y sumá memoria en el paso siguiente.
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from typing import Literal
def agent_node(state: SupportState):
reply = model_with_tools.invoke(state["messages"])
return {"messages": [reply]}
def should_continue(state: SupportState) -> Literal["tools", END]:
if state["messages"][-1].tool_calls:
return "tools"
return END
builder = StateGraph(SupportState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, ["tools", END])
builder.add_edge("tools", "agent")
graph = builder.compile() # la memoria viene después
from langchain_core.messages import HumanMessage
out = graph.invoke({
"messages": [HumanMessage(content="Where is order 8821?")],
"customer_tier": "pro",
})
print(out["messages"][-1].content)Probá Antes de Agregar Memoria
Corré primero el grafo sin memoria con una pregunta de consulta. Si el loop ReAct funciona acá, cualquier bug posterior vive en checkpointing o interrupts — acabás de partir tu superficie de debugging a la mitad.
5. Pasos 5–6: Memoria, Compuerta de Aprobación y Tracing
Recompilá con checkpointer, insertá el nodo refund_approval entre herramientas y ejecución, y retomá con Command. Reembolsos de más de $50 frenan para un humano; el resto fluye directo. LangSmith registra ambas mitades del run pausado automáticamente.
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
def refund_approval(state: SupportState):
last = state["messages"][-1]
calls = [c for c in last.tool_calls if c["name"] == "issue_refund"]
if not calls or calls[0]["args"].get("amount", 0) <= 50:
return {"messages": []} # montos chicos pasan directo
ok = interrupt({ # pausa acá, el payload lo ve el revisor
"question": "Approve this refund?",
"order": calls[0]["args"],
"tier": state["customer_tier"],
})
if not ok:
return {"messages": [HumanMessage(content="Refund declined by reviewer.")]}
return {"messages": []}
builder.add_node("refund_approval", refund_approval)
builder.add_edge("tools", "refund_approval")
# rewire: tools -> approval -> agent (rebuild + recompile)
checkpointer = InMemorySaver() # SqliteSaver / PostgresSaver en prod
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "support-123"}}
first = graph.invoke(
{"messages": [HumanMessage(content="Refund $340 to order 8821")],
"customer_tier": "pro"},
config=config,
)
print(first.get("__interrupt__")) # pausado, esperando un humano
final = graph.invoke(Command(resume=True), config=config) # aprobar
print(final["messages"][-1].content)Checklist de Producción
Cambiá InMemorySaver por PostgresSaver (sobrevive reinicios), mantené los thread_id bajo 255 caracteres, nunca envuelvas interrupt() en un try/except genérico y hacé los reembolsos idempotentes — el nodo de aprobación se re-ejecuta desde su inicio al retomar.
6. Errores Comunes (y el Fix de Una Línea para Cada Uno)
Estas cuatro fallas explican la mayoría de los hilos de soporte de LangGraph que vi. Cada una tiene una causa precisa y un fix de una línea — memorizalos antes de deployar.
El grafo olvida todo en el segundo turno
Causa: compilaste sin checkpointer o invocás sin thread_id. Fix: compilá con checkpointer=… y pasá siempre config={“configurable”: {“thread_id”: …}} — el mismo thread retoma, un thread nuevo empieza de cero.
Loop infinito entre agente y herramientas
Causa: should_continue nunca devuelve END — típico cuando una herramienta falla y el modelo reintenta para siempre. Fix: poné recursion_limit en el invoke y logueá los errores como ToolMessages para que el modelo vea la falla.
interrupt() nunca pausa — el grafo lo atraviesa
Causa: interrupt() envuelto en un try/except genérico que traga su excepción de control, o falta el checkpointer. Fix: dejá interrupt() fuera de los try/except y compilá con checkpointer.
Reembolsos duplicados después de aprobar
Causa: un efecto no idempotente antes de interrupt() se re-ejecuta al retomar. Fix: mové las escrituras después del interrupt, o hacelas idempotentes (upsert con clave de idempotencia, nunca insert ciego).
7. Cuándo LangGraph Rinde — y Cuándo No
LangGraph es overhead para prompts de un solo tiro y un superpoder para flujos con estado. Usá esta regla para decidir antes de commitear un grafo nuevo a tu codebase.
✅ Usá LangGraph
- • Soporte, onboarding u ops multi-turno con memoria
- • Herramientas con side effects que piden aprobación o auditoría
- • Runs largos que deben sobrevivir reinicios, timeouts y handoffs
- • Debugging con traces de LangSmith en vez de prints
- • Ruteo entre especialistas (triage → billing → técnico)
❌ Salteá el grafo
- • Una pregunta, una respuesta — llamá al modelo directo
- • Pipelines estáticos sin branching ni reintentos
- • Prototipos donde una abstracción simple shippea más rápido
- • Estados con una docena de campos ad-hoc que nadie documenta
Regla de oro
Si la tarea necesita memoria, branching o un botón de pausa humano, modelala como grafo. Si no necesita ninguno de los tres, el grafo es ceremonia. Esto lo reviso con cada equipo antes de agregar el primer nodo.
Fuentes
Cada API de este tutorial sale de la documentación oficial y guías actuales de la comunidad. Verificá antes de deployar — los frameworks se mueven rápido.
- LangGraph Quickstart (Graph API: StateGraph, patrón ToolNode, bind_tools)
- Interrupts: interrupt(), Command(resume=…), patrones de aprobación y revisión
- Persistencia: InMemorySaver, PostgresSaver, SqliteSaver, thread_id
- LangGraph overview: orquestación, ejecución durable, HITL
- Checkpointers: por qué thread_id es requerido, fault tolerance
Conclusión
Ya tenés un agente de soporte que recuerda la conversación, consulta órdenes y se niega a mover dinero sin que un humano diga que sí. Ese loop — estado, herramientas, checkpoint, interrupt, trace — es el template detrás de la mayoría de los agentes productivos que construyo o rescato.
Próximo paso: apuntá el mismo grafo a tus propias herramientas (lookup en Zendesk, reembolso en Stripe en modo test), cambiá InMemorySaver por PostgresSaver y mirá los primeros diez traces en LangSmith. Los traces te van a enseñar más que cualquier tutorial — incluido este.
Lo Que Construiste: Resumen
Grafo
- • StateGraph + ToolNode
- • Router should_continue
- • Compuerta refund_approval
Memoria & Seguridad
- • InMemorySaver → Postgres
- • thread_id por conversación
- • interrupt + resume
Observabilidad
- • LANGSMITH_TRACING=true
- • Trace de cada herramienta
- • Auditoría de aprobaciones



