A story for every builder who has stared at a blank Python file and wondered: “Where do I even begin?”
- The Day My First Agent Broke in Production
- Chapter 1: What Is LangGraph — And Why Should You Care?
- Chapter 2: Your First Real Graph — State, Nodes, and Edges
- Chapter 3: Adding Tools and Conditional Routing
- Chapter 4: Memory and Persistence with Checkpointers
- Chapter 5: Human-in-the-Loop — The Safety Net
- Chapter 6: Enter Deep Agents — The Harness Level
- Chapter 7: The Architecture Mental Model
- Chapter 8: What Makes This “Deep”?
- Chapter 9: Production Checklist Before You Ship
- The Lesson I Wish I’d Known Earlier
- Resources
The Day My First Agent Broke in Production
Let me take you back to a Monday morning. I had just shipped what I thought was a beautiful AI agent — it answered questions, called APIs, even had a nice streaming UI. By Tuesday afternoon, it was dead. It had lost track of its own conversation, forgotten what tools it had already used, and looped itself into oblivion on a complex multi-step task.
The real problem wasn’t the model. The model was smart enough. The problem was I had no framework for orchestrating the agent’s thinking — no shared memory, no controlled routing between steps, no way to pause for human review. I had built a racecar with no steering wheel.
That’s when I found LangGraph. And more recently — LangGraph’s Deep Agents harness.
This guide walks you through every concept you need, with working code at each step. By the end, you’ll have a fully functional deep research agent that plans tasks, delegates to subagents, and remembers its work across sessions.
Chapter 1: What Is LangGraph — And Why Should You Care?
Before we write a single line of code, you need to understand the mental model.
LangGraph is a low-level orchestration framework for building stateful, long-running agents. Trusted by companies like Klarna, Uber, and J.P. Morgan, it gives you precise control over how your agent thinks and moves through a problem.
The key idea is elegant: your agent’s behavior is a graph.
Every agent you build has three moving parts:
- State — a shared data structure representing a snapshot of everything the agent knows right now.
- Nodes — functions that do the actual work: calling an LLM, running a tool, grading a result.
- Edges — the routing logic that decides what happens next. They can be fixed transitions or conditional branches based on the current state.
“Nodes do the work. Edges tell what to do next.” — LangGraph Graph API docs
This is fundamentally different from a chain or a simple prompt loop. In LangGraph, the agent can cycle back, branch to a different path, pause for a human, or delegate to a subagent — all in a structured, observable way.
And sitting on top of LangGraph is the newer Deep Agents harness — a batteries-included layer that adds built-in planning, a virtual filesystem, subagent spawning, and long-term memory. Think of it like this:
| Layer | Role |
|---|---|
| LangGraph | Orchestration runtime — durable execution, streaming, human-in-the-loop |
| LangChain | Agent framework — models, tools, agent loops |
| Deep Agents | Agent harness — planning, subagents, context management |
| LangSmith | Observability — tracing, evaluation, debugging |
We’ll build from the bottom up — starting with a raw LangGraph graph, then upgrading to Deep Agents patterns.
Chapter 2: Your First Real Graph — State, Nodes, and Edges
Install the dependencies:
pip install langgraph langchain-anthropic
Now let’s build the simplest possible agent: one that receives a message and responds.
Step 1: Define Your State
State is the backbone. Everything your agent knows — messages, intermediate results, flags — lives here.
from typing import TypedDict
from langchain.messages import AnyMessage
class AgentState(TypedDict):
messages: list[AnyMessage]
task_complete: bool
Reference: Define state — LangGraph Graph API
Step 2: Define Your Nodes
Each node is a plain Python function. It receives the current state and returns updates to the state.
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
def call_llm(state: AgentState) -> AgentState:
"""Node: call the LLM with current message history."""
response = model.invoke(state["messages"])
return {"messages": state["messages"] + [response]}
def check_complete(state: AgentState) -> AgentState:
"""Node: mark task as complete (simplified)."""
return {"task_complete": True}
Step 3: Wire the Graph
from langgraph.graph import START, END, StateGraph
builder = StateGraph(AgentState)
# Add nodes
builder.add_node("call_llm", call_llm)
builder.add_node("check_complete", check_complete)
# Add edges
builder.add_edge(START, "call_llm")
builder.add_edge("call_llm", "check_complete")
builder.add_edge("check_complete", END)
graph = builder.compile()
Step 4: Run It
from langchain.messages import HumanMessage
result = graph.invoke({
"messages": [HumanMessage(content="What is LangGraph?")],
"task_complete": False
})
print(result["messages"][-1].content)
That’s your first graph. Four steps, a working agent. But this one can’t use tools, remember anything across sessions, or route conditionally. Let’s fix that.
Chapter 3: Adding Tools and Conditional Routing
Real agents don’t just chat — they act. Let’s add tool calling and teach the graph to route based on whether the model wants to use a tool.
Define Tools
from langchain_core.tools import tool
@tool
def web_search(query: str) -> str:
"""Search the web for current information."""
# In production, hook up to Tavily, SerpAPI, etc.
return f"Search results for: {query} — [placeholder result]"
@tool
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
tools = [web_search, calculator]
Bind Tools to the Model
model_with_tools = model.bind_tools(tools)
Add a ToolNode and Conditional Router
from langgraph.graph import START, END, StateGraph
from langgraph.prebuilt import ToolNode
from langchain.messages import AnyMessage
from typing import Literal
def agent_node(state: AgentState):
response = model_with_tools.invoke(state["messages"])
return {"messages": state["messages"] + [response]}
def route_after_agent(state: AgentState) -> Literal["tools", "__end__"]:
"""Conditional edge: go to tools if the model made tool calls, else end."""
last_message = state["messages"][-1]
if getattr(last_message, "tool_calls", None):
return "tools"
return "__end__"
tool_node = ToolNode(tools)
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", route_after_agent)
builder.add_edge("tools", "agent") # loop back after tool use
graph = builder.compile()
Reference: Agents — LangGraph workflows
Now your agent can loop: it calls the model, decides to use a tool, executes the tool, passes results back to the model, and continues until it’s done. This is the ReAct loop — the foundation of most production agents.
Chapter 4: Memory and Persistence with Checkpointers
Here’s where most tutorial agents fail: they forget everything between runs.
LangGraph solves this with checkpointers — a persistence layer that saves your agent’s state at every step. Resume a paused run, recover from a crash, or let a human review mid-task.
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
Now invoke with a thread_id to maintain session continuity:
config = {"configurable": {"thread_id": "user-session-001"}}
# First message
result = graph.invoke(
{"messages": [HumanMessage(content="My name is Satish. Remember that.")], "task_complete": False},
config=config
)
# Second message — same thread, same memory
result2 = graph.invoke(
{"messages": result["messages"] + [HumanMessage(content="What is my name?")]},
config=config
)
print(result2["messages"][-1].content)
# → "Your name is Satish."
Reference: Using in LangGraph — Persistence
For production, swap InMemorySaver for a Redis or PostgreSQL checkpointer. The API is identical — only the backend changes.
Chapter 5: Human-in-the-Loop — The Safety Net
An autonomous agent making decisions at scale is powerful. An autonomous agent making decisions without any oversight is a liability — especially in FSI or regulated environments.
LangGraph’s interrupt() lets you pause an agent mid-graph and wait for human input before continuing.
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict
class ReviewState(TypedDict):
task: str
draft_output: str
approved: bool
def draft_node(state: ReviewState):
# Simulate the agent drafting something
return {"draft_output": f"Draft response to: {state['task']}"}
def human_review_node(state: ReviewState):
# Pause here and surface the draft to a human
decision = interrupt({
"draft": state["draft_output"],
"instruction": "Approve or edit this output before we proceed."
})
return {"approved": decision.get("approved", False)}
def finalize_node(state: ReviewState):
if state["approved"]:
return {"draft_output": f"[APPROVED] {state['draft_output']}"}
return {"draft_output": "[REJECTED — needs revision]"}
checkpointer = InMemorySaver()
review_graph = (
StateGraph(ReviewState)
.add_node("draft", draft_node)
.add_node("human_review", human_review_node)
.add_node("finalize", finalize_node)
.add_edge(START, "draft")
.add_edge("draft", "human_review")
.add_edge("human_review", "finalize")
.add_edge("finalize", END)
.compile(checkpointer=checkpointer)
)
config = {"configurable": {"thread_id": "review-001"}}
# Run to the interrupt
review_graph.invoke({"task": "Write quarterly summary", "draft_output": "", "approved": False}, config)
# Human approves — resume
review_graph.invoke(Command(resume={"approved": True}), config)
Reference: Testing the agent — human-in-the-loop
This pattern maps directly onto governance gates in regulated industries: the agent drafts, a human reviews, execution continues only on explicit approval.
Chapter 6: Enter Deep Agents — The Harness Level
Now we level up. Deep Agents is the highest-level abstraction in the LangChain stack — an agent harness built on LangGraph that adds:
- Built-in planning tools — the agent can decompose complex tasks into steps
- Virtual filesystem — agents read and write files across long runs
- Subagent spawning — delegate subtasks to specialist agents running in isolated context windows
- Long-term memory — update and retrieve knowledge across sessions
“deepagents is a standalone library built on top of LangChain’s core building blocks for agents. It uses the LangGraph runtime for durable execution, streaming, human-in-the-loop, and other features.” — Deep Agents overview
Install:
pip install deepagents langchain-anthropic
Building a Deep Research Agent
Here’s a complete, testable example — a coordinator agent that plans a research task, delegates to a web-search subagent and a summarizer subagent, then synthesizes the final answer.
# deep_research_agent.py
from deepagents import create_deep_agent, SubAgent
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
# ─── Model ───────────────────────────────────────────────
model = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0)
# ─── Tools ───────────────────────────────────────────────
@tool
def web_search(query: str) -> str:
"""Search the web for information on a given topic."""
# Wire to Tavily or SerpAPI in production
return f"[Search results for '{query}']: LangGraph was released by LangChain in 2024. It is a stateful agent orchestration framework built on a graph model with nodes, edges, and shared state."
@tool
def summarize_text(text: str) -> str:
"""Summarize a block of text into key bullet points."""
# In production, call the model here
return f"Summary: {text[:200]}..."
# ─── Subagents ────────────────────────────────────────────
# The Researcher subagent: specialized in web search
researcher = SubAgent(
name="researcher",
description="Searches the web and retrieves relevant information on any topic. Use this for fact-finding tasks.",
tools=[web_search],
model=model,
)
# The Summarizer subagent: specialized in distillation
summarizer = SubAgent(
name="summarizer",
description="Takes raw text or search results and produces clean, structured summaries. Use this after research is complete.",
tools=[summarize_text],
model=model,
)
# ─── Coordinator (Deep Agent) ─────────────────────────────
checkpointer = InMemorySaver()
agent = create_deep_agent(
model=model,
subagents=[researcher, summarizer],
system_prompt="""You are a deep research coordinator.
When given a topic, you:
1. Plan which subtasks are needed
2. Delegate research to the researcher subagent
3. Delegate summarization to the summarizer subagent
4. Synthesize a final, structured answer
Always produce outputs in clear markdown with headings.""",
checkpointer=checkpointer,
)
# ─── Run ──────────────────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "research-session-001"}}
result = agent.invoke(
{"messages": [{"role": "user", "content": "Research how LangGraph works and give me a structured summary."}]},
config=config
)
# Print the final coordinator message
for message in result["messages"]:
if hasattr(message, "content") and message.content:
print(message.content)
Run it:
python deep_research_agent.py
Reference: Deep Agents overview, Subagents
Chapter 7: The Architecture Mental Model
Before you ship any of this to production, internalize this architecture. Deep Agents use a coordinator-worker model:
User Message
│
▼
┌─────────────────────────────┐
│ COORDINATOR (Deep Agent) │ ← Plans tasks, routes to subagents
│ - Receives user input │
│ - Decides delegation │
└────────┬───────────┬────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Researcher │ │ Summarizer │ ← Isolated context windows
│ Subagent │ │ Subagent │
└──────────────┘ └──────────────┘
│ │
└─────┬─────┘
▼
┌─────────────────┐
│ Final Synthesis │ ← Coordinator assembles final answer
└─────────────────┘
Reference: Architecture — Deep Agents frontend
Each subagent runs in its own isolated context window. This means:
- No context pollution between specialists
- Each subagent can run longer, focused tasks
- You can parallelize subagents for speed
- Memory and state are cleanly separated per agent
Chapter 8: What Makes This “Deep”?
You might ask: isn’t this just multi-agent? What’s the deep part?
The depth comes from the harness capabilities that LangGraph alone doesn’t give you out of the box:
Context management across long runs. A research task might span 50 tool calls and thousands of tokens. Deep Agents automatically summarize history and offload large results to the virtual filesystem so the agent never hits context limits mid-task.
Subagent isolation. Each specialist runs fresh — no shared message history. This is critical for reliability: the summarizer doesn’t need to know the researcher’s entire search history; it just needs the results.
Planning tools built in. The coordinator can use built-in planning capabilities to decompose “research LangGraph for my blog post” into: search → collect → summarize → structure → draft. This planning step is what separates a simple loop from a genuine reasoning agent.
Memory that persists. Lessons learned, user preferences, domain knowledge — all storable and retrievable across sessions using InMemorySaver in dev or LangGraph Store in production.
Chapter 9: Production Checklist Before You Ship
You’ve built your agent. Here’s what separates a demo from a production-grade deployment:
1. Swap InMemorySaver for a persistent checkpointer. Use Redis or PostgreSQL for langgraph-checkpoint-redis or langgraph-checkpoint-postgres. The compile interface is identical.
2. Add retry policies on fragile nodes.
builder.add_node(
"web_search_node",
search_node_fn,
{"retry_policy": {"max_attempts": 3}}
)
3. Instrument with LangSmith. Set your env vars and every graph invocation is traced automatically:
export LANGSMITH_API_KEY=your_key
export LANGSMITH_TRACING=true
4. Add human-in-the-loop gates for high-stakes actions. Any node that sends emails, modifies data, or calls external APIs should have an interrupt() gate before execution.
5. Test subagent namespace isolation. If you’re running multiple subagents in parallel, ensure each has a unique node name to prevent checkpoint collisions.
Reference: Multiple subgraph calls
The Lesson I Wish I’d Known Earlier
When my first agent broke on that Tuesday, I didn’t need a smarter model. I needed a smarter structure.
LangGraph gives you that structure: a graph that is observable, resumable, and testable at every node. Deep Agents adds the harness that makes complex, multi-step, multi-agent workflows practical to build and maintain.
The pattern we’ve walked through — State → Nodes → Edges → Tools → Checkpointer → Human Gate → Subagents — is the same pattern running inside production agents at enterprise scale today.
Start with the simple graph. Add tools. Add memory. Add governance gates. Then, when your task is complex enough to need specialists, introduce subagents. Don’t over-engineer day one. The graph scales with your ambition.
Resources
- LangGraph Python Overview
- Graph API — Nodes, Edges, State
- Use Graph API — Sequences
- Persistence & Checkpointers
- Human-in-the-Loop (interrupt)
- Deep Agents Overview (Python)
- Deep Agents — Subagents
- Agentic RAG with LangGraph
- Trace with LangSmith
Built with verified LangChain documentation. All code examples are production-compatible with LangGraph’s current API. Install requirements: pip install langgraph langchain-anthropic deepagents langsmith










