LangGraph Tutorial (Python): adding memory to agents for intermediate developers

By Cyprian AaronsUpdated 2026-04-21
langgraphadding-memory-to-agents-for-intermediate-developerspython

This tutorial shows you how to give a LangGraph agent persistent memory in Python using a checkpointer and thread IDs. You need this when you want the agent to remember prior turns across requests instead of treating every message like a brand-new conversation.

What You'll Need

  • Python 3.10+
  • langgraph
  • langchain-openai
  • python-dotenv
  • An OpenAI API key set as OPENAI_API_KEY
  • Basic familiarity with LangGraph nodes, edges, and state

Install the packages:

pip install langgraph langchain-openai python-dotenv

Step-by-Step

  1. Start with a minimal graph state that stores messages. LangGraph memory works best when your state is explicit, so use the built-in message reducer instead of rolling your own list handling.
from typing import Annotated, TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage


class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
  1. Create a node that calls the model and returns the next assistant message. This node reads from state["messages"], invokes the LLM, and returns only the new message in the same shape LangGraph expects.
import os
from langchain_openai import ChatOpenAI


llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)


def chat_node(state: AgentState):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}
  1. Build the graph and compile it with a checkpointer. The checkpointer is what gives you persistence across invocations; without it, every run starts from scratch.
from langgraph.checkpoint.memory import MemorySaver

builder = StateGraph(AgentState)
builder.add_node("chat", chat_node)
builder.add_edge(START, "chat")
builder.add_edge("chat", END)

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
  1. Invoke the graph with a thread_id. This is the key piece most people miss: the same thread ID tells LangGraph which conversation history to load and update.
from langchain_core.messages import HumanMessage

config = {"configurable": {"thread_id": "customer-123"}}

result_1 = graph.invoke(
    {"messages": [HumanMessage(content="My name is Priya. Remember it.")]},
    config=config,
)

result_2 = graph.invoke(
    {"messages": [HumanMessage(content="What is my name?")]},
    config=config,
)

print(result_2["messages"][-1].content)
  1. Inspect stored state to confirm memory is actually being persisted. In production, this is how you debug whether your app is reusing conversation context or silently losing it between requests.
snapshot = graph.get_state(config)
print("Stored messages:")
for msg in snapshot.values["messages"]:
    role = msg.__class__.__name__
    print(f"{role}: {msg.content}")
  1. If you want cleaner production behavior, separate user-facing input from internal memory growth. A common pattern is to trim or summarize older messages before they get too large, but keep that for a later pass once persistence is working.
def ask(graph, thread_id: str, text: str):
    config = {"configurable": {"thread_id": thread_id}}
    out = graph.invoke({"messages": [HumanMessage(content=text)]}, config=config)
    return out["messages"][-1].content


print(ask(graph, "customer-123", "What did I tell you my name was?"))
print(ask(graph, "customer-123", "And what should you call me?"))

Testing It

Run the script once and send two prompts using the same thread_id. On the second prompt, ask for something stated earlier in the conversation; if memory is wired correctly, the model should answer using prior context instead of guessing.

Then change only the thread_id and run the same two prompts again. You should see a fresh conversation with no access to the previous thread’s messages.

If you want to verify persistence more directly, call graph.get_state(config) after each turn and inspect the stored messages. In a real app, this is also where you’d confirm your checkpoint backend is writing to durable storage rather than an in-memory object that disappears on restart.

Next Steps

  • Replace MemorySaver with a durable checkpointer like SQLite or Postgres for real deployments.
  • Add message trimming or summarization so long-running threads don’t explode token usage.
  • Split memory into short-term conversation state and long-term customer profile data for cleaner agent design.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides