Tools and Function Calling
Function Calling: The Technical Foundation of Agent Tools
Function calling (also called tool use) is the mechanism by which LLMs interact with external tools and systems. It is the technical backbone of AI agents. Understanding how it works at the API level allows you to build custom tools that connect AI to any system.
Function calling was introduced by OpenAI in 2023 and has since been adopted by Anthropic (Claude), Google (Gemini), and most other major AI providers. The API interface is slightly different between providers, but the concept is identical.
How Function Calling Works
Instead of generating free-form text, the model can generate a structured JSON object that describes which function to call and what arguments to pass. Your code then executes that function and returns the result to the model.
The process:
- You define a set of functions (tools) with their names, descriptions, and parameter schemas
- You send a message to the API, including the function definitions
- The model reads the message and the function definitions
- Instead of responding with text, the model responds with a function call: {"name": "get_weather", "arguments": {"city": "Lagos"}}
- Your code receives this, executes the actual get_weather function with the city "Lagos"
- Your code sends the result back to the model
- The model uses the result to generate its final response
Defining Tools with the OpenAI API
// Run in Node.js
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Define the tools
const tools = [
{
type: 'function',
function: {
name: 'get_customer',
description: 'Retrieve customer information by email address. Use when you need to look up account details for a specific customer.',
parameters: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'The customer email address to look up'
},
fields: {
type: 'array',
items: { type: 'string' },
description: 'Optional list of specific fields to return (e.g. ["name", "plan", "created_at"])'
}
},
required: ['email']
}
}
},
{
type: 'function',
function: {
name: 'update_customer',
description: 'Update a customer record. Use only when explicitly instructed to make changes.',
parameters: {
type: 'object',
properties: {
email: { type: 'string', description: 'Customer email (used to identify the record)' },
updates: { type: 'object', description: 'Key-value pairs of fields to update' }
},
required: ['email', 'updates']
}
}
}
];
// Actual function implementations
const functionImplementations = {
get_customer: async ({ email, fields }) => {
// In production: query your actual database
return { name: 'Amara Osei', email, plan: 'pro', created_at: '2024-01-15', status: 'active' };
},
update_customer: async ({ email, updates }) => {
// In production: update your actual database
console.log('Updating', email, 'with', updates);
return { success: true, message: 'Customer updated successfully' };
}
};
async function runAgentWithTools(userMessage) {
const messages = [
{ role: 'system', content: 'You are a helpful customer support assistant. Use the provided tools to look up and manage customer accounts.' },
{ role: 'user', content: userMessage }
];
// First API call -- model may call a tool
let response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
tool_choice: 'auto', // Let model decide when to call tools
});
// Handle tool calls in a loop
while (response.choices[0].finish_reason === 'tool_calls') {
const assistantMessage = response.choices[0].message;
messages.push(assistantMessage);
// Execute each tool call
for (const toolCall of assistantMessage.tool_calls) {
const fnName = toolCall.function.name;
const fnArgs = JSON.parse(toolCall.function.arguments);
const fn = functionImplementations[fnName];
const result = fn ? await fn(fnArgs) : { error: 'Function not found' };
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
});
}
// Second API call -- model processes tool results
response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
tools,
});
}
return response.choices[0].message.content;
}
// Usage
const answer = await runAgentWithTools('What plan is customer amara@example.com on?');
console.log(answer);
Tool Choice Options
The tool_choice parameter controls whether and how the model uses tools:
tool_choice: 'auto' // Model decides when to call tools (most common)
tool_choice: 'required' // Model must call at least one tool
tool_choice: 'none' // Model cannot call tools (use regular text response)
tool_choice: { type: 'function', function: { name: 'specific_tool' } } // Force a specific tool
Designing Good Tool Schemas
The schema you provide for each tool is as important as the implementation. A well-designed schema:
- Has a clear, specific description explaining exactly when to use the tool
- Names parameters clearly -- "customer_email" is better than "email" or "e"
- Includes description for every parameter -- the model reads these to understand what to pass
- Marks required parameters as required
- Uses enums for constrained values -- if a field only accepts "active", "inactive", or "suspended", specify that
// Good schema example
{
name: 'send_notification',
description: 'Send a notification to a user. Use only after confirming the action with the user.',
parameters: {
type: 'object',
properties: {
user_id: { type: 'string', description: 'The unique identifier of the user to notify' },
channel: {
type: 'string',
enum: ['email', 'sms', 'push'],
description: 'Delivery channel for the notification'
},
message: { type: 'string', description: 'The notification message content, max 160 characters' },
priority: {
type: 'string',
enum: ['low', 'normal', 'urgent'],
description: 'Message priority. Use "urgent" only for time-sensitive critical alerts.'
}
},
required: ['user_id', 'channel', 'message']
}
}
Parallel Tool Calls
Modern LLMs can call multiple tools simultaneously when the calls are independent:
// The model might respond with multiple tool calls at once
response.choices[0].message.tool_calls = [
{ id: 'call_1', function: { name: 'get_weather', arguments: '{"city": "Lagos"}' } },
{ id: 'call_2', function: { name: 'get_weather', arguments: '{"city": "Nairobi"}' } },
{ id: 'call_3', function: { name: 'get_weather', arguments: '{"city": "Accra"}' } }
]
// Execute all three in parallel
const results = await Promise.all(
response.choices[0].message.tool_calls.map(async (call) => {
const args = JSON.parse(call.function.arguments);
const result = await functionImplementations[call.function.name](args);
return { tool_call_id: call.id, content: JSON.stringify(result) };
})
);
Parallel tool calls significantly speed up agents that need to gather data from multiple sources.
Security Considerations for Tool Design
Tools give the AI real power to take real actions. Design with security in mind:
Principle of least privilege: Only give the agent access to tools it needs for its specific task. A customer support agent does not need access to tools that modify financial records.
Confirmation for destructive actions: Add a confirmation step before any tool that deletes or irreversibly modifies data.
Input sanitisation: Validate all tool inputs before executing them. The model might occasionally pass unexpected values.
Audit logging: Log every tool call with the inputs used. This creates an audit trail for compliance and debugging.
Rate limiting: Tools that call external APIs should have rate limiting to prevent the agent from accidentally exhausting API quotas.
Key Takeaways
- Function calling is the mechanism that allows LLMs to trigger external tool execution by generating structured JSON instead of text.
- Your code is responsible for actually executing the function and returning results -- the LLM only generates the call specification.
- Tool descriptions and parameter schemas are critical -- write them as carefully as you write the function implementation itself.
- Modern LLMs support parallel tool calls, enabling agents to gather information from multiple sources simultaneously.
- Apply the principle of least privilege: only give agents access to the tools they need for their specific task.
Try it yourself
Key Takeaways
- Function calling allows LLMs to request tool execution by generating structured JSON -- your code executes the function and returns results.
- Tool schemas (descriptions and parameter definitions) are as important as implementations -- the model selects tools based entirely on reading these.
- Parallel tool calls enable agents to gather from multiple sources simultaneously, dramatically reducing execution time.
- Apply the principle of least privilege: grant each agent access only to the specific tools required for its task.
- Audit logging every tool call (inputs, outputs, timestamps) is essential for debugging, security, and compliance.
Quick Quiz
1.When a model uses function calling, what does it actually generate?
2.What does tool_choice: 'required' do in an OpenAI API call?
3.What is the principle of least privilege in the context of agent tool design?
4.What is the benefit of parallel tool calls in an AI agent?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx