You Probably Don't Need LangGraph | Generated by AI

Home 2026.06

Question: Why use LangGraph? How does it work? Why not just write plain Python with an LLM client?

Answer:

Short answer: You probably don’t need it

LangGraph is useful for a narrow set of problems. For most things you’re building — CLI agents, automation pipelines, prompt chains — plain Python + LLM client is better.


What LangGraph actually is

LangGraph models your agent as a directed graph (DAG or cyclic) where:

It’s built on top of LangChain, adds: checkpointing, human-in-the-loop pauses, streaming, persistence, and cycle support (nodes can loop back).


When LangGraph is actually worth it

Use case LangGraph Plain code
Simple LLM call ❌ overkill
Linear chain (prompt → parse → prompt) ❌ overkill
Multi-agent with handoffs gets messy
Long-running agent with checkpoints reinvent the wheel
Human-in-the-loop mid-execution painful
Stateful loops (ReAct, reflexion) manageable
Simple ReAct agent ✅ 50 lines

Plain Python ReAct agent (what you already know)

import anthropic
import json

client = anthropic.Anthropic()

tools = [{
    "name": "run_bash",
    "description": "Run a bash command",
    "input_schema": {
        "type": "object",
        "properties": {"cmd": {"type": "string"}},
        "required": ["cmd"]
    }
}]

def run_bash(cmd):
    import subprocess
    return subprocess.check_output(cmd, shell=True, text=True)

def agent_loop(user_msg):
    messages = [{"role": "user", "content": user_msg}]

    while True:
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1000,
            tools=tools,
            messages=messages
        )

        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "end_turn":
            return next(b.text for b in resp.content if hasattr(b, "text"))

        # Handle tool calls
        tool_results = []
        for block in resp.content:
            if block.type == "tool_use":
                result = run_bash(block.input["cmd"])
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        messages.append({"role": "user", "content": tool_results})

print(agent_loop("What's my current directory and what files are here?"))

This is your ww/zz agent pattern. It works. It’s debuggable. It’s 40 lines.


The equivalent in LangGraph

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from typing import TypedDict, Annotated
from langchain_core.messages import BaseMessage
import operator

@tool
def run_bash(cmd: str) -> str:
    """Run a bash command"""
    import subprocess
    return subprocess.check_output(cmd, shell=True, text=True)

class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]

llm = ChatAnthropic(model="claude-sonnet-4-6").bind_tools([run_bash])

def call_model(state):
    return {"messages": [llm.invoke(state["messages"])]}

def should_continue(state):
    last = state["messages"][-1]
    return "tools" if last.tool_calls else END

graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", ToolNode([run_bash]))
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue)
graph.add_edge("tools", "agent")
app = graph.compile()

result = app.invoke({"messages": [("user", "What's in my directory?")]})
print(result["messages"][-1].content)

More boilerplate, more abstraction, harder to debug — for the same result.


When the graph abstraction pays off

Checkpointing (resume after crash, human approval):

from langgraph.checkpoint.sqlite import SqliteSaver

checkpointer = SqliteSaver.from_conn_string("agent.db")
app = graph.compile(checkpointer=checkpointer)

# Pause mid-execution for human approval
app = graph.compile(checkpointer=checkpointer, interrupt_before=["dangerous_tool"])

Multi-agent handoffs (supervisor → subagent):

graph.add_conditional_edges("supervisor", route_to_agent, {
    "coder": "coder_agent",
    "researcher": "researcher_agent",
    "finish": END
})

This is genuinely hard to wire cleanly in plain Python across multiple agents with shared state.


My honest take for your setup

Given your ww/zz/iclaw tools and CLI-first workflow:

The LangChain ecosystem adds significant import overhead, abstraction tax, and debugging friction. For your token-efficient, fast-iteration style, raw API + Python is almost always the right default. Reach for LangGraph only when you’re hitting the specific pain points it solves: persistence, human-in-the-loop, or complex multi-agent routing.


Back Donate