Why LangGraph: The Limitations of Simple Chains
Simple LLM chains work beautifully for straightforward tasks like summarization, translation, or single-turn question answering. But real-world AI agents need capabilities that linear chains cannot provide. Consider a research agent that searches the web, evaluates whether the results are sufficient, searches again if not, synthesizes findings, and asks the user for approval before sending a report. This workflow has loops, conditional branching, and human interaction, none of which fit neatly into a linear chain.
LangGraph addresses this by modeling your agent as a graph where nodes are functions and edges define the flow between them. Unlike a chain where data flows in one direction, a LangGraph graph can have cycles: a node's output can route back to a previous node based on conditions. This is essential for the iterative nature of agentic work, where the agent often needs to retry, refine, or loop through multiple steps.
The other key innovation is state management. LangGraph maintains a typed state object that persists across the entire execution of the graph. Every node reads from and writes to this shared state, enabling complex coordination between steps. When combined with LangGraph's checkpointing system, this state can persist across sessions, meaning an agent can pause, wait for human input, and resume hours later exactly where it left off.
If you are coming from LangChain, think of LangGraph as the upgrade path for any workflow that outgrew a simple chain. LangChain handles the individual steps; LangGraph orchestrates them into complex, stateful processes.
Core Concepts: State, Nodes, and Edges
Every LangGraph application starts with a State definition. State is a TypedDict that declares all the data your graph needs to track. For a chatbot, this might include messages (the conversation history), a retrieved_context field, and a current_intent field. LangGraph uses a special Annotated type with reducer functions to define how state updates are merged. The most common reducer is operator.add for lists, which appends new messages rather than replacing the entire list.
Nodes are Python functions that take the current state as input and return a partial state update. A node named 'retrieve' might take the latest user message, search a vector database, and return {'retrieved_context': results}. A node named 'generate' might read the retrieved_context and the user's question, call an LLM, and return {'messages': [ai_response]}. Each node focuses on one responsibility.
Edges connect nodes and define the execution flow. Normal edges create a fixed path: after 'retrieve', always go to 'generate'. Conditional edges evaluate a function against the current state and route to different nodes based on the result. For example, after 'generate', a conditional edge might check if the response needs more information and route back to 'retrieve', or check if it is complete and route to the 'end' node.
The graph is compiled with graph.compile(), which validates the structure and produces an executable application. You invoke it with graph.invoke(initial_state) for synchronous execution or graph.astream(initial_state) for streaming. The compiled graph handles all the routing, state management, and checkpointing automatically.
Building Your First LangGraph Agent Step by Step
Let us build a simple ReAct agent using LangGraph. The agent will answer questions by optionally searching the web, deciding whether to search or respond directly based on the question.
First, define the state with a messages field using the add reducer, so every new message appends to the conversation history. Then define two nodes: 'agent' which calls the LLM with the current messages and bound tools, and 'tools' which executes any tool calls the LLM decided to make.
The 'agent' node is straightforward: it takes state['messages'], passes them to a ChatOpenAI model with tools bound, and returns the model's response as a new message. The 'tools' node is a ToolNode from langgraph.prebuilt, which automatically routes to the correct tool function based on the model's tool calls.
Now add a conditional edge after the 'agent' node. If the LLM's response contains tool calls, route to the 'tools' node. If there are no tool calls, the agent has decided to respond directly, so route to END. After the 'tools' node, always route back to the 'agent' node so the LLM can process the tool results and decide what to do next.
This creates the classic ReAct loop: the agent reasons, decides to act by calling a tool, observes the result, reasons again, and either acts again or provides a final answer. The graph naturally handles multiple tool calls in sequence because the tools-to-agent edge creates a cycle. Compile the graph with a MemorySaver checkpointer and you get automatic conversation persistence across invocations, identified by a thread_id in the config.
Human-in-the-Loop and Interrupt Patterns
One of LangGraph's most powerful features is human-in-the-loop (HITL) support. Many production agent workflows require human approval at critical points: before executing a database write, sending an email, or making a financial transaction. LangGraph handles this with interrupt patterns that pause the graph, wait for human input, and resume seamlessly.
The simplest HITL pattern uses the interrupt_before parameter when adding nodes. When the graph reaches a node marked with interrupt_before, it saves its state to the checkpointer and stops execution. Your application can then display the pending action to a human, collect their approval or modification, and resume the graph with graph.invoke(None, config) to continue from where it paused.
A more sophisticated pattern uses a dedicated 'human_review' node. This node can present the agent's proposed action, collect feedback, and update the state accordingly. If the human approves, execution continues. If they reject, the graph routes back to a revision node where the agent modifies its approach based on the feedback.
Checkpointing is what makes this work across time. LangGraph supports SQLite, PostgreSQL, and custom checkpointer backends. The entire graph state, including all messages, intermediate results, and metadata, is serialized and stored. When the human responds minutes or hours later, the graph rehydrates from the checkpoint and continues as if no time had passed. This pattern is essential for enterprise applications where automated actions require supervisory approval, and it is a major differentiator for LangGraph compared to simpler agent frameworks that assume continuous execution.
Debugging and Visualizing LangGraph Workflows
Debugging agentic workflows is notoriously difficult because the execution path is dynamic. An agent might take three steps on one input and fifteen on another. LangGraph provides several tools to make debugging tractable.
First, the graph.get_graph().draw_mermaid() method generates a visual diagram of your graph showing all nodes and edges. This is invaluable during development for verifying that your routing logic is correct. You can render the diagram in Jupyter notebooks, save it as an image, or include it in documentation.
Second, LangSmith integration provides full tracing of every graph execution. Each node invocation is logged with its input state, output state, and execution time. You can see exactly which path the agent took through the graph and inspect the LLM's reasoning at each step. When a user reports a problem, you can pull up the trace and identify exactly where things went wrong.
Third, use the step-by-step streaming mode to watch the agent work in real time. graph.astream(input, stream_mode='updates') yields state updates after each node, letting you build live dashboards that show the agent's progress through the workflow.
Common debugging scenarios include infinite loops (add a max_iterations parameter or a counter in state), unexpected routing (add print statements in your conditional edge functions or check the LangSmith trace), and state corruption (validate state updates in each node using Pydantic models). As your graphs grow more complex, invest in comprehensive test suites that verify each node independently and test end-to-end flows with mocked LLM responses. LangGraph's deterministic routing logic makes it very testable once you mock the non-deterministic LLM calls.
Code Example
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode, tools_condition
from typing import Annotated, TypedDict
from langchain_core.messages import AnyMessage
import operator
class State(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
model = ChatOpenAI(model="gpt-4o").bind_tools([search_tool])
def agent(state: State):
return {"messages": [model.invoke(state["messages"])]}
graph = StateGraph(State)
graph.add_node("agent", agent)
graph.add_node("tools", ToolNode([search_tool]))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", tools_condition)
graph.add_edge("tools", "agent")
app = graph.compile()