A small tool-calling agent is easiest to understand when the handoff is visible: the model proposes a call, your Python code validates and executes it, and the result returns to the conversation. This tutorial follows that loop with a bounded example, explicit inputs, and a stopping condition. It also distinguishes a demonstration tool from an action that needs real permissions or approval.
Most AI applications generate text. An agent goes one step further: it interprets a goal, decides whether it needs an external tool, uses that tool, observes the result, and then produces an answer.
This tutorial builds a minimal tool-calling agent in Python. The agent answers a weather-related question by calling a local get_weather function before making a recommendation.
The implementation is intentionally small. It demonstrates the core agent loop without introducing unnecessary frameworks, memory layers, or multi-agent orchestration.
What Makes a System an Agent?
A chatbot can answer a question directly, but that answer may not be based on current or external data. A tool-using agent follows a different process:
| 1. Goal Define the objective | → | 2. Decide Select the tool | → | 3. Run safely Run safely | → | 4. Observe Read the result | → | 5. Answer Write the answer |
Figure 1. The model proposes a function call; the application controls execution.
Key Takeaways
- An agent is a loop around a model, tools, state, and a stopping rule.
- Start with one deterministic tool and validate its output.
- Keep tool access allowlisted and observable.
- Require human approval before state-changing actions.
The Minimal Agent Loop
The smallest useful agent loop contains four stages:
- Send the user request and available tool definitions to the model.
- Check whether the model requested a function call.
- Execute the requested function in the application.
- Send the function result back to the model.
If the model does not request a tool, the loop ends and the response is returned directly.
Project Setup
pip install openai
setx OPENAI_API_KEY "your_api_key_here"
setx OPENAI_MODEL "gpt-5"The Responses API supports custom functions as tools, allowing the application to connect model decisions to its own code.
Define a Deterministic Tool
Our agent will use a local weather function. In production, this function could call a weather provider or an internal data service.
from typing import Dict
WEATHER_DATA: Dict[str, Dict[str, object]] = {
"Seoul": {
"condition": "light rain",
"temperature_c": 18,
"rain_probability": 80,
},
"Tokyo": {
"condition": "clear",
"temperature_c": 24,
"rain_probability": 10,
},
}
def get_weather(city: str) -> Dict[str, object]:
"""Return weather information for a supported city."""
if city not in WEATHER_DATA:
return {
"city": city,
"condition": "unknown",
"temperature_c": None,
"rain_probability": None,
"error": "Weather data is not available for this city.",
}
return {"city": city, **WEATHER_DATA[city]}The function is deterministic, so the example is reproducible. That is useful when testing the control loop before connecting a live service.
Describe the Tool to the Model
WEATHER_TOOL = {
"type": "function",
"name": "get_weather",
"description": "Get the current weather information for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, such as Seoul or Tokyo.",
}
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
}The description tells the model when the function is relevant and what arguments it should provide. The schema is a first validation layer; it does not replace validation inside the application.
Implement the Agent
The model does not execute Python code by itself. It requests a function call. The application validates the request, executes the registered function, and sends the result back.
import json
import os
from typing import Any, Dict
from openai import OpenAI
client = OpenAI()
MODEL = os.getenv("OPENAI_MODEL", "gpt-5")
TOOLS = [WEATHER_TOOL]
def execute_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute only tools explicitly registered by the application."""
if name == "get_weather":
return get_weather(**arguments)
return {"error": f"Unknown tool: {name}"}
def run_agent(user_request: str, max_steps: int = 4) -> str:
"""Run a bounded tool-calling agent."""
input_items = [{
"role": "user",
"content": user_request,
}]
for _ in range(max_steps):
response = client.responses.create(
model=MODEL,
tools=TOOLS,
input=input_items,
)
input_items.extend(response.output)
function_calls = [
item for item in response.output
if item.type == "function_call"
]
if not function_calls:
return response.output_text
for function_call in function_calls:
try:
arguments = json.loads(function_call.arguments)
tool_result = execute_tool(
function_call.name,
arguments,
)
except (json.JSONDecodeError, TypeError) as error:
tool_result = {
"error": f"Invalid tool arguments: {error}"
}
input_items.append({
"type": "function_call_output",
"call_id": function_call.call_id,
"output": json.dumps(tool_result),
})
return "The agent stopped because it reached the maximum number of steps."
if __name__ == "__main__":
print(run_agent(
"What is the weather in Seoul, and should I carry an umbrella?"
))The loop is bounded by max_steps. This prevents repeated calls, circular behavior, and unbounded API costs.
Example Execution
User:
What is the weather in Seoul, and should I carry an umbrella?
Agent decision:
Call get_weather(city="Seoul")
Tool result:
{
"city": "Seoul",
"condition": "light rain",
"temperature_c": 18,
"rain_probability": 80
}
Final answer:
Seoul is expected to have light rain with an 80% chance of precipitation.
The temperature is around 18°C, so carrying an umbrella would be a good idea.The agent is using an external result as part of its decision process rather than producing an unsupported guess.
Validation and Failure Handling
Without validation, a plausible string can be treated as a successful tool result. A production tool boundary should validate:
- argument types and required fields;
- authorization and allowed operations;
- response schema and required values;
- timeouts and retry limits; and
- business rules before state changes.
| Condition | Application response | Continue? |
|---|---|---|
| Valid result | Append observation to state | Yes, if another step is needed |
| Invalid arguments | Return a structured error | Only within retry budget |
| Unauthorized action | Reject and log the request | No |
| Retry budget exhausted | Return failure with evidence | No |
Human Approval for Risky Actions
Reading weather data is harmless, so the example executes automatically. Sending an email, publishing a blog post, deleting a file, or making a purchase should use an approval boundary.
| Agent proposes action + parameters | → | Policy check scope + permissions | → | Human approval approve or reject | → | Execute record the result |
Figure 2. The model can propose an action, but the application and human remain in control.
What This Example Does Not Demonstrate
This is a minimal tool-calling agent, not a complete autonomous system. It does not include long-term memory, a vector database, multi-agent collaboration, background scheduling, web browsing, automatic code execution, or guaranteed factual accuracy.
Those features can be added later, but the basic loop should be observable and testable first.
A Practical Development Path
- One tool: build a deterministic function and log every call.
- Read-only tools: add search, database lookup, or document retrieval.
- Validation: handle invalid arguments, unavailable data, timeouts, and API errors.
- Approval: require explicit approval before external side effects.
- Persistent state: store task status, tool results, and execution history.
- Evaluation: measure tool selection, argument validity, unnecessary calls, and failure explanations.
Conclusion
A useful AI agent does not need a complicated architecture. Its core behavior can be expressed as:
The important engineering work is controlling the boundary between the model and the tools. Start with one tool, one bounded loop, clear logs, and a human approval step for risky actions. Once that foundation works reliably, additional capabilities can be added without turning the system into an opaque collection of prompts and callbacks.
References
Frequently Asked Questions (FAQ)
1. Why shouldn't the LLM execute Python code directly on the host machine?
Allowing an AI model to generate and execute arbitrary bash or Python code directly on host infrastructure introduces critical remote code execution (RCE) vulnerabilities. By strictly decoupling intent declaration (the model outputting JSON function arguments) from execution (the host Python application executing an allowlisted, validated function), you enforce a zero-trust security perimeter.
2. What happens if the model provides invalid arguments to a function?
Always wrap function execution in robust try-except blocks. If argument decoding fails, append a structured error dictionary (e.g., {"error": "Invalid argument format"}) to the conversation history and let the model observe the error and self-correct on its next iteration.
3. How do I prevent infinite tool-calling loops?
Always enforce a strict max_steps counter (typically 3 to 6 turns) in your primary agent loop. If the loop reaches the ceiling without concluding, force a termination event and return an explicit fallback explanation to the caller.
4. Can this architecture support asynchronous or long-running tools?
Yes. In production systems, you can convert the execution dispatcher into an asynchronous queue (using Celery, Redis, or FastAPI background tasks). The agent emits a task ID, pauses the session state, and resumes once external telemetry fires a completion webhook.
Editorial Verdict: Clean Foundations Before Framework Bloat
Modern agent frameworks like LangChain, CrewAI, or AutoGen offer valuable abstractions, but they frequently obscure the fundamental dynamics of function execution. By building a minimal, pure-Python tool loop from scratch, engineers gain total visibility over state management, token budgets, and security gates—ensuring their production systems remain robust, explainable, and dependable.
Editorial record
Editorial update — September 13, 2026: This revision replaces the abstract discussion with a reproducible tool-calling implementation, explicit validation, bounded retries, and human approval for risky actions.
Update history: September 13, 2026 — technical rewrite with a complete agent loop and new diagrams. Original publication: February 6, 2025.