Integrating LLM APIs · Building Blocks

Tool Calling & Agent Loops

Letting the model call your functions safely.

With tool use you describe functions (name, description, JSON Schema input). The model responds with a tool call; your code validates and executes it, returns a tool result, and calls the model again until it stops calling tools — the agent loop. Keep tools narrow and well-described, validate inputs as untrusted, enforce authorization with the end user's identity inside tools, require human approval for consequential actions, and cap iterations. Return errors as tool results (is_error) so the model can recover.

Tool design guidelines

Design choice Prefer Avoid
Granularity Specific tools (get_order, refund_order) 'run_sql' or 'http_request' with full access
Authorization Tool runs as the end user Tool runs as a superuser service account
Side effects Approval step for writes/payments Auto-executing irreversible actions
Errors Return as tool_result with is_error Crash the loop
Loop control Max iterations + token budget Unbounded loops
tools = [{
    "name": "get_order_status",
    "description": "Look up the status and ETA of one of the current customer's orders.",
    "input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
                     "required": ["order_id"], "additionalProperties": False},
}]

messages = [{"role": "user", "content": question}]
for _ in range(MAX_STEPS):
    response = client.messages.create(model=MODEL, max_tokens=16000, system=SYSTEM,
                                      tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        break
    results = []
    for call in (b for b in response.content if b.type == "tool_use"):
        try:
            output = run_tool(call.name, call.input, user=current_user)   # validates + authorizes
            results.append({"type": "tool_result", "tool_use_id": call.id, "content": output})
        except ToolError as e:
            results.append({"type": "tool_result", "tool_use_id": call.id, "content": str(e), "is_error": True})
    messages.append({"role": "user", "content": results})   # all results in one message

Common pitfalls

  • Tools that bypass the user's permissions.
  • Executing writes without confirmation.
  • Splitting parallel tool results across multiple messages.

Key points

  • Narrow, well-described tools
  • Validate + authorize inside every tool
  • Approvals and iteration caps

Further reading: Tool use overview

Practice questions

Why should tools execute with the end user's permissions?

Answer

The model can be manipulated (e.g. via prompt injection); if tools run as a superuser, the model can access or modify data the user couldn't.

What should your loop do when a tool fails?

Answer

Return the error as a tool result marked is_error so the model can adjust (retry with different input or explain), rather than crashing the request.

Open in the app to mark lessons complete, take notes and track your progress.