Skip to main content
Back to Blog
LangGraphAI AgentsLangChainLLMPythonAgentic AI

Beyond Basic Chatbots: Developing Advanced AI Agents with LangGraph

Learn how to build stateful, multi-step AI agents with LangGraph. This guide covers core concepts, real-world architectures, code examples, best practices, and common pitfalls for developers moving beyond simple chatbots.

August 6, 202614 min readNiraj Kumar

Introduction

Ask any developer who shipped an AI chatbot in the last two years what happened after launch, and you'll hear a familiar story. The bot handled greetings and FAQs beautifully. Then a user asked something that required three steps of reasoning, a database lookup, a second opinion, and a follow-up action — and the whole thing fell apart. A single prompt-response loop, no matter how well-engineered, cannot reliably plan, retry, delegate, or remember what happened five turns ago.

This is the gap that AI agents are meant to fill, and it's also where most agent frameworks quietly struggle. Simple "chain" abstractions — prompt in, completion out, maybe a tool call in between — work fine for demos. They break down the moment you need cycles (an agent that re-checks its own work), conditional branching (different paths depending on what a tool returns), or coordination between multiple specialized agents.

LangGraph, built by the LangChain team, was designed specifically to close this gap. Instead of treating an AI workflow as a straight line, LangGraph treats it as a graph: a set of nodes (steps, agents, or tools) connected by edges that can loop, branch, and merge based on the evolving state of the task. It's the difference between a flowchart with one path and a flowchart that actually looks like how real work gets done — with decisions, retries, and parallel branches.

In this article, we'll go deep on what LangGraph actually is, how its core primitives fit together, and how to build a realistic multi-agent system with working code. By the end, you should be able to look at a complex, multi-step task and know how to model it as a graph instead of duct-taping together a chain of prompts.

What Makes LangGraph Different from a Basic Chatbot Framework?

Most "basic chatbot" implementations follow the same shape:

User message → Prompt template → LLM call → Response

This works when the task is genuinely one-shot. It stops working the moment your task needs any of the following:

  • Iteration — the agent needs to try something, evaluate the result, and try again if it's wrong.
  • Branching logic — the next step depends on what happened in the previous step (a tool failed, a classification came back ambiguous, a user needs to approve something).
  • Multiple specialized agents — a "planner" breaks work into steps, a "researcher" gathers information, a "writer" produces the final output, and they need to hand off work to each other.
  • Long-running or resumable execution — the task might take minutes or hours, span multiple sessions, or need to survive a server restart.
  • Human oversight — a human needs to approve, edit, or reject a step before the agent proceeds.

LangGraph addresses each of these directly, because its underlying model isn't a chain — it's a state machine. Every node in the graph receives the current state, does some work (call an LLM, call a tool, run some logic), and returns an update to that state. Edges decide which node runs next, and — critically — edges can loop back to earlier nodes, something a linear chain simply cannot express.

Here's a comparison that tends to make the difference click:

CapabilityLinear ChainLangGraph
Sequential steps
Conditional branching⚠️ Awkward, usually hardcoded✅ Native (conditional edges)
Loops / retries❌ Not supported✅ Native (cyclic graphs)
Multi-agent handoff❌ Manual orchestration✅ Native (supervisor / swarm patterns)
Persistent state across sessions❌ Requires custom code✅ Built-in checkpointers
Human-in-the-loop approval❌ Requires custom code✅ Built-in interrupts
Streaming intermediate steps⚠️ Partial✅ Per-node streaming

Core Concepts: The Building Blocks of a LangGraph Application

Before writing any code, it helps to internalize five concepts. Almost everything in LangGraph is a variation on these.

1. State

The state is a shared data structure that flows through the graph. Every node reads from it and writes updates back to it. In Python, state is typically defined as a TypedDict or a Pydantic model:

from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class AgentState(TypedDict):
    messages: Annotated[List, add_messages]
    task: str
    plan: List[str]
    completed_steps: List[str]
    final_answer: str

Notice the Annotated[List, add_messages] pattern — this is a reducer. Instead of a node overwriting the messages field entirely, the add_messages reducer appends new messages to the existing list. Reducers are how LangGraph avoids the classic "one node accidentally erases another node's work" bug that plagues hand-rolled state machines.

2. Nodes

A node is just a Python function (or a callable class) that takes the current state and returns a partial update:

def researcher_node(state: AgentState) -> dict:
    query = state["task"]
    results = search_tool.invoke(query)
    return {"completed_steps": state["completed_steps"] + [f"Researched: {query}"]}

Nodes can wrap an LLM call, a tool invocation, a database query, or plain Python logic. There's no requirement that every node touches an LLM — this is one of the most underappreciated features of the framework. Deterministic logic (validation, formatting, routing) belongs in plain nodes, not in prompts.

3. Edges

Edges connect nodes. There are two kinds:

  • Normal edges — always go from node A to node B.
  • Conditional edges — run a function against the current state and route to different nodes based on the result.
def route_after_research(state: AgentState) -> str:
    if len(state["completed_steps"]) < len(state["plan"]):
        return "researcher"
    return "writer"

graph.add_conditional_edges(
    "researcher",
    route_after_research,
    {"researcher": "researcher", "writer": "writer"}
)

This is what enables cycles: the conditional edge can route back to "researcher" itself, creating a loop that continues until the plan is complete.

4. Checkpointers (Persistence)

A checkpointer saves the graph's state after every step, keyed by a thread ID. This gives you three things almost for free: the ability to resume a crashed or interrupted run, multi-turn memory across separate invocations, and time-travel debugging (replaying the graph from any prior checkpoint).

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

For production, you'd swap MemorySaver for a durable backend (Postgres, SQLite, or a managed store) so state survives process restarts.

5. Human-in-the-Loop (Interrupts)

LangGraph lets you pause execution before or after a specific node and wait for human input — approval, edits, or a rejection — before continuing:

app = graph.compile(checkpointer=checkpointer, interrupt_before=["execute_transaction"])

This is essential for any agent that takes consequential actions: sending emails, moving money, modifying production data, or anything else you wouldn't want fully autonomous without a review step.

A Real-World Example: A Multi-Agent Research and Writing System

Let's put these pieces together into something closer to what you'd actually deploy: a system that takes a research question, plans how to answer it, gathers information, drafts a response, and critiques its own draft before finalizing it. This "planner → researcher → writer → critic" shape is one of the most common production patterns because it mirrors how a competent human team would actually divide the work.

Step 1: Define the State

from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class ResearchState(TypedDict):
    messages: Annotated[List, add_messages]
    question: str
    plan: List[str]
    findings: List[str]
    draft: str
    critique: str
    revision_count: int

Step 2: Define the Nodes

from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-4-6")

def planner(state: ResearchState) -> dict:
    prompt = f"Break this question into 3 concrete research steps: {state['question']}"
    response = llm.invoke(prompt)
    steps = [line.strip("- ") for line in response.content.split("\n") if line.strip()]
    return {"plan": steps}

def researcher(state: ResearchState) -> dict:
    next_step = state["plan"][len(state["findings"])]
    result = llm.invoke(f"Research and summarize: {next_step}")
    return {"findings": state["findings"] + [result.content]}

def writer(state: ResearchState) -> dict:
    combined = "\n".join(state["findings"])
    draft = llm.invoke(f"Write a concise answer to '{state['question']}' using:\n{combined}")
    return {"draft": draft.content}

def critic(state: ResearchState) -> dict:
    review = llm.invoke(f"Critique this draft for accuracy and clarity:\n{state['draft']}")
    return {"critique": review.content, "revision_count": state["revision_count"] + 1}

Step 3: Define the Routing Logic

def after_researcher(state: ResearchState) -> str:
    if len(state["findings"]) < len(state["plan"]):
        return "researcher"
    return "writer"

def after_critic(state: ResearchState) -> str:
    if "looks good" in state["critique"].lower() or state["revision_count"] >= 2:
        return "end"
    return "writer"

Notice the revision_count >= 2 guard — this caps the revision loop so the agent can't spiral indefinitely if the critic is never fully satisfied. We'll come back to why this matters in the "Common Mistakes" section.

Step 4: Assemble the Graph

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

graph = StateGraph(ResearchState)

graph.add_node("planner", planner)
graph.add_node("researcher", researcher)
graph.add_node("writer", writer)
graph.add_node("critic", critic)

graph.add_edge(START, "planner")
graph.add_edge("planner", "researcher")
graph.add_conditional_edges("researcher", after_researcher, {"researcher": "researcher", "writer": "writer"})
graph.add_edge("writer", "critic")
graph.add_conditional_edges("critic", after_critic, {"writer": "writer", "end": END})

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

Step 5: Run It

config = {"configurable": {"thread_id": "session-001"}}
result = app.invoke(
    {"question": "What caused the 2008 financial crisis?", "plan": [], "findings": [], "draft": "", "critique": "", "revision_count": 0},
    config=config
)
print(result["draft"])

Because the graph is compiled with a checkpointer, you can inspect state at any point, resume the same thread_id later, or even fork execution from an earlier checkpoint to try a different path — none of which is possible with a simple chain.

Streaming Intermediate Steps

Users don't want to stare at a spinner while four agents deliberate. LangGraph supports streaming at the node level, so you can surface progress in real time:

for event in app.stream(
    {"question": "What caused the 2008 financial crisis?", "plan": [], "findings": [], "draft": "", "critique": "", "revision_count": 0},
    config=config,
    stream_mode="updates"
):
    for node_name, update in event.items():
        print(f"[{node_name}] updated: {list(update.keys())}")

This pattern — surfacing which node just ran and what it changed — is what powers the "agent is thinking… now researching… now drafting…" UX you see in modern agent products.

Where This Pattern Shows Up in Practice

The planner/researcher/writer/critic shape generalizes surprisingly well:

  • Customer support triage — a classifier node routes tickets to a billing agent, a technical agent, or an escalation node, with a supervisor agent deciding when to loop back for clarification.
  • Code review assistants — a node analyzes a diff, a second node checks it against style guidelines, a third drafts comments, and a conditional edge decides whether the change needs a human reviewer.
  • Data pipeline orchestration — nodes wrap ETL steps, with conditional edges handling retries on transient failures and human-in-the-loop interrupts before any step that writes to a production table.
  • Compliance and audit workflows — enterprises in regulated industries increasingly rely on the graph structure itself as an audit trail, since every node transition and state change is recorded and replayable.

🚀 Pro Tips

  • Design your state schema before you write a single node. Almost every LangGraph headache traces back to a state shape that didn't anticipate what a later node would need. Sketch the full state on paper first.
  • Use reducers instead of raw overwrites for anything additive — message history, logs, accumulated findings. It prevents nodes from silently clobbering each other's work.
  • Keep nodes small and single-purpose. A node that both calls a tool and makes a routing decision is harder to test and debug than two separate nodes.
  • Always set a recursion_limit. LangGraph will happily loop forever if your conditional edges don't have a hard exit condition — treat this the same way you'd treat a while True loop in any other language.
  • Instrument early with LangSmith (or equivalent tracing). Once you have more than two or three nodes, debugging by reading console logs stops scaling. Trace visualization pays for itself immediately.
  • Put human approval gates on anything irreversible — financial transactions, outbound emails, database writes, deployments. interrupt_before is cheap insurance.
  • Prefer a durable checkpointer over MemorySaver the moment you leave local development. In-memory state disappears on restart, which is fine for prototyping and a liability in production.

Best Practices for Production-Grade LangGraph Agents

  1. Separate planning from execution. A node that decides what to do and a node that does it should not be the same function. This keeps your graph debuggable and lets you swap out the execution layer (different tools, different models) without touching the planning logic.
  2. Scope tool access per agent. If you have a document-extraction agent and a transaction agent, don't give both of them the same set of tools. Least-privilege design isn't optional once agents can take real-world actions.
  3. Version your graphs. As your state schema evolves, older checkpoints may no longer deserialize cleanly. Treat state schema changes with the same discipline you'd apply to a database migration.
  4. Test nodes in isolation. Because nodes are plain functions that take a state dict and return a state update, they're straightforward to unit test without spinning up the whole graph.
  5. Set timeouts and retries at the tool-call level, not just at the graph level. A single hung API call inside one node shouldn't be able to stall an entire multi-agent run indefinitely.
  6. Log state diffs, not just final output. When something goes wrong three nodes deep, you want to see exactly which update introduced the bad data, not just the final broken result.
  7. Cap loop iterations explicitly in your state (as the revision_count field did above) rather than relying solely on the LLM to decide when it's "done." Models are unreliable judges of their own completion.

Common Mistakes to Avoid

  • Unbounded cycles. The single most common LangGraph bug: a conditional edge that can loop back to itself with no counter, no timeout, and no fallback exit. Always pair a loop with an explicit cap.
  • Treating state like a global variable. It's tempting to stuff everything into one giant state object and let every node read and write whatever it wants. This works until two nodes race to update the same field. Use narrow, well-typed state and reducers deliberately.
  • Putting business logic inside prompts. If a decision can be made with an if statement, make it with an if statement. Routing logic belongs in conditional edge functions, not buried in an LLM's free-text output that you then have to parse.
  • Skipping checkpointing until "later." Retrofitting persistence into an agent that was built assuming synchronous, single-shot execution is far more painful than designing for it from the start.
  • No human checkpoint before irreversible actions. It's easy to demo an agent that "just works" and forget that production traffic includes edge cases the demo never hit. Anything with real-world consequences deserves a pause point.
  • Over-architecting simple tasks. Not every problem needs four specialized agents and a supervisor. If a single well-prompted node with one tool solves the task reliably, a five-node graph is added complexity with no benefit. Reach for LangGraph's full power when the task genuinely needs branching, looping, or delegation — not by default.
  • Ignoring token and cost budgets in loops. A retry loop that calls an LLM on every iteration can quietly become expensive. Track iteration count and cost alongside correctness.

Conclusion

The leap from "chatbot" to "agent" isn't really about a smarter model — it's about a better execution model. Linear chains are simple and predictable, but they can't express the loops, branches, and hand-offs that real multi-step tasks require. LangGraph's graph-based approach — explicit state, composable nodes, conditional routing, built-in persistence, and human-in-the-loop controls — gives you the primitives to build agents that plan, retry, delegate, and recover from failure, all while staying inspectable and debuggable.

None of this is free complexity for its own sake. The framework's value shows up precisely when your task outgrows a single prompt: when you need an agent to check its own work, coordinate with other agents, survive a restart, or pause for a human's sign-off before doing something consequential. Start simple, reach for the full graph model when the task actually demands it, and treat your state schema with the same care you'd give a database schema — because in a very real sense, that's exactly what it is.

If you're building anything more sophisticated than a single-turn Q&A bot in 2026, it's worth spending an afternoon sketching your workflow as a graph before you write a line of orchestration code. More often than not, you'll find the graph was the right mental model all along.

📌 Key Takeaways

  • LangGraph replaces linear prompt chains with a stateful graph model, enabling cycles, branching, and multi-agent coordination that basic chatbot frameworks can't express.
  • State design is the foundation of a good LangGraph application — use typed schemas and reducers to avoid nodes silently overwriting each other's work.
  • Built-in checkpointing and interrupt_before support give you persistence and human oversight without hand-rolling your own state-management layer.
  • The most common production failures are unbounded loops, tangled state, and skipping human checkpoints before irreversible actions — all avoidable with disciplined graph design.
  • Reach for LangGraph's full multi-agent power when a task genuinely needs planning, delegation, or retries — not as a default architecture for every LLM feature.

References

All Articles
LangGraphAI AgentsLangChainLLMPythonAgentic AI

Written by

Niraj Kumar

Software Developer — building scalable systems for businesses.