
A behind-the-scenes look at why I abandoned traditional linear pipelines and switched to LangGraph to build more reliable, state-driven AI agents.
I used to think building AI agents was simple. Just call an LLM, instruct it to output JSON, parse the result, trigger the corresponding Python tool, and call it a day.
But as soon as the agent's logic got slightly more complex—like needing a loop where the agent attempts a task, inspects its own output, and if it fails, corrects its own code before asking for feedback—my clean Python scripts quickly devolved into a massive pile of spaghetti code.
I tried writing my own custom state manager. I was passing message lists back and forth through function arguments, manually handling tool execution errors, and trying to pause the execution to wait for human approval (human-in-the-loop). It was a nightmare. The code was fragile, incredibly hard to debug, and prone to infinite loops that could drain my API keys overnight.
That's when I hit the LangChain wall and decided to migrate to LangGraph.
To be honest, I was skeptical at first: "Oh great, another LangChain wrapper to overcomplicate things." But after refactoring my assistant (Nouva) with it, I realized that their concept of state-driven graphs is the most pragmatic solution to this problem.
At its core, LangGraph simply maps your agent's workflow as a flow chart (a graph) that shares a common memory (the state). There are three main pillars you need to understand:
State, does its job, and returns an update to be written back to the State.Let's build a simple agent that can perform basic math operations using a tool. If the user asks a math question, the agent routes the request to a calculator tool; otherwise, it answers directly without invoking any tools.
First, install the required libraries. We'll be using OpenAI for our LLM:
pip install langgraph langchain-openai
We will define an AgentState that tracks our conversation messages. We use add_messages as a reducer so that new messages are automatically appended to the list rather than overwriting it.
from typing import Annotated, Sequence
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
# add_messages tells LangGraph to append new messages to the sequence
messages: Annotated[Sequence[BaseMessage], add_messages]
We define a simple tool called multiply to multiply two numbers, and bind it to our LLM.
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers together."""
return a * b
tools = [multiply]
# We bind the tools to the model so the LLM knows it has access to them
model = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(tools)
We need two nodes: one to call the LLM (call_model), and one to run the tools (tool_node). LangGraph provides a pre-built ToolNode helper so we don't have to write manual execution wrappers.
from langchain_core.messages import ToolMessage
from langgraph.prebuilt import ToolNode
# Node 1: Call the LLM
def call_model(state: AgentState):
messages = state["messages"]
response = model.invoke(messages)
# Return the LLM response to update the State
return {"messages": [response]}
# Node 2: Tool Executor
tool_node = ToolNode(tools)
This is the brain of our workflow. We inspect the last message from the LLM. Does it contain tool calls? If yes, route to the tools node. If not, stop the workflow.
def should_continue(state: AgentState):
last_message = state["messages"][-1]
# Check if the LLM generated any tool calls
if last_message.tool_calls:
return "tools"
# Otherwise, we route to the special '__end__' node to stop
return "__end__"
Now, we stitch everything together using StateGraph. This is where LangGraph shines: your agent's workflow is defined declaratively and is incredibly easy to read.
from langgraph.graph import StateGraph, START, END
# 1. Initialize the graph with our state definition
workflow = StateGraph(AgentState)
# 2. Register our worker nodes
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
# 3. Set the entry point (START) to go straight to the agent node
workflow.add_edge(START, "agent")
# 4. Add the conditional routing logic from the agent node
workflow.add_conditional_edges(
"agent",
should_continue,
{
# format: "condition": "target_node"
"tools": "tools",
"__end__": END
}
)
# 5. After the tools execute, route back to the agent to process the output
# This forms the cyclic loop
workflow.add_edge("tools", "agent")
# 6. Compile the workflow into a runnable application
app = workflow.compile()
Let's test our compiled graph application:
from langchain_core.messages import HumanMessage
# We run it with a math question that triggers the tool call
events = app.stream(
{"messages": [HumanMessage(content="What is 23 multiplied by 45?")]}
)
for event in events:
for value in event.values():
print("Assistant:", value["messages"][-1].content)
I won't tell you LangGraph is a flawless library. It has a steep learning curve, especially if you aren't familiar with Graph Theory or the sometimes verbose LangChain Expression Language (LCEL) syntax.
However, for complex agent architectures, the benefits far outweigh the initial friction:
while True loops in Python that are prone to runaway API calls. Cyclic workflows (LLM -> Tool -> LLM) are handled natively and safely by the graph runner.If your agent only needs a simple, linear workflow (ask -> query DB -> answer), stick to raw Python or standard LangChain. Don't over-engineer.
But as soon as your agent needs to think recursively, use tools dynamically, and require human supervision before running potentially dangerous commands, LangGraph is the most pragmatic choice to save you from spaghetti-code hell.