Una persona pregunta cuántas plazas quedan en un taller. Nuestra aplicación tiene que identificar a qué taller se refiere, consultar su catálogo y responder. Si el mensaje no permite identificarlo, debe pedir una aclaración en lugar de elegir uno al azar.
Podemos escribir todo con funciones y condiciones de Python. Cuando un recorrido reúne varias llamadas al modelo, consultas y decisiones, también podemos utilizar herramientas que ayuden a organizar esas piezas. Pero conviene saber qué parte resuelve cada una antes de añadirlas al proyecto.
En este ejemplo usaremos LangChain para pedir al modelo que identifique el taller y LangGraph para ordenar los pasos y elegir entre consultar las plazas o pedir una aclaración. El modelo interpretará el mensaje; el recorrido y la consulta al catálogo quedarán definidos en nuestro código.
Primero seguiremos los dos caminos posibles con datos pequeños. Después los expresaremos en Python.
Un mensaje, una selección y dos caminos
Tenemos este catálogo ficticio en memoria:
| Taller | Plazas disponibles |
|---|---|
| Fotografía nocturna | 3 |
| Cerámica | 0 |
La persona escribe «¿Cuántas plazas quedan para Fotografía nocturna?». Enviamos al modelo ese mensaje y una instrucción para que seleccione uno de los dos talleres admitidos. Si el taller falta, no existe en el catálogo o la petición es ambigua, deberá devolver una selección vacía.
Con una selección correcta, el recorrido es el siguiente:
- El modelo identifica Fotografía nocturna.
- Nuestro código comprueba si hay un taller seleccionado y elige el camino de consulta.
- La aplicación lee el catálogo, encuentra tres plazas y construye «Plazas disponibles en Fotografía nocturna: 3».
Si el mensaje fuera «¿Cuántas plazas quedan?», una selección adecuada estaría vacía. En ese caso el recorrido terminaría con «Indica el taller: Fotografía nocturna o Cerámica», sin consultar plazas.
La selección del modelo es ilustrativa, no un resultado medido. La consulta y las frases finales sí están definidas en el código. Cada camino realiza una llamada al modelo para interpretar el mensaje; la respuesta final se construye en Python.
Qué aporta cada herramienta
LangChain ofrece interfaces para trabajar con modelos, mensajes y herramientas, además de componentes para construir agentes. Aquí utilizaremos su interfaz de modelo y una salida estructurada. Su visión general también presenta create_agent, que reúne un ciclo de agente sin tener que escribir cada intercambio manualmente.
LangGraph permite representar el recorrido mediante un grafo: pasos conectados por transiciones. En este ejemplo necesitamos tres ideas:
- El estado contiene los datos que utilizan los pasos: mensaje original, taller seleccionado y respuesta.
- Los nodos son las funciones que realizan el trabajo: identificar, consultar o pedir aclaración.
- Las aristas indican qué paso viene después. Una arista condicional elige el camino según el estado.
Los agentes de LangChain se apoyan en LangGraph, pero utilizar LangGraph no obliga a utilizar LangChain. Podemos combinar sus componentes, como haremos aquí, o escribir nodos que utilicen otras bibliotecas.
Este caso es un flujo con caminos prefijados. El modelo produce una selección; una condición de Python elige el siguiente nodo. No estamos dejando que el modelo invente el recorrido ni decida qué código ejecutar.
El diagrama muestra los pasos y la bifurcación. Las flechas indican el orden de ejecución y las etiquetas muestran qué dato determina el camino:

Ver el diagrama a tamaño completo.
La pregunta sin taller sigue el camino de aclaración. Cerámica, en cambio, es una selección válida aunque tenga cero plazas: no confundimos ausencia de selección con falta de disponibilidad.
Definir la selección y el estado
El modelo devolverá un único dato: workshop_name, el nombre del taller seleccionado o None cuando no pueda identificarlo. En workshop_selection.py definimos esa salida con Pydantic:
from typing import Literal
from pydantic import BaseModel, ConfigDict
class WorkshopSelection(BaseModel):
"""A supported workshop name, or no selection."""
model_config = ConfigDict(extra="forbid", strict=True)
workshop_name: Literal["Fotografía nocturna", "Cerámica"] | None
Literal limita los nombres admitidos. None permite representar que falta una selección; no obliga al modelo a rellenar el campo con un taller cualquiera. Una selección válida en su forma todavía puede ser una interpretación equivocada del mensaje. Si los datos incumplen la estructura o la llamada falla, el error se propaga; no lo convertimos en una selección vacía.
El grafo necesita además conservar la pregunta y la respuesta final. En workshop_state.py describimos sus tres campos:
from typing import TypedDict
class WorkshopState(TypedDict):
"""Data shared by the steps of the workshop workflow."""
message: str
workshop_name: str | None
reply: str
TypedDict describe las claves y sus tipos para las herramientas de análisis. Los datos siguen siendo un diccionario; esta declaración no realiza la validación de Pydantic durante la ejecución.
Identificar el taller con LangChain
En otro módulo preparamos el catálogo y la conexión al modelo. Utilizamos los paquetes langchain, langchain-anthropic, langgraph y pydantic. La clave del proveedor se configura en ANTHROPIC_API_KEY; ANTHROPIC_MODEL indica un modelo compatible con la salida estructurada utilizada.
import os
from typing import Literal
from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from workshop_selection import WorkshopSelection
from workshop_state import WorkshopState
catalog = {"Fotografía nocturna": 3, "Cerámica": 0}
model = init_chat_model(
os.environ["ANTHROPIC_MODEL"],
model_provider="anthropic",
timeout=20.0,
max_retries=0,
max_tokens=256,
)
selector = model.with_structured_output(
WorkshopSelection,
method="json_schema",
)
init_chat_model crea la integración de LangChain con el proveedor. with_structured_output configura la obtención de un objeto WorkshopSelection. La documentación de la integración con Anthropic explica el uso de json_schema para esta salida estructurada.
El primer nodo utiliza esa configuración. Recibe el estado, envía la instrucción y el mensaje al modelo, y devuelve el dato que ha obtenido:
def identify_workshop(state: WorkshopState) -> dict[str, str | None]:
"""Ask the model to identify a supported workshop."""
selection = selector.invoke(
[
(
"system",
"Identify the workshop explicitly requested. Supported names: "
"Fotografía nocturna, Cerámica. Set workshop_name to null "
"if the workshop is missing, unsupported, or ambiguous. "
"Do not guess a workshop.",
),
("human", state["message"]),
]
)
return {"workshop_name": selection.workshop_name}
El nodo devuelve una actualización parcial: solo cambia workshop_name. LangGraph conserva los otros campos del estado. En este recorrido no necesitamos volver a copiar el mensaje ni construir aún la respuesta.
Definir las funciones y conectar los caminos
La función de decisión comprueba si hay selección. Los otros dos nodos construyen la respuesta: uno consulta el catálogo y el otro solicita el dato que falta.
def choose_next_step(state: WorkshopState) -> Literal["answer", "clarify"]:
"""Choose a path based on whether a workshop was selected."""
if state["workshop_name"] is None:
return "clarify"
return "answer"
def answer_availability(state: WorkshopState) -> dict[str, str]:
"""Read the catalog and prepare an availability reply."""
name = state["workshop_name"]
if name is None or name not in catalog:
raise ValueError("A supported workshop must be selected.")
return {"reply": f"Plazas disponibles en {name}: {catalog[name]}."}
def ask_for_workshop(state: WorkshopState) -> dict[str, str]:
"""Ask the person to identify a workshop."""
return {"reply": "Indica el taller: Fotografía nocturna o Cerámica."}
La comprobación is None decide sobre el nombre seleccionado, no sobre el número de plazas. El nodo de consulta también verifica que la selección pertenezca al catálogo antes de leerla.
Ahora registramos los nodos y sus conexiones:
builder = StateGraph(WorkshopState)
builder.add_node("identify", identify_workshop)
builder.add_node("answer", answer_availability)
builder.add_node("clarify", ask_for_workshop)
builder.add_edge(START, "identify")
builder.add_conditional_edges(
"identify",
choose_next_step,
{"answer": "answer", "clarify": "clarify"},
)
builder.add_edge("answer", END)
builder.add_edge("clarify", END)
workflow = builder.compile()
START señala dónde comienza el recorrido y END, dónde termina. add_conditional_edges llama a nuestra función de decisión después de identificar el taller. Su resultado indica cuál de los dos nodos ejecutar. compile prepara el grafo para utilizarlo; todavía no envía la pregunta al modelo.
La Graph API de LangGraph explica estos nodos, actualizaciones y conexiones. Los nombres registrados permiten relacionar las funciones del código con los pasos del diagrama.
Ejecutar y revisar el resultado
Ejecutamos el flujo con un estado inicial. Aún no hay taller seleccionado ni respuesta:
result = workflow.invoke(
{
"message": "¿Cuántas plazas quedan para Fotografía nocturna?",
"workshop_name": None,
"reply": "",
}
)
print(result["reply"])
Si el modelo identifica correctamente Fotografía nocturna, se imprime Plazas disponibles en Fotografía nocturna: 3.. Para el mensaje sin nombre, si devuelve None, se ejecuta el nodo de aclaración. Podemos comprobar ambos caminos con selecciones preparadas a mano, aunque medir cómo interpreta el modelo requiere ejecuciones con mensajes representativos.
El estado mostrado pertenece a una ejecución. No hemos configurado persistencia ni memoria entre conversaciones. Tampoco reservamos plazas. LangGraph organiza el recorrido, pero no sustituye el catálogo, las reglas de la aplicación o la evaluación de las selecciones del modelo.
Para un caso tan pequeño, unas funciones y una condición también resolverían el problema. El grafo empieza a resultar útil cuando queremos mantener explícitos varios caminos y sus datos compartidos. Aquí permite ver una decisión concreta: una selección válida conduce a la consulta; una selección ausente conduce a pedir el taller, sin inventar su disponibilidad.