Módulo 5: Introducción a LangGraph

Proyecto: Chatbot con Estado y Routing Condicional

Descripción del proyecto

En las siete cápsulas anteriores aprendiste a construir workflows como grafos con LangGraph: StateGraph para definir la estructura, nodos como funciones que transforman estado, edges y conditional edges para controlar el flujo, estado tipado con TypedDict y Annotated con reducers, compilación y ejecución, y la decisión fundamental de cuándo usar create_agent vs StateGraph. Cada concepto lo viste de forma individual con ejemplos aislados. Ahora vas a combinar todo en un sistema real.

Este proyecto es un chatbot que no trata todas las preguntas igual. Cuando un usuario escribe "¿Qué es machine learning?", el chatbot detecta que es una pregunta de Q&A y la rutea a un nodo especializado en dar explicaciones claras. Cuando escribe "Escríbeme un poema sobre la lluvia", detecta intención creativa y la envía a un nodo optimizado para escritura creativa. Cuando escribe "Cómo hago un for loop en Python", la envía al nodo de código. Cada nodo tiene su propio prompt y estilo de respuesta. El routing es automático, basado en conditional edges que evalúan la clasificación del intent.

Este es tu último mini-proyecto independiente. A partir del Módulo 6, todo lo que construyas será parte del proyecto evolutivo "AI Research Assistant" — un sistema que crece módulo a módulo desde un agente simple hasta un sistema multi-agente production-ready. Lo que construyes hoy es la base conceptual de esa evolución: entender cómo diseñar grafos con nodos especializados, routing condicional, y estado tipado es exactamente lo que necesitas para construir sistemas más complejos.

El resultado final es un chatbot interactivo en terminal con visualización del grafo, clasificación de intención visible en cada turno, y respuestas especializadas según el dominio.


Objetivo del proyecto

Construir un chatbot con LangGraph que clasifica la intención del usuario y rutea a nodos especializados usando conditional edges, con estado tipado y visualización del grafo.

Al completar este proyecto:

  • 🔧 Sabrás diseñar un estado tipado con TypedDict para un chatbot multi-dominio
  • 🔧 Implementarás un nodo clasificador que usa el LLM para detectar intención
  • 🔧 Crearás nodos especializados con prompts optimizados por dominio
  • 🔧 Configurarás conditional edges para routing dinámico basado en clasificación
  • 🔧 Visualizarás el grafo completo con draw_mermaid_png
  • 🔧 Construirás un loop de conversación multi-turno con estado persistente

Especificaciones técnicas

Stack tecnológico

ComponenteVersiónPropósito
Python3.11+Runtime
LangChainv1.2+Framework de LLMs
LangGraphv1.0+StateGraph, nodos, edges
langchain-openailatestProveedor de modelos
python-dotenvlatestVariables de entorno

Setup inicial

pip install langchain langgraph langchain-openai python-dotenv

Crea un archivo .env en la raíz de tu proyecto:

# .env
OPENAI_API_KEY=sk-...

Estructura del proyecto

routing-chatbot/
├── .env                    # API key
├── routing_chatbot.py      # Código principal (todo en un archivo)
└── requirements.txt        # Dependencias
# requirements.txt
langchain>=0.3.0
langgraph>=0.3.0
langchain-openai>=0.3.0
python-dotenv>=1.0.0

Paso 1: Diseñar el estado

El estado es el corazón del grafo. Define qué información fluye entre nodos. Para un chatbot con routing, necesitas: los mensajes de conversación, la intención clasificada, y un contador para tracking.

import operator
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class ChatState(TypedDict):
    messages: Annotated[list, add_messages]
    intent: str
    response_count: int

Tres campos, cada uno con un propósito específico:

  • messages: la conversación completa. Usa add_messages como reducer para que cada nodo agregue mensajes sin sobrescribir los anteriores. Este es el patrón estándar para chatbots en LangGraph
  • intent: la clasificación de la última pregunta ("qa", "creative", "code"). Lo asigna el nodo clasificador y lo leen los conditional edges para decidir el routing
  • response_count: cuántas respuestas ha generado el chatbot en la sesión. Útil para tracking y para limitar conversaciones

Paso 2: Crear el nodo clasificador

El clasificador es el primer nodo que procesa cada mensaje del usuario. Usa el LLM para analizar la intención y asignar una categoría. La clasificación determina a qué nodo especializado se envía el mensaje.

from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4.1-mini", temperature=0)

def classifier_node(state: ChatState) -> dict:
    """Clasifica la intención del último mensaje del usuario."""
    last_message = state["messages"][-1]

    classification_prompt = f"""Clasifica la intención del siguiente mensaje en exactamente UNA de estas categorías:

- qa: Preguntas factuales, explicaciones, definiciones, "qué es", "cómo funciona", consultas informativas
- creative: Escritura creativa, poemas, historias, canciones, contenido artístico, "escríbeme", "inventa"
- code: Preguntas sobre programación, código, debugging, implementación, "cómo hago", "código para"

Mensaje del usuario: {last_message.content}

Responde SOLO con la categoría (qa, creative, o code). Nada más."""

    response = model.invoke(classification_prompt)
    intent = response.content.strip().lower()

    if intent not in ("qa", "creative", "code"):
        intent = "qa"

    print(f"  🏷️  Intent clasificado: {intent}")
    return {"intent": intent}

El fallback a "qa" es importante: si el modelo retorna algo inesperado (puede pasar con modelos más pequeños), el sistema no falla — simplemente trata la pregunta como Q&A general.


Paso 3: Crear los nodos especializados

Cada nodo tiene un prompt diseñado para su dominio. No es el mismo modelo respondiendo igual a todo — cada nodo genera respuestas con estilo y profundidad apropiados para su tipo de contenido.

Nodo Q&A

def qa_node(state: ChatState) -> dict:
    """Genera respuestas informativas y educativas."""
    last_message = state["messages"][-1]

    qa_prompt = f"""Eres un experto en explicar conceptos de forma clara y precisa.

Instrucciones:
- Responde de forma informativa y educativa
- Usa ejemplos concretos cuando sea útil
- Estructura tu respuesta con claridad
- Si no sabes algo, dilo honestamente

Pregunta del usuario: {last_message.content}"""

    response = model.invoke(qa_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  📚 Q&A respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }

Nodo Creativo

def creative_node(state: ChatState) -> dict:
    """Genera contenido creativo: poemas, historias, canciones."""
    last_message = state["messages"][-1]

    creative_prompt = f"""Eres un escritor creativo talentoso con un estilo expresivo y evocador.

Instrucciones:
- Genera contenido original y artístico
- Usa lenguaje rico, metáforas y ritmo cuando sea apropiado
- Adapta el formato al tipo de contenido (poema, historia, canción, etc.)
- Sé creativo pero coherente

Solicitud del usuario: {last_message.content}"""

    response = model.invoke(creative_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  🎨 Creative respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }

Nodo Code

def code_node(state: ChatState) -> dict:
    """Genera código y explicaciones técnicas de programación."""
    last_message = state["messages"][-1]

    code_prompt = f"""Eres un programador senior experto en múltiples lenguajes.

Instrucciones:
- Genera código limpio, funcional y bien comentado
- Incluye imports necesarios
- Explica brevemente qué hace el código
- Si hay múltiples formas de resolver el problema, muestra la más pythónica/idiomática
- Incluye un ejemplo de uso cuando sea relevante

Solicitud del usuario: {last_message.content}"""

    response = model.invoke(code_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  💻 Code respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }

Cada nodo retorna un diccionario con messages (la respuesta como mensaje del asistente, que se agrega a la conversación gracias al reducer add_messages) y response_count (el contador incrementado).


Paso 4: Crear la función de routing y conditional edges

La función de routing lee el intent del estado y retorna el nombre del nodo al que debe ir la ejecución. Los conditional edges usan esta función para decidir el camino.

def route_by_intent(state: ChatState) -> str:
    """Rutea al nodo especializado según la intención clasificada."""
    intent = state.get("intent", "qa")
    route_map = {
        "qa": "qa_node",
        "creative": "creative_node",
        "code": "code_node",
    }
    destination = route_map.get(intent, "qa_node")
    print(f"  🔀 Routing → {destination}")
    return destination

Esta función es lo que hace que el grafo no sea lineal. En vez de classifier → qa_node → END, tienes classifier → (qa_node | creative_node | code_node) → END. El conditional edge ejecuta route_by_intent en runtime y decide el camino.


Paso 5: Compilar, visualizar y ejecutar

Ahora ensamblamos todo en un StateGraph, lo compilamos, y lo visualizamos.

from langgraph.graph import StateGraph, START, END

graph = StateGraph(ChatState)

graph.add_node("classifier", classifier_node)
graph.add_node("qa_node", qa_node)
graph.add_node("creative_node", creative_node)
graph.add_node("code_node", code_node)

graph.add_edge(START, "classifier")
graph.add_conditional_edges("classifier", route_by_intent)
graph.add_edge("qa_node", END)
graph.add_edge("creative_node", END)
graph.add_edge("code_node", END)

app = graph.compile()

Visualizar el grafo

from IPython.display import Image, display

img_data = app.get_graph().draw_mermaid_png()
with open("routing_chatbot_graph.png", "wb") as f:
    f.write(img_data)
print("Grafo guardado en routing_chatbot_graph.png")

El grafo visualizado muestra:

        ┌──────────┐
        │  START    │
        └────┬─────┘
             │
        ┌────▼─────┐
        │classifier │
        └────┬─────┘
             │ (conditional)
    ┌────────┼────────┐
    │        │        │
┌───▼──┐ ┌──▼───┐ ┌──▼──┐
│qa_node│ │creative│ │code │
│       │ │_node  │ │_node│
└───┬──┘ └──┬───┘ └──┬──┘
    │        │        │
    └────────┼────────┘
             │
        ┌────▼─────┐
        │   END     │
        └──────────┘

Paso 6: Agregar conversación multi-turno

Para convertir esto en un chatbot real, agregamos un loop interactivo que mantiene el estado entre turnos. Cada pregunta del usuario pasa por el classifier y se rutea al nodo correcto, pero los mensajes anteriores se preservan en el estado.

from langchain_core.messages import HumanMessage

def chat_loop():
    """Loop interactivo de conversación multi-turno."""
    print("=" * 60)
    print("  🤖 Chatbot con Routing Condicional")
    print("  Haz preguntas de Q&A, creatividad, o código.")
    print("  El chatbot detecta la intención y responde.")
    print("  Comandos: 'stats' (métricas), 'salir' (terminar)")
    print("=" * 60)

    conversation_state = {
        "messages": [],
        "intent": "",
        "response_count": 0,
    }

    while True:
        try:
            user_input = input("\n💬 Tú: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\n\n¡Hasta luego!")
            break

        if not user_input:
            continue

        if user_input.lower() in ("salir", "exit", "quit"):
            print(f"\n📊 Sesión finalizada:")
            print(f"   Respuestas generadas: {conversation_state['response_count']}")
            print(f"   Mensajes en conversación: {len(conversation_state['messages'])}")
            print("\n¡Hasta luego!")
            break

        if user_input.lower() == "stats":
            msg_count = len(conversation_state["messages"])
            resp_count = conversation_state["response_count"]
            last_intent = conversation_state.get("intent", "ninguno")
            print(f"\n📊 Estado actual:")
            print(f"   Mensajes: {msg_count}")
            print(f"   Respuestas: {resp_count}")
            print(f"   Último intent: {last_intent}")
            continue

        print(f"\n{'─' * 60}")

        conversation_state["messages"].append(
            HumanMessage(content=user_input)
        )

        try:
            result = app.invoke(conversation_state)
            conversation_state = result

            assistant_message = result["messages"][-1]
            print(f"{'─' * 60}")
            print(f"\n🤖 Chatbot [{result['intent'].upper()}]:\n")
            print(assistant_message.content)
            print(f"\n{'─' * 60}")

        except Exception as e:
            print(f"\n❌ Error: {e}")
            print("   Intenta con otra pregunta.")

El estado conversation_state se mantiene entre turnos. Cada vez que el usuario escribe algo, agregamos su mensaje a la lista de mensajes, invocamos el grafo, y actualizamos el estado con el resultado. El historial completo de conversación está disponible para los nodos especializados.


Código completo

Este es el archivo routing_chatbot.py completo. Cópialo y ejecútalo directamente.

"""
Chatbot con Estado y Routing Condicional
Módulo 5 — LangChain & LangGraph: From Chains to Agents

Clasifica intención del usuario y rutea a nodos especializados.
"""

from dotenv import load_dotenv
load_dotenv()

import operator
from typing import TypedDict, Annotated

from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages


# =============================================================================
# STATE
# =============================================================================

class ChatState(TypedDict):
    messages: Annotated[list, add_messages]
    intent: str
    response_count: int


# =============================================================================
# MODEL
# =============================================================================

model = init_chat_model("openai:gpt-4.1-mini", temperature=0)


# =============================================================================
# NODES
# =============================================================================

def classifier_node(state: ChatState) -> dict:
    """Clasifica la intención del último mensaje del usuario."""
    last_message = state["messages"][-1]

    classification_prompt = f"""Clasifica la intención del siguiente mensaje en exactamente UNA de estas categorías:

- qa: Preguntas factuales, explicaciones, definiciones, "qué es", "cómo funciona", consultas informativas
- creative: Escritura creativa, poemas, historias, canciones, contenido artístico, "escríbeme", "inventa"
- code: Preguntas sobre programación, código, debugging, implementación, "cómo hago", "código para"

Mensaje del usuario: {last_message.content}

Responde SOLO con la categoría (qa, creative, o code). Nada más."""

    response = model.invoke(classification_prompt)
    intent = response.content.strip().lower()

    if intent not in ("qa", "creative", "code"):
        intent = "qa"

    print(f"  🏷️  Intent clasificado: {intent}")
    return {"intent": intent}


def qa_node(state: ChatState) -> dict:
    """Genera respuestas informativas y educativas."""
    last_message = state["messages"][-1]

    qa_prompt = f"""Eres un experto en explicar conceptos de forma clara y precisa.

Instrucciones:
- Responde de forma informativa y educativa
- Usa ejemplos concretos cuando sea útil
- Estructura tu respuesta con claridad
- Si no sabes algo, dilo honestamente

Pregunta del usuario: {last_message.content}"""

    response = model.invoke(qa_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  📚 Q&A respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }


def creative_node(state: ChatState) -> dict:
    """Genera contenido creativo: poemas, historias, canciones."""
    last_message = state["messages"][-1]

    creative_prompt = f"""Eres un escritor creativo talentoso con un estilo expresivo y evocador.

Instrucciones:
- Genera contenido original y artístico
- Usa lenguaje rico, metáforas y ritmo cuando sea apropiado
- Adapta el formato al tipo de contenido (poema, historia, canción, etc.)
- Sé creativo pero coherente

Solicitud del usuario: {last_message.content}"""

    response = model.invoke(creative_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  🎨 Creative respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }


def code_node(state: ChatState) -> dict:
    """Genera código y explicaciones técnicas de programación."""
    last_message = state["messages"][-1]

    code_prompt = f"""Eres un programador senior experto en múltiples lenguajes.

Instrucciones:
- Genera código limpio, funcional y bien comentado
- Incluye imports necesarios
- Explica brevemente qué hace el código
- Si hay múltiples formas de resolver el problema, muestra la más pythónica/idiomática
- Incluye un ejemplo de uso cuando sea relevante

Solicitud del usuario: {last_message.content}"""

    response = model.invoke(code_prompt)
    count = state.get("response_count", 0) + 1
    print(f"  💻 Code respondió ({count} respuestas totales)")

    return {
        "messages": [("assistant", response.content)],
        "response_count": count,
    }


# =============================================================================
# ROUTING
# =============================================================================

def route_by_intent(state: ChatState) -> str:
    """Rutea al nodo especializado según la intención clasificada."""
    intent = state.get("intent", "qa")
    route_map = {
        "qa": "qa_node",
        "creative": "creative_node",
        "code": "code_node",
    }
    destination = route_map.get(intent, "qa_node")
    print(f"  🔀 Routing → {destination}")
    return destination


# =============================================================================
# GRAPH ASSEMBLY
# =============================================================================

graph = StateGraph(ChatState)

graph.add_node("classifier", classifier_node)
graph.add_node("qa_node", qa_node)
graph.add_node("creative_node", creative_node)
graph.add_node("code_node", code_node)

graph.add_edge(START, "classifier")
graph.add_conditional_edges("classifier", route_by_intent)
graph.add_edge("qa_node", END)
graph.add_edge("creative_node", END)
graph.add_edge("code_node", END)

app = graph.compile()


# =============================================================================
# GRAPH VISUALIZATION
# =============================================================================

def save_graph_image():
    """Guarda la visualización del grafo como PNG."""
    try:
        img_data = app.get_graph().draw_mermaid_png()
        with open("routing_chatbot_graph.png", "wb") as f:
            f.write(img_data)
        print("📊 Grafo guardado en routing_chatbot_graph.png")
    except Exception as e:
        print(f"⚠️  No se pudo generar la imagen del grafo: {e}")
        print("   Puedes ver el grafo en formato Mermaid:")
        print(app.get_graph().draw_mermaid())


# =============================================================================
# INTERACTIVE CHAT LOOP
# =============================================================================

def chat_loop():
    """Loop interactivo de conversación multi-turno."""
    print("=" * 60)
    print("  🤖 Chatbot con Routing Condicional")
    print("  Haz preguntas de Q&A, creatividad, o código.")
    print("  El chatbot detecta la intención y responde.")
    print("  Comandos: 'stats' | 'graph' | 'salir'")
    print("=" * 60)

    conversation_state = {
        "messages": [],
        "intent": "",
        "response_count": 0,
    }

    while True:
        try:
            user_input = input("\n💬 Tú: ").strip()
        except (KeyboardInterrupt, EOFError):
            print("\n\n¡Hasta luego!")
            break

        if not user_input:
            continue

        if user_input.lower() in ("salir", "exit", "quit"):
            resp_count = conversation_state["response_count"]
            msg_count = len(conversation_state["messages"])
            print(f"\n📊 Sesión finalizada:")
            print(f"   Respuestas generadas: {resp_count}")
            print(f"   Mensajes en conversación: {msg_count}")
            print("\n¡Hasta luego!")
            break

        if user_input.lower() == "graph":
            save_graph_image()
            continue

        if user_input.lower() == "stats":
            msg_count = len(conversation_state["messages"])
            resp_count = conversation_state["response_count"]
            last_intent = conversation_state.get("intent", "ninguno")
            print(f"\n📊 Estado actual:")
            print(f"   Mensajes: {msg_count}")
            print(f"   Respuestas: {resp_count}")
            print(f"   Último intent: {last_intent}")
            continue

        print(f"\n{'─' * 60}")

        conversation_state["messages"].append(
            HumanMessage(content=user_input)
        )

        try:
            result = app.invoke(conversation_state)
            conversation_state = result

            assistant_message = result["messages"][-1]
            print(f"{'─' * 60}")
            print(f"\n🤖 Chatbot [{result['intent'].upper()}]:\n")
            print(assistant_message.content)
            print(f"\n{'─' * 60}")

        except Exception as e:
            print(f"\n❌ Error: {e}")
            print("   Intenta con otra pregunta.")


if __name__ == "__main__":
    chat_loop()

Ejecútalo:

python routing_chatbot.py

Criterios de éxito

Tu proyecto está completo cuando cumples los cuatro criterios:

  • Clasificación rutea correctamente a nodo especializado — preguntas de Q&A van a qa_node, creativas a creative_node, código a code_node
  • Cada nodo genera respuesta apropiada para su dominio — Q&A responde informativo, creativo responde artístico, código responde con código funcional
  • Estado se mantiene entre turns de conversación — puedes hacer varias preguntas y el contador de respuestas se incrementa, los mensajes se acumulan
  • Grafo se visualiza correctamentedraw_mermaid_png genera una imagen con classifier, 3 nodos especializados, y conditional edges

Escenarios de prueba

Test 1: Pregunta Q&A (debe ir a qa_node)

💬 Tú: ¿Qué es la fotosíntesis?

──────────────────────────────────────────────────────────────
  🏷️  Intent clasificado: qa
  🔀 Routing → qa_node
  📚 Q&A respondió (1 respuestas totales)
──────────────────────────────────────────────────────────────

🤖 Chatbot [QA]:

La fotosíntesis es el proceso bioquímico mediante el cual las plantas,
algas y algunas bacterias convierten la energía luminosa del sol en
energía química...

Test 2: Solicitud creativa (debe ir a creative_node)

💬 Tú: Escríbeme un haiku sobre la programación

──────────────────────────────────────────────────────────────
  🏷️  Intent clasificado: creative
  🔀 Routing → creative_node
  🎨 Creative respondió (2 respuestas totales)
──────────────────────────────────────────────────────────────

🤖 Chatbot [CREATIVE]:

Líneas de código,
el cursor parpadea, piensa...
bug al compilar.

Test 3: Pregunta de código (debe ir a code_node)

💬 Tú: Cómo hago una función recursiva de fibonacci en Python

──────────────────────────────────────────────────────────────
  🏷️  Intent clasificado: code
  🔀 Routing → code_node
  💻 Code respondió (3 respuestas totales)
──────────────────────────────────────────────────────────────

🤖 Chatbot [CODE]:

Aquí tienes una implementación recursiva de Fibonacci:

```python
def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Ejemplo de uso
for i in range(10):
    print(f"F({i}) = {fibonacci(i)}")
```​

Para mejorar la performance, puedes usar memoización...

Test 4: Verificar estado persistente

💬 Tú: stats

📊 Estado actual:
   Mensajes: 6
   Respuestas: 3
   Último intent: code

El estado muestra 6 mensajes (3 del usuario + 3 del asistente) y 3 respuestas generadas, confirmando que el estado persiste entre turnos.


Errores comunes

1. ModuleNotFoundError: No module named 'langgraph'

Causa: No instalaste las dependencias.

pip install langchain langgraph langchain-openai python-dotenv

2. El clasificador siempre retorna "qa"

Causa: El prompt de clasificación no es lo suficientemente específico, o el modelo retorna la categoría con texto extra (ej: "La categoría es: qa") y el strip no lo limpia.

Solución: El fallback a "qa" está diseñado para seguridad, pero si ocurre demasiado, verifica que el modelo retorne solo la palabra. Puedes agregar validación más robusta:

intent = response.content.strip().lower()
for valid in ("qa", "creative", "code"):
    if valid in intent:
        intent = valid
        break
else:
    intent = "qa"

3. KeyError: 'messages' al invocar el grafo

Causa: El estado inicial no incluye la clave messages o está mal formada.

Solución: Asegúrate de que el estado inicial siempre tenga las tres claves:

conversation_state = {
    "messages": [],      # lista vacía, no None
    "intent": "",        # string vacío, no None
    "response_count": 0, # 0, no None
}

4. Los mensajes se duplican en cada turno

Causa: Estás pasando los mismos mensajes al grafo sin actualizar el estado con el resultado. El reducer add_messages agrega, no reemplaza.

Solución: Después de cada app.invoke(), actualiza conversation_state con el resultado completo:

result = app.invoke(conversation_state)
conversation_state = result  # ← reemplaza todo el estado

5. draw_mermaid_png falla con error de renderizado

Causa: La generación de PNG requiere acceso a la API de Mermaid (internet) o a la librería pyppeteer local.

Solución: Si la imagen no se genera, usa el formato texto:

print(app.get_graph().draw_mermaid())

Esto imprime el diagrama en formato Mermaid que puedes copiar y pegar en mermaid.live para visualizarlo.

6. El nodo creativo responde de forma genérica, no artística

Causa: El prompt del nodo creativo no es lo suficientemente directivo. El modelo por defecto tiende a responder de forma informativa.

Solución: Haz el prompt más explícito. Agrega ejemplos de estilo o instrucciones como "Responde SOLO con el contenido creativo, sin explicaciones ni disclaimers."

7. response_count no se incrementa correctamente

Causa: Si state.get("response_count", 0) retorna None en vez de 0, la suma falla. Esto puede pasar si en algún punto asignas None al campo.

Solución: Usa el valor por defecto de forma defensiva:

count = (state.get("response_count") or 0) + 1

8. El chatbot pierde contexto entre turnos

Causa: Estás creando un conversation_state nuevo en cada iteración del loop, en vez de reutilizar el estado actualizado por el grafo.

Solución: Inicializa conversation_state una sola vez fuera del loop, y actualízalo con el resultado de cada app.invoke(). El código completo ya hace esto correctamente.


Ideas para extender

Si terminaste el proyecto y quieres ir más allá:

  • Más categorías — Agrega intenciones como "math" (cálculos), "translation" (traducción), o "summary" (resumen de texto). Cada una con su nodo y prompt especializados. Actualiza el clasificador y los conditional edges
  • Clasificación con confidence — Modifica el clasificador para que retorne también un score de confianza. Si la confianza es baja (<0.7), rutea a un nodo que pide clarificación al usuario en vez de responder directamente
  • Nodo de respaldo — Agrega un nodo fallback_node para cuando la clasificación no es clara. Este nodo puede pedir al usuario que reformule su pregunta o elegir la categoría manualmente
  • Streaming — Cambia app.invoke() por app.stream() para ver las respuestas generarse en tiempo real. Procesa los chunks con stream_mode="values" o stream_mode="updates"
  • Memoria con MemorySaver — Agrega MemorySaver como checkpointer para que el chatbot recuerde conversaciones anteriores, incluso si cierras y vuelves a abrir. Esto lo aprenderás en el Módulo 8, pero puedes adelantarte
  • Métricas por intent — Trackea cuántas veces se clasifica cada intent durante la sesión. Muestra un breakdown en el comando stats (ej: "qa: 5, creative: 2, code: 3")
  • Evaluación del routing — Crea una lista de 20 preguntas con su intent esperado y ejecuta el clasificador contra todas. Calcula accuracy y identifica los tipos de preguntas que el clasificador falla

Conexión con el siguiente módulo

En este proyecto construiste un chatbot con routing condicional usando StateGraph — nodos especializados, conditional edges, estado tipado, y visualización del grafo. Este es exactamente el tipo de sistema que LangGraph fue diseñado para construir.

En el Módulo 6: Functional API, aprenderás otra forma de construir el mismo tipo de workflows. En vez de definir grafos explícitos con StateGraph, add_node, y add_edge, usarás funciones Python normales con @entrypoint y @task. El routing condicional que hiciste con conditional edges se convierte en un simple if/else. Los loops que harías con edges cíclicos se convierten en while loops estándar. Es la misma potencia, expresada en control flow de Python.

Y algo importante: a partir del Módulo 6, el proyecto evolutivo comienza. Ya no son mini-proyectos independientes — todo lo que construyas desde ahora forma parte del "AI Research Assistant", un sistema que crece contigo módulo a módulo hasta convertirse en un sistema multi-agente production-ready en el Módulo 12.


Recursos del proyecto

  1. LangGraph StateGraph Tutorial — Tutorial oficial para construir chatbots con StateGraph
  2. LangGraph Conditional Edges — Guía de branching y routing condicional
  3. LangGraph State Management — Conceptos de estado tipado y reducers
  4. LangGraph Visualization — Cómo visualizar grafos con draw_mermaid_png
  5. LangGraph MessagesState — El reducer add_messages para conversaciones
  6. LangChain init_chat_model — Inicialización de modelos multi-proveedor

Módulo 5 — LangChain & LangGraph: From Chains to Agents