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
Last updated: 10 October 2026
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.
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}")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
tracelist 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?
What is the recursion limit in LangGraph?
Can I test a LangGraph agent without calling an LLM?
Should an agent hide tool failures from the user?
Agents, tools and evaluation are taught in the Generative AI course.
Sources
- Use the graph API (LangChain docs, LangGraph) — LangChain
- Graph API overview (LangChain docs, LangGraph) — LangChain
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.
Semantic Search vs Keyword Search: What Changed with AI
NextMultimodal AI: Models That See, Hear and Speak
Related articles
LangGraph Tutorial: Building Stateful AI Agents
Build a small support-ticket agent with LangGraph, using only plain Python and no API key: typed state and reducers, conditional edges, a retry loop, checkpoints with thread IDs, and a human approval step with interrupt. Every output shown was produced by running the code.
AI Agents Guide: How Autonomous Agents Actually Work
An AI agent is a language model that chooses actions in a loop until a task is done. This guide explains the loop, the tools and the stopping rules, then runs a small agent against a sales database: it explores, recovers from its own SQL errors, and still gets three of four answers wrong in instructive ways.
What Is Generative AI? A Complete Beginner's Guide
What generative AI is, in plain language: how language models produce text one token at a time (shown with a tiny runnable model), how chat models are trained, how image models differ, what the technology is reliable for, and why it confabulates.