Claude for Developers

Claude API + LangGraph: Build Stateful AI Agents in Python

Discover how to build powerful, stateful AI agents using Claude API and LangGraph. This guide provides step-by-step Python code for persistent, multi-step automation that retains context across intera

A

Andrew Snyder

AI & Automation Editor

December 26, 2025 min read
Share:

Why Claude API + LangGraph for Stateful AI Agents?

Claude models from Anthropic excel in reasoning, safety, and handling complex instructions, making them ideal for AI agents. LangGraph, an extension of the LangChain ecosystem, enables building resilient, stateful multi-actor applications as graphs. Unlike stateless chains, LangGraph supports cycles, branching, and persistence, perfect for agents that maintain context over long-running tasks like research, planning, or customer support.

This tutorial walks you through creating a persistent conversational research agent using Claude 3.5 Sonnet via the Anthropic API. The agent will:

  • Accept user queries (e.g., "Research Python web frameworks").
  • Break tasks into steps.
  • Simulate tool calls (search, summarize).
  • Persist state across sessions using LangGraph's checkpointer.

By the end, you'll have a production-ready agent handling multi-turn interactions with full memory.

Prerequisites

  • Python 3.10+
  • Anthropic API key (get one at console.anthropic.com)
  • Basic familiarity with LangChain concepts

Set your API key as an environment variable:

export ANTHROPIC_API_KEY='your-api-key-here'

Installation

Install the required packages:

pip install langgraph langchain-anthropic langchain-core langchain-community pydantic
  • langgraph: Core library for graphs.
  • langchain-anthropic: Claude integration.
  • langchain-core: Shared abstractions.
  • pydantic: For state schemas.

Defining the Agent State

LangGraph uses a typed state to track progress. For our research agent, we'll use a message-based state with conversation history.

import operator
from typing import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]

This state appends messages immutably, enabling persistence.

Setting Up the Claude Model

Initialize Claude 3.5 Sonnet, optimized for agentic workflows:

from langchain_anthropic import ChatAnthropic

model = ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0)

Claude's large context window (200K tokens) shines here, handling extensive histories without truncation issues common in other models.

Defining Agent Nodes

Nodes are functions that update state. We'll create:

  • call_model: Invokes Claude to decide actions.
  • tool_node: Simulates research tools.
  • should_continue: Router to loop or end.

First, bind tools to the model (using LangChain's tool-calling format, which Claude supports natively):

def research_tool(state):
    """Simulate web research."""
    last_message = state['messages'][-1].content
    return "Research results: [Dummy data on " + last_message + "]: Django and FastAPI are top Python frameworks...]"

def summarize_tool(state):
    """Summarize findings."""
    return "Summary: Django for full-stack, FastAPI for APIs."

from langchain_core.tools import tool

@tool
def research(query: str) -> str:
    """Conduct research on a topic."""
    return research_tool({'messages': [type('msg', (), {'content': query})()]})

@tool
def summarize_findings(text: str) -> str:
    """Summarize research text."""
    return summarize_tool({'messages': [type('msg', (), {'content': text})()]})

tools = [research, summarize_findings]
model_with_tools = model.bind_tools(tools)

Now, the nodes:

def call_model(state):
    messages = state['messages']
    response = model_with_tools.invoke(messages)
    return {"messages": [response]}

def tool_node(state):
    outputs = []
    for tool_call in state["messages"][-1].tool_calls:
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]
        if tool_name == "research":
            output = research(tool_args["query"])
        elif tool_name == "summarize_findings":
            output = summarize_findings(tool_args["text"])
        tool_message = {
            "role": "tool",
            "tool_call_id": tool_call["id"],
            "content": output
        }
        outputs.append(tool_message)
    return {"messages": outputs}

def should_continue(state):
    last_message = state['messages'][-1]
    if last_message.tool_calls:
        return "tools"
    return END

Compiling the Stateful Graph

Connect nodes into a graph with persistence:

checkpointer = MemorySaver()

workflow = StateGraph(state_schema=AgentState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
workflow.add_edge("tools", "agent")

app = workflow.compile(checkpointer=checkpointer)

MemorySaver persists state in memory (use SqliteSaver for disk persistence in production).

Running the Agent with Persistence

Interact via thread_id for stateful sessions:

config = {"configurable": {"thread_id": "agent-thread-1"}}

# First interaction
input_message = {"messages": [("user", "Research top Python web frameworks.")]}
for chunk in app.stream(input_message, config, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

# Subsequent interaction - state persists!
input_message = {"messages": [("user", "Summarize the findings and compare Django vs FastAPI.")]}
for chunk in app.stream(input_message, config, stream_mode="values"):
    chunk["messages"][-1].pretty_print()

Output example:

================================ Run 1 ================================ 
User: Research top Python web frameworks.
Assistant: I need to research top Python web frameworks. [Invokes research tool]

================================ Tool ================================= 
Research results: [Dummy data... Django and FastAPI...]

================================ Run 2 ================================ 
User: Summarize... 
Assistant: [Uses prior context] Django excels in batteries-included apps...

Claude retains full history, reasoning over past tools seamlessly.

Advanced Features

Custom Tools with Real APIs

Replace dummies with real tools, e.g., Tavily search:

pip install tavily-python
import os
os.environ["TAVILY_API_KEY"] = "your-key"

from tavily import TavilyClient
client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))

@tool
def real_research(query: str) -> str:
    results = client.search(query)
    return str(results)

Claude's tool-calling precision outperforms GPT-4o in benchmarks for structured outputs.

Human-in-the-Loop

Add approval nodes:

def human_review(state):
    print("Approve? (y/n)")
    return {"messages": [("human", input())]}

workflow.add_node("human", human_review)
workflow.add_conditional_edges("agent", lambda s: "human" if "review" in s['messages'][-1].content else "tools")

Streaming and Async

For production, use astream:

async for chunk in app.astream(input_message, config):
    # Handle real-time UI updates
    pass

Deployment

  • LangGraph Platform: Host on LangGraph Cloud.
  • FastAPI: Wrap in an API server.
from fastapi import FastAPI
app = FastAPI()
@app.post("/invoke")
def invoke(body: dict):
    return app.invoke(body["input"], {"configurable": {"thread_id": body["thread_id"]}})

Best Practices for Claude Agents

  • Prompt Engineering: Use XML tags for Claude: <thinking>reason</thinking><action>tool</action>.
  • Error Handling: Wrap nodes in try-except, retry with Claude's max_tokens.
  • State Pruning: Compress history with Claude summaries to fit context.
  • Monitoring: Log checkpoints for debugging.
  • Cost Optimization: Use Haiku for simple nodes, Sonnet for reasoning.

Real-World Use Cases

  • HR: Resume screening agent with persistent applicant state.
  • Sales: Lead qualification bot remembering prior calls.
  • Engineering: Code review agent tracking iterations.
FeatureClaude + LangGraphGPT + LangGraph
Context200K tokens128K
Tool Precision95%+90%
SafetyConstitutional AIGuardrails needed

Conclusion

You've built a stateful research agent leveraging Claude's strengths and LangGraph's flexibility. Extend it with MCP servers for native tools or integrate into n8n/Zapier. Check Anthropic's API docs for updates.

Source code: [GitHub repo link placeholder]

Experiment—fork and deploy your agent today!

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
AI Agents
Python
Stateful Agents
ai-agents
A

About Andrew Snyder

AI & Automation Editor

Andrew covers practical AI automation, workflow design, and the tools teams use to streamline everyday operations.

Comments (0)