An agent that forgets the last message is a demo, not a product. The reason most first LangGraph builds feel broken is memory: the model has no idea what the user said two turns ago. The fix is three lines. Wrap a model and its tools with create_agent, attach a checkpointer, and pass a thread_id on every call so state survives between turns.
Key Takeaways
- The short answer:
create_agent(model, tools, checkpointer=InMemorySaver()), then invoke withconfig={"configurable": {"thread_id": "..."}}. - A checkpointer saves graph state after every node, not just at the end, which is what gives the agent memory and crash recovery.
- Same
thread_idmeans same conversation; a different one starts a fresh, isolated session. - Use
InMemorySaverfor local work,SqliteSaverfor a single machine, andPostgresSaverfor production. - Reach for a manual
StateGraphonly when you need branching, parallel nodes, or a supervisor pattern that the prebuilt agent cannot express.
What You Need Before You Start
You need Python 3.10 or newer and an API key for whatever model provider you choose. LangChain 1.0 ships the agent builder, and LangGraph supplies the runtime and the checkpointers.
pip install -U langchain langgraph langchain-anthropicExport your key so the provider string can resolve it. For Anthropic that is ANTHROPIC_API_KEY; swap the package and variable if you run OpenAI or another provider.
export ANTHROPIC_API_KEY=sk-ant-...
How to Build a Stateful Agent in Five Steps
Here is the whole task, start to finish. Run the block below and you have an agent that calls a tool and remembers the conversation.
- Define a tool as a plain Python function with a docstring. The docstring is the description the model reads, so make it accurate.
from langchain.tools import tool @tool def get_weather(city: str) -> str: """Return the current weather for a given city.""" return f"It's 72F and clear in {city}." - Create a checkpointer. This is the storage that turns a stateless call into a running conversation.
from langgraph.checkpoint.memory import InMemorySaver checkpointer = InMemorySaver() - Build the agent with a model string, your tools, a system prompt, and the checkpointer.
from langchain.agents import create_agent agent = create_agent( model="anthropic:claude-sonnet-4-6", tools=[get_weather], system_prompt="You are a concise assistant. Use tools when asked about weather.", checkpointer=checkpointer, ) - Pick a
thread_idand invoke. Every call that shares this id shares memory.config = {"configurable": {"thread_id": "user-42"}} r1 = agent.invoke( {"messages": [{"role": "user", "content": "Weather in Austin?"}]}, config=config, ) print(r1["messages"][-1].content) - Send a follow-up on the same thread that only makes sense if the agent remembers. It should answer without you repeating the city.
r2 = agent.invoke( {"messages": [{"role": "user", "content": "And is that warm for this time of year?"}]}, config=config, ) print(r2["messages"][-1].content)
That is the full loop: the agent reasons, decides whether to call get_weather, runs it, reads the result, and replies. Because state is checkpointed, the second turn sees the first.
Which Checkpointer Should You Use?
The checkpointer decides how long memory lasts and whether it survives a restart. Pick by where the agent runs, not by what looks fastest to type.
| Checkpointer | Import | Survives restart? | Best for |
|---|---|---|---|
| InMemorySaver | langgraph.checkpoint.memory | No | Notebooks, tests, quick demos |
| SqliteSaver | langgraph.checkpoint.sqlite | Yes, one file | Single-machine apps, prototypes |
| PostgresSaver | langgraph.checkpoint.postgres | Yes, shared | Production, multiple workers, crash recovery |
For Postgres, install langgraph-checkpoint-postgres, then call setup() once to create the tables before first use.
from langgraph.checkpoint.postgres import PostgresSaver
DB = "postgresql://user:pass@localhost:5432/agents"
with PostgresSaver.from_conn_string(DB) as checkpointer:
checkpointer.setup()
agent = create_agent(model="anthropic:claude-sonnet-4-6", tools=[get_weather], checkpointer=checkpointer)A checkpointer tracks one conversation thread. If you also need durable facts that outlive any single thread, such as a user's preferences, add a store and read it inside your agent. Threads and stores solve different problems.
When Do You Drop to a Manual StateGraph?
Move to a hand-built StateGraph when the linear reason-act loop stops fitting your problem. Parallel branches, a router that dispatches to specialist agents, or custom retry logic all need explicit nodes and edges that the prebuilt agent hides.
The mental model is small. You define a state object, add nodes that read and update it, and wire the nodes with edges. The add_messages reducer appends new messages instead of overwriting the list.
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
def chatbot(state: State):
# call your model here, return new messages
return {"messages": [("assistant", "...")]}
builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
graph = builder.compile(checkpointer=checkpointer)The same checkpointer and thread_id pattern works here without change. That is the point of the design: the prebuilt agent is a compiled StateGraph, so you graduate to the lower level without relearning persistence.
Note on the Older create_react_agent Name
If you follow a 2025 tutorial you will see from langgraph.prebuilt import create_react_agent. That function still runs, but LangChain 1.0 moved the recommended entry point to from langchain.agents import create_agent. Prefer create_agent for new code; the arguments map closely.
Troubleshooting the Errors You Will Hit
Most first-run failures come from three places: missing memory, a bad model string, or a tool the model refuses to call. Each has a direct fix.
- The agent forgets everything between calls. You either omitted the checkpointer at build time or changed the
thread_idbetween turns. Confirm both are present and identical across calls in the same conversation. - ValueError about the model or provider. The
"provider:model"string is wrong or the provider package is not installed. Install the matching integration (for examplelangchain-anthropic) and check the exact model name in the provider's docs. - Memory resets after a restart. That is
InMemorySaverworking as designed; it lives in process memory. Switch toSqliteSaverorPostgresSaverfor anything you expect to persist. - The model never calls your tool. The docstring is vague or the system prompt does not invite tool use. Rewrite the docstring to state exactly what the tool does and when to use it, since the model chooses tools from that text.
- Postgres raises a missing-table error. You skipped
checkpointer.setup(). Run it once against the database before the first invoke.
What to Do Next
You have a working stateful agent, so the next moves are reliability and scale.
- Give the agent real tools. Wire it to a retriever so it can answer from your own data; the pattern in how to add a reranker to your RAG pipeline pairs well with a tool-calling agent.
- Test it before you trust it. Set up evals with Promptfoo so a prompt change cannot quietly regress behavior.
- Package a tool as a server. If several agents share tools, expose them through an MCP server built with FastMCP instead of copying functions.
- Contain what the agent can touch. Before it runs anything with side effects, read how to isolate agents with scoped identities.
- Compare frameworks. If you are weighing raw chains against a graph runtime, using LangChain for workflow automation covers the non-graph side.