Claude for Developers

Claude API + LangGraph: Building Stateful AI Agents with Python

Tired of stateless AI chats that forget everything? Build persistent, memory-rich agents with Claude API and LangGraph in Python—full code included for conversations that evolve over time.

J

Jennifer Yu

Workflow Automation Specialist

December 19, 2025 min read
Share:

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:

  1. Greet you and learn your travel style.
  2. Recommend trips based on memory.
  3. Handle bookings/tools (simulated).
  4. 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:

  1. Say: "Hi, I love budget adventure trips."
    • Agent updates prefs.
  2. "Plan a trip to Bali."
    • Remembers budget/adventure.
  3. 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 MemorySaver for Postgres/SQLite via langgraph-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_id consistency.
  • Tool errors? Ensure tool_node handles failures.
  • Claude hallucinations? Strong system prompt + few-shot examples.
  • Rate limits? anthropic.rate_limit_headers for 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

The #1 Newsletter in AI

Stay ahead of the AI curve

The most important updates, news, and content — delivered in one weekly newsletter.

No spam. Unsubscribe anytime. Privacy policy

Claude API
LangGraph
Python
AI Agents
Stateful Agents
ai-agents
J

About Jennifer Yu

Workflow Automation Specialist

Jennifer covers workflow strategy, no-code platforms, and clear implementation guidance for teams adopting automation.

Comments (0)