Módulo 5: Introducción a LangGraph
create_agent vs StateGraph: Cuándo Usar Cada Uno
Descripción de la cápsula
Esta es la decisión más importante que vas a tomar en cada proyecto con LangChain y LangGraph: ¿uso create_agent o construyo un StateGraph custom? En las cápsulas anteriores aprendiste todo lo necesario sobre LangGraph — StateGraph, nodos, edges, conditional edges, estado tipado con TypedDict y Annotated, compilación y ejecución. Ahora tienes dos herramientas potentes para construir agentes. El problema es que ambas resuelven problemas similares, y sin un framework de decisión claro, vas a perder tiempo eligiendo o vas a elegir mal.
La respuesta honesta es que create_agent cubre el 80% de los casos. Es rápido de implementar, tiene el patrón ReAct integrado, soporta middleware para personalización, y en 10 líneas tienes un agente funcional. StateGraph es para el otro 20%: workflows con branching condicional, loops personalizados, human-in-the-loop, nodos especializados por tarea, y flujos que no siguen el patrón lineal de model → tools → model. Ni uno es "mejor" que el otro — son herramientas diferentes para problemas diferentes.
En esta cápsula vas a ver una tabla de decisión concreta, el mismo problema resuelto con ambos enfoques (side-by-side), el camino de progresión natural (empezar con create_agent, migrar cuando sea necesario), y los dos errores más comunes: usar StateGraph para todo (over-engineering) y quedarte con create_agent cuando necesitas más control (under-engineering).
La tabla de decisión
Antes de escribir una línea de código, consulta esta tabla:
| Criterio | create_agent | StateGraph |
|---|---|---|
| Tiempo de setup | ~5 minutos | 30+ minutos |
| Líneas de código | ~10-15 | ~50+ |
| Control de flujo | Limitado (middleware) | Total (nodos + edges) |
| Branching / routing | No (loop ReAct lineal) | Sí (conditional edges) |
| Human-in-the-loop | No | Sí (interrupt) |
| Visualización del grafo | No | Sí (draw_mermaid_png) |
| Curva de aprendizaje | Baja | Media |
| Middleware system | Sí (completo) | No aplica (tú defines todo) |
| Streaming | Sí (built-in) | Sí (manual) |
| Caso de uso | 80% de agentes | Workflows complejos |
La regla general: si puedes resolver tu problema con create_agent + middleware, hazlo. Solo baja a StateGraph cuando necesites algo que create_agent no puede darte.
Cuándo usar create_agent
create_agent es la opción correcta cuando:
- ✅ Tu agente tiene un propósito único (buscar, analizar, responder)
- ✅ El flujo es ReAct estándar: model → tools → model → tools → respuesta
- ✅ Necesitas un prototipo rápido que funcione en minutos
- ✅ La personalización que necesitas se logra con middleware (logging, routing de modelos, auth)
- ✅ No necesitas branching — todas las preguntas siguen el mismo flujo
- ✅ No necesitas pausar la ejecución para aprobación humana
Ejemplo: agente de búsqueda con create_agent
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Busca información sobre un tema."""
return f"Resultados sobre {query}: Python fue creado en 1991 por Guido van Rossum."
@tool
def summarize(text: str) -> str:
"""Resume un texto largo en puntos clave."""
return f"Resumen de '{text[:50]}...': 3 puntos clave identificados."
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model,
tools=[search, summarize],
prompt="Eres un investigador. Busca información y resúmela para el usuario.",
)
result = agent.invoke(
{"messages": [("user", "Investiga sobre Python y dame un resumen")]}
)
print(result["messages"][-1].content)
# Output esperado:
# Python fue creado en 1991 por Guido van Rossum...
# Resumen: 3 puntos clave identificados...
15 líneas de código. El agente decide cuándo llamar a search, cuándo a summarize, y cuándo responder. El loop ReAct maneja todo automáticamente.
Cuándo usar StateGraph
StateGraph es la opción correcta cuando:
- ✅ Necesitas que diferentes tipos de input sigan caminos diferentes (routing)
- ✅ Tu workflow tiene nodos especializados (clasificador, procesador, validador, formateador)
- ✅ Necesitas loops personalizados (retry con backoff, iteración hasta convergencia)
- ✅ Requieres human-in-the-loop (pausar para aprobación antes de ejecutar)
- ✅ Quieres visualizar el flujo para debugging y documentación
- ✅ El flujo tiene branching: "si es tipo A, ve por aquí; si es tipo B, ve por allá"
- ✅ Necesitas control total sobre qué pasa en cada paso
Ejemplo: workflow con routing condicional
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
query_type: str
results: Annotated[list[str], operator.add]
final_answer: str
model = init_chat_model("openai:gpt-4.1-mini")
def classify(state: ResearchState) -> dict:
query = state["query"].lower()
if any(word in query for word in ["código", "programa", "función", "python"]):
return {"query_type": "code"}
elif any(word in query for word in ["explica", "qué es", "concepto"]):
return {"query_type": "concept"}
return {"query_type": "general"}
def search_code(state: ResearchState) -> dict:
return {"results": [f"[CODE] Ejemplo de código para: {state['query']}"]}
def search_concept(state: ResearchState) -> dict:
return {"results": [f"[CONCEPT] Explicación conceptual de: {state['query']}"]}
def search_general(state: ResearchState) -> dict:
return {"results": [f"[GENERAL] Información general sobre: {state['query']}"]}
def generate_answer(state: ResearchState) -> dict:
combined = "\n".join(state["results"])
response = model.invoke(
f"Basándote en estos resultados, responde: {state['query']}\n\nResultados:\n{combined}"
)
return {"final_answer": response.content}
def route_by_type(state: ResearchState) -> str:
return {
"code": "search_code",
"concept": "search_concept",
"general": "search_general",
}[state["query_type"]]
graph = StateGraph(ResearchState)
graph.add_node("classify", classify)
graph.add_node("search_code", search_code)
graph.add_node("search_concept", search_concept)
graph.add_node("search_general", search_general)
graph.add_node("generate_answer", generate_answer)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_by_type)
graph.add_edge("search_code", "generate_answer")
graph.add_edge("search_concept", "generate_answer")
graph.add_edge("search_general", "generate_answer")
graph.add_edge("generate_answer", END)
app = graph.compile()
result = app.invoke({"query": "Explica qué es un decorator en Python"})
print(f"Tipo: {result['query_type']}")
print(f"Respuesta: {result['final_answer'][:100]}...")
# Output esperado:
# Tipo: concept
# Respuesta: Un decorator en Python es una función que modifica el comportamiento de otra función...
~50 líneas de código. Pero tienes control total: clasificación explícita, routing condicional, nodos especializados, y el flujo es visible y debuggeable.
Side-by-side: el mismo problema, dos enfoques
Vamos a resolver exactamente el mismo problema con ambos enfoques para que veas la diferencia real. El problema: "un agente que busca información y genera un resumen estructurado."
Versión create_agent (~15 líneas)
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def web_search(query: str) -> str:
"""Busca información en la web."""
return f"Resultados de búsqueda para '{query}': Python es un lenguaje de programación de alto nivel creado en 1991. Es usado en AI, data science, y desarrollo web."
@tool
def format_summary(content: str) -> str:
"""Formatea contenido como resumen estructurado con bullet points."""
return f"## Resumen\n- Punto 1: {content[:50]}\n- Punto 2: Análisis completado\n- Punto 3: Conclusiones generadas"
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model,
tools=[web_search, format_summary],
prompt=(
"Eres un investigador experto. Cuando te pidan investigar algo: "
"1) Busca información con web_search, "
"2) Formatea el resultado con format_summary, "
"3) Entrega el resumen final al usuario."
),
)
result = agent.invoke(
{"messages": [("user", "Investiga sobre Python")]}
)
print(result["messages"][-1].content)
# Output esperado:
# ## Resumen
# - Punto 1: Python es un lenguaje de programación...
# - Punto 2: Análisis completado
# - Punto 3: Conclusiones generadas
# [Seguido del análisis del modelo]
Versión StateGraph (~55 líneas)
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
raw_results: str
summary: str
formatted_output: str
model = init_chat_model("openai:gpt-4.1-mini")
def search_node(state: ResearchState) -> dict:
query = state["query"]
results = (
f"Resultados de búsqueda para '{query}': Python es un lenguaje de "
f"programación de alto nivel creado en 1991. Es usado en AI, data "
f"science, y desarrollo web."
)
return {"raw_results": results}
def summarize_node(state: ResearchState) -> dict:
response = model.invoke(
f"Resume estos resultados en 3 puntos clave:\n\n{state['raw_results']}"
)
return {"summary": response.content}
def format_node(state: ResearchState) -> dict:
formatted = f"## Investigación: {state['query']}\n\n"
formatted += f"### Resultados\n{state['raw_results'][:100]}...\n\n"
formatted += f"### Resumen\n{state['summary']}\n"
return {"formatted_output": formatted}
graph = StateGraph(ResearchState)
graph.add_node("search", search_node)
graph.add_node("summarize", summarize_node)
graph.add_node("format", format_node)
graph.add_edge(START, "search")
graph.add_edge("search", "summarize")
graph.add_edge("summarize", "format")
graph.add_edge("format", END)
app = graph.compile()
result = app.invoke({"query": "Python"})
print(result["formatted_output"])
# Output esperado:
# ## Investigación: Python
#
# ### Resultados
# Resultados de búsqueda para 'Python': Python es un lenguaje de programación de alto nivel...
#
# ### Resumen
# 1. Python es un lenguaje de alto nivel creado en 1991
# 2. Se usa ampliamente en AI, data science y desarrollo web
# 3. Es uno de los lenguajes más populares del mundo
Análisis: mismo resultado, diferentes trade-offs
| Aspecto | create_agent | StateGraph |
|---|---|---|
| Líneas de código | ~15 | ~55 |
| Tiempo de desarrollo | 5 minutos | 20 minutos |
| Control del flujo | El modelo decide el orden | Tú defines el orden |
| Debugging | Opaco (¿por qué llamó esa tool?) | Transparente (cada nodo es visible) |
| Modificabilidad | Agregar middleware | Agregar/cambiar nodos y edges |
| Visualización | No | Sí |
Para este problema específico (búsqueda + resumen), create_agent es la mejor opción. El flujo es lineal, no hay branching, y no necesitas control granular. Usar StateGraph aquí es over-engineering.
¿Cuándo el StateGraph se justifica para este mismo problema? Cuando necesitas:
- Routing: "si es una pregunta técnica, busca en docs; si es general, busca en web"
- Validación: "si el resumen tiene menos de 100 palabras, re-genera"
- Human-in-the-loop: "muestra el resumen al usuario antes de formatear"
- Logging granular: "quiero saber cuánto tardó cada paso por separado"
El camino de progresión: de create_agent a StateGraph
En proyectos reales, la progresión natural es:
1. Empiezas con create_agent
→ Funciona para el 80% del problema
2. Agregas middleware para personalizar
→ Logging, routing de modelos, auth
3. Llegas a un límite del middleware
→ "Necesito que el flujo haga un branch aquí"
→ "Necesito pausar para aprobación humana"
→ "Necesito un loop de retry con lógica custom"
4. Migras a StateGraph
→ Más código, pero control total
Este camino es saludable. No necesitas empezar con StateGraph "por si acaso". Empieza simple, migra cuando el dolor lo justifique.
Señales de que necesitas migrar
- ❌ Tu system prompt se convierte en una lista de "si X entonces Y" para controlar el flujo
- ❌ Tus middleware se vuelven cada vez más complejos para simular branching
- ❌ Necesitas que el agente haga cosas diferentes según el tipo de input
- ❌ Quieres que un humano apruebe algo antes de que el agente continúe
- ❌ Necesitas visualizar el flujo para debugging o documentación
- ❌ El agente llama tools en un orden que no tiene sentido y no puedes controlarlo
Señales de que NO necesitas migrar
- ✅ El agente funciona bien con el loop ReAct estándar
- ✅ La personalización que necesitas se logra con middleware
- ✅ El flujo es lineal: pregunta → razonamiento → tools → respuesta
- ✅ No necesitas branching ni human-in-the-loop
- ✅ El system prompt controla bien el comportamiento
Enfoque híbrido: create_agent como nodo de un StateGraph
Un patrón avanzado es usar create_agent como un nodo dentro de un StateGraph. Esto te da lo mejor de ambos mundos: la simplicidad de create_agent para tareas individuales, y el control de StateGraph para orquestar el flujo entre tareas.
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END
@tool
def search(query: str) -> str:
"""Busca información sobre un tema."""
return f"Información encontrada sobre {query}: datos relevantes y actualizados."
@tool
def code_gen(description: str) -> str:
"""Genera código basado en una descripción."""
return f"```python\n# Código para: {description}\ndef solution():\n pass\n```"
model = init_chat_model("openai:gpt-4.1-mini")
research_agent = create_agent(
model, [search],
prompt="Eres un investigador. Busca información relevante sobre el tema dado.",
)
code_agent = create_agent(
model, [code_gen],
prompt="Eres un programador. Genera código basado en la información proporcionada.",
)
class PipelineState(TypedDict):
query: str
query_type: str
research_result: str
code_result: str
final_output: str
def classify_node(state: PipelineState) -> dict:
query = state["query"].lower()
if "código" in query or "programa" in query or "implementa" in query:
return {"query_type": "code"}
return {"query_type": "research"}
def research_node(state: PipelineState) -> dict:
result = research_agent.invoke(
{"messages": [("user", state["query"])]}
)
return {"research_result": result["messages"][-1].content}
def code_node(state: PipelineState) -> dict:
context = state.get("research_result", "")
prompt = f"Basándote en: {context}\n\nGenera código para: {state['query']}"
result = code_agent.invoke(
{"messages": [("user", prompt)]}
)
return {"code_result": result["messages"][-1].content}
def output_node(state: PipelineState) -> dict:
if state["query_type"] == "code":
return {"final_output": f"Investigación:\n{state['research_result']}\n\nCódigo:\n{state['code_result']}"}
return {"final_output": state["research_result"]}
def route_by_type(state: PipelineState) -> str:
if state["query_type"] == "code":
return "research"
return "research_only"
graph = StateGraph(PipelineState)
graph.add_node("classify", classify_node)
graph.add_node("research", research_node)
graph.add_node("code", code_node)
graph.add_node("output", output_node)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_by_type, {
"research": "research",
"research_only": "research",
})
graph.add_edge("research", "code")
graph.add_edge("code", "output")
graph.add_edge("output", END)
app = graph.compile()
result = app.invoke({"query": "Investiga sobre decorators en Python e implementa un ejemplo"})
print(result["final_output"][:200])
# Output esperado:
# Investigación:
# [Resultados de investigación sobre decorators...]
#
# Código:
# ```python
# # Código para: decorators en Python...
Cada nodo es un create_agent independiente con sus propias tools y prompt. StateGraph orquesta el flujo entre ellos. Este patrón es la base de los sistemas multi-agente que verás en módulos posteriores.
Error común #1: Over-engineering con StateGraph
El error más frecuente al aprender LangGraph es querer usar StateGraph para todo. Un agente simple de Q&A que podría ser 10 líneas con create_agent se convierte en 80 líneas con StateGraph, 5 nodos, 3 conditional edges, y un estado con 8 campos.
Síntomas de over-engineering
- ❌ Tu StateGraph tiene un solo camino lineal sin branching
- ❌ Todos tus nodos simplemente llaman al modelo con prompts diferentes
- ❌ No usas conditional edges — todos los edges son fijos
- ❌ El estado tiene campos que nunca usas
- ❌ La visualización del grafo es una línea recta de START a END
La regla de los conditional edges
Si tu grafo no tiene conditional edges, probablemente no necesitas StateGraph. Un grafo con edges puramente lineales (A → B → C → D → END) es equivalente a un chain — y create_agent hace eso mejor con menos código.
# ❌ Over-engineering: StateGraph para un flujo lineal
graph.add_edge(START, "search")
graph.add_edge("search", "analyze")
graph.add_edge("analyze", "format")
graph.add_edge("format", END)
# ✅ Mejor: create_agent con un prompt bien diseñado
agent = create_agent(
model, [search, analyze, format_tool],
prompt="Busca información, analízala, y formatea el resultado.",
)
Error común #2: Under-engineering con create_agent
El error opuesto: quedarte con create_agent cuando necesitas más control. Tu system prompt se llena de instrucciones condicionales, tus middleware hacen malabarismos para simular branching, y el agente a veces toma decisiones que no entiendes.
Síntomas de under-engineering
- ❌ Tu system prompt tiene párrafos de "si el usuario pide X, haz Y; si pide Z, haz W"
- ❌ Usas middleware
wrap_model_callpara redirigir la llamada según el contexto - ❌ El agente llama tools en un orden inesperado y no puedes controlarlo
- ❌ Necesitas logging granular por paso pero solo tienes before/after del modelo completo
- ❌ Quieres que el agente se detenga a mitad del proceso para pedir input del usuario
Cuándo el dolor justifica la migración
# ❌ Under-engineering: system prompt intentando simular routing
prompt = """Eres un asistente con múltiples capacidades.
REGLAS DE ROUTING:
1. Si el usuario pide código, PRIMERO busca documentación, LUEGO genera código
2. Si el usuario pide explicación, busca y resume SIN generar código
3. Si el usuario pide comparación, busca AMBOS temas y luego compara
4. Si el usuario pide debugging, analiza el código SIN buscar
5. NUNCA generes código sin antes buscar documentación
6. Si la búsqueda no retorna resultados, intenta con términos diferentes
7. Para comparaciones, asegúrate de buscar cada tema por separado
...
"""
# ✅ Mejor: StateGraph con routing explícito
def route(state):
if state["intent"] == "code":
return "search_then_code"
elif state["intent"] == "explain":
return "search_then_summarize"
elif state["intent"] == "compare":
return "search_both"
return "direct_response"
Cuando tu system prompt se convierte en un mini-lenguaje de programación, es hora de migrar a StateGraph donde el flujo es explícito y debuggeable.
Troubleshooting
1. Parálisis de decisión: "¿cuál uso?"
Causa: No tienes criterios claros para decidir. Cada nuevo proyecto se siente como una decisión existencial.
Solución: Empieza siempre con create_agent. Si en los primeros 30 minutos de desarrollo sientes que el system prompt está controlando el flujo en vez de el comportamiento, migra a StateGraph. La decisión no es permanente — es un punto de partida.
2. Over-engineering temprano
Causa: Anticipas necesidades futuras que quizás nunca lleguen. "Tal vez después necesite human-in-the-loop, así que mejor hago StateGraph desde el inicio."
Solución: YAGNI (You Aren't Gonna Need It). Construye para lo que necesitas hoy. Migrar de create_agent a StateGraph es mucho más fácil que mantener un StateGraph complejo que no necesitas.
3. Under-engineering tardío
Causa: Te quedas con create_agent demasiado tiempo porque "ya funciona", aunque el agente tiene comportamiento impredecible y el system prompt tiene 50 líneas de reglas.
Solución: Define un "pain threshold." Si tu system prompt supera las 10-15 líneas de reglas condicionales, o si necesitas más de 2 middleware complejos para controlar el flujo, es señal de migrar.
4. El agente create_agent llama tools en orden incorrecto
Causa: create_agent usa ReAct, donde el modelo decide el orden de tools. No puedes forzar un orden específico con system prompt de forma confiable.
Solución: Si el orden importa, usa StateGraph. Cada nodo se ejecuta en el orden que defines con edges. No hay ambigüedad.
5. StateGraph se siente verbose para mi caso
Causa: Estás usando StateGraph para un problema que create_agent resuelve mejor. Si tu grafo es lineal sin branching, estás escribiendo boilerplate innecesario.
Solución: Revisa la regla de los conditional edges. Si no necesitas routing condicional, vuelve a create_agent.
Ejercicios
Ejercicio 1: Identificar el enfoque correcto (Básico)
Lee cada escenario y decide si usarías create_agent o StateGraph. Justifica tu respuesta.
Escenario A: Un chatbot de soporte técnico que responde preguntas usando una base de conocimiento. Todas las preguntas siguen el mismo flujo: buscar → responder.
Escenario B: Un sistema que recibe emails, los clasifica (urgente, normal, spam), y rutea cada tipo a un procesador diferente.
Escenario C: Un agente que traduce texto entre idiomas usando una tool de traducción.
Ver solución
Escenario A: create_agent
El flujo es lineal (buscar → responder), no hay branching. Un system prompt + tool de búsqueda es todo lo que necesitas. StateGraph sería over-engineering.
Escenario B: StateGraph
Hay clasificación + routing condicional. Cada tipo de email necesita un procesador diferente. Esto es exactamente lo que conditional edges resuelven. Con create_agent, tendrías que meter toda la lógica de routing en el system prompt.
Escenario C: create_agent
Propósito único, flujo lineal. El agente recibe texto, llama a la tool de traducción, retorna el resultado. No hay branching ni decisiones complejas.
Ejercicio 2: Migrar de create_agent a StateGraph (Básico)
Tienes este agente con create_agent. Conviértelo a StateGraph manteniendo el mismo comportamiento.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtiene el clima de una ciudad."""
weathers = {"madrid": "22°C, soleado", "london": "15°C, nublado", "tokyo": "28°C, húmedo"}
return weathers.get(city.lower(), f"No hay datos para {city}")
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, [get_weather], prompt="Reporta el clima cuando te pregunten.")
Ver solución
from dotenv import load_dotenv
load_dotenv()
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class WeatherState(TypedDict):
city: str
weather_data: str
response: str
model = init_chat_model("openai:gpt-4.1-mini")
WEATHERS = {"madrid": "22°C, soleado", "london": "15°C, nublado", "tokyo": "28°C, húmedo"}
def fetch_weather(state: WeatherState) -> dict:
city = state["city"]
data = WEATHERS.get(city.lower(), f"No hay datos para {city}")
return {"weather_data": data}
def generate_report(state: WeatherState) -> dict:
response = model.invoke(
f"Genera un reporte del clima para {state['city']}: {state['weather_data']}"
)
return {"response": response.content}
graph = StateGraph(WeatherState)
graph.add_node("fetch", fetch_weather)
graph.add_node("report", generate_report)
graph.add_edge(START, "fetch")
graph.add_edge("fetch", "report")
graph.add_edge("report", END)
app = graph.compile()
result = app.invoke({"city": "Madrid"})
print(result["response"])
# Output esperado:
# El clima en Madrid es de 22°C con cielo soleado...
Nota: en este caso, la versión StateGraph es más código para el mismo resultado. Esto confirma que create_agent era la herramienta correcta para el problema original.
Ejercicio 3: Diseñar un flujo con conditional edges (Intermedio)
Diseña (sin codificar completamente) un StateGraph para un sistema de soporte que:
- Clasifica el ticket (billing, technical, general)
- Rutea a un nodo especializado según la clasificación
- El nodo de billing consulta una API de pagos
- El nodo técnico busca en la base de conocimiento
- El nodo general responde directamente
Define: el estado (TypedDict), los nodos (nombres y qué hacen), y los edges (incluyendo la función de routing).
Ver solución
from typing import TypedDict
class SupportState(TypedDict):
ticket_text: str
category: str # "billing", "technical", "general"
context_data: str
response: str
# Nodos:
# 1. classify_ticket → Analiza ticket_text, asigna category
# 2. handle_billing → Consulta API de pagos, genera respuesta
# 3. handle_technical → Busca en knowledge base, genera respuesta
# 4. handle_general → Genera respuesta directa con el modelo
# 5. format_response → Formatea la respuesta final
# Edges:
# START → classify_ticket
# classify_ticket → (conditional) → handle_billing | handle_technical | handle_general
# handle_billing → format_response
# handle_technical → format_response
# handle_general → format_response
# format_response → END
# Función de routing:
def route_ticket(state: SupportState) -> str:
return {
"billing": "handle_billing",
"technical": "handle_technical",
"general": "handle_general",
}.get(state["category"], "handle_general")
# Este diseño justifica StateGraph porque:
# ✅ Tiene routing condicional (3 caminos diferentes)
# ✅ Cada nodo es especializado (diferente lógica y tools)
# ✅ El flujo es visible y debuggeable
# ✅ Se puede agregar nodos fácilmente (e.g., escalation)
Ejercicio 4: Detectar over-engineering (Intermedio)
Este StateGraph es over-engineering. Identifica por qué y reescríbelo como create_agent.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class QAState(TypedDict):
question: str
search_result: str
answer: str
def search_node(state):
return {"search_result": f"Info sobre: {state['question']}"}
def answer_node(state):
return {"answer": f"Respuesta basada en: {state['search_result']}"}
graph = StateGraph(QAState)
graph.add_node("search", search_node)
graph.add_node("answer", answer_node)
graph.add_edge(START, "search")
graph.add_edge("search", "answer")
graph.add_edge("answer", END)
Ver solución
Por qué es over-engineering:
- ❌ Flujo puramente lineal:
START → search → answer → END - ❌ No hay conditional edges — no hay branching
- ❌ Solo dos nodos que podrían ser tools
- ❌ El estado tiene campos simples sin reducers complejos
Versión con create_agent:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(question: str) -> str:
"""Busca información para responder una pregunta."""
return f"Info sobre: {question}"
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model, [search],
prompt="Busca información y responde la pregunta del usuario.",
)
result = agent.invoke({"messages": [("user", "¿Qué es Python?")]})
print(result["messages"][-1].content)
# Output esperado:
# Python es un lenguaje de programación...
Misma funcionalidad, un tercio del código, sin boilerplate innecesario.
Ejercicio 5: Agregar routing a un agente existente (Avanzado)
Tienes un create_agent que responde preguntas. Pero ahora necesitas que:
- Las preguntas sobre código vayan por un flujo que incluye búsqueda en documentación + generación de código
- Las preguntas generales sigan el flujo normal de búsqueda + respuesta
Implementa esto como un StateGraph con conditional edges.
Ver solución
from dotenv import load_dotenv
load_dotenv()
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class QAState(TypedDict):
question: str
intent: str
search_result: str
code_output: str
final_answer: str
model = init_chat_model("openai:gpt-4.1-mini")
CODE_KEYWORDS = ["código", "programa", "implementa", "función", "clase", "script"]
def classify_intent(state: QAState) -> dict:
question_lower = state["question"].lower()
if any(kw in question_lower for kw in CODE_KEYWORDS):
return {"intent": "code"}
return {"intent": "general"}
def search_docs(state: QAState) -> dict:
return {"search_result": f"Documentación encontrada para: {state['question']}"}
def search_general(state: QAState) -> dict:
return {"search_result": f"Información general sobre: {state['question']}"}
def generate_code(state: QAState) -> dict:
response = model.invoke(
f"Genera código Python para: {state['question']}\n"
f"Basándote en: {state['search_result']}"
)
return {"code_output": response.content}
def generate_answer(state: QAState) -> dict:
if state.get("code_output"):
answer = f"Documentación: {state['search_result']}\n\nCódigo:\n{state['code_output']}"
else:
response = model.invoke(
f"Responde esta pregunta: {state['question']}\n"
f"Información: {state['search_result']}"
)
answer = response.content
return {"final_answer": answer}
def route_intent(state: QAState) -> str:
return "search_docs" if state["intent"] == "code" else "search_general"
graph = StateGraph(QAState)
graph.add_node("classify", classify_intent)
graph.add_node("search_docs", search_docs)
graph.add_node("search_general", search_general)
graph.add_node("generate_code", generate_code)
graph.add_node("generate_answer", generate_answer)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_intent)
graph.add_edge("search_docs", "generate_code")
graph.add_edge("generate_code", "generate_answer")
graph.add_edge("search_general", "generate_answer")
graph.add_edge("generate_answer", END)
app = graph.compile()
result_code = app.invoke({"question": "Implementa una función para ordenar una lista"})
print(f"Intent: {result_code['intent']}")
print(f"Respuesta: {result_code['final_answer'][:100]}...")
# Output esperado:
# Intent: code
# Respuesta: Documentación: Documentación encontrada para: Implementa una función...
result_general = app.invoke({"question": "¿Qué es machine learning?"})
print(f"\nIntent: {result_general['intent']}")
print(f"Respuesta: {result_general['final_answer'][:100]}...")
# Output esperado:
# Intent: general
# Respuesta: Machine learning es una rama de la inteligencia artificial...
Ejercicio 6: Decision framework completo (Challenge)
Crea una función recommend_approach(requirements: dict) -> str que reciba un diccionario con estas claves booleanas y retorne "create_agent", "stategraph", o "hybrid":
needs_branching: ¿El flujo tiene caminos diferentes según el input?needs_hitl: ¿Necesitas pausar para aprobación humana?needs_visualization: ¿Necesitas visualizar el grafo?needs_custom_loops: ¿Necesitas loops con lógica custom (retry, convergence)?single_purpose: ¿El agente tiene un solo propósito?rapid_prototype: ¿Necesitas un prototipo rápido?
Implementa la lógica de decisión y prueba con al menos 4 escenarios diferentes.
Ver solución
def recommend_approach(requirements: dict) -> str:
"""Recomienda create_agent, stategraph, o hybrid según los requisitos."""
needs_branching = requirements.get("needs_branching", False)
needs_hitl = requirements.get("needs_hitl", False)
needs_visualization = requirements.get("needs_visualization", False)
needs_custom_loops = requirements.get("needs_custom_loops", False)
single_purpose = requirements.get("single_purpose", True)
rapid_prototype = requirements.get("rapid_prototype", False)
stategraph_signals = sum([
needs_branching,
needs_hitl,
needs_custom_loops,
])
if stategraph_signals == 0 and single_purpose:
return "create_agent"
if stategraph_signals >= 2:
return "stategraph"
if needs_branching and not needs_hitl and not needs_custom_loops:
return "hybrid"
if rapid_prototype and stategraph_signals <= 1:
return "create_agent"
if needs_hitl or needs_custom_loops:
return "stategraph"
return "hybrid"
# Test 1: Chatbot simple
r1 = recommend_approach({
"needs_branching": False,
"needs_hitl": False,
"needs_visualization": False,
"needs_custom_loops": False,
"single_purpose": True,
"rapid_prototype": True,
})
print(f"Test 1 (chatbot simple): {r1}")
# Output esperado: create_agent
# Test 2: Sistema de soporte con routing
r2 = recommend_approach({
"needs_branching": True,
"needs_hitl": True,
"needs_visualization": True,
"needs_custom_loops": False,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 2 (soporte con routing + HITL): {r2}")
# Output esperado: stategraph
# Test 3: Pipeline con branching simple
r3 = recommend_approach({
"needs_branching": True,
"needs_hitl": False,
"needs_visualization": True,
"needs_custom_loops": False,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 3 (pipeline con branching): {r3}")
# Output esperado: hybrid
# Test 4: Agente con retry loops
r4 = recommend_approach({
"needs_branching": False,
"needs_hitl": False,
"needs_visualization": False,
"needs_custom_loops": True,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 4 (retry loops): {r4}")
# Output esperado: stategraph
Resumen
En esta cápsula aprendiste:
create_agentcubre el 80% de los casos — es rápido, simple, y tiene middleware para personalización. Úsalo por defectoStateGraphes para el 20% restante — cuando necesitas branching, human-in-the-loop, loops custom, o visualización del flujo- Ni uno es "mejor" que el otro — son herramientas diferentes. La pregunta correcta no es "¿cuál es mejor?" sino "¿cuál necesito para este problema?"
- La progresión natural es empezar con
create_agenty migrar a StateGraph cuando el dolor lo justifique - El enfoque híbrido (
create_agentcomo nodo de un StateGraph) te da lo mejor de ambos mundos para sistemas multi-agente - Over-engineering (StateGraph para todo) desperdicia tiempo y complejiza el mantenimiento
- Under-engineering (
create_agentcon system prompt de 50 líneas intentando simular routing) produce agentes impredecibles - La regla de los conditional edges: si tu grafo no tiene conditional edges, probablemente no necesitas StateGraph
Próxima cápsula: Proyecto — construirás un chatbot con estado y routing condicional usando StateGraph. Es el último mini-proyecto independiente antes de que el proyecto evolutivo comience en el Módulo 6.
Recursos adicionales
- LangGraph Concepts: Why LangGraph? — Motivaciones del framework y cuándo es apropiado
- create_agent API Reference — Referencia completa del agente prebuilt
- LangGraph StateGraph Tutorial — Tutorial oficial de StateGraph
- LangGraph Agents Conceptual Guide — Cómo create_agent es un StateGraph internamente
- LangChain Agents Middleware — Sistema de middleware para personalizar create_agent
- YAGNI Principle (Martin Fowler) — El principio de diseño detrás de "empieza simple, migra cuando duela"
Módulo 5 — LangChain & LangGraph: From Chains to Agents