🤖 AI Explained
how agents work / 15 min read

Tools: A Deep Dive

What tools actually are, how the request/execute/return loop works, parallel calls, error handling, and how to write tool definitions that the model uses correctly.

What a Tool Actually Is

A tool is just a JSON schema — a description of a function. The LLM never executes anything. It reads the description, decides to “call” the function, and emits structured JSON saying what it wants called with what arguments. The host app does the actual work.

{
  "name": "get_weather",
  "description": "Get current weather for a city",
  "input_schema": {
    "type": "object",
    "properties": {
      "city": { "type": "string" },
      "units": { "type": "string", "enum": ["celsius", "fahrenheit"] }
    },
    "required": ["city"]
  }
}

That’s it. A name, a description, and a JSON Schema describing the inputs.


How the LLM Sees Tools

When you call the API, you pass tools alongside the messages:

response = client.messages.create(
    model="claude-sonnet-4-6",
    tools=[get_weather_tool],
    messages=[{"role": "user", "content": "What's the weather in Berlin?"}]
)

Under the hood, the tools get serialized into the prompt. The model doesn’t have a separate “tool pathway” — it’s just text. The model was fine-tuned to recognize tool definitions and respond with structured tool_use blocks when appropriate.


The Request / Execute / Return Loop

User: "What's the weather in Berlin?"

1. API call → model sees tools + messages
2. Model responds with tool_use block:
   { "type": "tool_use", "name": "get_weather", "input": { "city": "Berlin" } }

3. Host app receives this, executes get_weather("Berlin")
4. Host sends back tool_result:
   { "type": "tool_result", "tool_use_id": "abc", "content": "18°C, partly cloudy" }

5. Model sees the result, continues generation:
   "The weather in Berlin is currently 18°C and partly cloudy."

This loop can repeat multiple times in a single response — the model keeps emitting tool_use blocks until it has enough information to give a final answer.


What a tool_use Block Looks Like

{
  "type": "tool_use",
  "id": "toolu_01A09q90qw90lq917835lq9",
  "name": "get_weather",
  "input": {
    "city": "Berlin",
    "units": "celsius"
  }
}

And the corresponding tool_result:

{
  "type": "tool_result",
  "tool_use_id": "toolu_01A09q90qw90lq917835lq9",
  "content": "18°C, partly cloudy"
}

The tool_use_id links the result back to the specific call — critical when multiple tools are called in parallel.


Parallel Tool Calls

The model can emit multiple tool_use blocks in a single response:

[
  { "type": "tool_use", "id": "1", "name": "get_weather", "input": { "city": "Berlin" } },
  { "type": "tool_use", "id": "2", "name": "get_weather", "input": { "city": "Tokyo" } }
]

The host runs both, returns two tool_result entries, and the model synthesizes them. The model decides when calls are independent and can be parallelized.


stop_reason: tool_use

The API response has a stop_reason field. When the model wants to call a tool:

{ "stop_reason": "tool_use" }

This tells the host: “don’t show this to the user yet — execute the tools and send results back.” The conversation only ends when stop_reason is end_turn.


Tool Choice

# Default: model decides
tool_choice = { "type": "auto" }

# Force the model to use at least one tool
tool_choice = { "type": "any" }

# Force a specific tool
tool_choice = { "type": "tool", "name": "get_weather" }

Writing Good Tool Descriptions

Bad:

{ "name": "query", "description": "Query data" }

Good:

{
  "name": "query_database",
  "description": "Run a read-only SQL SELECT query against the production PostgreSQL database. Use this when you need to look up user records, orders, or analytics data. Do NOT use for writes — this will be rejected.",
  "input_schema": {
    "type": "object",
    "properties": {
      "sql": {
        "type": "string",
        "description": "A valid SQL SELECT statement. Must not contain INSERT, UPDATE, DELETE, or DROP."
      },
      "limit": {
        "type": "number",
        "description": "Max rows to return. Defaults to 100. Max 1000.",
        "default": 100
      }
    },
    "required": ["sql"]
  }
}

Rules of thumb:

  • Say what the tool does, when to use it, and when not to use it
  • Describe edge cases and constraints in field descriptions
  • Use enums where possible — they constrain the input space and reduce hallucination

Error Handling

Return errors as content, not exceptions:

{
  "type": "tool_result",
  "tool_use_id": "abc",
  "content": "Error: city 'Berlinn' not found. Did you mean 'Berlin'?",
  "is_error": true
}

With is_error: true, the model knows the tool failed and can reason about it — retry with corrected input, ask the user, or try a different approach.


Key Mental Models

ConceptReality
ToolA JSON schema describing a callable function
tool_useThe model’s request to execute a function
tool_resultThe host’s response after executing it
stop_reason: tool_use”Don’t stop yet — run these tools first”
The model “calling” a toolThe model emitting JSON; the host doing the actual work

The model is a stateless text transformer. It never runs code. Tools are the mechanism by which it can request that the host run code on its behalf.