Why Build Stateful Agents with Claude API and LangGraph?
Hey there, Claude enthusiasts! If you've ever chatted with an AI only to realize it has the memory of a goldfish, you're not alone. Stateless models like basic Claude API calls reset every time, making long-term interactions frustrating. Enter LangGraph: a powerful library from the LangChain ecosystem that lets you create stateful AI agents. These bad boys remember conversations, adapt to user preferences, and handle complex workflows with persistence baked in.
In this guide, we'll build a personalized travel assistant using Claude 3.5 Sonnet via the Anthropic API. It'll remember your past trips, budget prefs, and travel style—across multiple sessions. No more repeating yourself! By the end, you'll have a running agent with checkpoints for true persistence.
Why Claude + LangGraph?
- Claude's smarts: Superior reasoning, tool use, and safety for reliable agents.
- LangGraph's power: Graphs for multi-step logic, built-in state management, and easy persistence.
- Python simplicity: Quick to prototype and deploy.
Ready to level up? Let's roll.
What You'll Build: The Stateful Travel Buddy
Our agent will:
- Greet you and learn your travel style.
- Recommend trips based on memory.
- Handle bookings/tools (simulated).
- Persist state via checkpoints—restart and it remembers.
Perfect for demos, prototypes, or production agents in travel apps, CRMs, or personal assistants.
Prerequisites: Get Set Up in 5 Minutes
Before coding:
- Anthropic API key: Grab one from console.anthropic.com. Free tier works for testing.
- Python 3.10+: Fresh virtual env recommended (
python -m venv claude-agent). - LangSmith (optional but awesome): Sign up at smith.langchain.com for tracing/debugging.
Quick Install
Fire up your terminal:
pip install langgraph langchain-anthropic langchain-core python-dotenv
Create a .env file:
ANTHROPIC_API_KEY=your_key_here
LANGCHAIN_TRACING_V2=true # Optional
LANGCHAIN_API_KEY=ls__your_langsmith_key # Optional
LANGCHAIN_PROJECT=claude-travel-agent # Optional
Boom—environment ready!
Step 1: Define Your Agent's State
State is the heart of LangGraph. We'll use a TypedDict to track messages and user prefs.
import os
from typing import TypedDict, Annotated, List
from langchain_core.messages import BaseMessage
class TravelState(TypedDict):
messages: Annotated[List[BaseMessage], "append"]
user_prefs: dict # e.g., {'budget': 'low', 'style': 'adventure'}
past_trips: List[str]
This state persists across invocations. Annotated tells LangGraph how to merge updates (e.g., append messages).
Step 2: Initialize Claude Model
Claude 3.5 Sonnet is our pick—fast, smart, and agent-friendly.
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
load_dotenv()
model = ChatAnthropic(
model="claude-3-5-sonnet-20240620",
temperature=0.7,
system="You are a helpful travel agent. Remember user prefs and past trips. Be conversational and proactive."
)
Pro tip: Tweak temperature for creativity vs. consistency.
Step 3: Build the Agent Node
Nodes are functions that read/update state. Our agent calls Claude with full context.
def agent_node(state: TravelState) -> TravelState:
response = model.invoke(state["messages"])
return {"messages": [response]}
Simple? Yes. But Claude sees all history via messages, enabling memory.
Step 4: Add Tools for Real Power
Agents shine with tools. Let's add a fake "book_flight" tool and a real memory updater.
First, define tools:
from langchain_core.tools import tool
@tool
def update_prefs(preferences: str) -> str:
"""Update user's travel preferences."""
# In prod, save to DB. Here, just log.
print(f"Updated prefs: {preferences}")
return f"Prefs updated: {preferences}"
@tool
def book_trip(destination: str, dates: str) -> str:
"""Simulate booking a trip."""
return f"Booked {destination} for {dates}! Confirmation: ABC123."
tools = [update_prefs, book_trip]
model_with_tools = model.bind_tools(tools)
Update agent:
def agent_node(state: TravelState) -> TravelState:
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
Claude auto-calls tools via bind_tools—magic!
Step 5: Conditional Edges for Smarts
Route based on output: tool call? Go to tools. Final answer? End.
from langgraph.prebuilt import ToolNode
# Tool executor
tool_node = ToolNode(tools)
def should_continue(state: TravelState):
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools" # Continue to tools
return END # Done!
Step 6: Assemble the Graph
Now, wire it up with a checkpointer for persistence.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
checkpointer = MemorySaver()
workflow = StateGraph(state_schema=TravelState)
# Add nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
# Edges
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
workflow.add_edge("tools", "agent") # Tools -> back to agent
# Compile with persistence
app = workflow.compile(checkpointer=checkpointer)
Persistence via MemorySaver—state saved by config/thread ID.
Step 7: Initialize Persistent State
Handle user prefs on first run.
def init_state(config):
return {
"user_prefs": {},
"past_trips": [],
"messages": []
}
# Or load from DB in prod
Step 8: Run Your Agent!
Interactive loop:
from langchain_core.messages import HumanMessage
config = {"configurable": {"thread_id": "travel_session_1"}} # Unique per user
while True:
user_input = input("You: ")
if user_input.lower() == "exit":
break
input_message = HumanMessage(content=user_input)
result = app.invoke({"messages": [input_message]}, config)
for m in result["messages"]:
role = "You" if isinstance(m, HumanMessage) else "Agent"
print(f"{role}: {m.content}")
Test it:
- Say: "Hi, I love budget adventure trips."
- Agent updates prefs.
- "Plan a trip to Bali."
- Remembers budget/adventure.
- Restart script—same thread_id. Ask "What's my style?"
- It remembers!
Step 9: Advanced: Custom State Updates
Enhance with a reducer node for prefs/trips.
def update_state(state: TravelState) -> TravelState:
# Parse last response for prefs/trips
last_msg = state["messages"][-1].content
if "budget" in last_msg.lower():
state["user_prefs"]["budget"] = "low" # Simplified
state["past_trips"].append("Bali")
return state
# Add to graph
workflow.add_node("updater", update_state)
workflow.add_edge("agent", "updater")
Now state evolves dynamically.
Step 10: Production Tips
- Persistent Checkpointers: Swap
MemorySaverfor Postgres/SQLite vialanggraph-checkpoint-postgres. - Streaming:
app.stream(inputs, config)for real-time responses. - Error Handling: Wrap nodes in try/except, retry with Claude's
max_tokens. - Deploy: FastAPI + Streamlit for web UI.
- Costs: Monitor via LangSmith; Sonnet is ~$3/million tokens input.
- Scale: Human-in-loop via
add_edge("human", "agent").
Full code repo? [Link to GitHub in real post]. Fork and tweak!
Common Pitfalls & Fixes
- State not persisting? Check
thread_idconsistency. - Tool errors? Ensure
tool_nodehandles failures. - Claude hallucinations? Strong system prompt + few-shot examples.
- Rate limits?
anthropic.rate_limit_headersfor monitoring.
Next Level: Multi-Agent Graphs
Scale to teams: Researcher -> Planner -> Booker. Add nodes/edges.
# Example: Add researcher node
researcher = ChatAnthropic(...).bind_tools([search_tool])
workflow.add_node("researcher", lambda state: {"messages": [researcher.invoke(state["messages"])]})
Wrapping Up
You've just built a stateful Claude agent that remembers—no more amnesia! This pattern scales to HR bots, sales CRMs, or engineering copilots. Experiment with Opus for complex reasoning or Haiku for speed.
Questions? Drop 'em in comments. Share your agents on Claude Directory!
Word count: ~1450
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.