Tools & Agentic Loops

Tool calling turns an LLM from a text generator into a controller of external capabilities.

Why tool calling?

LLMs are powerful at language — but they can't do everything alone. Tool calling lets the model delegate to systems better suited for each sub-task.

What tools enable

Capability Examples
ComputationMath, statistics, precise calculations
Live dataAPIs, databases, current information
ActionsSend emails, update records, trigger workflows
Structured opsSQL queries, file operations, system commands

Example: weather forecast

1. User:    "What's the weather in Paris tomorrow?"
2. LLM:     → tool_call: get_weather(city="Paris", date="tomorrow")
3. Tool:    → {"forecast": "Partly cloudy, 18C, 30% chance of rain"}
4. LLM:     "Tomorrow in Paris: partly cloudy, around 18C with a 30% chance of rain."

The model can't check the weather — but it can call a tool that does, then synthesize the result into a natural answer.

Key insight

Tool calling is not just an output format problem. It is a control problem — the system must decide when to call a tool, which tool, and when to stop.

01 / TWO APPROACHES

Manual vs API-native tool calling

Manual (Prompt + Parse)

  1. Describe tools in the system prompt
  2. Ask model to output JSON
  3. Parse JSON from text response
  4. Execute function, feed result back

Strengths: Provider-agnostic, transparent, you control everything.

Weaknesses: Fragile — model may wrap JSON in code blocks, add conversational text, use wrong field names, or invent ad-hoc formats for multiple calls.

API-Native Tool Calling

  1. Register tools with JSON schemas
  2. Model returns structured tool_calls objects
  3. Execute function, send result back as tool message
  4. Repeat until model answers in text

Strengths: Guaranteed structure, multi-tool support, consistent across models and temperatures.

Weaknesses: Tied to provider API conventions.

Side by side

Manual API tool calling
FormatHope model outputs valid JSONGuaranteed structured objects
ParsingCustom regex / JSON extractionmsg.tool_calls[0].function.name
Multi-toolModel invents ad-hoc formatReturns a list of tool_calls
ReliabilityBreaks with temp & model changesConsistent across models
Code~30 lines of fragile parsing~5 lines of structured access

Native tool calling gives you better plumbing. It does not eliminate the orchestration problem.

02 / API TOOL CALLING

Registering tools & the single-call round trip

Tool schemas

You describe each tool as a JSON schema. The model never sees your Python code — only these descriptions.

{
  "type": "function",
  "function": {
    "name": "explore_data",
    "description": "Inspect dataset schema, missing values, and candidate targets. Call first when the dataset structure is unknown.",
    "parameters": {
      "properties": {
        "sample_rows": {"type": "integer", "description": "Number of sample rows to return"}
      },
      "required": ["sample_rows"]
    }
  }
}

The round trip (single tool call)

# Step 1: send query + tool schemas
response = client.chat.completions.create(
    model=MODEL,
    messages=[{"role": "user", "content": user_query}],
    tools=TOOL_SCHEMAS,
    tool_choice="auto",
)
msg = response.choices[0].message

# Step 2: model wants to call a tool
tc = msg.tool_calls[0]
args = json.loads(tc.function.arguments)
result = explore_data(**args)

# Step 3: send result back → model produces final answer
response2 = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "user", "content": user_query},
        msg,
        {"role": "tool", "tool_call_id": tc.id, "content": json.dumps(result)},
    ],
    tools=TOOL_SCHEMAS,
)
print(response2.choices[0].message.content)

Built-in tools

LLM providers also offer ready-made tools executed on their infrastructure — zero tool code from you:

ProviderBuilt-in tools
OpenAIweb_search, code_interpreter, file_search
Anthropiccomputer_use, text_editor, bash

Convenient for general capabilities, but for domain-specific work you need your own tools.

03 / THE MULTI-TOOL LOOP

Observe → Decide → Act → Observe → …

Real questions often need multiple tool calls before the model can answer.

while has_budget:
    response = LLM(messages, tools)
    if response has tool_calls:
        execute each tool; append results
    else:
        return response.text  # model decided it's done

Two critical design choices:

  1. Max iterations — prevent infinite loops
  2. The model decides when to stop — it produces text (not a tool call) when it has enough info

Example: data analyst agent

Three tools with natural preconditions and dependencies:

ToolPurposePrecondition
explore_dataInspect schema, missing values, candidate targetsNone — safe first step
cluster_dataDiscover natural segments (unsupervised)Needs numeric column names
classify_dataPredict a label like churn (supervised)Needs a valid target column

Different queries → different tool traces

"What kind of data is this?"
  → explore_data → ANSWER

"Are there natural customer segments?"
  → explore_data → cluster_data → ANSWER

"Segments AND churn prediction for age=29, income=42k..."
  → explore_data → cluster_data → classify_data → ANSWER

The model must reason about which tool to call next based on what it's learned so far.

04 / THE CONTROL PROBLEM

When to continue, when to stop

APIs solve the syntax of tool calling. The hard part is the control policy.

After each tool result, continue only if all three are true:

  1. Some part of the original goal is still unresolved
  2. Another available tool can resolve it
  3. The next tool call is necessary, not merely interesting

Common failure modes

Tool obsession

Calling tools because they exist, not because they're needed.

Skipping preconditions

Trying classification before checking that a target label exists. explore_data shows candidate_targets: [] — the model should stop and explain.

Looping without gain

Repeating near-identical calls that don't change the answer.

No termination

The agent never decides it has enough evidence. Always set a max_iterations budget.

The right question

"Do I need another tool?" — not "Can I call another tool?"

05 / STOPPING FOR USER INPUT

Asking instead of guessing

The model has three options at each step:

  1. Call a tool — if it knows which tool and has all parameters
  2. Ask a question — if information is missing or ambiguous
  3. Answer directly — if it already has enough information

Without vs with clarification instructions

User saysWithout clarificationWith clarification
"Find some clusters." GUESSES: cluster_data(["age","income"], k=3) ASKS: "Which features? How many clusters?"
"Predict something." GUESSES: classify_data("churned", ...) ASKS: "What should I predict? For which customer?"

This is just a system prompt instruction — the model chooses to produce text (a question) instead of a tool call:

IMPORTANT: If the user's request is ambiguous or missing required information
(e.g., which columns to use, what to predict, how many clusters), ask a
clarification question instead of guessing. Respond with text, not a tool call.

Full clarification flow

User:      "Predict something for a customer."
Assistant: "What should I predict? And what are the customer's details?"
User:      "Predict churn. age=29, income=42k, spend=180, visits=4, tickets=3."
Assistant: → explore_data(...)  → classify_data(...)  → "This customer has a 73% churn risk."
Key insight

Asking back is itself a "tool" — the model's ability to produce text is its most flexible capability. A tool call requires exact parameters; a clarification question can handle any ambiguity.

06 / TOOL DESIGN MATTERS

Good names and descriptions directly impact accuracy

The model picks tools based on their name, description, and parameter names. Bad metadata = wrong tool calls.

Good vs bad

GoodBad
Names explore_data, cluster_data, classify_data run_check, process_data, get_result
Descriptions "Inspect schema, missing values, candidate targets. Call first when unknown." "Checks the data."
Parameters feature_columns, target, new_row cols, col, row

Best practices

PracticeWhy
Descriptive function namesexplore_data vs run_check — model matches intent to name
Self-explaining parametersfeature_columns vs cols — maps user concepts to params
Rich descriptions with preconditions"Call explore_data first if unsure" guides the tool sequence
Type constraintsk: integer (2-10) prevents invalid values
Consistent naming patternsIf tools follow a convention, the model learns it
Handling errors & edge cases
  • Tool returns error: search_hotels("Atlantis") → {"error": "City not found"} — the model should explain, not retry blindly
  • Missing preconditions: classify_data with no target label — stop and explain that supervised prediction isn't possible
  • Ambiguous request: "Compare hotels" — which cities? what dates? Ask clarification questions instead of guessing
TAKEAWAYS

Key points

  1. Tool calling turns an LLM into a controller of external capabilities.
  2. Provider APIs give you reliable structure — no more parsing JSON from text.
  3. The real challenge is the loop: when to call another tool, when to stop, when to ask the user.
  4. Preconditions matter: explore first, then decide which tools are valid.
  5. Good tool names and descriptions directly impact model accuracy.
  6. Asking for clarification is itself a powerful capability — the model's most flexible "tool."
The bottom line

The key design challenge is not how to call tools, but how to decide when the job is finished.

Lab

Open lesson4b_tool_calling.py to walk through each concept with live API calls. Covers: built-in tools, manual vs API tool calling, the multi-tool loop, control policy, clarification mode, and tool naming experiments.

Three support modules: data_tools.py (good tools), data_tools_bad.py (bad naming), and prompt templates in prompt_templates/.