Tools & Function Calling

30 min read Module 3 of 12 Topic 3 of 12

What you'll learn

  • Define tools with JSON schemas the LLM can understand
  • Handle the tool-use loop: call → execute → return result → continue
  • Build a complete agent that uses multiple tools
  • Understand parallel tool calls for efficiency
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

Tools Are What Make Agents Agents

Without tools, an LLM is a text transformer. It reads input, thinks, and produces output, all in text. It can’t actually do anything in the world.

Tools change this. A tool is simply a Python function that the LLM can ask your code to execute. The LLM describes what it wants to call and what arguments to use. Your code actually runs the function and returns the result. The LLM then uses that result to continue its reasoning.

This is the mechanism behind everything you’ve seen: “search the web, ” “run this code, ” “check this price, ” “send this email”, all tools.


How Tools Work: The Complete Loop

Understanding this loop precisely is essential. Let’s trace it step by step:

  1. You define tools as Python functions + JSON schema descriptions
  2. You send the tools to the LLM along with the user’s message
  3. The LLM decides whether to call a tool or respond directly
  4. If it calls a tool: the LLM returns a structured tool_call (NOT the actual result, it can’t run code)
  5. Your code reads the tool_call, finds the right function, runs it, gets the result
  6. You send the result back to the LLM as a tool message
  7. The LLM continues: reads the result and either calls another tool or produces the final response

This loop repeats until the LLM produces a response without a tool call. Each iteration is one tool use.


Defining Tools with JSON Schema

OpenAI’s function calling format uses JSON Schema to describe tools. Here’s the schema for a web search tool:

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "Search the web for current information on a topic. Use when you need facts, news, or data not in your training.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query. Be specific: '2024 Python AI frameworks comparison' is better than 'AI tools'."
                    },
                    "num_results": {
                        "type": "integer",
                        "description": "Number of results to return. Default 5, max 10.",
                        "default": 5
                    }
                },
                "required": ["query"]
            }
        }
    }
]

The description fields are read by the LLM, it uses them to understand when to use the tool and what to put in each parameter. Invest time in writing clear descriptions; they’re the most important thing for tool call accuracy.


The Actual Python Functions

The schema is just the description. You also need the real Python functions that do the work:

import requests

def search_web(query: str, num_results: int = 5) -> list[dict]:
    """Real web search using the Serper API."""
    response = requests.post(
        'https://google.serper.dev/search',
        headers={'X-API-KEY': 'your-serper-api-key'},
        json={'q': query, 'num': num_results}
    )
    results = response.json().get('organic', [])
    return [
        {'title': r['title'], 'url': r['link'], 'snippet': r['snippet']}
        for r in results[:num_results]
    ]

def get_current_time() -> str:
    """Return the current date and time."""
    from datetime import datetime
    return datetime.now().strftime('%Y-%m-%d %H:%M UTC')

Keep a registry that maps tool names to functions:

TOOL_REGISTRY = {
    'search_web':        search_web,
    'get_current_time':  get_current_time,
}

The Tool-Use Loop in Code

Now let’s build the complete loop:

from openai import OpenAI
import json

client = OpenAI()

def run_agent(user_message: str) -> str:
    """Run an agent that can use tools."""
    messages = [
        {'role': 'system', 'content': 'You are a helpful research assistant.'},
        {'role': 'user',   'content': user_message},
    ]
    
    # Keep looping until the LLM stops calling tools
    while True:
        response = client.chat.completions.create(
            model='gpt-5.6-sol',
            messages=messages,
            tools=tools,           # send the tool definitions
            tool_choice='auto',    # let the model decide when to use tools
        )
        
        msg = response.choices[0].message
        messages.append(msg)   # always add the assistant's message to history

Now handle the two cases: tool call or final response:

        if msg.tool_calls:
            # Model wants to use one or more tools
            for tool_call in msg.tool_calls:
                name = tool_call.function.name
                args = json.loads(tool_call.function.arguments)
                
                print(f"→ Calling tool: {name}({args})")
                
                # Execute the real function
                func   = TOOL_REGISTRY[name]
                result = func(**args)
                
                # Add the result back as a tool message
                messages.append({
                    'role':         'tool',
                    'tool_call_id': tool_call.id,
                    'content':      json.dumps(result),
                })
        else:
            # No tool calls: this is the final response
            return msg.content

Test it:

answer = run_agent("What is the current date and time, and what are the latest developments in AI agents?")
print(answer)

The agent will call get_current_time() and search_web(), incorporate both results, and produce a comprehensive answer.


Parallel Tool Calls

OpenAI’s API (and others) support parallel tool calls: the LLM can request multiple tools in a single response when they’re independent:

# The model might produce this single response:
# tool_calls = [
#   ToolCall(name='search_web', args={'query': 'AI agents 2024'}),
#   ToolCall(name='search_web', args={'query': 'LangGraph framework'}),
#   ToolCall(name='get_current_time', args={}),
# ]

Your loop already handles this: for tool_call in msg.tool_calls iterates over all of them. To run them in parallel (important for performance):

import concurrent.futures

if msg.tool_calls:
    def execute_tool(tc):
        name   = tc.function.name
        args   = json.loads(tc.function.arguments)
        result = TOOL_REGISTRY[name](**args)
        return tc.id, json.dumps(result)
    
    with concurrent.futures.ThreadPoolExecutor() as executor:
        results = list(executor.map(execute_tool, msg.tool_calls))
    
    for tool_call_id, result in results:
        messages.append({
            'role':         'tool',
            'tool_call_id': tool_call_id,
            'content':      result,
        })

For 3 independent tool calls that each take 1 second, sequential execution takes 3 seconds; parallel execution takes ~1 second. At scale, this difference is enormous.

Exercise: Add a third tool to the agent: read_webpage(url: str) -> str that fetches the text content of a URL. Write the schema definition and the Python function (use requests.get and BeautifulSoup to extract text). Update the agent system prompt to mention this capability. Test by asking the agent to research a specific company and summarise their website.

Knowledge Check

3 questions to test your understanding

1 In the tool-use loop, what happens after the LLM produces a tool_call?

2 Why must a tool schema include clear descriptions of each parameter?

3 What is the benefit of parallel tool calls?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan