Skip to content

LangGraph

LangGraph builds stateful, multi-step AI agents as graphs — not linear chains. Nodes are functions, edges define what runs next, and a shared state object flows through the whole graph. This unlocks loops, branching, human-in-the-loop, and multi-agent patterns that LangChain's linear chains can't handle.


Why LangGraph? The Linear Chain Problem

LangChain linear chain:
  Step1 → Step2 → Step3 → Done
  No loops. No branching. No "go back if this fails."

Real agent behaviour needs:
  - Loops: "keep searching until you find the answer"
  - Branching: "if the tool fails, try a different approach"
  - Conditionals: "if user approved, continue; else, ask again"
  - Parallel paths: "call two tools simultaneously"
  - Sub-agents: "delegate to a specialist agent"

LangGraph solves all of this with an explicit graph structure.

Core Concepts Mindmap

mindmap
  root((LangGraph))
    StateGraph
      Typed state TypedDict
      Shared across all nodes
      Immutable updates via Annotated
      append vs replace
    Nodes
      Python functions
      Receive state return update
      LLM calls tool calls or logic
    Edges
      Normal fixed next node
      Conditional route based on state
      END terminal node
    Checkpointing
      MemorySaver in-memory
      SqliteSaver file-based
      PostgresSaver production
      Human-in-the-loop via interrupt
    Patterns
      ReAct Agent Loop
      Supervisor Multi-Agent
      Subgraph composition
      Parallel node execution
      Map-Reduce

The State Object

State is the central data structure. Every node receives the current state and returns updates to it. Nodes don't communicate with each other directly — only through state.

from typing import TypedDict, Annotated
from operator import add

# Define what your agent tracks
class AgentState(TypedDict):
    # Messages: annotated with add → new messages APPEND to list
    messages: Annotated[list, add]

    # Simple fields: replaced each time a node updates them
    user_query: str
    retrieved_docs: list[str]
    final_answer: str
    retry_count: int

add annotation vs regular field

messages: Annotated[list, add]
  → When a node returns {"messages": [new_msg]}
     it APPENDs to the existing list
  → History accumulates automatically

retrieved_docs: list[str]   (no annotation)
  → When a node returns {"retrieved_docs": [...]}
     it REPLACES the previous value entirely

Building a Graph

from langgraph.graph import StateGraph, END

def retrieve_node(state: AgentState) -> dict:
    """Fetch relevant documents."""
    docs = retriever.invoke(state["user_query"])
    return {"retrieved_docs": [d.page_content for d in docs]}

def generate_node(state: AgentState) -> dict:
    """Generate answer from docs."""
    context = "\n".join(state["retrieved_docs"])
    answer = llm.invoke(f"Context: {context}\nQuestion: {state['user_query']}")
    return {"messages": [answer], "final_answer": answer.content}

def should_retry(state: AgentState) -> str:
    """Conditional edge — decide next node."""
    if state["retry_count"] < 3 and not state["final_answer"]:
        return "retrieve"   # loop back
    return END              # done

# Build the graph
builder = StateGraph(AgentState)

# Add nodes
builder.add_node("retrieve", retrieve_node)
builder.add_node("generate", generate_node)

# Add edges
builder.set_entry_point("retrieve")           # where to start
builder.add_edge("retrieve", "generate")       # retrieve → generate (always)
builder.add_conditional_edges(                 # generate → ? (depends on state)
    "generate",
    should_retry,
    {"retrieve": "retrieve", END: END},        # map return value → node
)

# Compile
graph = builder.compile()

# Run
result = graph.invoke({"user_query": "How does RAG work?", "retry_count": 0})

The ReAct Agent Loop

The most common LangGraph pattern — implements the Thought → Action → Observation loop as a graph with a cycle.

                    ┌─────────────────┐
                    │   START         │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │  call_model     │  ← LLM decides: use tool? or answer?
                    │  (Thought step) │
                    └────────┬────────┘
                             │
               ┌─────────────▼─────────────┐
               │   should_continue?         │
               │   (conditional edge)       │
               └──────┬──────────┬──────────┘
                      │          │
              tool_call?       no tool call
                      │          │
            ┌─────────▼──┐    ┌──▼───────┐
            │  call_tools │    │   END    │
            │  (Action +  │    └──────────┘
            │  Observation│
            └─────────────┘
                      │
                      └──────────────▶ (back to call_model)
from langgraph.prebuilt import create_react_agent

# LangGraph's built-in ReAct agent (handles the loop for you)
agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[search_tool, calculator_tool],
)

result = agent.invoke({
    "messages": [("human", "What is 15% of the population of Tokyo?")]
})

Checkpointing & Human-in-the-Loop

Checkpointing saves the graph state after each node. This enables: - Persistence: resume a conversation after the process restarts - Human-in-the-loop: pause, ask a human to review, then continue - Time travel: inspect and replay state from any previous checkpoint

from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.sqlite import SqliteSaver  # production-safe

# Add checkpointer at compile time
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)

# Thread ID scopes the conversation (like session ID)
config = {"configurable": {"thread_id": "user-42-session-1"}}

# First invocation
graph.invoke({"messages": [("human", "Hi!")]}, config=config)

# Second invocation — graph remembers previous messages automatically
graph.invoke({"messages": [("human", "What did I just say?")]}, config=config)

# Human-in-the-loop: compile with interrupt_before
graph = builder.compile(
    checkpointer=memory,
    interrupt_before=["sensitive_action_node"],  # pause before this node
)

graph.invoke(input, config)
# Graph pauses at sensitive_action_node
# Human reviews state...
state = graph.get_state(config)
print(state.values)  # inspect current state

# Resume (optionally update state first)
graph.invoke(None, config)  # None = "continue from where you left off"

Multi-Agent Patterns

Supervisor Pattern

                    ┌──────────────────┐
    User ──────────▶│   Supervisor LLM │
                    │  (decides which  │
                    │   agent to call) │
                    └────────┬─────────┘
             ┌───────────────┼────────────────┐
             ▼               ▼                ▼
      ┌──────────┐   ┌──────────────┐   ┌──────────────┐
      │  Search  │   │ Code Writer  │   │  Data Analyst│
      │  Agent   │   │   Agent      │   │    Agent     │
      └──────────┘   └──────────────┘   └──────────────┘
             │               │                │
             └───────────────┴────────────────┘
                             │
                    ┌────────▼────────┐
                    │   Supervisor    │
                    │   (aggregate +  │
                    │    respond)     │
                    └─────────────────┘
from langgraph.graph import StateGraph
from langchain_core.messages import HumanMessage

# Each agent is itself a compiled graph (subgraph)
search_agent = create_react_agent(llm, [search_tool])
code_agent   = create_react_agent(llm, [python_repl_tool])

def supervisor_node(state):
    # Supervisor LLM decides: which agent to delegate to?
    decision = supervisor_llm.invoke(state["messages"])
    return {"next_agent": decision.next}   # "search", "code", or "FINISH"

def route(state) -> str:
    return state["next_agent"]

builder = StateGraph(SupervisorState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("search",     search_agent)
builder.add_node("code",       code_agent)

builder.set_entry_point("supervisor")
builder.add_conditional_edges("supervisor", route,
    {"search": "search", "code": "code", "FINISH": END})
builder.add_edge("search", "supervisor")  # always report back
builder.add_edge("code",   "supervisor")

LangGraph vs LangChain Chains

┌─────────────────────┬──────────────────────┬────────────────────────────┐
│ Feature             │ LangChain Chain       │ LangGraph                  │
├─────────────────────┼──────────────────────┼────────────────────────────┤
│ Execution flow      │ Linear (A→B→C)        │ Graph (any topology)       │
│ Loops               │ ❌                    │ ✅                         │
│ Branching           │ Limited via routers   │ ✅ Conditional edges       │
│ State management    │ Manual                │ ✅ Typed state object       │
│ Human-in-the-loop   │ ❌                    │ ✅ interrupt_before/after  │
│ Checkpointing       │ ❌                    │ ✅ Built-in                 │
│ Multi-agent         │ ❌                    │ ✅ Subgraphs + supervisor   │
│ Debugging           │ Callbacks             │ ✅ LangSmith integration    │
│ Complexity          │ Low                   │ Higher                     │
│ Best for            │ Simple pipelines      │ Agents, complex workflows  │
└─────────────────────┴──────────────────────┴────────────────────────────┘

Quick Reference

Key imports:
  from langgraph.graph import StateGraph, END
  from langgraph.prebuilt import create_react_agent
  from langgraph.checkpoint.memory import MemorySaver

State:
  TypedDict with Annotated[list, add] for accumulating lists
  Regular fields get replaced, annotated fields get merged

Graph building:
  builder = StateGraph(MyState)
  builder.add_node("name", function)
  builder.set_entry_point("name")
  builder.add_edge("a", "b")                         # fixed edge
  builder.add_conditional_edges("a", router_fn, map) # conditional edge
  graph = builder.compile(checkpointer=MemorySaver())

Running:
  graph.invoke(input, config={"configurable": {"thread_id": "abc"}})
  graph.stream(input, config=...)        # yields state after each node
  graph.get_state(config)                # inspect current checkpoint