Home Blog Contact
Home/Blog/How to Build a Stateful AI Agent with LangGra…
How toAILangGraphAI AgentsPython

How to Build a Stateful AI Agent with LangGraph

8 min readBy Miloš Mitrović

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 with config={"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_id means same conversation; a different one starts a fresh, isolated session.
  • Use InMemorySaver for local work, SqliteSaver for a single machine, and PostgresSaver for production.
  • Reach for a manual StateGraph only 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-anthropic

Export 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.

  1. 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}."
  2. Create a checkpointer. This is the storage that turns a stateless call into a running conversation.
    from langgraph.checkpoint.memory import InMemorySaver
    
    checkpointer = InMemorySaver()
  3. 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,
    )
  4. Pick a thread_id and 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)
  5. 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.

CheckpointerImportSurvives restart?Best for
InMemorySaverlanggraph.checkpoint.memoryNoNotebooks, tests, quick demos
SqliteSaverlanggraph.checkpoint.sqliteYes, one fileSingle-machine apps, prototypes
PostgresSaverlanggraph.checkpoint.postgresYes, sharedProduction, 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_id between 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 example langchain-anthropic) and check the exact model name in the provider's docs.
  • Memory resets after a restart. That is InMemorySaver working as designed; it lives in process memory. Switch to SqliteSaver or PostgresSaver for 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.

Sources

M
Miloš Mitrović
Email Marketing for Ecommerce

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.