← Todo el material
Curso Básico 20 de agosto de 2026

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 listaAnnotated[list[...], add]
Acumule mensajes de chatAnnotated[list[AnyMessage], add_messages]
Se combine con una lógica propiaUna 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

LangGraphPythonEstadoReducersMensajes

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.