En el módulo anterior encadenaste con LCEL: prompt | modelo | parser.
Ese operador impone una forma —un DAG, un camino sin retorno—. La salida de un eslabón
es la entrada del siguiente, y no hay sitio donde escribir «llevo dos intentos» ni
forma de volver atrás.
En cuanto necesitas volver —reintentar una validación, dejar que el modelo lea el resultado de una herramienta y decida otra vez— la cadena obliga a montar la lógica de control fuera de ella. LangGraph invierte el reparto: el flujo lo define un grafo explícito y los datos viven en un objeto compartido.
// LangGraph no sustituye a LangChain: los nodos siguen llamando a sus modelos, herramientas y parsers. Lo que cambia es quién decide el orden.| Pieza | Qué es | Firma típica |
|---|---|---|
| State | El esquema de la memoria compartida | class Estado(TypedDict) |
| Node | Función que recibe el estado y devuelve un parche | def nodo(state) -> dict |
| Edge | El camino de un nodo al siguiente | add_edge("a", "b") |
Y dos constantes: START, el punto de entrada, y END, el nodo
virtual que corta la ejecución.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class EstadoDelGrafo(TypedDict):
texto_original: str
texto_procesado: str
pasos: list[str]
Un nodo no devuelve el estado completo. Devuelve solo las claves que ha tocado, y LangGraph las fusiona sobre lo que había, clave a clave.
def nodo_mayusculas(state: EstadoDelGrafo):
return {"texto_procesado": state["texto_original"].upper()}
Recorrido del estado por ese grafo, clave a clave:
| Momento | texto_original | texto_procesado | El nodo devuelve |
|---|---|---|---|
| Entrada | "Hola LangGraph" | "" | — |
Tras mayusculas | "Hola LangGraph" | "HOLA LANGGRAPH" | solo texto_procesado |
Tras reverso | "Hola LangGraph" | "HPARGGNAL ALOH" | solo texto_procesado |
La consecuencia práctica, y la fuente del error más común del módulo: si una clave
no aparece en el return, sobrevive intacta; si aparece,
se sobrescribe —no se acumula—. Esa asimetría tiene lección propia.
builder = StateGraph(EstadoDelGrafo)
builder.add_node("mayusculas", nodo_mayusculas)
builder.add_node("reverso", nodo_reverso)
builder.add_edge(START, "mayusculas") # equivale a set_entry_point("mayusculas")
builder.add_edge("mayusculas", "reverso")
builder.add_edge("reverso", END)
app = builder.compile()
resultado = app.invoke({"texto_original": "Hola LangGraph",
"texto_procesado": "", "pasos": []})
compile() valida la topología: que todo nodo sea alcanzable y que ninguno
se quede sin salida. Después ya no se añaden nodos. invoke() devuelve el
estado final completo, no el parche del último nodo.
En el ejemplo de la guía, nodo_mayusculas devuelve
"pasos": ["mayusculas"] con el comentario «se añadirá a la lista
existente si el reducer lo permite», y nodo_reverso hace
state["pasos"] + ["reverso"] a mano. Pero en ese grafo no hay ningún
reducer declarado.
Si el primer nodo no acumula y el segundo sí, ¿qué está pasando de verdad con esa
clave? Comprueba qué valor tiene pasos justo después del primer nodo y
decide si el resultado final ['mayusculas', 'reverso'] sale por diseño
o por la casualidad de que la lista empezara vacía.
Invoca el grafo omitiendo "pasos": [] del diccionario inicial:
app.invoke({"texto_original": "Hola LangGraph", "texto_procesado": ""})
Anota qué excepción sale, en qué nodo y en qué línea. Después responde: ¿el
TypedDict te ha protegido de algo, o solo ha documentado la forma esperada?