LangGraph + LangSmith: crear y monitorizar sistemas multiagente en producción (2026)

28 de marzo de 2026
14 min de lectura

28 de marzo de 2026
14 min de lectura
Casi todos los equipos que construyen agentes de IA chocan con el mismo muro. La demo funciona. El prototipo impresiona a los responsables. Y entonces llega producción, y todo se vuelve opaco. Los agentes entran en bucle sin motivo aparente. Las llamadas a herramientas fallan en silencio. Los costes se disparan. Nadie sabe por qué ha fallado una ejecución, porque no hay forma de ver qué ocurrió por dentro.
Esta guía resuelve los dos problemas. LangGraph aporta una orquestación multiagente con estado y controlable, que no se desmorona al crecer. LangSmith aporta la capa de observabilidad para trazar cada ejecución, evaluar cada salida y depurar cada fallo, tanto en desarrollo como en producción. Juntos son el stack de producción para sistemas de agentes serios.
Las cadenas LCEL (LangChain Expression Language) son excelentes para pipelines lineales: entrada → prompt → LLM → salida. Pero los sistemas de agentes reales no son lineales. Necesitan:
LangGraph se creó justo para esto. Modela el flujo de tus agentes como un grafo dirigido en el que los nodos son funciones (agentes, herramientas, comprobaciones condicionales) y las aristas definen el flujo de ejecución. El estado es un diccionario de Python tipado que se pasa y se actualiza en cada paso. El checkpointer persiste ese estado, así que obtienes memoria, capacidad de reintento y puntos de interrupción humana sin esfuerzo adicional.
pip install langgraph langsmith langchain-openai langchain-anthropic
# Set environment variables
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls__your_api_key_here
export LANGCHAIN_PROJECT=production-agents
export OPENAI_API_KEY=sk-your-key
Antes de escribir un solo agente, entiende las cuatro primitivas sobre las que se construye todo lo demás.
Un TypedDict que contiene todos los datos que circulan por el grafo. Cada nodo lee y escribe en el estado. Es la única fuente de verdad.
Funciones de Python que reciben el estado y devuelven una actualización parcial. Un nodo puede ser una llamada a un LLM, la ejecución de una herramienta, una comprobación condicional: lo que necesites.
Conexiones entre nodos. Pueden ser fijas (ir siempre al siguiente nodo) o condicionales (enrutar según los valores del estado, con una simple función de Python como lógica de enrutamiento).
Persisten el estado entre pasos. SqliteSaver para desarrollo, RedisSaver o PostgresSaver para producción. Habilitan la memoria, los reintentos y la aprobación humana.
El patrón planificar y ejecutar: el planificador descompone el objetivo, el ejecutor recorre los pasos, un enrutador condicional decide si replanificar o terminar, y el replanificador corrige si hace falta.
Este es un agente de atención al cliente listo para producción que clasifica tickets, consulta una base de conocimiento RAG, resuelve de forma autónoma o escala a una persona, usando todas las capacidades de LangGraph.
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator
class SupportAgentState(TypedDict):
# All messages in the conversation
messages: Annotated[Sequence[BaseMessage], operator.add]
# Classified ticket category
category: str
# Retrieved knowledge base docs
kb_results: list[dict]
# Generated resolution
resolution: str | None
# Whether ticket needs escalation
needs_escalation: bool
# Escalation reason if applicable
escalation_reason: str | None
# Final response sent to customer
final_response: str | None
# Confidence score (0-1)
confidence: float
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Node 1: Classify the incoming ticket
def classifier_node(state: SupportAgentState) -> dict:
prompt = ChatPromptTemplate.from_messages([
("system", """Classify this support ticket into one of:
billing, technical, account, shipping, general
Also assess confidence (0-1) that you can auto-resolve it.
Return JSON: {{"category": "...", "confidence": 0.0}}"""),
("human", "{ticket}"),
])
chain = prompt | llm
ticket_text = state["messages"][-1].content
result = chain.invoke({"ticket": ticket_text})
parsed = parse_json(result.content)
return {
"category": parsed["category"],
"confidence": parsed["confidence"],
}
# Node 2: Query the RAG knowledge base
def rag_node(state: SupportAgentState) -> dict:
query = state["messages"][-1].content
# Your vector store query here (Pinecone, pgvector, etc.)
results = vector_store.similarity_search(
query, k=5, filter={"category": state["category"]}
)
return {"kb_results": [r.page_content for r in results]}
# Node 3: Generate resolution using retrieved context
def resolver_node(state: SupportAgentState) -> dict:
context = "\n\n".join(state["kb_results"])
prompt = ChatPromptTemplate.from_messages([
("system", f"""You are a support agent. Use this context to
resolve the ticket. If you cannot resolve with high confidence,
return null for resolution.\n\nContext:\n{context}"""),
("human", "{ticket}"),
])
chain = prompt | llm
result = chain.invoke({"ticket": state["messages"][-1].content})
return {"resolution": result.content}
# Node 4: Human escalation node
def escalation_node(state: SupportAgentState) -> dict:
return {
"needs_escalation": True,
"escalation_reason": (
f"Low confidence ({state['confidence']:.0%}) — "
f"routed to human agent"
),
"final_response": (
"We've escalated your ticket to a specialist "
"who will follow up within 2 business hours."
),
}
# Node 5: Format and send final response
def responder_node(state: SupportAgentState) -> dict:
return {
"final_response": state["resolution"],
"messages": [AIMessage(content=state["resolution"])],
}
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
# Conditional routing function
def route_after_classification(state: SupportAgentState) -> str:
"""Route based on category and confidence score."""
if state["confidence"] < 0.7:
return "escalate" # Low confidence → human
if state["category"] == "billing":
return "escalate" # Always escalate billing
return "rag" # High confidence → try auto-resolve
def route_after_resolution(state: SupportAgentState) -> str:
"""Check if resolution is good enough to send."""
if state["resolution"] is None:
return "escalate"
if len(state["resolution"]) < 50:
return "escalate" # Too short, probably incomplete
return "respond"
# Build the graph
builder = StateGraph(SupportAgentState)
# Add all nodes
builder.add_node("classifier", classifier_node)
builder.add_node("rag", rag_node)
builder.add_node("resolver", resolver_node)
builder.add_node("escalation", escalation_node)
builder.add_node("responder", responder_node)
# Add edges
builder.add_edge(START, "classifier")
builder.add_conditional_edges(
"classifier",
route_after_classification,
{
"rag": "rag",
"escalate": "escalation",
},
)
builder.add_edge("rag", "resolver")
builder.add_conditional_edges(
"resolver",
route_after_resolution,
{
"respond": "responder",
"escalate": "escalation",
},
)
builder.add_edge("responder", END)
builder.add_edge("escalation", END)
# Compile with checkpointer for persistent memory
memory = SqliteSaver.from_conn_string(":memory:") # Use PostgresSaver in prod
graph = builder.compile(
checkpointer=memory,
interrupt_before=["escalation"], # Human-in-the-loop checkpoint
)
# Run the graph
config = {"configurable": {"thread_id": "ticket-48291"}}
result = graph.invoke(
{"messages": [HumanMessage(content="My invoice is wrong")]},
config=config,
)
print(result["final_response"])
🔑 La potencia de thread_id
Cada invocación con el mismo thread_id retoma la ejecución donde la dejó la anterior. El checkpointer se encarga de ello automáticamente. Así consigues memoria entre turnos, tareas de larga duración y la posibilidad de pausar y reanudar, sin escribir ni una línea de gestión de sesiones.
Los sistemas multiagente tienen un modo de fallo que no existe en el software tradicional: pueden parecer que funcionan mientras producen salidas sutilmente incorrectas. El código se ejecuta. No salta ninguna excepción. Pero el agente clasificó mal el ticket, recuperó contexto irrelevante y envió al cliente una respuesta equivocada con tono seguro. Sin observabilidad, nunca te enteras.
LangSmith captura una traza completa de cada ejecución: cada llamada al LLM, cada ejecución de herramienta, cada transición de estado y cada token consumido. Define dos variables de entorno y empieza a funcionar automáticamente con LangGraph, sin código de instrumentación:
# These two variables are all you need to start tracing
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls__your_api_key
# Optional but recommended
export LANGCHAIN_PROJECT=customer-support-prod
export LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
# That's it. Your LangGraph runs are now fully traced.
# No code changes required.
LangSmith traza cada nodo, cada llamada a herramienta y cada invocación del LLM, con recuento de tokens, latencia, coste y valoraciones humanas en un solo panel.
Las trazas te dicen qué ocurrió. La evaluación te dice cómo de bien funcionó. El framework de evaluación de LangSmith permite ejecutar evaluaciones puntuadas contra conjuntos de datos, con evaluadores propios o con un LLM como juez.
from langsmith import Client
from langsmith.evaluation import evaluate, LangChainStringEvaluator
client = Client()
# Create or use an existing dataset
dataset = client.create_dataset(
dataset_name="support-tickets-v2",
description="Golden set of 200 resolved support tickets",
)
# Add examples: input ticket → expected resolution
client.create_examples(
inputs=[
{"messages": [{"role": "user", "content": "How do I cancel my subscription?"}]},
{"messages": [{"role": "user", "content": "My payment failed but I was charged."}]},
],
outputs=[
{"final_response": "To cancel, go to Settings → Billing → Cancel Plan."},
{"final_response": "We've issued a refund. It will appear in 3-5 business days."},
],
dataset_id=dataset.id,
)
# Define evaluators
correctness_evaluator = LangChainStringEvaluator(
"criteria",
config={"criteria": "correctness"},
prepare_output_fn=lambda run, example: {
"prediction": run.outputs.get("final_response", ""),
"reference": example.outputs.get("final_response", ""),
},
)
# Run evaluation against your compiled graph
def run_agent(inputs: dict) -> dict:
config = {"configurable": {"thread_id": f"eval-{id(inputs)}"}}
result = graph.invoke(inputs, config=config)
return {"final_response": result.get("final_response", "")}
results = evaluate(
run_agent,
data=dataset.name,
evaluators=[correctness_evaluator],
experiment_prefix="gpt-4o-support-v3",
num_repetitions=1,
)
print(f"Average correctness: {results.to_pandas()['feedback.correctness'].mean():.2f}")
Para acciones delicadas —enviar correos, tramitar reembolsos, actualizar registros del CRM— quieres que una persona apruebe antes de que el agente actúe. El parámetro interrupt_before de LangGraph pausa la ejecución en cualquier nodo y persiste el estado mediante el checkpointer. Después se reanuda con una sola llamada.
# Compile with human checkpoint before escalation
graph = builder.compile(
checkpointer=memory,
interrupt_before=["escalation"], # Pause here for human review
)
config = {"configurable": {"thread_id": "ticket-48291"}}
# First run — graph pauses at "escalation" node
result = graph.invoke(
{"messages": [HumanMessage(content="My invoice is wrong")]},
config=config,
)
print("Graph paused. Current state:", result)
# → Graph stopped before escalation node
# Human reviews in your dashboard / Slack / UI...
# When approved, resume from the same thread_id:
graph.update_state(
config,
{"escalation_reason": "Human approved escalation — billing dispute"},
as_node="escalation",
)
# Resume — graph continues from where it paused
final_result = graph.invoke(None, config=config)
print("Final response:", final_result["final_response"])
LangGraph admite streaming en tres niveles: por token (caracteres según se generan), por evento (inicio y fin de cada nodo) y por valor (el estado completo tras cada nodo). Usa el que encaje con tu interfaz.
# Stream tokens as they're generated (for chat UIs)
async for event in graph.astream_events(
{"messages": [HumanMessage(content="Help me understand my bill")]},
config=config,
version="v2",
):
kind = event["event"]
if kind == "on_chat_model_stream":
# Token-level streaming — send to frontend via SSE/WebSocket
chunk = event["data"]["chunk"]
if chunk.content:
print(chunk.content, end="", flush=True)
elif kind == "on_chain_start":
# Node started — useful for progress indicators
node_name = event["name"]
print(f"\n[{node_name} started]")
elif kind == "on_tool_start":
# Tool call started
tool_name = event["name"]
inputs = event["data"]["input"]
print(f"\n🔧 Using tool: {tool_name}({inputs})")
elif kind == "on_chain_end":
# Node completed — log duration
node_name = event["name"]
print(f"\n[{node_name} completed]")
SqliteSaver está bien para desarrollo local, pero no funciona en entornos distribuidos ni serverless. Cambia a PostgresSaver para despliegues con varias réplicas. Las dos tienen la misma API: cambia un import y listo.
from langgraph.checkpoint.postgres import PostgresSaver
memory = PostgresSaver.from_conn_string(os.getenv('DATABASE_URL'))
Los agentes de planificar y ejecutar pueden dar vueltas para siempre si el replanificador nunca converge. Añade un contador de iteraciones al estado y una arista condicional que fuerce END pasadas N iteraciones.
def should_continue(state):
if state['iteration'] >= 10:
return 'end' # Force termination
return 'continue'
Etiqueta cada ejecución con la versión del modelo, la del prompt y los flags de funcionalidad. Así comparar resultados entre experimentos en la interfaz de LangSmith es inmediato.
config = {
'configurable': {'thread_id': tid},
'tags': ['gpt-4o', 'prompt-v3', 'prod'],
'metadata': {'customer_tier': 'enterprise'}
}
Define tus herramientas una sola vez y vincúlalas al LLM. El ToolNode de LangGraph se encarga de ejecutarlas automáticamente, lo que mantiene las funciones de los nodos limpias y comprobables por separado.
from langgraph.prebuilt import ToolNode
tools = [search_web, query_rag, send_email]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
Así es un despliegue de LangGraph + LangSmith en producción, capa por capa:
| Capa | Herramienta | Por qué |
|---|---|---|
| Orquestación | LangGraph | Ejecución de grafos con estado, bucles, ramificación y aprobación humana |
| LLM | GPT-4o / Claude 3.5 Sonnet | El mejor razonamiento para tareas complejas de agentes |
| Llamadas a herramientas | LangGraph ToolNode | Despacha automáticamente las llamadas del LLM a funciones de Python |
| Memoria y estado | PostgresSaver | Persiste el estado del hilo entre llamadas en producción |
| RAG | Pinecone + pgvector | Búsqueda vectorial para recuperar de la base de conocimiento |
| Observabilidad | LangSmith | Trazas, evaluaciones, conjuntos de datos y control de costes |
| Capa de API | FastAPI + SSE | Envía tokens al frontend y gestiona la autenticación |
| Frontend | Next.js + Vercel AI SDK | El AI SDK se encarga de renderizar la respuesta en streaming |
| Colas | Redis / Celery | Para tareas de agentes asíncronas y de larga duración |
| Despliegue | Railway / AWS ECS | Servidor FastAPI + LangGraph en contenedores |
→Un agente de atención al cliente que resuelve más del 70 % de los tickets por su cuenta
→Un agente de prospección comercial que investiga, escribe y hace seguimiento
→Un pipeline de procesamiento documental con extracción de datos estructurados
→Un agente de selección de personal que criba, puntúa y agenda entrevistas
→Un agente de informes financieros que vigila la cuenta de resultados y señala anomalías
→Un agente de conocimiento interno que responde a las preguntas del equipo a partir de la documentación
LangGraph resuelve el problema de la orquestación: grafos con estado, enrutamiento condicional, memoria persistente y puntos de control humanos. LangSmith resuelve el problema de la visibilidad en producción: cada traza, cada evaluación y cada fallo a la vista y depurables. Juntos te dan el control y la observabilidad necesarios para lanzar sistemas de agentes que funcionan de verdad en producción, no solo en una demo.
Configurar LangSmith con dos variables de entorno es uno de los cambios con mejor retorno que puedes hacer hoy. Cero cambios de código. Visibilidad inmediata de cada ejecución. Si lanzas agentes de IA y no lo usas, vas a ciegas.
IdeaToMVP Academy
4-week live cohort for founders. Learn to ship AI agents, scope MVPs, and automate your business — taught by the same team that writes these guides.