Module 5: Introduction to LangGraph
create_agent vs StateGraph: When to Use Each One
Capsule overview
This is the most important decision you're going to make in every project with LangChain and LangGraph: do I use create_agent or do I build a custom StateGraph? In the previous capsules you learned everything you need about LangGraph — StateGraph, nodes, edges, conditional edges, typed state with TypedDict and Annotated, compilation and execution. Now you have two powerful tools for building agents. The problem is that both solve similar problems, and without a clear decision framework, you'll either waste time choosing or you'll choose wrong.
The honest answer is that create_agent covers 80% of cases. It's fast to implement, it has the ReAct pattern built in, it supports middleware for customization, and in 10 lines you have a working agent. StateGraph is for the other 20%: workflows with conditional branching, custom loops, human-in-the-loop, task-specialized nodes, and flows that don't follow the linear model → tools → model pattern. Neither one is "better" than the other — they're different tools for different problems.
In this capsule you're going to see a concrete decision table, the same problem solved with both approaches (side-by-side), the natural progression path (start with create_agent, migrate when you need to), and the two most common mistakes: using StateGraph for everything (over-engineering) and sticking with create_agent when you need more control (under-engineering).
The decision table
Before writing a single line of code, check this table:
| Criterion | create_agent | StateGraph |
|---|---|---|
| Setup time | ~5 minutes | 30+ minutes |
| Lines of code | ~10-15 | ~50+ |
| Flow control | Limited (middleware) | Total (nodes + edges) |
| Branching / routing | No (linear ReAct loop) | Yes (conditional edges) |
| Human-in-the-loop | No | Yes (interrupt) |
| Graph visualization | No | Yes (draw_mermaid_png) |
| Learning curve | Low | Medium |
| Middleware system | Yes (complete) | Not applicable (you define everything) |
| Streaming | Yes (built-in) | Yes (manual) |
| Use case | 80% of agents | Complex workflows |
The general rule: if you can solve your problem with create_agent + middleware, do it. Only drop down to StateGraph when you need something create_agent can't give you.
When to use create_agent
create_agent is the right choice when:
- ✅ Your agent has a single purpose (search, analyze, answer)
- ✅ The flow is standard ReAct: model → tools → model → tools → answer
- ✅ You need a quick prototype that works in minutes
- ✅ The customization you need can be achieved with middleware (logging, model routing, auth)
- ✅ You don't need branching — every question follows the same flow
- ✅ You don't need to pause execution for human approval
Example: search agent with create_agent
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Searches for information about a topic."""
return f"Results about {query}: Python was created in 1991 by Guido van Rossum."
@tool
def summarize(text: str) -> str:
"""Summarizes a long text into key points."""
return f"Summary of '{text[:50]}...': 3 key points identified."
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model,
tools=[search, summarize],
prompt="You are a researcher. Search for information and summarize it for the user.",
)
result = agent.invoke(
{"messages": [("user", "Research Python and give me a summary")]}
)
print(result["messages"][-1].content)
# Expected output:
# Python was created in 1991 by Guido van Rossum...
# Summary: 3 key points identified...
15 lines of code. The agent decides when to call search, when to call summarize, and when to answer. The ReAct loop handles everything automatically.
When to use StateGraph
StateGraph is the right choice when:
- ✅ You need different types of input to follow different paths (routing)
- ✅ Your workflow has specialized nodes (classifier, processor, validator, formatter)
- ✅ You need custom loops (retry with backoff, iteration until convergence)
- ✅ You require human-in-the-loop (pause for approval before executing)
- ✅ You want to visualize the flow for debugging and documentation
- ✅ The flow has branching: "if it's type A, go this way; if it's type B, go that way"
- ✅ You need total control over what happens at every step
Example: workflow with conditional routing
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
query_type: str
results: Annotated[list[str], operator.add]
final_answer: str
model = init_chat_model("openai:gpt-4.1-mini")
def classify(state: ResearchState) -> dict:
query = state["query"].lower()
if any(word in query for word in ["code", "program", "function", "python"]):
return {"query_type": "code"}
elif any(word in query for word in ["explain", "what is", "concept"]):
return {"query_type": "concept"}
return {"query_type": "general"}
def search_code(state: ResearchState) -> dict:
return {"results": [f"[CODE] Code example for: {state['query']}"]}
def search_concept(state: ResearchState) -> dict:
return {"results": [f"[CONCEPT] Conceptual explanation of: {state['query']}"]}
def search_general(state: ResearchState) -> dict:
return {"results": [f"[GENERAL] General information about: {state['query']}"]}
def generate_answer(state: ResearchState) -> dict:
combined = "\n".join(state["results"])
response = model.invoke(
f"Based on these results, answer: {state['query']}\n\nResults:\n{combined}"
)
return {"final_answer": response.content}
def route_by_type(state: ResearchState) -> str:
return {
"code": "search_code",
"concept": "search_concept",
"general": "search_general",
}[state["query_type"]]
graph = StateGraph(ResearchState)
graph.add_node("classify", classify)
graph.add_node("search_code", search_code)
graph.add_node("search_concept", search_concept)
graph.add_node("search_general", search_general)
graph.add_node("generate_answer", generate_answer)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_by_type)
graph.add_edge("search_code", "generate_answer")
graph.add_edge("search_concept", "generate_answer")
graph.add_edge("search_general", "generate_answer")
graph.add_edge("generate_answer", END)
app = graph.compile()
result = app.invoke({"query": "Explain what a decorator is in Python"})
print(f"Type: {result['query_type']}")
print(f"Answer: {result['final_answer'][:100]}...")
# Expected output:
# Type: concept
# Answer: A decorator in Python is a function that modifies the behavior of another function...
~50 lines of code. But you have total control: explicit classification, conditional routing, specialized nodes, and the flow is visible and debuggable.
Side-by-side: the same problem, two approaches
Let's solve exactly the same problem with both approaches so you can see the real difference. The problem: "an agent that searches for information and generates a structured summary."
create_agent version (~15 lines)
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def web_search(query: str) -> str:
"""Searches for information on the web."""
return f"Search results for '{query}': Python is a high-level programming language created in 1991. It's used in AI, data science, and web development."
@tool
def format_summary(content: str) -> str:
"""Formats content as a structured summary with bullet points."""
return f"## Summary\n- Point 1: {content[:50]}\n- Point 2: Analysis complete\n- Point 3: Conclusions generated"
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model,
tools=[web_search, format_summary],
prompt=(
"You are an expert researcher. When asked to research something: "
"1) Search for information with web_search, "
"2) Format the result with format_summary, "
"3) Deliver the final summary to the user."
),
)
result = agent.invoke(
{"messages": [("user", "Research Python")]}
)
print(result["messages"][-1].content)
# Expected output:
# ## Summary
# - Point 1: Python is a programming language...
# - Point 2: Analysis complete
# - Point 3: Conclusions generated
# [Followed by the model's analysis]
StateGraph version (~55 lines)
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
raw_results: str
summary: str
formatted_output: str
model = init_chat_model("openai:gpt-4.1-mini")
def search_node(state: ResearchState) -> dict:
query = state["query"]
results = (
f"Search results for '{query}': Python is a high-level "
f"programming language created in 1991. It's used in AI, data "
f"science, and web development."
)
return {"raw_results": results}
def summarize_node(state: ResearchState) -> dict:
response = model.invoke(
f"Summarize these results in 3 key points:\n\n{state['raw_results']}"
)
return {"summary": response.content}
def format_node(state: ResearchState) -> dict:
formatted = f"## Research: {state['query']}\n\n"
formatted += f"### Results\n{state['raw_results'][:100]}...\n\n"
formatted += f"### Summary\n{state['summary']}\n"
return {"formatted_output": formatted}
graph = StateGraph(ResearchState)
graph.add_node("search", search_node)
graph.add_node("summarize", summarize_node)
graph.add_node("format", format_node)
graph.add_edge(START, "search")
graph.add_edge("search", "summarize")
graph.add_edge("summarize", "format")
graph.add_edge("format", END)
app = graph.compile()
result = app.invoke({"query": "Python"})
print(result["formatted_output"])
# Expected output:
# ## Research: Python
#
# ### Results
# Search results for 'Python': Python is a high-level programming language...
#
# ### Summary
# 1. Python is a high-level language created in 1991
# 2. It's widely used in AI, data science, and web development
# 3. It's one of the most popular languages in the world
Analysis: same result, different trade-offs
| Aspect | create_agent | StateGraph |
|---|---|---|
| Lines of code | ~15 | ~55 |
| Development time | 5 minutes | 20 minutes |
| Flow control | The model decides the order | You define the order |
| Debugging | Opaque (why did it call that tool?) | Transparent (every node is visible) |
| Modifiability | Add middleware | Add/change nodes and edges |
| Visualization | No | Yes |
For this specific problem (search + summarize), create_agent is the better option. The flow is linear, there's no branching, and you don't need granular control. Using StateGraph here is over-engineering.
When would StateGraph be justified for this same problem? When you need:
- Routing: "if it's a technical question, search the docs; if it's general, search the web"
- Validation: "if the summary is under 100 words, regenerate it"
- Human-in-the-loop: "show the summary to the user before formatting"
- Granular logging: "I want to know how long each step took separately"
The progression path: from create_agent to StateGraph
In real projects, the natural progression is:
1. You start with create_agent
→ Works for 80% of the problem
2. You add middleware to customize
→ Logging, model routing, auth
3. You hit a middleware limit
→ "I need the flow to branch here"
→ "I need to pause for human approval"
→ "I need a retry loop with custom logic"
4. You migrate to StateGraph
→ More code, but total control
This path is healthy. You don't need to start with StateGraph "just in case." Start simple, migrate when the pain justifies it.
Signs you need to migrate
- ❌ Your system prompt turns into a list of "if X then Y" rules to control the flow
- ❌ Your middleware keeps getting more complex just to simulate branching
- ❌ You need the agent to do different things depending on the type of input
- ❌ You want a human to approve something before the agent continues
- ❌ You need to visualize the flow for debugging or documentation
- ❌ The agent calls tools in an order that makes no sense and you can't control it
Signs you do NOT need to migrate
- ✅ The agent works fine with the standard ReAct loop
- ✅ The customization you need can be achieved with middleware
- ✅ The flow is linear: question → reasoning → tools → answer
- ✅ You don't need branching or human-in-the-loop
- ✅ The system prompt controls the behavior well
Hybrid approach: create_agent as a node in a StateGraph
An advanced pattern is using create_agent as a node inside a StateGraph. This gives you the best of both worlds: the simplicity of create_agent for individual tasks, and StateGraph's control to orchestrate the flow between tasks.
from dotenv import load_dotenv
load_dotenv()
import operator
from typing import TypedDict, Annotated
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END
@tool
def search(query: str) -> str:
"""Searches for information about a topic."""
return f"Information found about {query}: relevant, up-to-date data."
@tool
def code_gen(description: str) -> str:
"""Generates code based on a description."""
return f"```python\n# Code for: {description}\ndef solution():\n pass\n```"
model = init_chat_model("openai:gpt-4.1-mini")
research_agent = create_agent(
model, [search],
prompt="You are a researcher. Search for relevant information about the given topic.",
)
code_agent = create_agent(
model, [code_gen],
prompt="You are a programmer. Generate code based on the information provided.",
)
class PipelineState(TypedDict):
query: str
query_type: str
research_result: str
code_result: str
final_output: str
def classify_node(state: PipelineState) -> dict:
query = state["query"].lower()
if "code" in query or "program" in query or "implement" in query:
return {"query_type": "code"}
return {"query_type": "research"}
def research_node(state: PipelineState) -> dict:
result = research_agent.invoke(
{"messages": [("user", state["query"])]}
)
return {"research_result": result["messages"][-1].content}
def code_node(state: PipelineState) -> dict:
context = state.get("research_result", "")
prompt = f"Based on: {context}\n\nGenerate code for: {state['query']}"
result = code_agent.invoke(
{"messages": [("user", prompt)]}
)
return {"code_result": result["messages"][-1].content}
def output_node(state: PipelineState) -> dict:
if state["query_type"] == "code":
return {"final_output": f"Research:\n{state['research_result']}\n\nCode:\n{state['code_result']}"}
return {"final_output": state["research_result"]}
def route_by_type(state: PipelineState) -> str:
if state["query_type"] == "code":
return "research"
return "research_only"
graph = StateGraph(PipelineState)
graph.add_node("classify", classify_node)
graph.add_node("research", research_node)
graph.add_node("code", code_node)
graph.add_node("output", output_node)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_by_type, {
"research": "research",
"research_only": "research",
})
graph.add_edge("research", "code")
graph.add_edge("code", "output")
graph.add_edge("output", END)
app = graph.compile()
result = app.invoke({"query": "Research decorators in Python and implement an example"})
print(result["final_output"][:200])
# Expected output:
# Research:
# [Research results about decorators...]
#
# Code:
# ```python
# # Code for: decorators in Python...
Each node is an independent create_agent with its own tools and prompt. StateGraph orchestrates the flow between them. This pattern is the foundation of the multi-agent systems you'll see in later modules.
Common mistake #1: Over-engineering with StateGraph
The most frequent mistake when learning LangGraph is wanting to use StateGraph for everything. A simple Q&A agent that could be 10 lines with create_agent turns into 80 lines with StateGraph, 5 nodes, 3 conditional edges, and a state with 8 fields.
Symptoms of over-engineering
- ❌ Your StateGraph has a single linear path with no branching
- ❌ All your nodes simply call the model with different prompts
- ❌ You don't use conditional edges — every edge is fixed
- ❌ The state has fields you never use
- ❌ The graph visualization is a straight line from START to END
The conditional edge rule
If your graph has no conditional edges, you probably don't need StateGraph. A graph with purely linear edges (A → B → C → D → END) is equivalent to a chain — and create_agent does that better with less code.
# ❌ Over-engineering: StateGraph for a linear flow
graph.add_edge(START, "search")
graph.add_edge("search", "analyze")
graph.add_edge("analyze", "format")
graph.add_edge("format", END)
# ✅ Better: create_agent with a well-designed prompt
agent = create_agent(
model, [search, analyze, format_tool],
prompt="Search for information, analyze it, and format the result.",
)
Common mistake #2: Under-engineering with create_agent
The opposite mistake: sticking with create_agent when you need more control. Your system prompt fills up with conditional instructions, your middleware does acrobatics to simulate branching, and the agent sometimes makes decisions you don't understand.
Symptoms of under-engineering
- ❌ Your system prompt has paragraphs of "if the user asks for X, do Y; if they ask for Z, do W"
- ❌ You use
wrap_model_callmiddleware to redirect the call depending on the context - ❌ The agent calls tools in an unexpected order and you can't control it
- ❌ You need granular per-step logging but you only have before/after of the whole model call
- ❌ You want the agent to stop mid-process to ask the user for input
When the pain justifies migrating
# ❌ Under-engineering: system prompt trying to simulate routing
prompt = """You are an assistant with multiple capabilities.
ROUTING RULES:
1. If the user asks for code, FIRST search the documentation, THEN generate code
2. If the user asks for an explanation, search and summarize WITHOUT generating code
3. If the user asks for a comparison, search BOTH topics and then compare
4. If the user asks for debugging, analyze the code WITHOUT searching
5. NEVER generate code without first searching the documentation
6. If the search returns no results, try different terms
7. For comparisons, make sure to search each topic separately
...
"""
# ✅ Better: StateGraph with explicit routing
def route(state):
if state["intent"] == "code":
return "search_then_code"
elif state["intent"] == "explain":
return "search_then_summarize"
elif state["intent"] == "compare":
return "search_both"
return "direct_response"
When your system prompt turns into a mini programming language, it's time to migrate to StateGraph, where the flow is explicit and debuggable.
Troubleshooting
1. Decision paralysis: "which one do I use?"
Cause: You don't have clear criteria to decide. Every new project feels like an existential decision.
Solution: Always start with create_agent. If in the first 30 minutes of development you feel like the system prompt is controlling the flow instead of the behavior, migrate to StateGraph. The decision isn't permanent — it's a starting point.
2. Early over-engineering
Cause: You anticipate future needs that may never arrive. "Maybe later I'll need human-in-the-loop, so I'd better build a StateGraph from the start."
Solution: YAGNI (You Aren't Gonna Need It). Build for what you need today. Migrating from create_agent to StateGraph is much easier than maintaining a complex StateGraph you don't need.
3. Late under-engineering
Cause: You stick with create_agent for too long because "it already works," even though the agent behaves unpredictably and the system prompt has 50 lines of rules.
Solution: Define a "pain threshold." If your system prompt goes past 10-15 lines of conditional rules, or if you need more than 2 complex middleware to control the flow, that's your signal to migrate.
4. The create_agent agent calls tools in the wrong order
Cause: create_agent uses ReAct, where the model decides the order of tools. You can't reliably force a specific order with a system prompt.
Solution: If order matters, use StateGraph. Each node runs in the order you define with edges. There's no ambiguity.
5. StateGraph feels verbose for my case
Cause: You're using StateGraph for a problem create_agent solves better. If your graph is linear with no branching, you're writing unnecessary boilerplate.
Solution: Revisit the conditional edge rule. If you don't need conditional routing, go back to create_agent.
Exercises
Exercise 1: Identify the right approach (Basic)
Read each scenario and decide whether you'd use create_agent or StateGraph. Justify your answer.
Scenario A: A technical support chatbot that answers questions using a knowledge base. Every question follows the same flow: search → answer.
Scenario B: A system that receives emails, classifies them (urgent, normal, spam), and routes each type to a different processor.
Scenario C: An agent that translates text between languages using a translation tool.
See solution
Scenario A: create_agent
The flow is linear (search → answer), there's no branching. A system prompt + a search tool is all you need. StateGraph would be over-engineering.
Scenario B: StateGraph
There's classification + conditional routing. Each email type needs a different processor. This is exactly what conditional edges solve. With create_agent, you'd have to cram all the routing logic into the system prompt.
Scenario C: create_agent
Single purpose, linear flow. The agent receives text, calls the translation tool, returns the result. No branching, no complex decisions.
Exercise 2: Migrate from create_agent to StateGraph (Basic)
You have this agent built with create_agent. Convert it to StateGraph while keeping the same behavior.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Gets the weather for a city."""
weathers = {"madrid": "22°C, sunny", "london": "15°C, cloudy", "tokyo": "28°C, humid"}
return weathers.get(city.lower(), f"No data for {city}")
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, [get_weather], prompt="Report the weather when asked.")
See solution
from dotenv import load_dotenv
load_dotenv()
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class WeatherState(TypedDict):
city: str
weather_data: str
response: str
model = init_chat_model("openai:gpt-4.1-mini")
WEATHERS = {"madrid": "22°C, sunny", "london": "15°C, cloudy", "tokyo": "28°C, humid"}
def fetch_weather(state: WeatherState) -> dict:
city = state["city"]
data = WEATHERS.get(city.lower(), f"No data for {city}")
return {"weather_data": data}
def generate_report(state: WeatherState) -> dict:
response = model.invoke(
f"Generate a weather report for {state['city']}: {state['weather_data']}"
)
return {"response": response.content}
graph = StateGraph(WeatherState)
graph.add_node("fetch", fetch_weather)
graph.add_node("report", generate_report)
graph.add_edge(START, "fetch")
graph.add_edge("fetch", "report")
graph.add_edge("report", END)
app = graph.compile()
result = app.invoke({"city": "Madrid"})
print(result["response"])
# Expected output:
# The weather in Madrid is 22°C with sunny skies...
Note: in this case, the StateGraph version is more code for the same result. That confirms create_agent was the right tool for the original problem.
Exercise 3: Design a flow with conditional edges (Intermediate)
Design (without fully coding it) a StateGraph for a support system that:
- Classifies the ticket (billing, technical, general)
- Routes to a specialized node based on the classification
- The billing node queries a payments API
- The technical node searches the knowledge base
- The general node answers directly
Define: the state (TypedDict), the nodes (names and what they do), and the edges (including the routing function).
See solution
from typing import TypedDict
class SupportState(TypedDict):
ticket_text: str
category: str # "billing", "technical", "general"
context_data: str
response: str
# Nodes:
# 1. classify_ticket → Analyzes ticket_text, assigns category
# 2. handle_billing → Queries the payments API, generates a response
# 3. handle_technical → Searches the knowledge base, generates a response
# 4. handle_general → Generates a direct response with the model
# 5. format_response → Formats the final response
# Edges:
# START → classify_ticket
# classify_ticket → (conditional) → handle_billing | handle_technical | handle_general
# handle_billing → format_response
# handle_technical → format_response
# handle_general → format_response
# format_response → END
# Routing function:
def route_ticket(state: SupportState) -> str:
return {
"billing": "handle_billing",
"technical": "handle_technical",
"general": "handle_general",
}.get(state["category"], "handle_general")
# This design justifies StateGraph because:
# ✅ It has conditional routing (3 different paths)
# ✅ Each node is specialized (different logic and tools)
# ✅ The flow is visible and debuggable
# ✅ You can add nodes easily (e.g., escalation)
Exercise 4: Spot the over-engineering (Intermediate)
This StateGraph is over-engineering. Identify why and rewrite it as a create_agent.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class QAState(TypedDict):
question: str
search_result: str
answer: str
def search_node(state):
return {"search_result": f"Info about: {state['question']}"}
def answer_node(state):
return {"answer": f"Answer based on: {state['search_result']}"}
graph = StateGraph(QAState)
graph.add_node("search", search_node)
graph.add_node("answer", answer_node)
graph.add_edge(START, "search")
graph.add_edge("search", "answer")
graph.add_edge("answer", END)
See solution
Why it's over-engineering:
- ❌ Purely linear flow:
START → search → answer → END - ❌ No conditional edges — no branching
- ❌ Only two nodes that could be tools
- ❌ The state has simple fields with no complex reducers
Version with create_agent:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(question: str) -> str:
"""Searches for information to answer a question."""
return f"Info about: {question}"
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(
model, [search],
prompt="Search for information and answer the user's question.",
)
result = agent.invoke({"messages": [("user", "What is Python?")]})
print(result["messages"][-1].content)
# Expected output:
# Python is a programming language...
Same functionality, a third of the code, no unnecessary boilerplate.
Exercise 5: Add routing to an existing agent (Advanced)
You have a create_agent that answers questions. But now you need:
- Code questions to go through a flow that includes searching the documentation + generating code
- General questions to follow the normal search + answer flow
Implement this as a StateGraph with conditional edges.
See solution
from dotenv import load_dotenv
load_dotenv()
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START, END
class QAState(TypedDict):
question: str
intent: str
search_result: str
code_output: str
final_answer: str
model = init_chat_model("openai:gpt-4.1-mini")
CODE_KEYWORDS = ["code", "program", "implement", "function", "class", "script"]
def classify_intent(state: QAState) -> dict:
question_lower = state["question"].lower()
if any(kw in question_lower for kw in CODE_KEYWORDS):
return {"intent": "code"}
return {"intent": "general"}
def search_docs(state: QAState) -> dict:
return {"search_result": f"Documentation found for: {state['question']}"}
def search_general(state: QAState) -> dict:
return {"search_result": f"General information about: {state['question']}"}
def generate_code(state: QAState) -> dict:
response = model.invoke(
f"Generate Python code for: {state['question']}\n"
f"Based on: {state['search_result']}"
)
return {"code_output": response.content}
def generate_answer(state: QAState) -> dict:
if state.get("code_output"):
answer = f"Documentation: {state['search_result']}\n\nCode:\n{state['code_output']}"
else:
response = model.invoke(
f"Answer this question: {state['question']}\n"
f"Information: {state['search_result']}"
)
answer = response.content
return {"final_answer": answer}
def route_intent(state: QAState) -> str:
return "search_docs" if state["intent"] == "code" else "search_general"
graph = StateGraph(QAState)
graph.add_node("classify", classify_intent)
graph.add_node("search_docs", search_docs)
graph.add_node("search_general", search_general)
graph.add_node("generate_code", generate_code)
graph.add_node("generate_answer", generate_answer)
graph.add_edge(START, "classify")
graph.add_conditional_edges("classify", route_intent)
graph.add_edge("search_docs", "generate_code")
graph.add_edge("generate_code", "generate_answer")
graph.add_edge("search_general", "generate_answer")
graph.add_edge("generate_answer", END)
app = graph.compile()
result_code = app.invoke({"question": "Implement a function to sort a list"})
print(f"Intent: {result_code['intent']}")
print(f"Answer: {result_code['final_answer'][:100]}...")
# Expected output:
# Intent: code
# Answer: Documentation: Documentation found for: Implement a function...
result_general = app.invoke({"question": "What is machine learning?"})
print(f"\nIntent: {result_general['intent']}")
print(f"Answer: {result_general['final_answer'][:100]}...")
# Expected output:
# Intent: general
# Answer: Machine learning is a branch of artificial intelligence...
Exercise 6: Complete decision framework (Challenge)
Write a function recommend_approach(requirements: dict) -> str that takes a dictionary with these boolean keys and returns "create_agent", "stategraph", or "hybrid":
needs_branching: Does the flow have different paths depending on the input?needs_hitl: Do you need to pause for human approval?needs_visualization: Do you need to visualize the graph?needs_custom_loops: Do you need loops with custom logic (retry, convergence)?single_purpose: Does the agent have a single purpose?rapid_prototype: Do you need a quick prototype?
Implement the decision logic and test it with at least 4 different scenarios.
See solution
def recommend_approach(requirements: dict) -> str:
"""Recommends create_agent, stategraph, or hybrid based on the requirements."""
needs_branching = requirements.get("needs_branching", False)
needs_hitl = requirements.get("needs_hitl", False)
needs_visualization = requirements.get("needs_visualization", False)
needs_custom_loops = requirements.get("needs_custom_loops", False)
single_purpose = requirements.get("single_purpose", True)
rapid_prototype = requirements.get("rapid_prototype", False)
stategraph_signals = sum([
needs_branching,
needs_hitl,
needs_custom_loops,
])
if stategraph_signals == 0 and single_purpose:
return "create_agent"
if stategraph_signals >= 2:
return "stategraph"
if needs_branching and not needs_hitl and not needs_custom_loops:
return "hybrid"
if rapid_prototype and stategraph_signals <= 1:
return "create_agent"
if needs_hitl or needs_custom_loops:
return "stategraph"
return "hybrid"
# Test 1: Simple chatbot
r1 = recommend_approach({
"needs_branching": False,
"needs_hitl": False,
"needs_visualization": False,
"needs_custom_loops": False,
"single_purpose": True,
"rapid_prototype": True,
})
print(f"Test 1 (simple chatbot): {r1}")
# Expected output: create_agent
# Test 2: Support system with routing
r2 = recommend_approach({
"needs_branching": True,
"needs_hitl": True,
"needs_visualization": True,
"needs_custom_loops": False,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 2 (support with routing + HITL): {r2}")
# Expected output: stategraph
# Test 3: Pipeline with simple branching
r3 = recommend_approach({
"needs_branching": True,
"needs_hitl": False,
"needs_visualization": True,
"needs_custom_loops": False,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 3 (pipeline with branching): {r3}")
# Expected output: hybrid
# Test 4: Agent with retry loops
r4 = recommend_approach({
"needs_branching": False,
"needs_hitl": False,
"needs_visualization": False,
"needs_custom_loops": True,
"single_purpose": False,
"rapid_prototype": False,
})
print(f"Test 4 (retry loops): {r4}")
# Expected output: stategraph
Summary
In this capsule you learned:
create_agentcovers 80% of cases — it's fast, simple, and has middleware for customization. Use it by defaultStateGraphis for the remaining 20% — when you need branching, human-in-the-loop, custom loops, or flow visualization- Neither one is "better" than the other — they're different tools. The right question isn't "which is better?" but "which one do I need for this problem?"
- The natural progression is to start with
create_agentand migrate to StateGraph when the pain justifies it - The hybrid approach (
create_agentas a node in a StateGraph) gives you the best of both worlds for multi-agent systems - Over-engineering (StateGraph for everything) wastes time and complicates maintenance
- Under-engineering (
create_agentwith a 50-line system prompt trying to simulate routing) produces unpredictable agents - The conditional edge rule: if your graph has no conditional edges, you probably don't need StateGraph
Next capsule: Project — you'll build a chatbot with state and conditional routing using StateGraph. It's the last standalone mini-project before the evolving project kicks off in Module 6.
Additional resources
- LangGraph Concepts: Why LangGraph? — The framework's motivations and when it's appropriate
- create_agent API Reference — Full reference for the prebuilt agent
- LangGraph StateGraph Tutorial — Official StateGraph tutorial
- LangGraph Agents Conceptual Guide — How create_agent is a StateGraph internally
- LangChain Agents Middleware — Middleware system for customizing create_agent
- YAGNI Principle (Martin Fowler) — The design principle behind "start simple, migrate when it hurts"
Module 5 — LangChain & LangGraph: From Chains to Agents