LangGraph — Módulo 3: el estado y los reducers
Tercer módulo del curso de LangGraph: cómo se actualiza el estado cuando varios nodos escriben el mismo campo. Reducers con Annotated y operator.add, add_messages y MessagesState para manejar el historial de un chat.
☰ Índice del contenido
Tercer módulo del Curso completo de LangGraph. En el Módulo 2 un nodo devolvía {"borrador": ...} y LangGraph lo mezclaba con el estado. Pero, ¿cómo lo mezcla exactamente? ¿Sobrescribe o acumula? Esa decisión la toman los reducers, y entenderlos es la diferencia entre un grafo que funciona y uno que borra datos sin querer.
La tesis del módulo
Un reducer define cómo se combina lo viejo con lo nuevo. Por defecto LangGraph sobrescribe cada campo. Cuando quieres acumular (una lista que crece, un historial de chat), le pones un reducer al campo. Es la pieza que hace que la memoria del grafo funcione.
El comportamiento por defecto: sobrescribir
Cuando un nodo devuelve un campo, LangGraph reemplaza el valor anterior:
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
valor: int
def poner_uno(state: State) -> dict:
return {"valor": 1}
def poner_dos(state: State) -> dict:
return {"valor": 2}
builder = StateGraph(State)
builder.add_node("poner_uno", poner_uno)
builder.add_node("poner_dos", poner_dos)
builder.add_edge(START, "poner_uno")
builder.add_edge("poner_uno", "poner_dos")
builder.add_edge("poner_dos", END)
print(builder.compile().invoke({"valor": 0}))
# → {'valor': 2} ← el 1 se perdió: cada nodo sobrescribió
Esto está bien para muchos casos. Pero si lo que quieres es ir juntando cosas —por ejemplo, cada nodo agrega un mensaje a una conversación— sobrescribir es exactamente lo que no quieres.
Los reducers: acumular en vez de sobrescribir
Un reducer es una función que recibe (valor_actual, valor_nuevo) y devuelve el valor combinado. Se lo asignas a un campo del estado con Annotated.
Para acumular listas, el reducer más simple es operator.add (que para listas significa “concatenar”):
from typing import Annotated
from typing_extensions import TypedDict
from operator import add
class State(TypedDict):
# "cada actualización de 'pasos' se SUMA a la anterior, no la reemplaza"
pasos: Annotated[list[str], add]
Ahora, con el mismo grafo de antes pero usando este estado:
def paso_a(state: State) -> dict:
return {"pasos": ["hice A"]}
def paso_b(state: State) -> dict:
return {"pasos": ["hice B"]}
# ... construir el grafo igual que antes ...
print(graph.invoke({"pasos": []}))
# → {'pasos': ['hice A', 'hice B']} ← ¡se acumularon!
La diferencia es toda la sintaxis Annotated[list[str], add]. Se lee como: “este campo es una lista de strings, y su reducer es add”. Cada nodo aporta su pedazo y LangGraph los une.
estado inicial: pasos = []
paso_a devuelve ["hice A"] → add([], ["hice A"]) → ["hice A"]
paso_b devuelve ["hice B"] → add(["hice A"], ["hice B"]) → ["hice A", "hice B"]
SOBRESCRIBIR vs ACUMULAR
sin reducer con reducer (add)
┌─────────┐ ┌─────────┐
│ valor=1 │ │ [A] │
└────┬────┘ └────┬────┘
│ nodo escribe 2 │ nodo escribe [B]
▼ ▼
┌─────────┐ ┌─────────┐
│ valor=2 │ el 1 │ [A, B] │ se suma
└─────────┘ se pierde └─────────┘
El caso estrella: el historial de mensajes
Casi todos los agentes conversacionales necesitan acumular mensajes (los del usuario y los del modelo) en un historial. LangGraph trae un reducer hecho a medida para esto: add_messages.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import AnyMessage
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
add_messages es más inteligente que add: no solo concatena, también actualiza mensajes existentes por su id (útil para streaming) y convierte automáticamente diccionarios sueltos en objetos de mensaje de LangChain. Es el reducer que vas a usar en casi todos tus agentes de chat.
El atajo: MessagesState
Como este patrón es tan común, LangGraph ofrece un estado prehecho que ya trae el campo messages con su reducer. En vez de declararlo tú, heredas de MessagesState:
from langgraph.graph import MessagesState
class State(MessagesState):
# messages ya viene incluido con add_messages;
# aquí solo agregas tus campos extra:
documentos: list[str]
Con eso tienes messages listo y le sumas los campos que tu caso necesite. Lo usaremos constantemente a partir del bloque intermedio.
Cómo elegir el reducer
| Quiero que el campo… | Reducer |
|---|---|
| Se reemplace con el último valor | (ninguno — es el default) |
| Acumule en una lista | Annotated[list[...], add] |
| Acumule mensajes de chat | Annotated[list[AnyMessage], add_messages] |
| Se combine con una lógica propia | Una función (actual, nuevo) -> combinado |
Errores comunes
- Esperar acumulación sin reducer. Si tu lista “se borra” entre nodos, te faltó el
Annotated[..., add]. - Devolver el historial completo con
add_messages. Devuelve solo el mensaje nuevo ({"messages": [nuevo_mensaje]}); el reducer lo agrega al historial. Devolver toda la lista puede duplicar. - Confundir el default. Sobrescribir no es un bug: para campos como “estado actual” o “última respuesta” es justo lo que quieres. Reserva los reducers para lo que debe crecer.
La frase para llevarse
“Sin reducer, el estado olvida. Con reducer, el estado recuerda. La memoria de tu agente empieza aquí.”
Qué viene
En el Módulo 4 cerramos el bloque básico con lo que hace de un grafo un verdadero tomador de decisiones: las aristas condicionales y los ciclos. Un nodo que decide a dónde ir según el estado.
Material relacionado
- Glosario de LangGraph
- Memoria de agentes — corto y largo plazo, el concepto detrás del estado
- Módulo 2: tu primer grafo
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.