From Conversation to Action¶
Imagine you’re working on a group project, and instead of just talking about what needs to be done, your teammate can actually do things—check the calendar, send emails, look up information, even write code. That’s the leap we’re making in this chapter: from language models that can only talk about actions to AI agents that can take actions.
In the early days of large language models, we were thrilled when they could answer questions and generate text. But there was always a gap: they lived in a world of words, disconnected from the tools and systems we use every day. Tool calling bridges that gap, transforming language models from eloquent conversationalists into capable assistants.
The Core Idea: Structured Function Calling¶
At its heart, tool calling is about giving language models a structured way to say “I need to use this specific function with these specific parameters.” Instead of just generating text that describes what should happen, the model generates structured data that your code can execute.
Let’s see this in action with Ollama and the Qwen3 model:
import ollama
import json
# Define a simple tool: get the current weather
tools = [{
'type': 'function',
'function': {
'name': 'get_weather',
'description': 'Get the current weather for a location',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': 'City name, e.g. San Francisco'
},
'unit': {
'type': 'string',
'enum': ['celsius', 'fahrenheit'],
'description': 'Temperature unit'
}
},
'required': ['location']
}
}
}]
response = ollama.chat(
model='qwen3',
messages=[{'role': 'user', 'content': 'What is the weather in Paris?'}],
tools=tools
)
print(response['message']['tool_calls'])[ToolCall(function=Function(name='get_weather', arguments={'location': 'Paris', 'unit': 'celsius'}))]
What’s happening here? We’ve given the model a schema—a formal description of what the get_weather function expects. When the model sees “What is the weather in Paris?”, it recognizes it needs to call a tool and outputs structured JSON rather than freeform text.
Anatomy of a Tool Definition¶
Tool definitions follow a specific structure that tells the model everything it needs to know:
weather_tool = {
'type': 'function', # Currently, 'function' is the only type
'function': {
'name': 'get_weather', # Unique identifier
'description': 'Get weather for a location', # Helps model decide when to use it
'parameters': { # JSON Schema for the parameters
'type': 'object',
'properties': {
'location': {'type': 'string', 'description': 'City name'},
'unit': {'type': 'string', 'enum': ['celsius', 'fahrenheit']}
},
'required': ['location']
}
}
}The description field is crucial—it’s how the model decides when to use the tool. Good descriptions are clear, specific, and include examples when helpful.
Building Your First Agent Loop¶
An agent isn’t just one tool call—it’s a conversation between the model and your tools. Here’s the basic loop:
User sends a message
Model decides if it needs a tool
If yes, model generates tool call(s)
You execute the tool(s)
You send results back to model
Model responds to user (or calls more tools!)
Let’s implement this:
def get_weather(location, unit='celsius'):
"""Simulated weather API"""
return {
'location': location,
'temperature': 22 if unit == 'celsius' else 72,
'conditions': 'sunny',
'unit': unit
}
def run_agent(user_message):
messages = [{'role': 'user', 'content': user_message}]
response = ollama.chat(
model='qwen2.5:latest',
messages=messages,
tools=[weather_tool]
)
# Check if model wants to call a tool
if response['message'].get('tool_calls'):
# Add model's response to messages
messages.append(response['message'])
# Execute each tool call
for tool in response['message']['tool_calls']:
if tool['function']['name'] == 'get_weather':
args = tool['function']['arguments']
result = get_weather(**args)
# Add tool result to messages
messages.append({
'role': 'tool',
'content': json.dumps(result),
})
# Get final response with tool results
final_response = ollama.chat(
model='qwen2.5:latest',
messages=messages
)
return final_response['message']['content']
return response['message']['content']
# Try it!
print(run_agent("What's the weather like in Tokyo?"))Notice the message flow: user message → model response with tool call → tool result → final model response. This is the fundamental pattern of agentic AI.
Multiple Tools: Expanding Capabilities¶
Real agents have access to multiple tools. Let’s create a more interesting agent:
tools = [
{
'type': 'function',
'function': {
'name': 'calculate',
'description': 'Perform mathematical calculations',
'parameters': {
'type': 'object',
'properties': {
'expression': {
'type': 'string',
'description': 'Math expression like "2 + 2" or "sqrt(16)"'
}
},
'required': ['expression']
}
}
},
{
'type': 'function',
'function': {
'name': 'search_database',
'description': 'Search a product database',
'parameters': {
'type': 'object',
'properties': {
'query': {'type': 'string', 'description': 'Search query'},
'max_results': {'type': 'integer', 'description': 'Max results'}
},
'required': ['query']
}
}
}
]
def calculate(expression):
"""Safe calculator"""
try:
return {'result': eval(expression, {'__builtins__': {}},
{'sqrt': __import__('math').sqrt})}
except:
return {'error': 'Invalid expression'}
def search_database(query, max_results=5):
"""Simulated database"""
products = {
'laptop': {'name': 'UltraBook Pro', 'price': 1299},
'phone': {'name': 'SmartPhone X', 'price': 899}
}
return [v for k, v in products.items() if query.lower() in k]The model will now automatically choose which tool to use based on the user’s request!
Parallel Tool Calls: Efficiency Matters¶
Sometimes an agent needs to call multiple tools at once. Modern models support parallel tool calls:
# User asks: "What's the weather in London and Paris?"
response = ollama.chat(
model='qwen3:latest',
messages=[{
'role': 'user',
'content': 'What is the weather in London and Paris?'
}],
tools=[weather_tool]
)
# Model might return multiple tool calls at once!
for tool_call in response['message'].get('tool_calls', []):
print(f"Calling {tool_call['function']['name']} with args:")
print(tool_call['function']['arguments'])Calling get_weather with args:
{'location': 'London'}
Calling get_weather with args:
{'location': 'Paris'}
This is more efficient than sequential calls and shows how agents can be surprisingly sophisticated in their planning.
Chain of Thought with Tools¶
Sometimes agents need to “think” before acting. Reasoning models like DeepSeek-R1 make their thought process explicit through a special thinking field. This gives us unprecedented insight into why an agent chooses to use certain tools:
import ollama
import json
# Define tools for our thoughtful agent
math_tools = [
{
'type': 'function',
'function': {
'name': 'calculate',
'description': 'Evaluate a mathematical expression',
'parameters': {
'type': 'object',
'properties': {
'expression': {
'type': 'string',
'description': 'Math expression like "2 + 2" or "sqrt(16)"'
}
},
'required': ['expression']
}
}
},
{
'type': 'function',
'function': {
'name': 'get_constant',
'description': 'Get the value of mathematical constants',
'parameters': {
'type': 'object',
'properties': {
'constant': {
'type': 'string',
'enum': ['pi', 'e', 'golden_ratio'],
'description': 'The mathematical constant to retrieve'
}
},
'required': ['constant']
}
}
}
]
def calculate(expression):
"""Safe mathematical calculator"""
import math
safe_dict = {
'sqrt': math.sqrt,
'sin': math.sin,
'cos': math.cos,
'pi': math.pi,
'e': math.e
}
try:
result = eval(expression, {"__builtins__": {}}, safe_dict)
return {'result': result, 'expression': expression}
except Exception as e:
return {'error': str(e)}
def get_constant(constant):
"""Retrieve mathematical constants"""
import math
constants = {
'pi': math.pi,
'e': math.e,
'golden_ratio': (1 + math.sqrt(5)) / 2
}
return {'constant': constant, 'value': constants[constant]}
def thoughtful_agent(user_message):
"""Agent that shows its reasoning process"""
messages = [{'role': 'user', 'content': user_message}]
print(f"\n{'='*60}")
print(f"USER: {user_message}")
print(f"{'='*60}\n")
max_iterations = 5 # Prevent infinite loops
iteration = 0
while iteration < max_iterations:
iteration += 1
# Get response from DeepSeek-R1
response = ollama.chat(
model='qwen3:latest',
messages=messages,
tools=math_tools
)
# DeepSeek-R1 exposes internal reasoning in 'thinking' field
if response['message'].get('thinking'):
print(f"AGENT'S INTERNAL REASONING (Step {iteration}):")
print("-" * 60)
# Truncate long thinking for readability
thinking = response['message']['thinking']
if len(thinking) > 500:
print(thinking[:500] + "...")
else:
print(thinking)
print("-" * 60)
print()
# Check if model wants to use tools
if not response['message'].get('tool_calls'):
print("AGENT RESPONSE:")
print(response['message']['content'])
return response['message']['content']
# Add assistant's message to conversation
messages.append(response['message'])
# Execute each tool call
print("🔧 TOOL CALLS:")
for tool_call in response['message']['tool_calls']:
func_name = tool_call['function']['name']
args = tool_call['function']['arguments']
print(f" → {func_name}({', '.join(f'{k}={repr(v)}' for k, v in args.items())})")
# Execute the function
if func_name == 'calculate':
result = calculate(args['expression'])
elif func_name == 'get_constant':
result = get_constant(args['constant'])
else:
result = {'error': f'Unknown function: {func_name}'}
print(f" ✓ Result: {result}")
# Add tool result to messages
messages.append({
'role': 'tool',
'content': json.dumps(result)
})
print()
return "Max iterations reached"
# Try it with a complex query!
result = thoughtful_agent(
"If I have a circle with radius 5, what's its area? "
"Also, what's that area divided by the golden ratio?"
)
============================================================
USER: If I have a circle with radius 5, what's its area? Also, what's that area divided by the golden ratio?
============================================================
AGENT'S INTERNAL REASONING (Step 1):
------------------------------------------------------------
Okay, the user is asking about the area of a circle with radius 5 and then wants that area divided by the golden ratio. Let me break this down.
First, the area of a circle is π multiplied by the radius squared. The radius here is 5, so the formula would be π * 5². That simplifies to π * 25. So the area is 25π. I can calculate that numerically using the get_constant function for π. Alternatively, maybe the user wants the exact value in terms of π, but since they mentioned dividing by the golden ...
------------------------------------------------------------
🔧 TOOL CALLS:
→ get_constant(constant='pi')
✓ Result: {'constant': 'pi', 'value': 3.141592653589793}
→ get_constant(constant='golden_ratio')
✓ Result: {'constant': 'golden_ratio', 'value': 1.618033988749895}
AGENT'S INTERNAL REASONING (Step 2):
------------------------------------------------------------
Okay, let's see. The user asked for two things: the area of a circle with radius 5 and then that area divided by the golden ratio.
First, I need to calculate the area of the circle. The formula is π multiplied by radius squared. The radius is 5, so 5 squared is 25. Then multiply by π, which we got from the get_constant function as approximately 3.141592653589793. So 25 * π equals about 78.53981633974483.
Next, the user wants that area divided by the golden ratio. The golden ratio value from th...
------------------------------------------------------------
🔧 TOOL CALLS:
→ calculate(expression='25 * 3.141592653589793')
✓ Result: {'result': 78.53981633974483, 'expression': '25 * 3.141592653589793'}
→ calculate(expression='78.53981633974483 / 1.618033988749895')
✓ Result: {'result': 48.54027596813666, 'expression': '78.53981633974483 / 1.618033988749895'}
AGENT'S INTERNAL REASONING (Step 3):
------------------------------------------------------------
Okay, let me wrap this up. The user asked for two things: the area of a circle with radius 5 and then that area divided by the golden ratio.
First, I calculated the area using the formula πr². With r = 5, that's 25π. Using the value of π from the get_constant function (3.141592653589793), the area came out to approximately 78.5398.
Next, I divided that area by the golden ratio, which was retrieved as 1.618033988749895. The result of that division is roughly 48.5403.
So, the final answers are:...
------------------------------------------------------------
AGENT RESPONSE:
The area of a circle with radius 5 is calculated as:
$$
\text{Area} = \pi \times r^2 = 3.141592653589793 \times 25 \approx 78.54
$$
Dividing this area by the golden ratio ($\phi \approx 1.618033988749895$):
$$
\frac{78.5398}{1.618034} \approx 48.54
$$
**Final Answers:**
- **Area of the circle:** $ \boxed{78.54} $
- **Area divided by the golden ratio:** $ \boxed{48.54} $
The thinking field is like having x-ray vision into the model’s decision-making process! This is invaluable for:
Debugging: Understanding why the agent chose specific tools
Trust: Seeing the logical steps builds confidence in results
Education: Learning how to break down complex problems
Error diagnosis: Spotting flawed reasoning before tool execution
Qwen3 and DeepSeek-R1 and similar reasoning models make this explicit, but the principle applies broadly: transparent agent reasoning leads to more reliable and debuggable systems.