AI & LLM APIs cookbook · Intermediate

Write a bounded tool-calling loop

Let the model call your functions to answer, without infinite loops, crashes or invented results.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Run the standard loop (call, execute tool calls, return results) with a step limit, return failures as is_error tool results instead of raising, and finish gracefully when the budget runs out.

Solution

import json

import anthropic

client = anthropic.Anthropic()
MODEL = "claude-sonnet-5-5"

TOOLS = [{
    "name": "get_order",
    "description": "Get an order's status and items by its 8-digit order number.",
    "input_schema": {"type": "object", "properties": {"order_id": {"type": "string", "pattern": "^[0-9]{8}$"}},
                     "required": ["order_id"], "additionalProperties": False},
}]


def get_order(order_id: str) -> dict:
    order = ORDERS_DB.get(order_id)                  # your data access
    if order is None:
        raise LookupError(f"No order {order_id}. Ask the customer to check the number on their receipt.")
    return order


HANDLERS = {"get_order": get_order}


def run_tool(block) -> dict:
    try:
        result = HANDLERS[block.name](**block.input)
        return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
    except (LookupError, ValueError) as exc:
        return {"type": "tool_result", "tool_use_id": block.id, "content": str(exc), "is_error": True}


def answer(question: str, max_steps: int = 6) -> str:
    messages = [{"role": "user", "content": question}]
    for _ in range(max_steps):
        response = client.messages.create(model=MODEL, max_tokens=1024, tools=TOOLS, messages=messages,
                                          system="Answer order questions. Never invent order data.")
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if b.type == "text")
        results = [run_tool(b) for b in response.content if b.type == "tool_use"]
        messages.append({"role": "user", "content": results})
    return "Sorry, I couldn't finish that. A colleague will follow up."

Pitfalls

  • while True loops with no step limit.
  • Letting exceptions escape (the conversation crashes) or returning empty strings (the model invents data).
  • Vague tool descriptions and free-text parameters; use precise schemas.

Variations

  • The SDK's tool runner (client.beta.messages.tool_runner) runs the loop for you with decorated Python functions.
  • Add an approval step before tools that change data.

Further reading: Implement tool use

Reference: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/implement-tool-use

More AI & LLM APIs recipes

Was this page helpful?