iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

LangGraph (stateful agents)

LangGraph is LangChain’s framework for building stateful, multi-step LLM applications as graphs. Where a chain runs straight through, a graph can branch, loop, retry, and resume — the right tool for agents, multi-turn workflows, and human-in-the-loop systems.

Nodes, edges, state, conditional flow

EXAMPLE
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
import operator

llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)

# 1) State — a typed dict that flows through the graph
class State(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]    # accumulated
    question: str
    answer: str
    confidence: float

# Annotated[..., operator.add] means returned values are APPENDED, not replaced.
# Other reducers: operator.or_ for sets, custom functions for dicts.

# 2) Nodes — functions that take state, return (partial) state
def classify(state: State) -> dict:
    q = state['question']
    out = llm.invoke([
        SystemMessage(content='Classify the question as "factual" or "opinion".'),
        HumanMessage(content=q),
    ])
    return { 'messages': [out], 'confidence': 1.0 if 'factual' in out.content.lower() else 0.5 }

def answer_factual(state: State) -> dict:
    out = llm.invoke([HumanMessage(content=f'Answer factually: ${state["question"]}')])
    return { 'messages': [out], 'answer': out.content }

def answer_opinion(state: State) -> dict:
    out = llm.invoke([HumanMessage(content=f'Give a nuanced opinion: ${state["question"]}')])
    return { 'messages': [out], 'answer': out.content }

# 3) Routing — conditional edges based on state
def route(state: State) -> str:
    return 'factual' if state['confidence'] > 0.8 else 'opinion'

# 4) Build the graph
graph = StateGraph(State)
graph.add_node('classify',  classify)
graph.add_node('factual',   answer_factual)
graph.add_node('opinion',   answer_opinion)

graph.add_edge(START, 'classify')
graph.add_conditional_edges('classify', route, {'factual': 'factual', 'opinion': 'opinion'})
graph.add_edge('factual', END)
graph.add_edge('opinion', END)

app = graph.compile()

# 5) Run it
result = app.invoke({
    'question': 'When was the eiffel tower built?',
    'messages': [], 'answer': '', 'confidence': 0.0,
})
print(result['answer'])

# 6) Streaming
for event in app.stream({'question': 'Why is the sky blue?', 'messages': [], 'answer': '', 'confidence': 0.0}):
    print(event)
# Yields each node's output as it produces — great for streaming progress to the UI.

# 7) Cycles + loops — try until a quality threshold is met
class DraftState(TypedDict):
    topic:      str
    draft:      str
    feedback:   str
    iterations: int

def write(state: DraftState) -> dict:
    msg = f'Write a one-paragraph essay on ${state["topic"]}'
    if state.get('feedback'):
        msg += f'\\n\\nRevise based on: ${state["feedback"]}'
    out = llm.invoke([HumanMessage(content=msg)])
    return { 'draft': out.content, 'iterations': state['iterations'] + 1 }

def critique(state: DraftState) -> dict:
    if state['iterations'] >= 3:
        return { 'feedback': 'DONE' }
    out = llm.invoke([HumanMessage(content=f'Critique this draft, or say DONE if good:\\n${state["draft"]}')])
    return { 'feedback': out.content }

def should_loop(state: DraftState) -> str:
    return END if 'DONE' in state['feedback'].upper() else 'write'

g = StateGraph(DraftState)
g.add_node('write',    write)
g.add_node('critique', critique)
g.add_edge(START, 'write')
g.add_edge('write', 'critique')
g.add_conditional_edges('critique', should_loop, {'write': 'write', END: END})

draft_app = g.compile()
final = draft_app.invoke({'topic': 'sustainable cities', 'draft': '', 'feedback': '', 'iterations': 0})
print(final['draft'])

# 8) Tool use — agents with proper planning
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

@tool
def get_weather(city: str) -> str:
    'Return current weather for a city.'
    return f'It is 22°C and sunny in {city}.'

@tool
def search_web(query: str) -> str:
    'Search the web.'
    return 'Search results placeholder.'

agent = create_react_agent(llm, tools=[get_weather, search_web])
result = agent.invoke({'messages': [HumanMessage(content='What is the weather in Sydney?')]})
print(result['messages'][-1].content)

# 9) Checkpointing — pause and resume
checkpointer = MemorySaver()
app_with_memory = graph.compile(checkpointer=checkpointer)

config = { 'configurable': { 'thread_id': 'session-42' } }
app_with_memory.invoke({'question': 'Tell me about Sydney', 'messages': [], 'answer': '', 'confidence': 0.0}, config)

# Same thread_id → resumes with prior state
app_with_memory.invoke({'question': 'What about its harbour?', 'messages': [], 'answer': '', 'confidence': 0.0}, config)

# For production, use a persistent checkpointer (Postgres, Redis):
# from langgraph.checkpoint.postgres import PostgresSaver

# 10) Human-in-the-loop — interrupt before / after a node
graph.compile(checkpointer=checkpointer, interrupt_before=['answer_factual'])

# UI side: receive the state, render approval UI, then call:
app_with_memory.update_state(config, { 'answer': 'Approved' }, as_node='answer_factual')
app_with_memory.invoke(None, config)        # continues from the checkpoint

# 11) Subgraphs — encapsulate complex sub-workflows
def build_research_subgraph() -> StateGraph:
    g = StateGraph(State)
    g.add_node('search', lambda s: { 'messages': [llm.invoke([HumanMessage(content='Search…')])] })
    g.add_node('summarise', lambda s: { 'messages': [llm.invoke([HumanMessage(content='Summarise the results…')])] })
    g.add_edge(START, 'search')
    g.add_edge('search', 'summarise')
    g.add_edge('summarise', END)
    return g.compile()

main = StateGraph(State)
main.add_node('research', build_research_subgraph())
main.add_node('answer',   answer_factual)
main.add_edge(START, 'research')
main.add_edge('research', 'answer')
main.add_edge('answer', END)

# 12) Parallel branches — fan out / fan in
from langgraph.graph import StateGraph, START, END

class FanState(TypedDict):
    question: str
    results:  Annotated[list[str], operator.add]

def search_web(state):  return { 'results': ['web result'] }
def search_db(state):   return { 'results': ['db result'] }
def merge(state):       return { 'answer': '\\n'.join(state['results']) }

g = StateGraph(FanState)
g.add_node('web', search_web)
g.add_node('db',  search_db)
g.add_node('merge', merge)
g.add_edge(START, 'web')
g.add_edge(START, 'db')
g.add_edge('web',   'merge')
g.add_edge('db',    'merge')
g.add_edge('merge', END)
# 'merge' runs once both upstream nodes finish; results auto-accumulated via operator.add.

# 13) Visualising the graph
print(graph.get_graph().draw_mermaid())     # ASCII / Mermaid
# Or render with graphviz / save to a PNG for docs

# 14) Observability — LangSmith integration
import os
os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_PROJECT'] = 'my-graph'
# Every node, every LLM call, every retry traced in the LangSmith UI.

# 15) Patterns to reach for
# • Plan → Execute → Reflect → Loop until done (Tree of Thoughts, ReAct, Reflexion)
# • Reviewer + Executor — second LLM checks the first's work before returning
# • Multi-agent — supervisor routes to specialists (researcher, coder, summariser)
# • Async/sync transitions — node returns a coroutine for long-running work
# • Streaming user updates while planning the next step

# 16) Common bugs
# • Mutating state inside a node — return a NEW dict instead
# • Forgetting Annotated reducers — list overwrites instead of appending
# • Infinite loops — always have a termination check + an iteration count guard
# • Conditional routes pointing to nonexistent nodes — runtime error
# • Tool calls inside a node that returns nothing — agent can't see the result; emit a Message
# • MemorySaver in production — fine for demos; switch to PostgresSaver for real apps
# • Forgetting thread_id when using checkpoints — every call looks like a fresh conversation

Why it matters

LangGraph turns LLM apps into proper state machines — nodes operate on a typed state, edges route conditionally, checkpoints pause for humans or external events. Reach for it when you outgrow linear chains: agents that retry, multi-agent supervision, drafts with critique loops, or anything that needs to resume across requests.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
from langgraph.graph import StateGraph, END
g = StateGraph(dict)
g.add_node('decide', decide)
g.add_node('do_work', do_work)
g.add_edge('decide', 'do_work')
g.add_edge('do_work', END)
g.set_entry_point('decide')
app = g.compile()
Try it Yourself »

Discussion

Loading…