Failures, parallel calls and keeping results small
By the end: Your tools survive errors, run concurrently where possible, and do not flood the context.
- Returning errors as results rather than raising
- Several tool calls in one turn
- Validating arguments before executing
- Why tool output size matters
New to tool calling? A model can only produce text, so by itself it cannot look anything up, do arithmetic you would trust, or change anything in the world. Tool calling is the arrangement that fixes this. You describe the functions available to it — their names, what they do, what arguments they take — and when the model wants one, its reply is a structured request to call it. Your code runs the function and sends the result back as another message. Why it matters
A tool that behaves in a demo behaves differently under real traffic: arguments arrive malformed, an API times out, a query comes back with half a megabyte of rows. This section covers what to do in each case, and the answer is usually not what an exception handler would normally do. Done well, these are recoverable situations the model works around on its own; done badly, one of them ends the run or quietly fills the window with output nobody will read.
An error is a result, not a crash#
When a tool fails, the natural instinct is to let the exception propagate. That ends the run. Usually the better move is to hand the failure back to the model as the tool's result, because the model can often recover: fix a typo in an argument, try a different approach, or tell the user what went wrong.
def run_tool(call):
try:
return {"ok": True, "data": TOOL_FUNCTIONS[call.name](**call.arguments)}
except KeyError:
return {"ok": False, "error": f"No tool named {call.name}"}
except Exception as e:
return {"ok": False, "error": f"{type(e).__name__}: {e}"}Some failures should still stop everything. An authentication error will not be fixed by the model retrying, and letting it try ten times wastes money and time. Distinguish recoverable from terminal deliberately.
Several calls at once#
A model can request more than one tool in a single turn when the calls are independent. Run them concurrently, since they are usually network-bound, and then return every result together in one message.
results = await asyncio.gather(*(run_tool(c) for c in response.tool_calls))
messages.append(response.message)
for call, result in zip(response.tool_calls, results):
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})Return all results in one message
Splitting tool results across several separate messages quietly teaches the model to stop making parallel calls, because the pattern it sees no longer matches the pattern it produced. Your agent gets slower over a conversation and the cause is very hard to spot.
Validate before you execute#
The model generates arguments, so treat them as untrusted input. Schema constraints catch type errors, but they do not catch a valid-looking value that is wrong, and they certainly do not catch a destructive one.
def delete_records(query: str, confirm: bool = False):
if not confirm:
return {"ok": False,
"error": "Destructive action needs confirm=true and human approval"}
...Keep results small#
Tool results go into the context and stay there for the rest of the conversation, being resent every turn. A tool that returns a 5,000-token payload has just made every subsequent turn more expensive, permanently.
- Return the fields the model needs, not the whole database row.
- Cap list results, and say so in the response, for example that ten of forty-two matches are shown.
- Summarise long text before returning it, or return an identifier the model can use to fetch more.
- Prefer several small focused tools over one that returns everything.
Where people go wrong
Letting a tool return whatever the underlying API returns. One verbose response inflates every later turn in that conversation, and the cost increase is invisible until you look at a trace.
If this comes up in an interview
A tool throws an exception. What should happen?
In most cases return the error to the model as the tool result, so it can recover, retry with different arguments or explain the problem. Terminal failures such as authentication errors should stop the run instead, because retrying cannot help.
How do you handle a model requesting several tools in one turn?
Execute them concurrently since they are usually network-bound, then return all results together in a single message. Splitting them across messages trains the model out of making parallel calls.
Why does the size of a tool result matter?
It enters the context and is resent on every subsequent turn, so a large result raises the cost of the whole remaining conversation. Return only what is needed and cap anything unbounded.
How do you stop an agent taking a destructive action?
Architecturally, not by instruction. Scope the tool's permissions, require an explicit confirmation argument, add a human approval gate, and make the operation idempotent so a repeat is harmless. Prompt instructions alone are not a security boundary.
Then they push further
Would you rely on the system prompt saying never delete anything?
No. Instructions make it rarer but they are not a control. The control is in code: do not give the agent a delete tool, or require human approval before one runs, and scope credentials so the action is impossible even if requested.
Which actions need a person in the loop?
Ones that are irreversible, costly, or visible to other people: payments and refunds above a threshold, deletions, messages sent to customers. Reads and reversible changes can usually run unattended.
What gets marked up. Strong answers put the control in permissions and code, not in the prompt, and reason about reversibility. Weak answers rely on instructions.
Build this: Build three tools where one throws, one returns a very large payload and one is slow. Make the agent handle all three without crashing, without bloating the context, and without running them one after another.
Watch it explained
This is one section of Tool calling, 7 hours across 2 sections. Read it in full to get the whole topic in order.
Stop 15 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.