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 |
|---|---|
| Computation | Math, statistics, precise calculations |
| Live data | APIs, databases, current information |
| Actions | Send emails, update records, trigger workflows |
| Structured ops | SQL 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.
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.
Manual vs API-native tool calling
Manual (Prompt + Parse)
- Describe tools in the system prompt
- Ask model to output JSON
- Parse JSON from text response
- 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
- Register tools with JSON schemas
- Model returns structured
tool_callsobjects - Execute function, send result back as
toolmessage - 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 | |
|---|---|---|
| Format | Hope model outputs valid JSON | Guaranteed structured objects |
| Parsing | Custom regex / JSON extraction | msg.tool_calls[0].function.name |
| Multi-tool | Model invents ad-hoc format | Returns a list of tool_calls |
| Reliability | Breaks with temp & model changes | Consistent 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.
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:
| Provider | Built-in tools |
|---|---|
| OpenAI | web_search, code_interpreter, file_search |
| Anthropic | computer_use, text_editor, bash |
Convenient for general capabilities, but for domain-specific work you need your own tools.
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:
- Max iterations — prevent infinite loops
- 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:
| Tool | Purpose | Precondition |
|---|---|---|
explore_data | Inspect schema, missing values, candidate targets | None — safe first step |
cluster_data | Discover natural segments (unsupervised) | Needs numeric column names |
classify_data | Predict 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.
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:
- Some part of the original goal is still unresolved
- Another available tool can resolve it
- 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.
"Do I need another tool?" — not "Can I call another tool?"
Asking instead of guessing
The model has three options at each step:
- Call a tool — if it knows which tool and has all parameters
- Ask a question — if information is missing or ambiguous
- Answer directly — if it already has enough information
Without vs with clarification instructions
| User says | Without clarification | With 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."
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.
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
| Good | Bad | |
|---|---|---|
| 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
| Practice | Why |
|---|---|
| Descriptive function names | explore_data vs run_check — model matches intent to name |
| Self-explaining parameters | feature_columns vs cols — maps user concepts to params |
| Rich descriptions with preconditions | "Call explore_data first if unsure" guides the tool sequence |
| Type constraints | k: integer (2-10) prevents invalid values |
| Consistent naming patterns | If 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_datawith 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
Key points
- Tool calling turns an LLM into a controller of external capabilities.
- Provider APIs give you reliable structure — no more parsing JSON from text.
- The real challenge is the loop: when to call another tool, when to stop, when to ask the user.
- Preconditions matter: explore first, then decide which tools are valid.
- Good tool names and descriptions directly impact model accuracy.
- Asking for clarification is itself a powerful capability — the model's most flexible "tool."
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/.