Building AI Agents from Scratch with Python · Tools and Real-World Robustness
Error Handling, Retries, and Guardrails
The moment an agent can take real actions, three things become non-optional, not nice-to-haves.
1. Bound the loop
Nothing in the code above stops the agent from calling tools forever. Always cap the number of iterations:
MAX_ITERATIONS = 10
for _ in range(MAX_ITERATIONS):
response = client.messages.create(...)
if response.stop_reason != "tool_use":
break
# ... execute tools ...
else:
messages.append({"role": "assistant", "content": "Stopped: too many steps."})
2. Never let a tool failure crash the loop
A tool raising an uncaught exception kills the whole conversation. Catch errors inside the tool and return them as a normal string result — the model can usually recover from "error: division by zero" far better than your process can recover from an unhandled exception.
3. Retry transient failures, don't retry logical ones
import time
def call_with_retry(fn, *args, retries=3, **kwargs):
for attempt in range(retries):
try:
return fn(*args, **kwargs)
except (TimeoutError, ConnectionError):
if attempt == retries - 1:
raise
time.sleep(2 ** attempt) # exponential backoff
Only retry errors that are plausibly transient (timeouts, rate limits, connection resets). Retrying a ValueError from bad tool arguments just burns tokens repeating the same mistake — that's a case for feeding the error back to the model instead, so it can correct its own arguments on the next turn.
Where to go from here: you now have every piece a production agent framework provides — a loop, tool calling, memory, and guardrails. Frameworks like LangGraph mainly add: parallel tool execution, persistence adapters, and visual graph definitions on top of exactly this structure. Understanding this chapter first means you'll know precisely what a framework is doing for you instead of treating it as a black box.