Hitting the LangChain Wall: Why I Migrated to LangGraph for AI Agents

A behind-the-scenes look at why I abandoned traditional linear pipelines and switched to LangGraph to build more reliable, state-driven AI agents.

Apr 06, 2026
•
6 min read

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.


Core Concepts of LangGraph (Without the Fluff)

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:

  1. State (The Shared Notebook): The single source of truth for your graph. Imagine a physical notebook passed from desk to desk. All conversation history, tool outputs, and custom variables are stored here. Every step (node) in the graph can read from and write to this notebook.
  2. Nodes (The Workers): These are just standard Python functions that perform a specific task. One node might call the LLM, another might query a database, and another might execute a tool. A node receives the current State, does its job, and returns an update to be written back to the State.
  3. Edges (The Conveyor Belt): The lines connecting the nodes. They define where the shared notebook should be sent next.
    • Normal Edges: Direct paths from one node to another (e.g., after calling the LLM, always go to the tool execution node).
    • Conditional Edges: Branching paths based on logic (e.g., check the last response from the LLM. If the LLM requested a calculator tool, route to the calculator node. If it's done, route to the end).

Hands-on: Building Your First Agent

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.

1. Setup

First, install the required libraries. We'll be using OpenAI for our LLM:

pip install langgraph langchain-openai

2. Define the State

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.

state.py
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]

3. Define Tools and the Model

We define a simple tool called multiply to multiply two numbers, and bind it to our LLM.

agent.py
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)

4. Define the Nodes (The Workers)

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.

nodes.py
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)

5. Define Routing Logic (Conditional Edge)

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.

routing.py
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__"

6. Assemble the Graph

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.

graph.py
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()

7. Running the Graph

Let's test our compiled graph application:

main.py
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)

Why I Ended Up Loving LangGraph (Trade-offs & Verdict)

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:

  • First-Class Cycles: No more writing messy 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.
  • Pain-free Human-in-the-Loop: This is the killer feature. I can easily pause the graph execution whenever the agent wants to run a sensitive tool (like writing a file or deleting data), wait for my approval via chat, and resume it from its exact last state.
  • Automatic State Persistence: LangGraph comes with built-in checkpointers. Your conversation state is automatically saved to memory (or a database like Postgres/Sqlite) without you having to write manual serialization code.

Summary

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.