Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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:

  1. User sends a message

  2. Model decides if it needs a tool

  3. If yes, model generates tool call(s)

  4. You execute the tool(s)

  5. You send results back to model

  6. 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.