Building AI Agents with LangGraph: A Hands-On Tutorial

Agent tutorials stop when the demo works. Production agents fail in other ways: tools time out, loops never end, and behaviour changes silently. This hands-on guide builds a small LangGraph agent and makes it reliable, with a retry policy on its tool node, a recursion limit and deterministic tests.

GloryTecks Team

Training & Placement Faculty

8 February 2026 7 min read

Last updated: 10 October 2026

Share:

Building AI agents with LangGraph is easy to start: define state, add nodes, connect them, and the demo works. The LangGraph tutorial covers those foundations, including pausing for a human. This guide picks up where demos stop. Real agents call services that time out, sometimes loop without deciding to stop, and change behaviour when prompts or models change. LangGraph has features for each problem; here they are applied to a small agent and run, so you can see exactly how each behaves.

The agent answers banking questions. A planner chooses a tool, the tool runs, and an answer node replies. To keep the example deterministic and testable, the planner is a plain function standing in for a language model's tool choice, and the tools are local stand-ins.

python
import operator
from typing import Annotated, TypedDict

from langgraph.errors import GraphRecursionError
from langgraph.graph import END, START, StateGraph
from langgraph.types import RetryPolicy


class State(TypedDict, total=False):
    question: str
    tool: str
    result: str
    answer: str
    trace: Annotated[list, operator.add]          # every node appends what it did


calls = {"exchange_rate": 0}
FAIL_FIRST = {"exchange_rate": 2}                 # the rate service times out twice, then answers


def exchange_rate():
    calls["exchange_rate"] += 1
    if calls["exchange_rate"] <= FAIL_FIRST["exchange_rate"]:
        raise ConnectionError("rate service timed out")
    return "1 USD = 83.2 INR (sample value)"


def account_balance():
    return "balance: 12,400"


TOOLS = {"exchange_rate": exchange_rate, "balance": account_balance}


def plan(state):                                  # stands in for the language model's tool choice
    q = state["question"].lower()
    tool = "exchange_rate" if "rate" in q else "balance" if "balance" in q else "none"
    return {"tool": tool, "trace": [f"plan -> {tool}"]}


def call_tool(state):
    return {"result": TOOLS[state["tool"]](), "trace": [f"tool {state['tool']} ok"]}


def answer(state):
    text = state.get("result") or "I can only help with balances and exchange rates."
    return {"answer": text, "trace": ["answer"]}


def build():
    g = StateGraph(State)
    g.add_node("plan", plan)
    g.add_node("tool", call_tool, retry_policy=RetryPolicy(max_attempts=3, initial_interval=0.01,
                                                          jitter=False, retry_on=ConnectionError))
    g.add_node("answer", answer)
    g.add_edge(START, "plan")
    g.add_conditional_edges("plan", lambda s: "answer" if s["tool"] == "none" else "tool")
    g.add_edge("tool", "answer")
    g.add_edge("answer", END)
    return g.compile()


agent = build()

print("R1 a flaky tool: two timeouts, then success, absorbed by the node's retry policy")
out = agent.invoke({"question": "What is the USD to INR rate?"})
print(f"   answer: {out['answer']} | tool attempts: {calls['exchange_rate']} | trace: {out['trace']}")

print("\nR2 a tool that keeps failing: the error surfaces after the last attempt")
calls["exchange_rate"], FAIL_FIRST["exchange_rate"] = 0, 99
try:
    agent.invoke({"question": "rate please"})
except ConnectionError as err:
    print(f"   {type(err).__name__}: {err} after {calls['exchange_rate']} attempts")


def runaway():                                     # an agent that keeps 'reflecting' and never decides to stop
    g = StateGraph(State)
    g.add_node("think", lambda s: {"trace": ["think"]})
    g.add_node("reflect", lambda s: {"trace": ["reflect"]})
    g.add_edge(START, "think")
    g.add_edge("think", "reflect")
    g.add_conditional_edges("reflect", lambda s: "think")
    return g.compile()


print("\nR3 a loop that never ends is stopped by the recursion limit")
try:
    runaway().invoke({"question": "x"}, config={"recursion_limit": 6})
except GraphRecursionError:
    print("   GraphRecursionError raised with recursion_limit=6")

print("\nR4 scripted checks of the routing, with no model and no network")
FAIL_FIRST["exchange_rate"], calls["exchange_rate"] = 0, 0
cases = {"What's my balance?": "balance: 12,400", "Tell me a joke": "I can only help with balances and exchange rates.",
         "INR rate today": "1 USD = 83.2 INR (sample value)"}
for q, expected in cases.items():
    got = agent.invoke({"question": q})["answer"]
    print(f"   {'PASS' if got == expected else 'FAIL'}  {q!r} -> {got}")
text
R1 a flaky tool: two timeouts, then success, absorbed by the node's retry policy
   answer: 1 USD = 83.2 INR (sample value) | tool attempts: 3 | trace: ['plan -> exchange_rate', 'tool exchange_rate ok', 'answer']

R2 a tool that keeps failing: the error surfaces after the last attempt
   ConnectionError: rate service timed out after 3 attempts

R3 a loop that never ends is stopped by the recursion limit
   GraphRecursionError raised with recursion_limit=6

R4 scripted checks of the routing, with no model and no network
   PASS  "What's my balance?" -> balance: 12,400
   PASS  'Tell me a joke' -> I can only help with balances and exchange rates.
   PASS  'INR rate today' -> 1 USD = 83.2 INR (sample value)

Problem 1: tools that fail sometimes

External calls fail transiently: timeouts, rate limits, brief outages. Handling that inside every tool clutters the code. LangGraph lets you attach a retry policy to a node instead, by passing retry_policy to add_node with a RetryPolicy object (LangGraph docs: use the graph API).

In the run, the exchange-rate service timed out twice and answered on the third attempt. The retry policy absorbed both failures, and the trace shows a clean path: plan, tool, answer. The policy here retries only on ConnectionError, with three attempts. Choosing which exceptions to retry matters: retrying a timeout makes sense; retrying a bug or a validation error just wastes time and money.

Problem 2: failures that do not go away

When the service kept failing, the node gave up after the third attempt and the ConnectionError reached the caller. That is the right behaviour: a permanent failure should be visible, not hidden behind an invented answer. In production, catch it at the edge of your application and return a clear message, raise an alert, or route to a fallback. Recent LangGraph versions also let a node define an error handler that runs after retries are exhausted, which is worth considering for graceful fallbacks.

Problem 3: loops that never end

Agents with reflection or self-correction loops can cycle forever if the stopping condition is never met, burning tokens and time. LangGraph caps the number of super-steps in one run; when the cap is reached it raises GraphRecursionError. The default since version 1.0.6 is 1,000 steps, and you can set your own per run in the invoke config (LangGraph docs: graph API overview).

The deliberately broken think-reflect loop in the example was stopped with a limit of 6. For real agents, set a limit that matches the longest legitimate path with some margin, rather than relying on the default of 1,000. The same documentation describes a RemainingSteps value that lets a node see how many steps are left and route to a wrap-up answer before the limit, which is better for users than an exception.

Problem 4: knowing it still works

Because routing lives in ordinary Python functions and graph edges, much of an agent can be checked without calling a model at all. The scripted checks sent three questions through the graph and compared the answers: a balance question, an off-topic joke request and an exchange-rate question, all passing. In a real project, these become unit tests that run on every change, with a fake model returning fixed tool choices. Model-dependent behaviour still needs an evaluation set, but routing, retries, limits and error paths do not.

Putting the language model back in

In a real agent, the planner node calls a model and asks it to choose a tool, ideally with structured output so the choice is a validated field rather than free text. Keep that call inside one node, with the same contract as the stand-in function: question in, tool name out. That design has two benefits. The rest of the graph, retries, limits, answers and error handling, does not care whether the decision came from a model or a script, so the tests above keep working. And when the model chooses something unexpected, such as a tool name that does not exist, you can validate the choice in that node and route to a safe answer instead of crashing. Give the model node its own retry policy for rate limits and timeouts, separate from the tools' policies.

A reliability checklist for LangGraph agents

  • Retry policies on nodes that call external services, retrying only transient errors.
  • A recursion limit sized to your graph, plus a graceful wrap-up using remaining steps.
  • Visible failures: errors that reach monitoring, not invented answers.
  • Deterministic tests for routing and error paths with a scripted planner.
  • Traces of every run, like the trace list here, so you can see which path an answer took.
  • An evaluation set for the model's decisions, rerun after every prompt or model change.

For how agents work more broadly, see the AI agents guide.

How do I retry a failing tool call in LangGraph?

Pass a retry_policy to add_node for the node that calls the tool, with a RetryPolicy that sets the number of attempts and which exceptions to retry, such as connection errors.

What is the recursion limit in LangGraph?

The maximum number of super-steps in one run. When it is reached, LangGraph raises GraphRecursionError. The default is 1,000 steps since version 1.0.6, and you can set it per run in the config.

Can I test a LangGraph agent without calling an LLM?

Yes, for routing, retries, limits and error handling. Replace the model's decision with a scripted function and assert on the outputs. Model quality still needs a separate evaluation set.

Should an agent hide tool failures from the user?

No. Retry transient failures, but let permanent ones surface to your application, which can show a clear message, alert someone or use a fallback.

Agents, tools and evaluation are taught in the Generative AI course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Generative AI?

Join GloryTecks’ expert-led Agentic AI training in Hyderabad with real-time projects and 100% placement support.

Explore course