Episode 4 — Generative AI Engineering / 4.5 — Generating JSON Responses from LLMs
4.5.c — Function Calling Basics
In one sentence: Function calling (also called tool calling) lets the model return a structured function name and arguments instead of free-form text — it's like JSON mode with a purpose, because the model is saying "call this function with these parameters" rather than just returning data.
Navigation: ← 4.5.b — Schema-Based Prompting · 4.5.d — Validating Returned Structure →
1. What Is Function Calling?
Function calling is a feature where you describe functions (tools) that the model can "call." The model doesn't actually execute any code — instead, it returns a structured message saying which function to call and what arguments to pass. Your application code then executes the function and optionally feeds the result back to the model.
┌─────────────────────────────────────────────────────────────────────────┐
│ FUNCTION CALLING FLOW │
│ │
│ 1. You define functions: get_weather(city, unit) │
│ search_users(query, limit) │
│ │
│ 2. User says: "What's the weather in Paris?" │
│ │
│ 3. Model returns (NOT text, but a tool call): │
│ { │
│ "function": "get_weather", │
│ "arguments": { "city": "Paris", "unit": "celsius" } │
│ } │
│ │
│ 4. YOUR CODE calls the actual get_weather() function │
│ │
│ 5. You send the result back to the model │
│ │
│ 6. Model generates a natural language response: │
│ "It's currently 18°C and partly cloudy in Paris." │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key insight: The model never executes code. It only decides which function to call and generates the arguments. Execution is always in your application.
2. How Function Calling Differs from JSON Mode
Both produce structured output, but they serve different purposes:
| Aspect | JSON Mode | Function Calling |
|---|---|---|
| Purpose | Return structured data | Trigger an action in your system |
| Output | A JSON object (any shape) | Function name + typed arguments |
| Schema | Not enforced (unless Structured Outputs) | Defined via function schemas — strictly typed |
| Response field | message.content (string) | message.tool_calls (array of objects) |
| Model's intent | "Here is the data you asked for" | "Please call this function with these arguments" |
| Multi-action | Single JSON object | Can return multiple tool calls at once |
| Follow-up | No follow-up needed | Send function result back for final response |
When the distinction matters
// JSON mode: "Give me data"
// → Model returns: { "temperature": 18, "condition": "cloudy" }
// → You use the data directly in your UI
// Function calling: "I need to DO something"
// → Model returns: call get_weather("Paris", "celsius")
// → You execute get_weather, get real data from a weather API
// → You send the real data back to the model
// → Model generates a user-friendly response
3. OpenAI Function Calling: The tools Parameter
Here's how to define and use functions with OpenAI:
Step 1: Define your tools
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get the current weather for a city. Call this when the user asks about weather conditions.',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'The city name, e.g., "Paris" or "New York"'
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature unit'
}
},
required: ['city'],
additionalProperties: false
}
}
},
{
type: 'function',
function: {
name: 'search_profiles',
description: 'Search for user profiles matching certain criteria.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for finding profiles'
},
min_age: {
type: 'integer',
description: 'Minimum age filter'
},
max_age: {
type: 'integer',
description: 'Maximum age filter'
},
interests: {
type: 'array',
items: { type: 'string' },
description: 'Filter by interests'
},
limit: {
type: 'integer',
description: 'Maximum number of results to return',
}
},
required: ['query'],
additionalProperties: false
}
}
}
];
Step 2: Make the API call
import OpenAI from 'openai';
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: 'You are a helpful dating app assistant. Use the available tools to help users.'
},
{
role: 'user',
content: 'Find me someone aged 25-35 who likes hiking and cooking'
}
],
tools: tools,
tool_choice: 'auto', // Model decides whether to call a tool
});
Step 3: Handle the response
const message = response.choices[0].message;
// Check if the model wants to call a function
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
console.log('Function:', toolCall.function.name);
console.log('Arguments:', toolCall.function.arguments);
// Function: search_profiles
// Arguments: {"query":"hiking and cooking","min_age":25,"max_age":35,"interests":["hiking","cooking"],"limit":10}
// Parse the arguments (they come as a JSON string)
const args = JSON.parse(toolCall.function.arguments);
console.log(args.query); // "hiking and cooking"
console.log(args.min_age); // 25
console.log(args.interests); // ["hiking", "cooking"]
}
} else {
// Model responded with text (no tool call needed)
console.log(message.content);
}
4. The Response Format with tool_calls
When the model decides to call a function, the response structure differs from a normal text response:
// Normal text response
{
choices: [{
message: {
role: 'assistant',
content: 'Hello! How can I help you today?', // Text in content
tool_calls: null // No tool calls
},
finish_reason: 'stop'
}]
}
// Function calling response
{
choices: [{
message: {
role: 'assistant',
content: null, // No text content!
tool_calls: [ // Tool calls instead
{
id: 'call_abc123', // Unique ID for this call
type: 'function',
function: {
name: 'search_profiles', // Which function
arguments: '{"query":"hiking","min_age":25,"max_age":35}' // JSON string
}
}
]
},
finish_reason: 'tool_calls' // finish_reason is "tool_calls"
}]
}
Key observations
contentisnullwhen the model calls a function — it doesn't generate text.tool_callsis an array — the model can call multiple functions in one response.argumentsis a JSON string — you mustJSON.parse()it.- Each tool call has an
id— you use this ID when sending the result back. finish_reasonis"tool_calls"— not"stop".
5. The Complete Round-Trip (Tool Call + Result)
For the model to generate a final user-facing response, you need to send the function result back:
// Step 1: Initial request
const firstResponse = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a weather assistant.' },
{ role: 'user', content: 'What\'s the weather like in Tokyo?' }
],
tools: [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string' }
},
required: ['city'],
additionalProperties: false
}
}
}],
});
// Step 2: Model returns a tool call
const toolCall = firstResponse.choices[0].message.tool_calls[0];
const args = JSON.parse(toolCall.function.arguments);
// { city: "Tokyo" }
// Step 3: Execute the actual function (your code, not the model!)
async function getWeather(city) {
// In reality, this calls a weather API
return { temperature: 22, condition: 'sunny', humidity: 45 };
}
const weatherData = await getWeather(args.city);
// Step 4: Send the result back to the model
const secondResponse = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a weather assistant.' },
{ role: 'user', content: 'What\'s the weather like in Tokyo?' },
firstResponse.choices[0].message, // Include the assistant's tool call message
{
role: 'tool', // Special "tool" role for function results
tool_call_id: toolCall.id, // Match to the tool call ID
content: JSON.stringify(weatherData) // Function result as string
}
],
tools: [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string' }
},
required: ['city'],
additionalProperties: false
}
}
}],
});
// Step 5: Model generates a user-friendly response
console.log(secondResponse.choices[0].message.content);
// "It's currently 22°C and sunny in Tokyo with 45% humidity. Great weather for being outdoors!"
6. Controlling Tool Usage with tool_choice
You can control whether and which tools the model uses:
// "auto" — model decides (default)
// Use when: you want the model to decide if a tool call is needed
tool_choice: 'auto'
// "none" — model cannot call tools (text only)
// Use when: you want a follow-up that's purely conversational
tool_choice: 'none'
// "required" — model MUST call at least one tool
// Use when: you always want structured output via tool calling
tool_choice: 'required'
// Specific function — model MUST call this specific function
// Use when: you know which function should be called
tool_choice: { type: 'function', function: { name: 'search_profiles' } }
Practical example: Forcing structured output via tool calling
// Use tool_choice to FORCE the model to return structured data
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'user', content: 'Analyze this profile: Maya, 27, loves hiking and coffee' }
],
tools: [{
type: 'function',
function: {
name: 'analyze_profile',
description: 'Return a structured analysis of a dating profile',
parameters: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
interests: { type: 'array', items: { type: 'string' } },
personality_summary: { type: 'string' },
compatibility_score: { type: 'integer', description: '0-100' }
},
required: ['name', 'age', 'interests', 'personality_summary', 'compatibility_score'],
additionalProperties: false
}
}
}],
// Force the model to call analyze_profile
tool_choice: { type: 'function', function: { name: 'analyze_profile' } }
});
// The model MUST return structured data matching the schema
const args = JSON.parse(response.choices[0].message.tool_calls[0].function.arguments);
console.log(args.compatibility_score); // 78
console.log(args.interests); // ["hiking", "coffee"]
This is a powerful pattern: use function calling as a structured output mechanism even when you don't need a "real" function. The schema in the tool definition enforces structure more reliably than JSON mode alone.
7. Multiple Tool Calls in One Response
The model can call multiple functions simultaneously:
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get weather for a city',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
additionalProperties: false
}
}
},
{
type: 'function',
function: {
name: 'get_local_events',
description: 'Get events happening in a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string' },
date: { type: 'string', description: 'ISO date string' }
},
required: ['city'],
additionalProperties: false
}
}
}
];
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'user', content: 'Plan my Saturday in Paris — what\'s the weather and what events are happening?' }
],
tools: tools,
});
// Model may return TWO tool calls:
const toolCalls = response.choices[0].message.tool_calls;
console.log(toolCalls.length); // 2
// toolCalls[0]: get_weather({ city: "Paris" })
// toolCalls[1]: get_local_events({ city: "Paris", date: "2025-04-12" })
8. Function Calling with Anthropic (Claude)
Anthropic uses the term "tool use" instead of "function calling," but the concept is the same:
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
tools: [
{
name: 'get_weather',
description: 'Get current weather for a city',
input_schema: { // "input_schema" not "parameters"
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] }
},
required: ['city']
}
}
],
messages: [
{ role: 'user', content: 'What\'s the weather in London?' }
],
});
// Claude's response format is different from OpenAI's
for (const block of response.content) {
if (block.type === 'tool_use') {
console.log('Tool:', block.name); // "get_weather"
console.log('Args:', block.input); // { city: "London", unit: "celsius" }
console.log('ID:', block.id); // "toolu_abc123"
}
}
Key differences from OpenAI
| Aspect | OpenAI | Anthropic (Claude) |
|---|---|---|
| Parameter name | tools[].function.parameters | tools[].input_schema |
| Response location | message.tool_calls | message.content (mixed with text blocks) |
| Arguments | JSON.parse(toolCall.function.arguments) (string) | block.input (already parsed object) |
| Tool call ID | toolCall.id | block.id |
| Forcing a tool | tool_choice: { type: "function", function: { name: "..." } } | tool_choice: { type: "tool", name: "..." } |
9. When to Use Function Calling vs JSON Mode
┌────────────────────────────────────────────────────────────────┐
│ DECISION GUIDE │
│ │
│ Do you need to EXECUTE ACTIONS (API calls, DB queries)? │
│ ├── YES → Function Calling │
│ │ (model picks function + args, you execute) │
│ └── NO │
│ ├── Do you need STRICT schema enforcement? │
│ │ ├── YES → Structured Outputs (json_schema) │
│ │ │ or Function Calling with forced tool_choice │
│ │ └── NO → JSON Mode (json_object) + schema in prompt │
│ └── Is the output structure SIMPLE (flat, few fields)? │
│ ├── YES → JSON Mode is fine │
│ └── NO → Consider Function Calling for type safety │
└────────────────────────────────────────────────────────────────┘
Summary table
| Scenario | Best Approach | Why |
|---|---|---|
| Extract data from text → display in UI | JSON mode + schema prompt | Simple one-way data extraction |
| Model needs to search a database | Function calling | Model triggers a real action |
| Strict type contract with backend | Structured Outputs | Schema enforced at API level |
| Model decides between multiple actions | Function calling with multiple tools | Model picks the right tool |
| Quick prototype, simple JSON | JSON mode | Lowest setup overhead |
| Complex nested objects, type safety | Function calling (forced) | Schema validation built in |
10. Brief Preview: Detailed Coverage in 4.7
This section covers function calling basics — enough to understand how it fits into the JSON generation story. For the complete picture, including:
- Multi-turn tool use conversations
- Error handling in tool execution
- Chaining multiple tool calls
- Building agent-like behavior with tools
- Parallel vs sequential tool calls
- Security considerations
See 4.7 — Function Calling / Tool Calling for deep coverage.
11. Key Takeaways
- Function calling lets the model return structured function names and arguments — it never executes code itself.
- The response comes in
message.tool_calls(notmessage.content), withfinish_reason: "tool_calls". - Arguments are a JSON string in OpenAI's API — always
JSON.parse()them. - Use
tool_choiceto control whether the model must call a tool ("required"), may call one ("auto"), or cannot ("none"). - Function calling as structured output — force a tool call to get schema-validated JSON without needing a real function.
- The model can return multiple tool calls in a single response for parallel execution.
- Anthropic uses
input_schemaand returns tool use blocks inmessage.content— different format, same concept. - For detailed function calling patterns, see Section 4.7.
Explain-It Challenge
- A colleague asks: "Why would I use function calling when JSON mode already gives me structured output?" Explain the key difference with an example.
- In the complete round-trip flow (user question → tool call → function result → final response), why does the model need to see the function result? Why can't you just show the result to the user directly?
- When would you use
tool_choice: "required"even though you're not planning to execute a real function?
Navigation: ← 4.5.b — Schema-Based Prompting · 4.5.d — Validating Returned Structure →