Post

LangGraph vs LangChain: Building Stateful AI Agents in Python

Compare LangGraph and LangChain for building AI agents in Python. Learn when to use chains vs graphs, how state and cycles work, and build a working multi-step agent with LangGraph.

LangGraph vs LangChain: Building Stateful AI Agents in Python

The Core Difference

LangChain models workflows as chains — mostly linear sequences of steps. LangGraph models them as graphs — nodes and edges that can loop, branch, and maintain state across steps. If your agent needs to retry, re-plan, or wait for a human approval before continuing, a chain gets awkward fast. A graph handles it naturally.

If you’ve already built agents from Building AI Agents with Python or used the OpenAI Agents SDK, LangGraph solves the same problem with an explicit state machine instead of an SDK’s built-in loop.

1
pip install langgraph langchain langchain-openai

When LangChain Is Enough

For straightforward pipelines — retrieve, then prompt, then parse — a chain is simpler and easier to read:

1
2
3
4
5
6
7
8
9
10
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template("Summarize this in one sentence: {text}")

chain = prompt | llm | StrOutputParser()
result = chain.invoke({"text": "Long article text here..."})
print(result)

This is fine until the workflow needs to branch based on the output, retry on failure, or call itself again with refined input. That’s where graphs win.

When You Need LangGraph

LangGraph represents your agent as a graph of nodes (functions) and edges (transitions), with a shared state object that persists across the whole run.

1
2
3
4
5
6
7
8
from typing import TypedDict
from langgraph.graph import StateGraph, END

class AgentState(TypedDict):
    question: str
    draft: str
    attempts: int
    final: str

This AgentState is the single source of truth every node reads from and writes to — no passing context manually between functions.

Building a Self-Correcting Agent

Here’s a graph that drafts an answer, critiques it, and retries up to 3 times if the critique fails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

def draft_node(state: AgentState) -> AgentState:
    response = llm.invoke(f"Answer this question: {state['question']}")
    return {**state, "draft": response.content, "attempts": state["attempts"] + 1}

def critique_node(state: AgentState) -> AgentState:
    critique = llm.invoke(
        f"Is this answer correct and complete? Reply only YES or NO.\n\nQ: {state['question']}\nA: {state['draft']}"
    )
    passed = "YES" in critique.content.upper()
    return {**state, "final": state["draft"] if passed else ""}

def should_retry(state: AgentState) -> str:
    if state["final"]:
        return "end"
    if state["attempts"] >= 3:
        return "end"
    return "retry"

graph = StateGraph(AgentState)
graph.add_node("draft", draft_node)
graph.add_node("critique", critique_node)

graph.set_entry_point("draft")
graph.add_edge("draft", "critique")
graph.add_conditional_edges("critique", should_retry, {"retry": "draft", "end": END})

app = graph.compile()
1
2
result = app.invoke({"question": "What is the time complexity of binary search?", "draft": "", "attempts": 0, "final": ""})
print(result["final"])
1
Binary search has O(log n) time complexity because it halves the search space on each comparison.

The add_conditional_edges call is the part LangChain can’t express cleanly — a loop that runs until a condition is met, with full state carried across every iteration.

Adding a Human-in-the-Loop Checkpoint

LangGraph supports interrupting execution to wait for human input — useful for approval workflows:

1
2
3
4
5
6
7
8
9
10
from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()
app = graph.compile(checkpointer=memory, interrupt_before=["critique"])

config = {"configurable": {"thread_id": "session-1"}}
app.invoke({"question": "Draft a refund policy", "draft": "", "attempts": 0, "final": ""}, config=config)

# Execution pauses before "critique" — inspect state, then resume:
app.invoke(None, config=config)

This pattern — pause, inspect, resume — is exactly what production agents need before taking an irreversible action like sending an email or charging a card.

Decision Table

ScenarioUse
Linear retrieve → prompt → parseLangChain
Retry loops, self-correctionLangGraph
Multi-agent handoff with shared stateLangGraph
Human approval checkpointsLangGraph
Simple RAG question-answeringLangChain

Key Takeaways

  • LangChain is best for linear, single-pass pipelines
  • LangGraph is best when your agent needs loops, branches, or persistent state across steps
  • StateGraph with a TypedDict state gives every node a shared, typed context
  • add_conditional_edges is how you implement retry and self-correction loops
  • Checkpointing with interrupt_before enables human-in-the-loop approval gates
  • You can use both together: LangChain components as nodes inside a LangGraph
Khushal Jethava
Khushal Jethava

Machine Learning Engineer at Codiste, specializing in Generative AI, NLP, and Computer Vision. Building production AI systems with Python.

This post is licensed under CC BY 4.0 by the author.