LangGraph MessagesState: Manage Chat History

In Chapter 8, our chatbot forgot everything between questions. To fix that, we need to store the whole conversation in the state. LangGraph has a ready-made tool for this: MessagesState.

Message types

Chat models work with a list of messages. Each message has a type:

  • SystemMessage: instructions for the model (“You are a tutor…”)
  • HumanMessage: what the user said
  • AIMessage: what the model replied
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

What is MessagesState?

It is a built-in state with one field, messages, which already has a reducer attached (Chapter 5). The reducer is called add_messages, and it appends new messages to the list instead of replacing it.

from langgraph.graph import MessagesState

That is equivalent to writing this yourself:

from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

See the reducer in action

from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage

existing = [HumanMessage("Hi")]
new = [AIMessage("Hello! How can I help?")]

merged = add_messages(existing, new)
print([m.content for m in merged])

Output:

['Hi', 'Hello! How can I help?']

New messages are added to the end, so history builds up automatically.

A chatbot with memory of the conversation

from dotenv import load_dotenv
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph, MessagesState, START, END

load_dotenv()
llm = ChatAnthropic(model="claude-sonnet-5-5")

def chatbot(state: MessagesState):
    system = SystemMessage("You are a friendly tutor. Keep answers under 50 words.")
    reply = llm.invoke([system] + state["messages"])
    return {"messages": [reply]}

builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)

graph = builder.compile()

How the node works:

  1. It reads the full history from state[“messages”]
  2. It adds a system prompt at the front (we don’t store it in state, so it isn’t duplicated every turn)
  3. It sends everything to the model
  4. It returns the reply inside a list, and add_messages appends it

If you use OpenAI, swap the model line for ChatOpenAI as in Chapter 8.

Running a multi-turn chat

For now, we keep memory ourselves by passing the history back in each time. (Chapter 13 shows how LangGraph can do this for you.)

history = []

while True:
    text = input("You: ")
    if text.lower() in ("quit", "exit"):
        break

    history.append(HumanMessage(text))
    result = graph.invoke({"messages": history})
    history = result["messages"]

    print("Bot:", history[-1].content)

Sample conversation:

You: My name is Asha and I'm learning Python.
Bot: Nice to meet you, Asha! Python is a great choice. What would you like to learn first?
You: What's my name?
Bot: Your name is Asha!

The second answer works because the whole conversation is sent to the model each time.

Shortcut: tuples instead of message objects

LangGraph converts simple tuples for you:

graph.invoke({"messages": [("human", "Hello!")]})

This is the same as passing HumanMessage(“Hello!”), and it is handy for quick tests.

Printing messages nicely

result = graph.invoke({"messages": [("human", "What is a list in Python?")]})

for message in result["messages"]:
    message.pretty_print()

This prints each message with a label showing who said it, which is useful for debugging.

Adding your own fields

Need more than messages? Extend MessagesState:

class State(MessagesState):
    user_name: str
    topic: str

You get the messages field with its reducer, plus your own fields.

Keeping long chats affordable

Every call sends the entire history, so long chats cost more and can hit the model’s limit. A simple fix is to send only the last few messages:

def chatbot(state: MessagesState):
    recent = state["messages"][-6:]          # last 6 messages only
    reply = llm.invoke([SystemMessage("You are a friendly tutor.")] + recent)
    return {"messages": [reply]}

The full history stays in state. Only the model call is trimmed.

Common mistakes

  • Returning a single message instead of a list: use {“messages”: [reply]}, not {“messages”: reply}
  • Returning the whole history from a node: return only new messages, or you will get duplicates
  • Not passing history back into invoke(): without a checkpointer, each call starts fresh
  • Storing the system prompt in state: add it inside the node so it doesn’t pile up
  • Forgetting the model sees everything: never put secrets in the message history

Summary

  • Chat history is a list of SystemMessage, HumanMessage, and AIMessage
  • MessagesState gives you a messages field that appends automatically
  • Nodes return new messages in a list, and the reducer merges them
  • Trim what you send to the model to control cost, while keeping the full history in state

Practice

  1. Change the system prompt so the bot answers like a quiz master who ends every reply with a question
  2. Add a user_name field by extending MessagesState, and print it after a run
  3. Change the trim from [-6:] to [-2:], then test whether the bot still remembers your name after a few turns

If you liked the tutorial, spread the word and share the link and our website, Studyopedia, with others.


For Videos, Join Our YouTube Channel: Join Now


Read More:

Add an LLM Node in LangGraph: Build Your First Chatbot
LangGraph ToolNode: Build a ReAct Agent from Scratch
Studyopedia Editorial Staff
contact@studyopedia.com

We work to create programming tutorials for all.

No Comments

Post A Comment