LangGraph: Stateful Agents
What You'll Build Today
Welcome to Day 55. Today marks a significant shift in your journey. Up until now, we have built "Chains"—sequences where step A leads to step B, which leads to step C. It’s a straight line.
But real life isn't a straight line. Sometimes you try step A, realize you didn't get enough information, go back to step A, try step B, realize you made a mistake, and go back to step A again.
Today, you are going to build a Research Agent. This isn't just a chatbot that answers a question once. This is a system that can:
Here are the concepts you will master:
* State Management: Why your AI needs a "memory" that persists across different steps of logic, not just a chat history.
* Cycles (Loops): How to allow an AI to repeat a task until it gets it right (something a standard Chain cannot do).
* Conditional Edges: How to let the AI act as a traffic cop, deciding which path to take based on the data it finds.
* Human-in-the-loop: How to pause the AI right before a critical action so a human can approve or reject it.
Let's get started.
---
The Problem
To understand why we need LangGraph, we have to look at how painful it is to build a "looping" agent using standard Python and simple LLM calls.
Imagine you want an AI to write a short bio about a person. You want it to search, check if the bio is detailed enough, and if not, search again.
If you try to code this with a standard while loop and a basic chain, you end up managing a lot of messy variables manually. You have to track the conversation history, the number of attempts, the current summary, and the decision logic all in one giant block of code.
Here is what that "painful" code looks like. Read it and notice how brittle it feels.
import os
# Assuming you have set your OPENAI_API_KEY in your environment
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
llm = ChatOpenAI(model="gpt-4o-mini")
def painful_research_agent(topic):
# We have to manually manage all this state
messages = [SystemMessage(content="You are a researcher.")]
messages.append(HumanMessage(content=f"Research this: {topic}"))
iterations = 0
max_retries = 3
is_satisfied = False
# The "Loop" of Pain
while not is_satisfied and iterations < max_retries:
print(f"--- Iteration {iterations + 1} ---")
# Call the LLM
response = llm.invoke(messages)
content = response.content
messages.append(response) # We have to manually append history
# Now we have to ask the LLM specifically if it's done
# This requires a SECOND call or complex parsing
decision_messages = [
SystemMessage(content="Analyze the previous text. Does it have enough info? Respond ONLY with 'YES' or 'NO'."),
HumanMessage(content=content)
]
decision = llm.invoke(decision_messages).content
if "YES" in decision:
is_satisfied = True
print("Agent is satisfied.")
else:
print("Agent needs more info. Looping...")
messages.append(HumanMessage(content="That wasn't enough. Search for more specific details."))
iterations += 1
return messages[-1].content
# Running this is unpredictable and hard to debug
# painful_research_agent("The history of the Python programming language")
Why this is frustrating:
while loop would become a nightmare of if/else statements.There has to be a way to define the flow (the graph) separately from the work (the nodes). That is LangGraph.
---
Let's Build It
We are going to build this properly using LangGraph. Think of LangGraph as a flowchart engine. We define the boxes (Nodes) and the arrows (Edges), and LangGraph handles the movement between them.
Step 1: Dependencies and Setup
First, we need to install the library specifically designed for this.
``bash
pip install langgraph langchain langchain_openai
`
Now, let's import our tools. We will also define a "Mock Search" tool so you can run this code without needing a Google Search API key today.
import operator
from typing import Annotated, List, TypedDict, Union
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langgraph.graph import StateGraph, END
# Setup the LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# A Mock Search Tool (so we don't need extra API keys for this lesson)
def search_web(query: str):
print(f" [Tool] Searching web for: {query}")
# Simulating results based on the query
if "weather" in query.lower():
return "The weather in New York is currently 72 degrees and sunny."
elif "population" in query.lower():
return "The population of New York City is approximately 8.4 million."
else:
return "New York City is a large city in the United States."
Step 2: Define the State
This is the most important concept in LangGraph. The State is a dictionary that holds all the information the agent knows. Every node (step) receives this State, modifies it, and passes it to the next node.
We use
TypedDict to ensure we know exactly what data is available.
# The State tracks the conversation history
class AgentState(TypedDict):
# Annotated[list, operator.add] means:
# When a node returns a new message, ADD it to the existing list
# instead of overwriting the whole list.
messages: Annotated[List[BaseMessage], operator.add]
# We also track a loop count to prevent infinite loops
loop_count: int
Step 3: Define the Nodes
Nodes are just Python functions. They take the current
state as input and return a dictionary with updates to the state.
We need two nodes:
Agent Node: Decides what to do (search or answer).
Tool Node: Performs the search.
def agent_node(state: AgentState):
print("--- Agent Node: Thinking ---")
messages = state['messages']
# We ask the LLM what to do
# We bind the tool so the LLM knows it exists
# Note: In a real app, we'd use bind_tools, but let's keep it manual for clarity
last_message = messages[-1]
# Simple logic: If the last message was a tool output, summarize it.
# If the last message was a user query, call the tool.
# If we have looped too many times, force a finish
if state.get('loop_count', 0) > 2:
return {"messages": [SystemMessage(content="I have searched enough. Here is the summary based on what I found.")]}
# If the last message is from the user, we generate a search query
if isinstance(last_message, HumanMessage):
# The LLM decides the search query
response = llm.invoke(
[SystemMessage(content="You are a researcher. Output ONLY a search query for the user's topic.")] + messages
)
return {"messages": [response], "loop_count": state.get("loop_count", 0)}
# If the last message was a search result (SystemMessage in our simple logic), we summarize
return {"messages": [SystemMessage(content="Here is the answer based on the search.")]}
def tool_node(state: AgentState):
print("--- Tool Node: Searching ---")
messages = state['messages']
last_message = messages[-1] # This is the search query from the agent
# Extract query text
query = last_message.content
# Run the search
result = search_web(query)
# Return the result as a message
# We increment the loop count here
current_count = state.get("loop_count", 0)
return {
"messages": [SystemMessage(content=f"Search Result: {result}")],
"loop_count": current_count + 1
}
Step 4: Define the Edges (The Logic)
Now we connect the nodes. We need a Conditional Edge. This is a function that looks at the state and decides: "Where do we go next?"
def should_continue(state: AgentState):
messages = state['messages']
last_message = messages[-1]
# If the agent just summarized the final answer, we stop.
if "Here is the answer" in last_message.content or "searched enough" in last_message.content:
return "end"
# If the agent just generated a search query, we go to the tool.
return "continue"
Step 5: Build and Compile the Graph
We assemble the pieces. This creates the "flowchart."
# Initialize the graph
workflow = StateGraph(AgentState)
# Add the nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tool", tool_node)
# Set the entry point (where we start)
workflow.set_entry_point("agent")
# Add a standard edge: After the tool runs, always go back to the agent
workflow.add_edge("tool", "agent")
# Add a conditional edge: After the agent runs, decide what to do
workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "tool",
"end": END
}
)
# Compile the graph into a runnable application
app = workflow.compile()
Step 6: Run It!
Let's see our cyclic agent in action.
print("Starting the Research Agent...")
initial_state = {
"messages": [HumanMessage(content="What is the weather in New York?")],
"loop_count": 0
}
# Stream the output so we can see steps happening
for output in app.stream(initial_state):
# The output is a dictionary of which node ran and its result
for key, value in output.items():
print(f"Finished running: {key}")
Expected Output Analysis:
Agent Node runs first. It sees the user question and generates a search query.
Conditional Edge sees a query, so it routes to tool.
Tool Node runs. It calls search_web and returns the weather data. It increments loop_count.
Agent Node runs again (The Loop!). It sees the search result. It decides it has enough info. It generates the final answer.
Conditional Edge sees the final answer, routes to END.
We just built a thinking loop!
---
Now You Try
You have a working graph. Now, let's make it smarter.
Add a "Critique" Node:
Create a new function
critique_node. Insert it after the tool_node but before the agent_node. Have this node use the LLM to look at the search result and prepend "CRITIQUE: Good result" or "CRITIQUE: Bad result" to the message.
Dynamic Query Generator:
In the
agent_node, if the loop count is greater than 0 (meaning we are on the second try), change the prompt to: "The previous search wasn't enough. Generate a DIFFERENT search query."
Save to File:
Add a final node called
save_node connected to the END condition. Have it write the final conversation history to a text file named research_log.txt.
---
Challenge Project: The Human Checkpoint
Real agents sometimes need permission. Imagine an agent that drafts an email for you. You don't want it to send the email automatically; you want to review it first.
LangGraph makes this incredibly easy with Checkpoints.
The Goal:
Modify your graph so that before the
tool_node executes (simulating an "expensive" or "dangerous" action like searching or sending data), the graph pauses and waits for you to type "y" in the console.
Requirements:
Use memory = MemorySaver() (you will need to import MemorySaver from langgraph.checkpoint.memory).
Pass checkpointer=memory when you compile the graph.
Use interrupt_before=["tool"] in the compile options.
Run the graph. It should stop before the tool.
Manually resume the graph using app.invoke(None, config=...).
Hints:
* You will need to pass a
config dictionary with a thread_id when you run the app (e.g., config={"configurable": {"thread_id": "1"}}). This separates different conversations.
When the graph pauses, the script will finish its current execution block. You will need to check the state, ask for input, and then run app.invoke again with the same* thread ID and None` as the input (signaling "continue").
Example Flow:
Agent: I want to search for "Python History".
[SYSTEM PAUSED]
User Input: Do you approve this search? (y/n): y
[SYSTEM RESUMES]
Tool: Searching for "Python History"...
---
What You Learned
Today you moved from linear chains to stateful graphs.
* StateGraph: The blueprint of your AI application.
* Nodes: The workers that do the tasks.
* Edges: The logic that connects the workers.
* Cycles: The ability to loop back and improve results iteratively.
* Conditional Logic: The ability to make decisions dynamically based on data.
Why This Matters:In the real world, AI agents are rarely "one-shot" wonders. They are iterative. A coding agent writes code, runs it, sees an error, and corrects it. A legal agent reads a contract, finds a missing clause, and searches for precedents. You cannot build these systems with simple linear chains. You need the graph.
Tomorrow: We take this one step further. If one agent is powerful, what happens when we have a Team of Agents? Tomorrow we build Multi-Agent Systems, where a "Researcher" agent hands off work to a "Writer" agent. See you there!