Building Stateful AI Agents with LangGraph: Core Concepts & Code
This tutorial teaches how to build stateful AI agents using LangGraph, covering core abstractions (StateGraph, nodes, edges, checkpoints), adding long-term memory with thread IDs, integrating tools like Tavily search, customizing state with reducers, debugging with state history, and choosing production-ready checkpoint backends.
Why LangGraph?
Previous lessons (L3-L4) implemented ReAct loops with smolagents or raw OpenAI API, but production environments face three problems:
Chaotic state management : Conversation history, tool results, and user preferences are scattered in messages, making them hard to manage.
No long-term memory : Each call starts fresh — the agent forgets previous interactions.
Opaque flow : The ReAct loop is an invisible cycle; you can only debug via logs and cannot visualize the flow.
LangGraph solves these by modeling the agent as a directed state graph that is visual, executable, and memorable (every state can be persisted).
START → assistant (LLM) → [decide to call tool?]
↓ yes
tools (execute)
↓
assistant (loop)
↓ no
ENDLangGraph as Assembly Blueprint
L3-L4 agents are like hand-assembled cars: smolagents gives you parts (@tool, CodeAgent) to assemble yourself; handwritten OpenAI API builds from the wheel up. LangGraph provides the assembly blueprint + semi-finished chassis : it defines where engine, wheels, seats go; offers standard interfaces (nodes) you fill in; and the assembled car natively supports complex electrical systems (state management) and onboard computers (long-term memory). LangGraph isn't new functionality — it's a better organization method. It shines for multi-step workflows, multi-agent collaboration, and human-in-the-loop processes.
Three Core Concepts
State (状态) — the bill of materials: stores all agent data (conversation history, intermediate results).
Node (节点) — assembly step: receives State, does something, returns updated State.
Edge (边) — assembly arrow: decides which step comes next.
Plus two helpers:
StateGraph — assembles all nodes and edges into a graph.
Checkpoint — saves each step's state to disk, enabling long-term memory.
First LangGraph Agent: A Thinking Chatbot
Minimal 30-line example demonstrating the five concepts:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
# 1. Define State
class State(TypedDict):
messages: Annotated[list, add_messages] # reducer: auto-append new messages
# 2. Define Node
llm = ChatOpenAI(model="gpt-4o-mini")
def chatbot_node(state: State):
return {"messages": [llm.invoke(state["messages")]}
# 3. Assemble graph
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot_node)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
# 4. Run
result = graph.invoke({"messages": [{"role": "user", "content": "Hi, I'm Zhang San"}]})
print(result["messages"][-1].content) # Hello Zhang San!
result = graph.invoke({"messages": [{"role": "user", "content": "What's my name?"}]})
print(result["messages"][-1].content) # Sorry, I don't have your name.Problem exposed : Each invoke starts fresh — LangGraph doesn't know previous conversation. Solution : Use Checkpoint + Thread ID .
Checkpoint + Thread: Long-Term Memory
LangGraph's memory mechanism has two components:
Checkpoint : Saves a snapshot on every state change.
Thread ID : Unique identifier per session; all checkpoints for the same thread form the complete conversation history.
Code with memory (key lines 24-28):
from langgraph.checkpoint.memory import MemorySaver
# ... State, Node definitions same as above ...
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot_node)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
# Key change: add checkpointer
memory = MemorySaver() # in-memory (lost on restart)
graph = graph_builder.compile(checkpointer=memory)
# Key change: add thread_id
config = {"configurable": {"thread_id": "user-123"}}
# First conversation
result = graph.invoke({"messages": [{"role": "user", "content": "Hi, I'm Zhang San"}]}, config=config)
print(result["messages"][-1].content) # Hello Zhang San!
# Second conversation (agent should remember name)
result = graph.invoke({"messages": [{"role": "user", "content": "What's my name?"}]}, config=config)
print(result["messages"][-1].content) # You're Zhang San! You just said so. MemorySaverstores the first message list. On second invoke, LangGraph loads history from the same thread_id, so the LLM sees "I'm Zhang San" and answers correctly.
Adding Tools: Let the Agent Do Things
Integrate Tavily search (from LangGraph official quickstart):
from langchain_tavily import TavilySearch
from langgraph.prebuilt import ToolNode, tools_condition
# 1. Define tool
search_tool = TavilySearch(max_results=2)
tools = [search_tool]
llm_with_tools = llm.bind_tools(tools)
# 2. Define nodes
def chatbot_node(state: State):
return {"messages": [llm_with_tools.invoke(state["messages")]}
tool_node = ToolNode(tools) # built-in tool execution node
# 3. Assemble graph (with conditional edge)
graph_builder = StateGraph(State)
graph_builder.add_node("chatbot", chatbot_node)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "chatbot")
# Conditional edge: decide tool or end
graph_builder.add_conditional_edges(
"chatbot",
tools_condition,
{"tools": "tools", END: END}
)
graph_builder.add_edge("tools", "chatbot") # tool result → back to think (loop)
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
# 4. Run with streaming
config = {"configurable": {"thread_id": "user-456"}}
for chunk in graph.stream({"messages": [{"role": "user", "content": "Who won 2026 Nobel Physics?"}]}, config=config, stream_mode="values"):
chunk["messages"][-1].pretty_print()Execution flow (simplified):
=== Round 1: Think ===
Human: Who won 2026 Nobel Physics?
AI: (decide to call search) → tool call
=== Round 2: Tool ===
Tool: Search results...
=== Round 3: Think (with tool result) ===
AI: 2026 Nobel Physics awarded to XXX for YYY.
=== End ===The full graph visualizes the ReAct loop: chatbot → tools_condition → tools → chatbot.
Advanced State: Custom Fields
Production state often needs more than messages:
from typing import Annotated
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages] # conversation history (built-in reducer)
user_name: str # user preference (direct overwrite)
tool_call_count: Annotated[int, lambda current, update: current + update] # custom reducer
search_results: list[str] # cache (no reducer, direct assign)Reducer (归约函数) is LangGraph's core mechanism — it defines how new data merges with old.
Message list : append new messages → add_messages Counter : accumulate → lambda old, new: old + new State overwrite : replace with new value → direct assignment (no Annotated)
Deduplicated list : dedupe then append → custom lambda
Inspecting History: get_state & get_state_history
Debugging tools to see every step:
# View current state
current_state = graph.get_state(config)
print(current_state.values) # full state dict
# View full history (all checkpoints)
history = list(graph.get_state_history(config))
for i, state in enumerate(history):
print(f"Step {i}: {state.values['messages'][-1].content[:50]}")This is a lifesaver in production: when the agent answers wrong, you can trace exactly which step went off track.
Production Checkpoints: In-Memory vs Persistent
MemorySaveris in-memory; restart loses data. Production needs persistence:
# In-memory (demo)
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
# SQLite (small production)
from langgraph.checkpoint.sqlite import SqliteSaver
memory = SqliteSaver.from_conn_string("checkpoints.db")
# PostgreSQL (medium production)
from langgraph.checkpoint.postgres import PostgresSaver
memory = PostgresSaver.from_conn_string("postgresql://...")
# Redis (high concurrency)
from langgraph.checkpoint.redis import RedisSaver
memory = RedisSaver.from_conn_string("redis://...")Selection guide:
Demo/test: MemorySaver Personal projects: SqliteSaver (single file, easy backup)
Medium production: PostgresSaver (transactions, concurrency)
High-concurrency production: RedisSaver (fastest)
Comparison with L3-L4
Code lines : L3 smolagents ~30, L4 handwritten ~60, L5 LangGraph ~50
State management : L3 built-in, L4 manual, L5 explicit State + Reducer
Long-term memory : L3 needs custom DB, L4 custom impl, L5 Checkpoint built-in
Flow visualization : L3 none, L4 none, L5 state graph visualization
Complexity ceiling : L3 medium, L4 low, L5 high (multi-agent ready)
Learning curve : L3 low, L4 low, L5 medium
Conclusion : L3 for quick prototypes/single agent; L4 for understanding principles; L5 LangGraph is the standard for production multi-step/multi-agent systems.
Hands-On Exercises
Step 1: Run Tool-Enabled Agent (10 min)
pip install langgraph langchain-openai langchain-tavily tavily-pythonSet OPENAI_API_KEY and TAVILY_API_KEY (free tier at tavily.com)
Run and observe streaming output
Step 2: Visualize Your Graph (5 min)
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))You'll see a real state diagram matching the ASCII flow.
Step 3: Add Custom State Field (10 min)
Add tool_call_count to State, increment on each tool call. Verify with graph.get_state(config).
Summary
LangGraph core abstractions : State + Node + Edge = StateGraph
MessagesState : built-in conversation state, Annotated[list, add_messages] auto-appends messages
Checkpoint + Thread : Thread ID = session room number, Checkpoint = snapshot archive — together enable long-term memory
Conditional Edge : tools_condition lets LLM decide tool call or end
Reducer : defines how new data merges with old
Production checkpoints : SQLite (small), Postgres (medium), Redis (high concurrency)
Next Lesson
L6 Multi-Agent Systems: From Solo to Team Collaboration — final lesson. You'll learn why complex tasks need multi-agent division of labor, master three mainstream patterns (Manager-Worker, Group Chat, Debate), and design a multi-agent architecture for a real business scenario.
Sources
src-001 — Microsoft AI Agents for Beginners · T1 · multi-agent
src-002 — Hugging Face Agents Course · T1 · Course
LangGraph Official Docs · T1 · Persistence Concepts
LangGraph Official Tutorial · T1 · Quickstart
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
