Codetail

Article 7 of 15

Agents and Tool Use

Plan, call a tool, observe, repeat.

24 min read

An agent is the tool-calling loop from three articles ago, repeated

Structured Output and Tool Calling covered one round of this: the model decides to call a function, your code runs it, the result goes back as a message. An agent is that same exchange, just not stopped after one round. It keeps going until the model stops asking for tools and returns a plain answer instead.

The loop, not just the single exchange

Python
1messages = [{"role": "user", "content": user_request}]
2
3while True:
4 response = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
5 message = response.choices[0].message
6 messages.append(message)
7
8 if not message.tool_calls:
9 return message.content # the model is done, this is the final answer
10
11 for call in message.tool_calls:
12 result = execute_tool(call)
13 messages.append({
14 "role": "tool", "tool_call_id": call.id, "content": json.dumps(result),
15 })

Nothing about this loop is exotic, it's the same request/response exchange from earlier in this series wrapped in a while that keeps feeding tool results back until the model decides it has enough to answer. The word "agentic" describes this loop, not some separate piece of machinery.

A wrong turn doesn't throw an exception

A traditional program that hits a bug tends to announce it, a stack trace, a crash, an obviously malformed output. An agent that goes down the wrong reasoning path does none of that. It keeps calling tools, keeps getting real results back, keeps producing plausible intermediate steps, and can arrive at a confidently wrong final answer that reads exactly like a correct one.

Worse failure modes are quieter still: a loop that calls the same tool repeatedly with minor variations, chasing a solution that isn't there, or one that never recognizes it has enough information and keeps gathering more. None of these look like an error from the outside. They look like an agent doing exactly what agents do, working through a problem step by step, right up until someone checks whether the steps actually led anywhere.

The practical response is instrumentation, not optimism: a maximum iteration count so a confused loop terminates instead of running indefinitely, and real visibility into every step it took, not just the final answer, which is exactly what the Observability article later in this series is built around.

The loop is worth it when you can't predict the steps ahead of time

A task with a fixed, known sequence, look up the account, then check the invoice, then format a response, isn't an argument for a loop at all. That's regular code that happens to call a model once or twice, and writing it as an unbounded agent loop adds cost, latency, and an extra place for something to go wrong, for zero benefit, the sequence was never actually in question.

The loop earns its complexity specifically when the next step genuinely depends on what the last tool call returned, and can't be known before that. Debugging an error message that could point to five different root causes, where which one to check next depends on what the last check turned up, is the shape of problem a loop is actually built for.

The test worth applying before reaching for a loop: can you write down the steps in advance. If yes, write them down, as code, and let the model fill in one or two of them. If the honest answer is no, that's the signal an agent is the right shape for the problem, not a default to reach for because it sounds more capable.

Every loop needs a way to stop that isn't "trust the model to know when"

The loop from the first section has no ceiling. Left alone, a confused run keeps calling tools, and every call costs tokens and time whether or not it's making progress.

A hard ceiling on iterations, not a suggestion

Python
1MAX_ITERATIONS = 8
2
3for i in range(MAX_ITERATIONS):
4 response = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
5 message = response.choices[0].message
6 messages.append(message)
7 if not message.tool_calls:
8 return message.content
9 # ... execute tools, append results ...
10
11return "Couldn't complete this within the allowed steps, escalating to a human."

A cost ceiling per run and a hard timeout belong next to it, for the same reason: a loop that's technically still making progress but has already burned ten times the expected budget is its own kind of failure, whether or not it eventually gets there.

For any tool call with a real cost if it's wrong, sending an email, refunding a charge, deleting a record, a hard pause for human confirmation before execution is worth more than any amount of tuning the agent's judgment. This is the same instinct as requiring a second confirmation step for a high-impact action in any other system, an agent doesn't get a pass on that just because the decision came from a model instead of a person clicking a button. The Guardrails article later in this series covers this, and the broader safety picture, in depth.