LangGraph has become the production-grade framework for building stateful AI agents. It gives you explicit control over every step, every decision point, and every transition — which is exactly what you need when your agent needs to loop, branch, and recover from failures.
This tutorial builds a real working agent from scratch: a research agent that takes a question, searches for relevant information, evaluates whether it has enough to answer, and either continues searching or synthesizes a final response.
By the end, you'll understand the core LangGraph concepts and have working code you can adapt for your own use cases.
What You'll Build
A research agent that:
- Takes a user question
- Decides what to search for
- Runs a web search
- Evaluates whether the results are sufficient
- Loops back for more research if needed
- Synthesizes a final answer when ready
This covers the most common agentic pattern: a loop with a conditional exit.
Prerequisites
pip install langgraph langchain-anthropic langchain-community python-dotenv
You'll need an Anthropic API key. Set it in a .env file:
ANTHROPIC_API_KEY=your_key_here
For search, we'll use a mock search function you can swap for a real implementation (Tavily, SerpAPI, etc.).
Core Concepts First
Before the code, the mental model.
StateGraph: The container for your agent. You define a typed state object, then add nodes and edges that operate on that state.
Nodes: Functions that take the current state and return an updated state. Each node does one thing: call the LLM, run a tool, make a routing decision.
Edges: Connections between nodes. They can be unconditional (always go to node B after node A) or conditional (look at the current state and decide which node to go to next).
State: A typed dictionary (usually a TypedDict or Pydantic model) that flows through the graph. Every node reads from it and writes back to it.
Checkpointing: LangGraph can persist state between steps, enabling pause-and-resume, human-in-the-loop patterns, and debugging.
Step 1: Define Your State
from typing import TypedDict, Annotated, List
from langgraph.graph.message import add_messages
class ResearchState(TypedDict):
# The original user question
question: str
# Accumulated search queries we've tried
search_queries: List[str]
# Raw search results collected so far
search_results: List[str]
# How many search iterations we've done
iteration_count: int
# Final answer (empty until we're done)
final_answer: str
Keep state flat and typed. Every piece of information that needs to flow between nodes goes in state. If a node needs to know something, it reads it from state. If a node produces something, it writes it back to state.
Step 2: Set Up the LLM
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
load_dotenv()
llm = ChatAnthropic(model="claude-haiku-4-5-20251001")
We're using Haiku for speed and cost efficiency. For production research agents where answer quality matters more, use Sonnet.
Step 3: Define the Nodes
Each node is a function. It takes the full state and returns a dictionary with the fields it wants to update.
Node 1: Plan — decide what to search for
def plan_search(state: ResearchState) -> dict:
"""Generate a search query based on the question and what we've already searched."""
already_tried = ", ".join(state["search_queries"]) if state["search_queries"] else "nothing yet"
prompt = f"""
You are a research assistant. The user asked: "{state['question']}"
You have already searched for: {already_tried}
Generate ONE specific search query that will help answer this question.
Return ONLY the search query, nothing else.
"""
response = llm.invoke(prompt)
query = response.content.strip()
return {
"search_queries": state["search_queries"] + [query]
}
Node 2: Search — execute the query
def execute_search(state: ResearchState) -> dict:
"""Run the most recent search query."""
# Get the latest query (last one added)
latest_query = state["search_queries"][-1]
# In production, replace this with Tavily, SerpAPI, etc.
result = mock_search(latest_query)
return {
"search_results": state["search_results"] + [result],
"iteration_count": state["iteration_count"] + 1
}
def mock_search(query: str) -> str:
"""Placeholder — replace with real search API."""
return f"[Search results for '{query}': Sample information relevant to the query would appear here in a real implementation.]"
Node 3: Evaluate — decide if we have enough
def evaluate_results(state: ResearchState) -> dict:
"""Decide whether we have enough information to answer the question."""
# This node doesn't update state directly — it's used for routing
# The actual routing logic goes in the conditional edge function
return {}
Node 4: Synthesize — write the final answer
def synthesize_answer(state: ResearchState) -> dict:
"""Write the final answer based on all collected research."""
results_text = "\n\n".join([
f"Search {i+1}: {result}"
for i, result in enumerate(state["search_results"])
])
prompt = f"""
Based on the following research, answer this question: "{state['question']}"
Research collected:
{results_text}
Write a clear, accurate answer based only on the information provided above.
If the research is insufficient, say so clearly.
"""
response = llm.invoke(prompt)
return {
"final_answer": response.content
}
Step 4: Define the Routing Logic
This is where the loop logic lives. After evaluating results, we either search again or synthesize.
def should_continue_searching(state: ResearchState) -> str:
"""Decide whether to search again or synthesize the answer."""
# Hard limit: never search more than 3 times
if state["iteration_count"] >= 3:
return "synthesize"
# Ask the LLM if we have enough information
results_summary = f"We have {len(state['search_results'])} search results covering: {', '.join(state['search_queries'])}"
prompt = f"""
Question: "{state['question']}"
{results_summary}
Do we have enough information to answer this question well?
Reply with ONLY "yes" or "no".
"""
response = llm.invoke(prompt)
answer = response.content.strip().lower()
if "yes" in answer:
return "synthesize"
else:
return "search_more"
Step 5: Build the Graph
from langgraph.graph import StateGraph, END
def build_research_agent():
graph = StateGraph(ResearchState)
# Add nodes
graph.add_node("plan", plan_search)
graph.add_node("search", execute_search)
graph.add_node("evaluate", evaluate_results)
graph.add_node("synthesize", synthesize_answer)
# Set the entry point
graph.set_entry_point("plan")
# Unconditional edges
graph.add_edge("plan", "search")
graph.add_edge("search", "evaluate")
# Conditional edge: evaluate → either more searching or synthesize
graph.add_conditional_edges(
"evaluate",
should_continue_searching,
{
"search_more": "plan", # Loop back to plan another search
"synthesize": "synthesize" # We're done, write the answer
}
)
# Synthesize → END
graph.add_edge("synthesize", END)
return graph.compile()
Step 6: Run It
def run_research_agent(question: str) -> str:
agent = build_research_agent()
# Initial state
initial_state = ResearchState(
question=question,
search_queries=[],
search_results=[],
iteration_count=0,
final_answer=""
)
# Run the graph
final_state = agent.invoke(initial_state)
return final_state["final_answer"]
# Try it
if __name__ == "__main__":
answer = run_research_agent(
"What are the key differences between LangGraph and CrewAI for production use?"
)
print(answer)
Step 7: Add Checkpointing
Checkpointing lets you persist state between steps — enabling pause-and-resume, debugging, and human-in-the-loop patterns.
from langgraph.checkpoint.memory import MemorySaver
def build_research_agent_with_checkpointing():
graph = StateGraph(ResearchState)
# ... (same node and edge definitions as above)
# Add a checkpointer
memory = MemorySaver()
return graph.compile(checkpointer=memory)
# Run with a thread ID for state persistence
def run_with_checkpointing(question: str, thread_id: str = "default"):
agent = build_research_agent_with_checkpointing()
config = {"configurable": {"thread_id": thread_id}}
initial_state = ResearchState(
question=question,
search_queries=[],
search_results=[],
iteration_count=0,
final_answer=""
)
final_state = agent.invoke(initial_state, config=config)
return final_state["final_answer"]
With checkpointing enabled, you can also inspect state at any step:
# Get the state at any point
state_snapshot = agent.get_state(config)
print(state_snapshot.values) # Full current state
print(state_snapshot.next) # Which node runs next
Common Patterns and Pitfalls
Pattern: Max iterations as a hard stop
Always include a maximum iteration count as a hard stop. LLM-based routing decisions can get stuck in loops. The hard limit is your safety net.
Pattern: Accumulate in lists
When nodes add to collections (search results, tool outputs, messages), use list concatenation (state["list"] + [new_item]) rather than mutation. LangGraph works better with immutable-style state updates.
Pitfall: Giant monolithic nodes
If your node is doing three different things, split it into three nodes. Nodes that do one thing are easier to debug, test, and modify. The graph is the control flow — don't hide control flow inside nodes.
Pitfall: State explosion
Don't put everything in state. State is for information that multiple nodes need to share. If something is only used within one node, keep it local to that node.
Pitfall: Skipping the hard limit
It's tempting to trust the LLM's routing judgment and skip the iteration limit. Don't. Models make routing mistakes. You want a guaranteed exit.
What's Next
This is the simplest useful LangGraph pattern: plan → act → evaluate → loop. Real production agents add:
- Tool binding: structured tool calls instead of prompting for actions
- Parallel execution: running multiple searches simultaneously
- Human-in-the-loop: pausing at specific nodes for human review
- Streaming: streaming intermediate steps to the UI
- Subgraphs: composing multiple smaller graphs into a larger pipeline
All of these build on the same core primitives: StateGraph, nodes, edges, and checkpointing. Once you have those down, the advanced patterns follow naturally.
Phase 5 of the MindloomHQ Agentic AI curriculum covers LangGraph in depth — including tool binding, multi-agent coordination, and production deployment patterns. If you're serious about building agents, that's the complete treatment.