Function Calling
Function/tool calling patterns for connecting LLMs to external APIs and data sources
You are an expert in function/tool calling patterns for connecting LLMs to external APIs, databases, and system operations. ## Key Points 1. You define tools with names, descriptions, and JSON Schema parameters. 2. The model analyzes the user's request and decides whether to call a tool. 3. The model outputs a tool call with structured arguments (not free text). 4. Your code executes the function and sends results back. 5. The model incorporates the result into its final response. - Write precise tool descriptions; the model relies on them to decide which tool to call and how. - Use `required` in JSON Schema to mark mandatory parameters; optional ones should have clear defaults. - Return structured JSON from tools, not free-form strings; it helps the model interpret results. - Implement a tool execution loop (not just one round) so the model can chain multiple tool calls. - Validate tool arguments server-side before executing; the model can produce invalid values. - Return helpful error messages from tools so the model can recover or explain the failure. - Use `tool_choice` to constrain behavior when you know exactly which tool should be called.
skilldb get llm-integration-skills/Function CallingFull skill: 319 linesFunction Calling — LLM Integration
You are an expert in function/tool calling patterns for connecting LLMs to external APIs, databases, and system operations.
Overview
Function calling (also called tool use) allows LLMs to invoke structured functions rather than producing free-form text. The model receives function definitions with typed parameters, decides when to call them, generates the arguments as structured JSON, and then your code executes the function and returns results. This bridges the gap between natural language understanding and deterministic API calls.
Core Concepts
How Function Calling Works
- You define tools with names, descriptions, and JSON Schema parameters.
- The model analyzes the user's request and decides whether to call a tool.
- The model outputs a tool call with structured arguments (not free text).
- Your code executes the function and sends results back.
- The model incorporates the result into its final response.
OpenAI Function Calling
import OpenAI from "openai";
const openai = new OpenAI();
const tools: OpenAI.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "lookup_order",
description: "Look up an order by its ID and return the order details.",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "The order ID, e.g., ORD-12345",
},
},
required: ["order_id"],
},
},
},
{
type: "function",
function: {
name: "cancel_order",
description: "Cancel an existing order. Only works for orders not yet shipped.",
parameters: {
type: "object",
properties: {
order_id: { type: "string" },
reason: { type: "string", description: "Reason for cancellation" },
},
required: ["order_id"],
},
},
},
];
async function handleChat(userMessage: string) {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: "You are a customer support agent." },
{ role: "user", content: userMessage },
];
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
});
const choice = response.choices[0];
if (choice.finish_reason === "tool_calls" && choice.message.tool_calls) {
messages.push(choice.message);
for (const toolCall of choice.message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const result = await executeTool(toolCall.function.name, args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
// Get the final response incorporating tool results
const finalResponse = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
});
return finalResponse.choices[0].message.content;
}
return choice.message.content;
}
Anthropic Tool Use
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "search_products",
description: "Search the product catalog by keyword, category, or price range.",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Search keywords" },
category: { type: "string", enum: ["electronics", "clothing", "books"] },
max_price: { type: "number", description: "Maximum price in USD" },
},
required: ["query"],
},
},
];
async function chat(userMessage: string) {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage },
];
let response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools,
messages,
});
while (response.stop_reason === "tool_use") {
const toolBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
);
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = toolBlocks.map((block) => ({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(executeTool(block.name, block.input)),
}));
messages.push({ role: "user", content: toolResults });
response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
tools,
messages,
});
}
return response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
}
Implementation Patterns
Tool Executor with Validation
type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
class ToolRegistry {
private handlers = new Map<string, ToolHandler>();
register(name: string, handler: ToolHandler) {
this.handlers.set(name, handler);
}
async execute(name: string, args: Record<string, unknown>): Promise<string> {
const handler = this.handlers.get(name);
if (!handler) {
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
try {
const result = await handler(args);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({ error: error.message });
}
}
}
const registry = new ToolRegistry();
registry.register("lookup_order", async (args) => {
const orderId = args.order_id as string;
return db.orders.findById(orderId);
});
registry.register("cancel_order", async (args) => {
const orderId = args.order_id as string;
return db.orders.cancel(orderId, args.reason as string);
});
Parallel Tool Calls
OpenAI can request multiple tool calls in a single response:
if (choice.message.tool_calls && choice.message.tool_calls.length > 1) {
// Execute tools in parallel
const results = await Promise.all(
choice.message.tool_calls.map(async (toolCall) => {
const args = JSON.parse(toolCall.function.arguments);
const result = await registry.execute(toolCall.function.name, args);
return {
role: "tool" as const,
tool_call_id: toolCall.id,
content: result,
};
})
);
messages.push(choice.message);
messages.push(...results);
}
Guarding Dangerous Operations
const DANGEROUS_TOOLS = new Set(["cancel_order", "delete_account", "send_email"]);
async function executeWithConfirmation(
name: string,
args: Record<string, unknown>,
confirmFn: (action: string) => Promise<boolean>
): Promise<string> {
if (DANGEROUS_TOOLS.has(name)) {
const confirmed = await confirmFn(
`Allow ${name} with args: ${JSON.stringify(args)}?`
);
if (!confirmed) {
return JSON.stringify({ error: "Action was not confirmed by the user." });
}
}
return registry.execute(name, args);
}
Forcing or Preventing Tool Use
// Force the model to use a specific tool
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: { type: "function", function: { name: "lookup_order" } },
});
// Prevent any tool use (text-only response)
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "none",
});
Best Practices
- Write precise tool descriptions; the model relies on them to decide which tool to call and how.
- Use
requiredin JSON Schema to mark mandatory parameters; optional ones should have clear defaults. - Return structured JSON from tools, not free-form strings; it helps the model interpret results.
- Implement a tool execution loop (not just one round) so the model can chain multiple tool calls.
- Validate tool arguments server-side before executing; the model can produce invalid values.
- Return helpful error messages from tools so the model can recover or explain the failure.
- Use
tool_choiceto constrain behavior when you know exactly which tool should be called. - Keep tool count manageable (under 20); too many tools degrade selection accuracy.
Core Philosophy
Function calling transforms LLMs from text generators into decision-making agents that can interact with the real world through structured interfaces. The model does not execute functions -- it decides which function to call and generates typed arguments. Your code executes the function and returns the result. This separation of concerns is fundamental: the model handles natural language understanding and intent mapping; your code handles execution, validation, and side effects.
Tool descriptions are the interface contract between the model and your system. The model has no access to your implementation; it relies entirely on the tool name, description, and parameter schema to decide when and how to use it. Vague descriptions produce vague tool use. Precise descriptions that include what the tool does, when it should be used, what its parameters mean, and what it returns enable the model to make good decisions consistently.
Every function calling system needs a tool execution loop, not just a single round-trip. Many real-world tasks require the model to gather information from one tool before it can decide what arguments to pass to another. A single-round implementation forces the model to accomplish everything in one tool call, which either fails or requires the user to orchestrate multi-step interactions manually. The loop should continue until the model returns a text response (indicating it has finished) or a maximum iteration limit is reached.
Anti-Patterns
-
No maximum iteration limit on the tool loop: Allowing the model to call tools indefinitely without a cap. If the model enters a reasoning loop or repeatedly calls the same tool with the same arguments, the loop never terminates. Always set a maximum number of iterations.
-
Trusting model-generated arguments without validation: Executing tool calls with the arguments exactly as the model produced them, without server-side validation. The model can generate invalid types, out-of-range values, or arguments that do not match required constraints. Validate before executing.
-
Vague or missing tool descriptions: Providing tool names and schemas without meaningful descriptions, forcing the model to guess when and how to use them. The description is the primary signal the model uses for tool selection; it must clearly state the tool's purpose and appropriate use cases.
-
Returning raw error stack traces as tool results: When a tool execution fails, passing the full Python or Node.js stack trace back to the model. Stack traces waste tokens, confuse the model, and may leak internal system details. Return a concise, structured error message instead.
-
Exposing dangerous operations without confirmation gates: Allowing the model to execute destructive actions (delete records, send emails, process payments) without a human-in-the-loop confirmation step. Function calling should include safeguards proportional to the severity of the operation.
Common Pitfalls
- Not parsing
tool_calls.function.argumentsas JSON; it is a string, not an object. - Forgetting to include the assistant's tool call message before the tool results in the messages array.
- Not handling the case where the model calls a tool that does not exist in your registry.
- Assuming the model will always call a tool when one is available; it may respond with text instead.
- Leaking sensitive data through tool results (e.g., returning full database records with internal fields).
- Not implementing a maximum iteration limit on the tool loop, risking infinite cycles.
- Using overly vague tool descriptions like "do stuff", making the model guess when to use them.
- Passing raw exception stack traces as tool results; they waste tokens and confuse the model.
Install this skill directly: skilldb add llm-integration-skills
Related Skills
Anthropic API
Anthropic Claude API integration for messages, streaming, and tool use
Embeddings
Text embeddings and semantic search with vector databases for LLM applications
Langchain
LangChain orchestration for chains, agents, memory, and retrieval workflows
Openai API
OpenAI API integration patterns for chat completions, embeddings, and assistants
Rag Pipeline
Building retrieval-augmented generation pipelines with document ingestion, retrieval, and synthesis
Streaming
Streaming LLM responses with SSE, WebSockets, and backpressure handling