Module 11: Deep Agents

create_agent vs LangGraph vs Deep Agents: The Decision Tree

Capsule overview

This is the most important capsule of the module. Possibly the most important one in the whole guide.

After 11 modules, you have command of three levels of abstraction for building agents. You can use create_agent to solve 80% of problems in 15 lines of code. You can use LangGraph for complex workflows where you need control over every node, edge, and condition. And now you can use Deep Agents for long-running autonomous tasks with built-in planning, filesystem, and subagents.

The problem isn't that you don't know how to use each tool — it's that in a real project, you have to decide which one to use. And that decision affects everything: development time, maintainability, execution costs, and how debuggable the thing is.

This capsule gives you a definitive decision framework. Not opinions — technical criteria with explicit trade-offs.


The three levels of abstraction

Quick view

LevelToolLines of codeControlBest for
Highcreate_agent~10-20Limited (ReAct loop)80% of agent tasks
MediumLangGraph~50-200Total (custom workflow)Complex workflows, HITL
BatteriesDeep Agents~20-50Moderate (the framework decides)Long autonomous tasks

Level 1: create_agent — simple, fast, enough

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults

model = ChatOpenAI(model="gpt-4.1-mini")
tools = [TavilySearchResults(max_results=3)]

agent = create_agent(model, tools)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What is RAG and why does it matter?"}]}
)

print(result["messages"][-1].content[:200])
# Expected output (varies by model):
# RAG (Retrieval-Augmented Generation) is a technique that combines text
# generation with retrieving information from external sources...

What it does for you:

  • ✅ Binds the tools to the model
  • ✅ The ReAct loop (reason → act → observe → repeat)
  • ✅ Automatic tool-calling handling

What it does NOT do:

  • ❌ Custom workflow (you can't control the execution order)
  • ❌ Persistence (it's gone when the run ends)
  • ❌ Conditional branching
  • ❌ Multi-agent
  • ❌ HITL

Lines of code: ~15

Level 2: LangGraph — full control, you design everything

from dotenv import load_dotenv
load_dotenv()

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from typing import TypedDict, Annotated
import operator

class ResearchState(TypedDict):
    query: str
    messages: Annotated[list, operator.add]
    sources: list[dict]
    analysis: str
    report: str
    quality_score: float

model = ChatOpenAI(model="gpt-4.1-mini")
search = TavilySearchResults(max_results=5)

def search_node(state: ResearchState) -> dict:
    results = search.invoke(state["query"])
    sources = [{"title": r.get("title", ""), "content": r.get("content", "")[:500]} for r in results]
    return {
        "sources": sources,
        "messages": [{"role": "system", "content": f"Found {len(sources)} sources"}],
    }

def analyze_node(state: ResearchState) -> dict:
    sources_text = "\n".join(s["content"][:200] for s in state["sources"])
    response = model.invoke(f"Analyze these sources about '{state['query']}':\n{sources_text}")
    return {
        "analysis": response.content,
        "messages": [{"role": "system", "content": "Analysis complete"}],
    }

def report_node(state: ResearchState) -> dict:
    response = model.invoke(
        f"Generate a report about '{state['query']}' based on:\n{state['analysis']}"
    )
    return {
        "report": response.content,
        "quality_score": 0.85,
        "messages": [{"role": "system", "content": "Report generated"}],
    }

def route_by_quality(state: ResearchState) -> str:
    if state.get("quality_score", 0) < 0.7:
        return "search"
    return END

builder = StateGraph(ResearchState)
builder.add_node("search", search_node)
builder.add_node("analyze", analyze_node)
builder.add_node("report", report_node)
builder.add_edge(START, "search")
builder.add_edge("search", "analyze")
builder.add_edge("analyze", "report")
builder.add_conditional_edges("report", route_by_quality, {"search": "search", END: END})

graph = builder.compile(checkpointer=MemorySaver())

result = graph.invoke(
    {"query": "The state of RAG in 2025", "messages": [], "sources": [], "analysis": "", "report": "", "quality_score": 0.0},
    config={"configurable": {"thread_id": "research-001"}},
)

print(f"Sources found: {len(result['sources'])}")
print(f"Quality score: {result['quality_score']}")
print(f"Report length: {len(result['report'])} chars")
# Expected output (varies by model):
# Sources found: 5
# Quality score: 0.85
# Report length: ~1500 chars

What it does for you:

  • ✅ Typed state with TypedDict
  • ✅ Nodes and edges you define yourself
  • ✅ Conditional branching (route_by_quality)
  • ✅ Checkpointing and persistence
  • ✅ Retry, error handling, HITL
  • ✅ Multi-agent (supervisors, subgraphs)

What it does NOT do:

  • ❌ Automatic planning (you define the workflow)
  • ❌ Built-in filesystem
  • ❌ Dynamic subagent spawning

Lines of code: ~70 (simplified example), ~150+ for a complete system

Level 3: Deep Agents — autonomous, batteries-included

from dotenv import load_dotenv
load_dotenv()

from deep_agents import create_deep_agent
from langchain_community.tools import TavilySearchResults

web_search = TavilySearchResults(max_results=5)

agent = create_deep_agent(
    "openai:gpt-4.1",
    tools=[web_search],
    name="Research Assistant",
    instructions=(
        "Research complex topics. Break the research into steps, "
        "write findings into separate files per source, "
        "delegate specialized searches to subagents, "
        "and generate a consolidated final report."
    ),
)

result = agent.run("Research the state of RAG in 2025 and generate a report")

print(f"Files generated: {list(result.files.keys())}")
print(f"Todos completed: {sum(1 for t in result.todos if t['status'] == 'completed')}/{len(result.todos)}")
print(f"Subagents used: {len(result.subagents_spawned)}")
# Expected output (varies by model):
# Files generated: ['research/web_search.md', 'analysis/synthesis.md', 'output/report.md']
# Todos completed: 5/5
# Subagents used: 2

What it does for you:

  • ✅ Automatic planning with write_todos
  • ✅ Virtual filesystem for context offloading
  • ✅ Dynamic subagent spawning
  • ✅ Long-term memory with pluggable backends
  • ✅ Built-in CLI

What it does NOT do:

  • ❌ Fine-grained control over each step (the framework decides the flow)
  • ❌ Custom conditional branching
  • ❌ Granular HITL (you approve the task, not each step)

Lines of code: ~20-40


The decision tree

This is the framework you take with you. Print it, memorize it, or pull it up whenever you start a new project.

Does your agent need to use tools?
  └─ NO → You don't need an agent. Use the model directly.
  └─ YES ↓

Is it a simple pattern? (Q&A with tools, chatbot, direct search)
  └─ YES → create_agent (~15 lines, done)
  └─ NO ↓

Do you need full control over the decision flow?
  (conditional branching, retry with custom logic, HITL at specific points)
  └─ YES → LangGraph (StateGraph or the Functional API)
  └─ NO ↓

Is the task autonomous, long-running, and multi-step?
  (research, code generation, complex analysis)
  └─ YES → Deep Agents
  └─ NO ↓

Do you need multi-agent with fixed roles and explicit coordination?
  └─ YES → LangGraph (supervisors, handoffs, subgraphs from M10)
  └─ NO ↓

Do you need dynamic multi-agent where the agent decides what to delegate?
  └─ YES → Deep Agents (subagent spawning)
  └─ NO → LangGraph (more flexible than Deep Agents, without the planning overhead)

Simplified version for quick reference

┌─────────────────────────────────────────────────────────────┐
│                   Which agent do I need?                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Simple (Q&A, chatbot, search)      →  create_agent         │
│                                                             │
│  Custom workflow (branching,        →  LangGraph            │
│  HITL, retry, fixed roles)                                  │
│                                                             │
│  Autonomous + planning + delegation →  Deep Agents          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Side-by-side: the same Research Agent in 3 versions

The best test of a decision framework is applying it to the same problem. Here's the same research agent implemented at all three levels.

Version 1: create_agent (~15 lines)

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults

model = ChatOpenAI(model="gpt-4.1-mini")
tools = [TavilySearchResults(max_results=5)]

agent = create_agent(
    model,
    tools,
    prompt="You are a research assistant. Find relevant information and generate clear answers.",
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Research the state of AI agents in 2025"}]}
)

print(f"Lines of code: ~15")
print(f"Answer: {result['messages'][-1].content[:100]}...")
# Expected output:
# Lines of code: ~15
# Answer: AI agents in 2025 have evolved significantly...
  • ✅ Works right away
  • ✅ Searches with TavilySearch
  • ❌ Doesn't plan — it searches and answers in a single step
  • ❌ No persistence — it's gone when the run ends
  • ❌ No organization of findings — everything lives in the context window

Version 2: LangGraph (~150 lines)

from dotenv import load_dotenv
load_dotenv()

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from typing import TypedDict, Annotated
import operator
import json

class ResearchState(TypedDict):
    query: str
    messages: Annotated[list, operator.add]
    plan: list[str]
    sources: list[dict]
    analysis: str
    report: str
    iteration: int
    max_iterations: int
    quality_score: float

model_mini = ChatOpenAI(model="gpt-4.1-mini")
model_full = ChatOpenAI(model="gpt-4.1")
search = TavilySearchResults(max_results=5)

def plan_node(state: ResearchState) -> dict:
    response = model_mini.invoke(
        f"Break this research down into 3-5 concrete steps. "
        f"Topic: {state['query']}. Answer ONLY with a JSON array of strings."
    )
    try:
        plan = json.loads(response.content)
    except json.JSONDecodeError:
        plan = ["Search for sources", "Analyze the information", "Generate the report"]
    return {
        "plan": plan,
        "messages": [{"role": "system", "content": f"Plan: {len(plan)} steps"}],
    }

def search_node(state: ResearchState) -> dict:
    results = search.invoke(state["query"])
    sources = []
    for r in results:
        sources.append({
            "title": r.get("title", "Unknown"),
            "content": r.get("content", "")[:500],
            "url": r.get("url", ""),
        })
    return {
        "sources": sources,
        "messages": [{"role": "system", "content": f"Found {len(sources)} sources"}],
    }

def analyze_node(state: ResearchState) -> dict:
    sources_text = "\n\n".join(
        f"**{s['title']}**: {s['content']}" for s in state["sources"]
    )
    response = model_full.invoke(
        f"Analyze these sources about '{state['query']}'.\n\n{sources_text}\n\n"
        f"Identify: main patterns, contradictions, and key insights."
    )
    return {
        "analysis": response.content,
        "messages": [{"role": "system", "content": "Analysis complete"}],
    }

def report_node(state: ResearchState) -> dict:
    response = model_full.invoke(
        f"Generate a research report about '{state['query']}'.\n\n"
        f"Analysis: {state['analysis']}\n\n"
        f"Sources: {len(state['sources'])}\n\n"
        f"The report must have: Executive Summary, Findings, Conclusions, Sources."
    )
    return {
        "report": response.content,
        "quality_score": 0.85,
        "iteration": state.get("iteration", 0) + 1,
        "messages": [{"role": "system", "content": "Report generated"}],
    }

def evaluate_quality(state: ResearchState) -> str:
    if state.get("quality_score", 0) < 0.7 and state.get("iteration", 0) < state.get("max_iterations", 3):
        return "search"
    return "end"

builder = StateGraph(ResearchState)
builder.add_node("plan", plan_node)
builder.add_node("search", search_node)
builder.add_node("analyze", analyze_node)
builder.add_node("report", report_node)

builder.add_edge(START, "plan")
builder.add_edge("plan", "search")
builder.add_edge("search", "analyze")
builder.add_edge("analyze", "report")
builder.add_conditional_edges("report", evaluate_quality, {"search": "search", "end": END})

checkpointer = MemorySaver()
store = InMemoryStore()
graph = builder.compile(checkpointer=checkpointer, store=store)

result = graph.invoke(
    {
        "query": "The state of AI agents in 2025",
        "messages": [],
        "plan": [],
        "sources": [],
        "analysis": "",
        "report": "",
        "iteration": 0,
        "max_iterations": 3,
        "quality_score": 0.0,
    },
    config={"configurable": {"thread_id": "research-002"}},
)

print(f"Lines of code: ~150")
print(f"Plan: {len(result['plan'])} steps")
print(f"Sources: {len(result['sources'])}")
print(f"Quality: {result['quality_score']}")
print(f"Report length: {len(result['report'])} chars")
# Expected output (varies by model):
# Lines of code: ~150
# Plan: 4 steps
# Sources: 5
# Quality: 0.85
# Report length: ~2000 chars
  • ✅ Explicit planning (you designed plan_node)
  • ✅ Retry with a quality evaluation (conditional edge)
  • ✅ Persistence with a checkpointer
  • ✅ Different models per node (mini for planning, full for analysis)
  • ❌ You wrote every node and edge — ~150 lines
  • ❌ No filesystem (everything lives in state)
  • ❌ No subagent spawning (the flow is fixed)

Version 3: Deep Agents (~40 lines)

from dotenv import load_dotenv
load_dotenv()

from deep_agents import create_deep_agent
from deep_agents.memory import FilesystemMemoryBackend
from langchain_community.tools import TavilySearchResults

web_search = TavilySearchResults(max_results=5)

memory = FilesystemMemoryBackend(base_path="./research_memory")

agent = create_deep_agent(
    "openai:gpt-4.1",
    tools=[web_search],
    name="Research Assistant",
    instructions=(
        "You are a research assistant. "
        "For every topic: "
        "1) Break the research into steps with write_todos. "
        "2) Search diverse sources (academic, industry, news). "
        "3) Write the findings from each source into separate files. "
        "4) Delegate specialized searches to subagents when the topic is broad. "
        "5) Synthesize and generate a final report at output/report.md."
    ),
    memory=memory,
    max_iterations=15,
)

result = agent.run("Research the state of AI agents in 2025 and generate a detailed report")

print(f"Lines of code: ~40")
print(f"Files: {list(result.files.keys())}")
print(f"Todos: {sum(1 for t in result.todos if t['status'] == 'completed')}/{len(result.todos)}")
print(f"Subagents: {len(result.subagents_spawned)}")
# Expected output (varies by model):
# Lines of code: ~40
# Files: ['research/web_search.md', 'research/academic.md', 'analysis/synthesis.md', 'output/report.md']
# Todos: 5/5
# Subagents: 2
  • ✅ Automatic planning (write_todos)
  • ✅ Virtual filesystem (organized files)
  • ✅ Subagent spawning (dynamic delegation)
  • ✅ Memory across sessions
  • ❌ You don't control the exact flow
  • ❌ You don't define conditional branching
  • ❌ Debugging is more opaque

The full comparison table

Aspectcreate_agentLangGraphDeep Agents
Lines of code~15~50-200~20-50
PlanningNoneYou design itAutomatic (write_todos)
WorkflowFixed ReAct loopCustom (you define nodes/edges)The framework decides
PersistenceNone (or +checkpointer)Built-in checkpointerVirtual filesystem
Multi-agentNoYou implement itAutomatic subagent spawning
HITLNointerrupt() at exact pointsApproval of the overall task
MemoryNoManual Store (M8)Configurable backend
BranchingNoConditional edgesThe framework decides
RetryNoYou implement itAutomatic (re-planning)
DebuggingDirect (model logs)State inspection per nodePlanning logs + files
Cost per runLow (1-3 LLM calls)Medium (5-15 LLM calls)High (10-30+ LLM calls)
Setup time5 minutes30-60 minutes10 minutes
CLIYou build itYou build itBuilt-in
Best forChatbots, Q&A, searchBusiness workflows, pipelinesResearch, long analysis

What each level abstracts away

Understanding this is key to choosing well. Each level abstracts away things the previous one forces you to build.

create_agent abstracts:

- The reason → act → observe → repeat loop
- Binding tools to the model
- Parsing tool calls and executing them

You still handle: state, flow, persistence, multi-agent, HITL.

LangGraph abstracts:

- Typed state, reachable from every node
- Execution flow with edges (sequential, conditional, parallel)
- Persistence with checkpointers
- Interrupt/resume for HITL
- Subgraphs for multi-agent

You still handle: planning, filesystem, dynamic subagent spawning, cross-session memory.

Deep Agents abstracts:

- Planning with write_todos (decomposition, tracking, re-planning)
- Virtual filesystem (read, write, organize outputs)
- Subagent spawning (create agents on demand, isolated context)
- Long-term memory (pluggable backends, automatic retrieve/store)
- CLI (a complete terminal interface)

You still handle: the instructions that steer the agent, the tool selection, and the evaluation of results.

The spectrum, visualized

                    Control
                      ▲
                      │
          LangGraph ──┤  Total: nodes, edges, state, flow
                      │
    create_agent ─────┤  Partial: tools yes, flow no
                      │
      Deep Agents ────┤  Minimal: instructions and tools
                      │
                      └──────────────────────────► Convenience

10 real scenarios: what would you use?

For each scenario, the answer includes the tool and the reason.

1. A technical support chatbot

Tool: create_agent

Why: the pattern is simple — take a question, search a knowledge base, answer. It needs neither planning nor multi-agent. A ReAct loop with search tools resolves 95% of tickets.

2. A loan approval pipeline

Tool: LangGraph

Why: there's a regulated flow: verify identity → assess credit → risk analysis → human approval → notification. Each step has specific rules. You need HITL at exact points (human approval). You need an audit trail (checkpoints). This is the textbook LangGraph case.

3. Autonomous market research

Tool: Deep Agents

Why: "Research the market for X, compare 5 competitors, generate a report." It's autonomous, long-running, multi-step. Planning, filesystem, and subagents for parallel searches are exactly what Deep Agents provides.

4. A personalized email generator

Tool: create_agent

Why: input → look up the customer's data → generate the email → output. It's a linear flow with no branching. It needs no planning. create_agent with a CRM tool solves this in 15 lines.

5. A multi-step code review system

Tool: LangGraph

Why: the flow requires: read code → identify issues → classify by severity → generate suggestions → if there are critical issues, notify the lead. Conditional branching, different roles per step, and HITL for critical issues. LangGraph gives you the control you need.

6. Generating documentation for a whole project

Tool: Deep Agents

Why: read all the project's code, understand the architecture, generate docs for each module, and create a consolidated README. It needs planning (what to document first), filesystem (one doc per module), and potentially subagents (one subagent per large module).

7. A calendar/scheduling assistant

Tool: create_agent

Why: "What do I have today?" → look at the calendar → answer. "Schedule a meeting with X" → create an event. Simple tools, linear flow, no need for planning.

8. An ETL pipeline with validation

Tool: LangGraph

Why: extract data → validate format → transform → load. If validation fails, retry with different parameters. If it fails 3 times, notify and stop. This flow needs conditional edges, retry with custom logic, and persistent state. LangGraph.

9. A multi-source financial analysis agent

Tool: Deep Agents

Why: research indicators across multiple sources (market, SEC filings, news), analyze correlations, generate a report with charts. It's autonomous, multi-source, multi-step, and it needs specialized subagents per source type. Deep Agents abstracts the coordination away.

10. A content moderator with escalation

Tool: LangGraph

Why: analyze content → classify risk → if it's low, approve automatically; if it's medium, flag for review; if it's high, block and notify. HITL at the medium level, strict business rules, and a mandatory audit trail. LangGraph gives you the control a content moderator requires.

Visual summary

create_agent (simple):     1, 4, 7
LangGraph (control):       2, 5, 8, 10
Deep Agents (autonomous):  3, 6, 9

The distribution reflects reality: ~30% of projects are simple (create_agent), ~40% need a custom workflow (LangGraph), ~30% are long autonomous tasks (Deep Agents).


Hybrid approaches

You aren't limited to picking one. The tools compose with each other.

create_agent inside LangGraph

Use create_agent as a node inside a LangGraph graph:

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
import operator

class State(TypedDict):
    messages: Annotated[list, operator.add]
    query: str

model = ChatOpenAI(model="gpt-4.1-mini")
search_tools = [TavilySearchResults(max_results=3)]
search_agent = create_agent(model, search_tools)

def search_node(state: State) -> dict:
    result = search_agent.invoke(
        {"messages": [{"role": "user", "content": state["query"]}]}
    )
    return {"messages": [{"role": "system", "content": result["messages"][-1].content}]}

def summarize_node(state: State) -> dict:
    response = model.invoke(f"Summarize in 3 bullet points:\n{state['messages'][-1]['content']}")
    return {"messages": [{"role": "assistant", "content": response.content}]}

builder = StateGraph(State)
builder.add_node("search", search_node)
builder.add_node("summarize", summarize_node)
builder.add_edge(START, "search")
builder.add_edge("search", "summarize")
builder.add_edge("summarize", END)

graph = builder.compile()

result = graph.invoke({"query": "AI agents trends 2025", "messages": []})
print(f"Messages generated: {len(result['messages'])}")
print(f"Last message: {result['messages'][-1]['content'][:100]}...")
# Expected output:
# Messages generated: 2
# Last message: • AI agents have evolved toward...

When: when a specific node needs autonomous tool calling but the overall flow has custom logic.

LangGraph as a component of Deep Agents

A Deep Agent can invoke a LangGraph graph as a tool:

from dotenv import load_dotenv
load_dotenv()

from deep_agents import create_deep_agent
from deep_agents.tools import create_graph_tool
from langgraph.graph import StateGraph, START, END
from typing import TypedDict

class AnalysisState(TypedDict):
    data: str
    result: str

def analyze_node(state: AnalysisState) -> dict:
    return {"result": f"Analysis of: {state['data'][:50]}..."}

builder = StateGraph(AnalysisState)
builder.add_node("analyze", analyze_node)
builder.add_edge(START, "analyze")
builder.add_edge("analyze", END)

analysis_graph = builder.compile()

analysis_tool = create_graph_tool(
    graph=analysis_graph,
    name="detailed_analysis",
    description="Runs detailed analysis on provided data using a custom pipeline",
)

agent = create_deep_agent(
    "openai:gpt-4.1",
    tools=[analysis_tool],
    name="Research Agent",
    instructions="Use detailed_analysis for analyses that require a custom pipeline.",
)

print(f"Available tools: {[t.name for t in agent.tools]}")
# Expected output:
# Available tools: ['detailed_analysis', 'write_todos', 'read_file', 'write_file', 'spawn_agent']

When: when the Deep Agent needs to run a flow with fine-grained control as part of a larger autonomous task.


The professional's perspective

Knowing the three levels isn't an academic luxury — it's what separates a developer from an AI Engineer.

In interviews

Interviewer: "How would you build a research agent?"

Junior: "I'd use LangGraph because that's the one I know."

Senior: "It depends on the case. If it's simple Q&A, create_agent with TavilySearch
solves it in 15 lines. If I need a pipeline with approvals and retry,
LangGraph gives me control over every step. If it's long-running autonomous
research, Deep Agents includes planning and a filesystem out of the box.
What's the specific case?"

In production

A real decision:

Project A: FAQ chatbot → create_agent
  → In production in 1 day
  → Cost: ~$0.001 per query
  → Maintenance: minimal

Project B: Onboarding pipeline → LangGraph
  → In production in 1 week
  → Cost: ~$0.05 per run
  → Maintenance: every flow change = edit the graph

Project C: Research automation → Deep Agents
  → In production in 2 days
  → Cost: ~$0.10-0.50 per research run
  → Maintenance: tune the instructions and tools

In your portfolio

If you have all three versions of the Research Assistant (create_agent, LangGraph, Deep Agents), you can show that:

  • ✅ You can build from the basics up to the complex
  • ✅ You understand the convenience vs control trade-off
  • ✅ You can pick the right tool for the problem
  • ✅ You aren't dependent on a single framework

That's what makes you an AI Engineer, not a "LangGraph user."


Exercises

Exercise 1: Level diagnosis

For each description, identify which tool you'd use and justify it in one sentence:

  1. A Slack bot that answers questions about the internal documentation
  2. A bug triage system that classifies, prioritizes, and assigns
  3. An agent that researches competitors and generates an automatic weekly report
  4. A code assistant that suggests refactorings
  5. A legal contract generation pipeline with approvals
See solution
  1. create_agent — Q&A with a docs-search tool, linear flow, no need for a custom workflow.
  2. LangGraph — a flow with classification → prioritization → assignment with specific business rules, plus HITL for ambiguous cases.
  3. Deep Agents — recurring autonomous research, multi-source, needs planning and a filesystem for the reports.
  4. create_agent — reads code, suggests improvements. A single flow: analyze → suggest. No planning needed.
  5. LangGraph — a regulated flow with approvals at specific points, a mandatory audit trail, strict business rules.

Exercise 2: Implementation at 3 levels

Pick a simple agent: "an agent that looks up the weather in a city and answers in English." Implement it in create_agent (~10 lines), LangGraph (~40 lines), and Deep Agents (~15 lines). Compare: which one makes sense for this case?

See solution

create_agent:

from dotenv import load_dotenv
load_dotenv()

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults

model = ChatOpenAI(model="gpt-4.1-mini")
agent = create_agent(model, [TavilySearchResults(max_results=1)])

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's the weather in Madrid today?"}]}
)
print(result["messages"][-1].content)
# Expected output: The weather in Madrid today is...

LangGraph:

from dotenv import load_dotenv
load_dotenv()

from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from typing import TypedDict, Annotated
import operator

class State(TypedDict):
    messages: Annotated[list, operator.add]
    city: str

search = TavilySearchResults(max_results=1)
model = ChatOpenAI(model="gpt-4.1-mini")

def search_weather(state: State) -> dict:
    results = search.invoke(f"weather today {state['city']}")
    return {"messages": [{"role": "system", "content": str(results)}]}

def format_response(state: State) -> dict:
    response = model.invoke(f"State the weather in {state['city']} in English:\n{state['messages'][-1]}")
    return {"messages": [{"role": "assistant", "content": response.content}]}

builder = StateGraph(State)
builder.add_node("search", search_weather)
builder.add_node("format", format_response)
builder.add_edge(START, "search")
builder.add_edge("search", "format")
builder.add_edge("format", END)
graph = builder.compile()

result = graph.invoke({"city": "Madrid", "messages": []})
print(result["messages"][-1]["content"])
# Expected output: The weather in Madrid today is...

Deep Agents:

from dotenv import load_dotenv
load_dotenv()

from deep_agents import create_deep_agent
from langchain_community.tools import TavilySearchResults

agent = create_deep_agent(
    "openai:gpt-4.1-mini",
    tools=[TavilySearchResults(max_results=1)],
    name="Weather Agent",
    instructions="Look up the weather and answer in English.",
)

result = agent.run("What's the weather in Madrid today?")
print(result.output)
# Expected output: The weather in Madrid today is...

Verdict: create_agent is the right choice. LangGraph adds unnecessary complexity for a linear flow. Deep Agents carries planning overhead for a task that needs no planning.

Exercise 3: Designing a hybrid system

Design (in pseudocode or a diagram) a system that uses all three levels: a main Deep Agent that coordinates the research, a LangGraph graph for the analysis pipeline with human approval, and create_agent for individual searches inside nodes.

See solution
Hybrid architecture:

Deep Agent (top level):
  └─ Planning: write_todos to break the research down
  └─ Tools:
      ├─ web_search_agent (create_agent with TavilySearch)
      │   → Simple searches, one question → one answer
      ├─ analysis_pipeline (a LangGraph graph as a tool)
      │   → Pipeline: validate → analyze → human_approve → report
      │   → HITL: approval before publishing findings
      └─ write_file, read_file (built-in)
  └─ Filesystem: results in research/, analysis in analysis/
  └─ Subagents: for each complex source, spawn a subagent

Flow:
1. The Deep Agent plans: "Research X in 5 steps"
2. Step 1: uses web_search_agent (create_agent) for a quick search
3. Step 2: writes findings to research/web.md
4. Step 3: spawns a subagent for academic search
5. Step 4: invokes analysis_pipeline (LangGraph) with HITL
6. Step 5: generates the final report at output/report.md
from dotenv import load_dotenv
load_dotenv()

from deep_agents import create_deep_agent
from deep_agents.tools import create_graph_tool
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
from langgraph.graph import StateGraph, START, END
from typing import TypedDict

model = ChatOpenAI(model="gpt-4.1-mini")
simple_search = create_agent(model, [TavilySearchResults(max_results=3)])

class AnalysisState(TypedDict):
    data: str
    approved: bool
    result: str

def validate(state): return {"result": "Validated"}
def analyze(state): return {"result": "Analyzed: " + state["data"][:50]}
def approve(state): return {"approved": True}

builder = StateGraph(AnalysisState)
builder.add_node("validate", validate)
builder.add_node("analyze", analyze)
builder.add_node("approve", approve)
builder.add_edge(START, "validate")
builder.add_edge("validate", "analyze")
builder.add_edge("analyze", "approve")
builder.add_edge("approve", END)
analysis_graph = builder.compile()

analysis_tool = create_graph_tool(analysis_graph, "analysis_pipeline", "Runs analysis with approval")

agent = create_deep_agent(
    "openai:gpt-4.1",
    tools=[TavilySearchResults(max_results=5), analysis_tool],
    name="Hybrid Research Agent",
    instructions="Use web search for data, analysis_pipeline for analysis that needs approval.",
)

print(f"Tools: {[t.name for t in agent.tools]}")
# Expected output:
# Tools: ['tavily_search_results', 'analysis_pipeline', 'write_todos', 'read_file', 'write_file', 'spawn_agent']

Exercise 4: Trade-off analysis

For a project described as "a legal assistant that reviews contracts, flags problematic clauses, and suggests changes with a lawyer's approval," argue:

  1. Why you might use LangGraph
  2. Why you might use Deep Agents
  3. Which one you'd choose and why
See solution

The case for LangGraph:

  • Legal contracts require a regulated, auditable flow
  • HITL is mandatory at specific points (a lawyer approves every suggestion)
  • The rules for flagging clauses are specific and require conditional branching
  • You need a detailed audit trail (what was reviewed, who approved it, when)
  • ~150-200 lines give you total control over every step

The case for Deep Agents:

  • Reviewing long contracts is an autonomous multi-step task
  • Automatic planning: the agent decides how to break down a 50-page contract
  • Filesystem: each analyzed clause gets saved to a separate file
  • Subagents: one subagent per contract section

The choice: LangGraph.

Legal regulations demand predictable, auditable flows with HITL at exact points. A lawyer needs to approve each suggestion individually, not the task as a whole. Deep Agents is too autonomous for a domain where every step carries legal implications. The overhead of writing more code is justified by the auditability and the control.


Summary

  • You have three levels of abstraction: create_agent (simple, ~15 lines, 80% of cases), LangGraph (full control, ~150 lines, custom workflows), and Deep Agents (autonomous, ~40 lines, batteries-included)
  • The decision tree is: simple? → create_agent. Need control? → LangGraph. Long autonomous task? → Deep Agents
  • Side-by-side: the same Research Agent implemented in 3 versions shows that the difference isn't capability — it's how much you control vs how much the framework delegates to itself
  • Each level abstracts away what the previous one forces you to build: create_agent abstracts the ReAct loop; LangGraph abstracts state and flow; Deep Agents abstracts planning, filesystem, subagents, and memory
  • 10 real scenarios with justifications give you practice applying the framework: ~30% create_agent, ~40% LangGraph, ~30% Deep Agents
  • Hybrid approaches are valid and common: create_agent inside LangGraph nodes, LangGraph graphs as Deep Agents tools
  • The professional's perspective: knowing all three levels lets you pick the right tool for each problem. That's what defines an AI Engineer

Next capsule: the project — reimplementing the Research Assistant as a Deep Agent and doing the definitive side-by-side comparison with the LangGraph version.


Additional resources

  1. LangChain — Agent Architectures — Overview of create_agent and its API
  2. LangGraph — Overview — Documentation for StateGraph, edges, checkpointers
  3. Deep Agents — Documentation — API reference for create_deep_agent and its capabilities
  4. Building Effective Agents (Anthropic) — A perspective on when to use simple vs complex agents
  5. LangGraph Multi-Agent Systems — Multi-agent patterns in LangGraph
  6. Cognitive Architectures for Language Agents — The academic paper that informs the design of the three levels

Module 11 — LangChain & LangGraph: From Chains to Agents