Tool Calling Agent
Build an AI agent that uses functions and APIs to accomplish tasks
Tool Calling Agent
TL;DR
Build an AI agent that can call external functions and APIs. Define tool schemas that describe what each function does, let the LLM decide when to use them, execute the functions, and return results. This is the foundation of every modern AI agent — from ChatGPT plugins to autonomous coding assistants.
| Difficulty | Beginner |
| Time | ~2 hours |
| Code Size | ~200 LOC |
| Prerequisites | Python basics, familiarity with APIs (OpenAI API key) |
Why Do LLMs Need Tools?
Large Language Models are powerful — but they have fundamental limits. They can only generate text. They cannot:
What LLMs Cannot Do Alone
Check the current time or date
LLMs have no clock. They were trained on data with a cutoff date and have no idea what time it is right now.
Access live data (weather, stocks, APIs)
LLMs cannot make HTTP requests. Any "live" data they mention is either hallucinated or from training data.
Do reliable math
LLMs predict the next token — they don't actually compute. "What is 7,391 × 4,823?" will often be wrong.
Read your files or databases
LLMs have no access to your system. They can only work with what you put in the prompt.
Tools solve this. A tool is a regular Python function that the LLM can ask the agent to run. The LLM decides when and which tool to call, the agent executes it, and feeds the result back. This gives LLMs "hands" — the ability to take actions in the real world.
What You'll Learn
- What function calling is and how LLMs use it to invoke tools
- How to define tool schemas — the JSON format that tells the LLM what tools exist
- The agent loop — the core pattern (send → call → execute → return → repeat)
- Building a complete agent with 4 tools exposed via a REST API
Tech Stack
| Component | Technology | Why |
|---|---|---|
| LLM | OpenAI GPT-4o-mini | Fast, cheap, great for tool calling |
| Framework | LangChain | Optional — we use raw OpenAI SDK here for clarity |
| Tools | Custom Python functions | Weather, time, calculator, Wikipedia |
| API | FastAPI | Production-grade Python web framework |
How Tool Calling Works
Tool Calling Sequence
The key insight: The LLM never runs the function itself. It outputs a structured request like get_weather(city="Paris"), and your code (the agent) runs the actual function. The LLM is the brain that decides what to do; the agent is the body that actually does it.
Project Structure
tool-calling-agent/
├── src/
│ ├── __init__.py
│ ├── agent.py # Main agent logic
│ ├── tools.py # Tool definitions
│ ├── schemas.py # Pydantic models
│ └── api.py # FastAPI application
├── tests/
│ └── test_agent.py
├── requirements.txt
└── README.mdImplementation
Step 1: Project Setup
mkdir tool-calling-agent && cd tool-calling-agent
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activateopenai>=1.0.0
langchain>=0.1.0
langchain-openai>=0.0.5
fastapi>=0.100.0
uvicorn>=0.23.0
pydantic>=2.0.0
httpx>=0.25.0
python-dotenv>=1.0.0pip install -r requirements.txtCreate a .env file in your project root to store your API key securely. The python-dotenv package loads this automatically — never hardcode API keys in your source code.
OPENAI_API_KEY=sk-your-key-hereSecurity
Add .env to your .gitignore file. Never commit API keys to version control.
Step 2: Define Tools
Tools are functions that the LLM can call. Each tool needs a clear description and typed parameters.
"""
Tool definitions for the agent.
Each tool is a Python function with:
- Type hints for parameters
- A docstring describing what it does
- Return type annotation
"""
import httpx
from datetime import datetime
from typing import Optional
import json
def get_current_time(timezone: str = "UTC") -> str:
"""
Get the current time in a specified timezone.
Args:
timezone: The timezone name (e.g., "UTC", "US/Eastern", "Europe/London")
Returns:
Current time as a formatted string
"""
from zoneinfo import ZoneInfo
try:
tz = ZoneInfo(timezone)
now = datetime.now(tz)
return now.strftime("%Y-%m-%d %H:%M:%S %Z")
except Exception as e:
return f"Error: Invalid timezone '{timezone}'"
def get_weather(city: str, units: str = "celsius") -> dict:
"""
Get current weather for a city.
Args:
city: The city name to get weather for
units: Temperature units - "celsius" or "fahrenheit"
Returns:
Weather data including temperature, conditions, humidity
"""
# Simulated weather data (replace with real API in production)
weather_data = {
"paris": {"temp_c": 18, "conditions": "sunny", "humidity": 65},
"london": {"temp_c": 14, "conditions": "cloudy", "humidity": 78},
"new york": {"temp_c": 22, "conditions": "partly cloudy", "humidity": 55},
"tokyo": {"temp_c": 26, "conditions": "humid", "humidity": 80},
}
city_lower = city.lower()
if city_lower not in weather_data:
return {"error": f"Weather data not available for {city}"}
data = weather_data[city_lower]
temp = data["temp_c"]
if units == "fahrenheit":
temp = (temp * 9/5) + 32
unit_symbol = "F"
else:
unit_symbol = "C"
return {
"city": city,
"temperature": f"{temp}{unit_symbol}",
"conditions": data["conditions"],
"humidity": f"{data['humidity']}%"
}
def calculate(expression: str) -> str:
"""
Evaluate a mathematical expression safely.
Args:
expression: A mathematical expression like "2 + 2" or "sqrt(16)"
Returns:
The result of the calculation
"""
import math
# Safe evaluation with limited namespace
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max,
"sum": sum, "pow": pow,
"sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
"tan": math.tan, "log": math.log, "log10": math.log10,
"pi": math.pi, "e": math.e
}
try:
# Remove dangerous characters
safe_expr = expression.replace("__", "")
result = eval(safe_expr, {"__builtins__": {}}, allowed_names)
return str(result)
except Exception as e:
return f"Error: Could not evaluate '{expression}' - {str(e)}"
def search_wikipedia(query: str, sentences: int = 3) -> str:
"""
Search Wikipedia and return a summary.
Args:
query: The search term
sentences: Number of sentences to return (1-10)
Returns:
Wikipedia summary text
"""
sentences = max(1, min(10, sentences))
try:
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{query}"
response = httpx.get(url, timeout=10.0)
if response.status_code == 200:
data = response.json()
extract = data.get("extract", "No information found.")
# Limit to requested sentences
sentences_list = extract.split(". ")
return ". ".join(sentences_list[:sentences]) + "."
else:
return f"Could not find Wikipedia article for '{query}'"
except Exception as e:
return f"Error searching Wikipedia: {str(e)}"
# Tool registry for easy access
TOOLS = {
"get_current_time": get_current_time,
"get_weather": get_weather,
"calculate": calculate,
"search_wikipedia": search_wikipedia,
}Understanding the Code:
Each tool is just a regular Python function. The LLM will never see or run this code — it only sees the schema (defined in the next step). But the function design matters:
- Type hints (
city: str,units: str = "celsius") — These match the schema parameters. They tell Python (and you) what the function expects. - Docstrings — Although the LLM doesn't see docstrings directly, they document intent for human developers.
- Error handling — Every tool returns an error as data (
{"error": "..."}) instead of raising exceptions. This is critical — if a tool crashes, the entire agent loop breaks. When it returns an error message, the LLM can explain the problem to the user or try a different approach. TOOLSdictionary — A registry mapping tool names (strings) to actual functions. When the LLM says "callget_weather", the agent looks upTOOLS["get_weather"]and calls it.
About eval() in the Calculator
The calculate() function uses Python's eval() — which normally lets someone run any code. This is dangerous. We limit it by passing {"__builtins__": {}} (disabling all built-in functions) and only allowing math functions in allowed_names. This is acceptable for a tutorial, but in production you'd use a proper math parser library like sympy instead.
Understanding Tool Design Patterns:
Tool Design Patterns
Input Validation
RecommendedValidate inside the tool even if the schema constrains inputs. Handle unexpected values gracefully instead of crashing.
Return Structured Data
RecommendedReturn JSON like {"temperature": "18C", "conditions": "sunny"} instead of plain text. Structured data lets the LLM combine multiple tool results.
Return Errors, Don't Raise
RecommendedReturn {"error": "City not found"} instead of raising exceptions. The LLM can explain errors to the user or try a different approach.
Plain Text Returns
Returning "It's 18 degrees and sunny" loses structure and makes it harder for the LLM to combine with other tool results.
Raising Exceptions
Using raise ValueError("City not found") crashes the agent loop instead of letting the LLM recover gracefully.
Step 3: Create Tool Schemas
OpenAI and other LLMs need tools defined in a specific JSON schema format.
"""
Tool schemas for LLM function calling.
These schemas tell the model what tools are available
and how to use them.
"""
from typing import Any
TOOL_SCHEMAS = [
{
"type": "function",
"function": {
"name": "get_current_time",
"description": "Get the current time in a specified timezone",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone (e.g., 'UTC', 'US/Eastern', 'Europe/Paris')",
"default": "UTC"
}
},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name (e.g., 'Paris', 'London', 'New York')"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units",
"default": "celsius"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports basic math, sqrt, sin, cos, log, etc.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "The math expression to evaluate (e.g., '2 + 2', 'sqrt(16)', 'sin(pi/2)')"
}
},
"required": ["expression"]
}
}
},
{
"type": "function",
"function": {
"name": "search_wikipedia",
"description": "Search Wikipedia and get a summary of the topic",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search term or topic"
},
"sentences": {
"type": "integer",
"description": "Number of sentences to return (1-10)",
"default": 3
}
},
"required": ["query"]
}
}
}
]
def get_tool_schemas() -> list[dict[str, Any]]:
"""Return all tool schemas."""
return TOOL_SCHEMASUnderstanding Tool Schemas:
This is what the LLM actually "sees" — not your Python code, but this JSON description. Let's break down how the LLM uses each field:
| Schema Field | What It Does | Example |
|---|---|---|
name | How the LLM refers to the tool | "get_weather" |
description | Most important — the LLM reads this to decide when to use the tool | "Get current weather for a city" |
parameters.properties | Each parameter the tool accepts, with type and description | city: string, units: enum |
required | Parameters the LLM must provide (vs optional with defaults) | ["city"] — city is required, units is optional |
Why Descriptions Matter So Much
The LLM decides which tool to call based almost entirely on the description field. A vague description like "weather function" will cause the LLM to use the tool at wrong times or miss it entirely. Write descriptions as if you're explaining the tool to a new colleague.
What the LLM Actually Receives:
When you call the OpenAI API with tools, the request looks like this behind the scenes:
{
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": "You are a helpful assistant..."},
{"role": "user", "content": "What's the weather in Paris?"}
],
"tools": [
{"type": "function", "function": {"name": "get_weather", "description": "...", "parameters": {...}}},
{"type": "function", "function": {"name": "calculate", "description": "...", "parameters": {...}}}
],
"tool_choice": "auto"
}The LLM reads the user message, scans the tool descriptions, and decides: "The user is asking about weather → I should call get_weather with city='Paris'." It then returns a structured tool call instead of a text response.
Step 4: Build the Agent
The agent orchestrates the conversation between the user, LLM, and tools.
"""
Tool-calling agent implementation.
This agent:
1. Receives user messages
2. Sends them to the LLM with tool definitions
3. Executes any tool calls the LLM requests
4. Returns tool results to the LLM
5. Repeats until the LLM provides a final response
"""
import json
from typing import Optional
from dataclasses import dataclass, field
from openai import OpenAI
from .tools import TOOLS
from .schemas import get_tool_schemas
@dataclass
class Message:
"""A message in the conversation."""
role: str
content: str
tool_calls: Optional[list] = None
tool_call_id: Optional[str] = None
name: Optional[str] = None
@dataclass
class AgentResponse:
"""Response from the agent."""
content: str
tool_calls_made: list[dict] = field(default_factory=list)
total_tokens: int = 0
class ToolCallingAgent:
"""
An agent that uses tools to answer questions.
The agent follows a loop:
1. Send message to LLM
2. If LLM wants to use a tool, execute it
3. Send tool result back to LLM
4. Repeat until LLM gives final answer
"""
def __init__(
self,
model: str = "gpt-4o-mini",
max_iterations: int = 10,
temperature: float = 0.7
):
"""
Initialize the agent.
Args:
model: The OpenAI model to use
max_iterations: Maximum tool calls before stopping
temperature: LLM temperature (0-1)
"""
self.client = OpenAI()
self.model = model
self.max_iterations = max_iterations
self.temperature = temperature
self.tools = get_tool_schemas()
self.system_prompt = """You are a helpful assistant with access to tools.
Use tools when they would help answer the user's question accurately.
Always explain what you're doing and why you're using a tool.
If a tool returns an error, explain the issue to the user.
Available tools:
- get_current_time: Get current time in any timezone
- get_weather: Get weather for any city
- calculate: Evaluate math expressions
- search_wikipedia: Look up information on Wikipedia"""
def _execute_tool(self, name: str, arguments: dict) -> str:
"""Execute a tool and return the result."""
if name not in TOOLS:
return json.dumps({"error": f"Unknown tool: {name}"})
try:
result = TOOLS[name](**arguments)
if isinstance(result, dict):
return json.dumps(result)
return str(result)
except Exception as e:
return json.dumps({"error": str(e)})
def _format_messages(self, messages: list[Message]) -> list[dict]:
"""Convert Message objects to OpenAI format."""
formatted = []
for msg in messages:
m = {"role": msg.role, "content": msg.content}
if msg.tool_calls:
m["tool_calls"] = msg.tool_calls
if msg.tool_call_id:
m["tool_call_id"] = msg.tool_call_id
if msg.name:
m["name"] = msg.name
formatted.append(m)
return formatted
def run(self, user_message: str) -> AgentResponse:
"""
Run the agent with a user message.
Args:
user_message: The user's input
Returns:
AgentResponse with the final answer and metadata
"""
messages = [
Message(role="system", content=self.system_prompt),
Message(role="user", content=user_message)
]
tool_calls_made = []
total_tokens = 0
iterations = 0
while iterations < self.max_iterations:
iterations += 1
# Call the LLM
response = self.client.chat.completions.create(
model=self.model,
messages=self._format_messages(messages),
tools=self.tools,
tool_choice="auto",
temperature=self.temperature
)
total_tokens += response.usage.total_tokens
assistant_message = response.choices[0].message
# Check if we're done (no tool calls)
if not assistant_message.tool_calls:
return AgentResponse(
content=assistant_message.content or "",
tool_calls_made=tool_calls_made,
total_tokens=total_tokens
)
# Add assistant message with tool calls
messages.append(Message(
role="assistant",
content=assistant_message.content or "",
tool_calls=[
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
}
for tc in assistant_message.tool_calls
]
))
# Execute each tool call
for tool_call in assistant_message.tool_calls:
name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Execute and record
result = self._execute_tool(name, arguments)
tool_calls_made.append({
"tool": name,
"arguments": arguments,
"result": result
})
# Add tool result to messages
messages.append(Message(
role="tool",
content=result,
tool_call_id=tool_call.id,
name=name
))
# Max iterations reached
return AgentResponse(
content="I've reached the maximum number of tool calls. Please try a simpler question.",
tool_calls_made=tool_calls_made,
total_tokens=total_tokens
)
# Convenience function for quick usage
def run_agent(message: str) -> str:
"""Run the agent and return just the response text."""
agent = ToolCallingAgent()
response = agent.run(message)
return response.contentUnderstanding the Agent Loop — The Core of Every AI Agent:
The run() method is the heart of this project. Let's walk through exactly what happens:
- Build the message list — Start with a system prompt (instructions for the LLM) and the user's message.
- Enter the while loop — This is the agent loop. It runs until the LLM responds with text (no tool calls) or we hit
max_iterations. - Call the LLM — Send all messages + tool schemas to OpenAI. The LLM returns either a text response or one or more tool call requests.
- Check for tool calls — If
assistant_message.tool_callsis empty, the LLM is done — return the text response. - Execute tools — For each tool call, parse the arguments (JSON string → Python dict), run the function, and capture the result.
- Append results — Add both the assistant's tool call request AND the tool results to the message list. This gives the LLM the full context for its next decision.
- Loop — Go back to step 3. The LLM now sees the tool results and can either call more tools or generate a final answer.
Key parameters explained:
| Parameter | Value | Why |
|---|---|---|
tool_choice="auto" | LLM decides whether to use a tool | Use "required" to force a tool call, "none" to disable tools |
temperature=0.7 | Controls randomness (0 = deterministic, 1 = creative) | 0.7 is a good balance for agents — predictable but not robotic |
max_iterations=10 | Safety limit on tool call rounds | Prevents infinite loops if the LLM keeps calling tools without finishing |
Step 5: Create the API
"""
FastAPI application for the tool-calling agent.
"""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional
from .agent import ToolCallingAgent, AgentResponse
app = FastAPI(
title="Tool Calling Agent API",
description="An AI agent that can use tools to answer questions",
version="1.0.0"
)
# Initialize agent
agent = ToolCallingAgent()
class ChatRequest(BaseModel):
"""Request model for chat endpoint."""
message: str = Field(..., min_length=1, max_length=2000)
temperature: Optional[float] = Field(default=0.7, ge=0.0, le=1.0)
class ToolCall(BaseModel):
"""A single tool call."""
tool: str
arguments: dict
result: str
class ChatResponse(BaseModel):
"""Response model for chat endpoint."""
response: str
tool_calls: list[ToolCall]
total_tokens: int
@app.get("/")
async def root():
"""Health check endpoint."""
return {"status": "healthy", "agent": "tool-calling-agent"}
@app.get("/tools")
async def list_tools():
"""List available tools."""
return {
"tools": [
{
"name": "get_current_time",
"description": "Get current time in any timezone"
},
{
"name": "get_weather",
"description": "Get weather for a city"
},
{
"name": "calculate",
"description": "Evaluate math expressions"
},
{
"name": "search_wikipedia",
"description": "Search Wikipedia for information"
}
]
}
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""
Send a message to the agent.
The agent will use tools as needed to answer your question.
"""
try:
# Update temperature if provided
agent.temperature = request.temperature
# Run the agent
result = agent.run(request.message)
return ChatResponse(
response=result.content,
tool_calls=[
ToolCall(
tool=tc["tool"],
arguments=tc["arguments"],
result=tc["result"]
)
for tc in result.tool_calls_made
],
total_tokens=result.total_tokens
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
What About Streaming?
This API returns the complete response after all tool calls finish. For streaming (showing text as it generates), you'd use Server-Sent Events (SSE). See the Chatbot project for a full streaming implementation.
Step 6: Create Tests
"""Tests for the tool-calling agent."""
import pytest
from src.tools import get_current_time, get_weather, calculate, search_wikipedia
from src.agent import ToolCallingAgent
class TestTools:
"""Test individual tools."""
def test_get_current_time_utc(self):
"""Test getting UTC time."""
result = get_current_time("UTC")
assert "UTC" in result
assert "Error" not in result
def test_get_current_time_invalid(self):
"""Test invalid timezone."""
result = get_current_time("Invalid/Zone")
assert "Error" in result
def test_get_weather_valid_city(self):
"""Test weather for known city."""
result = get_weather("Paris")
assert "temperature" in result
assert "conditions" in result
assert result["city"] == "Paris"
def test_get_weather_fahrenheit(self):
"""Test weather in Fahrenheit."""
result = get_weather("Paris", units="fahrenheit")
assert "F" in result["temperature"]
def test_get_weather_unknown_city(self):
"""Test weather for unknown city."""
result = get_weather("UnknownCity")
assert "error" in result
def test_calculate_basic(self):
"""Test basic calculation."""
assert calculate("2 + 2") == "4"
assert calculate("10 * 5") == "50"
def test_calculate_advanced(self):
"""Test advanced math."""
assert calculate("sqrt(16)") == "4.0"
assert float(calculate("sin(0)")) == 0.0
def test_calculate_invalid(self):
"""Test invalid expression."""
result = calculate("invalid")
assert "Error" in result
class TestAgent:
"""Test the full agent."""
@pytest.fixture
def agent(self):
"""Create a test agent."""
return ToolCallingAgent(
model="gpt-4o-mini", # Use cheaper model for tests
max_iterations=3
)
def test_agent_initialization(self, agent):
"""Test agent initializes correctly."""
assert agent.model == "gpt-4o-mini"
assert agent.max_iterations == 3
assert len(agent.tools) > 0
@pytest.mark.skipif(
True, # Skip by default (requires API key)
reason="Requires OpenAI API key"
)
def test_agent_simple_query(self, agent):
"""Test agent with a simple query."""
response = agent.run("What is 2 + 2?")
assert "4" in response.content
@pytest.mark.skipif(
True,
reason="Requires OpenAI API key"
)
def test_agent_tool_usage(self, agent):
"""Test that agent uses tools."""
response = agent.run("What's the weather in Paris?")
assert len(response.tool_calls_made) > 0
assert any(tc["tool"] == "get_weather" for tc in response.tool_calls_made)Running the Application
Start the API
# Make sure your .env file has OPENAI_API_KEY set, then:
uvicorn src.api:app --reload --host 0.0.0.0 --port 8000You should see:
INFO: Uvicorn running on http://0.0.0.0:8000
INFO: Started reloader processOpen http://localhost:8000/docs to see the auto-generated Swagger UI — FastAPI creates this from your Pydantic models.
Test with curl
# List available tools
curl http://localhost:8000/tools
# Ask a question
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "What time is it in Tokyo and what is the weather there?"}'
# Math calculation
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "What is the square root of 144 plus 25?"}'
# Wikipedia search
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Tell me about the Eiffel Tower"}'Example Response:
When you ask "What time is it in Tokyo and what's the weather there?", the agent:
- Calls
get_current_time(timezone="Asia/Tokyo")→"2026-03-25 14:32:10 JST" - Calls
get_weather(city="Tokyo")→{"temperature": "26C", "conditions": "humid", "humidity": "80%"} - Combines both results into a natural answer:
{
"response": "It's currently 2:32 PM in Tokyo (JST). The weather is 26°C and humid with 80% humidity.",
"tool_calls": [
{"tool": "get_current_time", "arguments": {"timezone": "Asia/Tokyo"}, "result": "2026-03-25 14:32:10 JST"},
{"tool": "get_weather", "arguments": {"city": "Tokyo"}, "result": "{\"temperature\": \"26C\", \"conditions\": \"humid\", \"humidity\": \"80%\"}"}
],
"total_tokens": 847
}Notice how the agent used two tools in sequence to answer a single question. The LLM decided on its own that it needed both time and weather data.
Key Concepts
Tool Definition Best Practices
- Clear descriptions: The model uses descriptions to decide when to use tools
- Typed parameters: Always specify types and constraints
- Required vs optional: Mark truly required parameters
- Error handling: Return structured errors the model can understand
The Agent Loop
The Agent Loop
User Input
Receive the user's question or request
Send to LLM
Send message + tool schemas to the language model
Tool Calls?
Check if the LLM wants to call any tools
Execute Tools
Run the requested tool functions and collect results
Add Results to Context
Append tool results to the conversation
Return Response
If no tool calls, return the LLM's final answer to the user
When to Use Multiple Tools
The agent can chain tools together:
- "What time is it in London and what's the weather?" - Uses both time and weather tools
- "Calculate 2+2 and search Wikipedia for mathematics" - Uses both calculate and search
Next Steps
Now that you've built a basic tool-calling agent, continue to:
- ReAct Agent - Add reasoning traces
- Conversational Agent - Add memory
- Planning Agent - Plan before acting
Key Concepts Recap
| Concept | What It Is | Why It Matters |
|---|---|---|
| Tool Schema | JSON definition of function signature | Tells LLM what tools exist and how to use them |
| Function Calling | LLM outputting structured tool requests | Enables LLM to take actions in the real world |
| Agent Loop | Send → Call → Execute → Return cycle | Core pattern for all tool-using agents |
| Tool Choice | LLM decides which tool to use | Autonomous decision-making based on context |
| Max Iterations | Limit on tool call cycles | Prevents infinite loops and runaway costs |
| Tool Result | Output returned to LLM | Gives LLM information to form final answer |
Summary
You've built a tool-calling agent that:
- Defines tools with clear schemas
- Sends tool definitions to the LLM
- Executes tool calls and returns results
- Loops until the model provides a final answer
- Exposes everything via a REST API
This pattern is the foundation for all modern AI agents.