Skip to main content

LLM Function Calling and Tool Use: A Developer Guide

June 2, 2026

When an LLM generates text, it is predicting the most likely next token. When an LLM uses a function call (also called "tool use"), it is making a structured decision: "to answer this request, I need to call this specific function with these specific arguments." The result comes back, and the model continues reasoning with real data.

This transforms an LLM from a text generator into an agent that can search databases, call APIs, execute computations, and interact with external systems — all within the same reasoning session.

Function calling is the foundational primitive for building AI-powered features. This post explains how it works, how to implement it with the Anthropic and OpenAI APIs, and how to handle the agentic loop correctly.


How Function Calling Works

The flow has four steps that repeat until the model has enough information to give a final answer:

FUNCTION CALLING LOOP:
┌──────────────┐
  User Query  
└──────┬───────┘
       
       
┌──────────────┐    "I need to call         ┌─────────────────┐
│     LLM      ├──── search_database ──────►│   Your Code     │
│  (Reasoning) │    with {query: 'X'}       │  (Tool Executor)│
└──────────────┘                            └────────┬────────┘
       ▲                                             │
       │   Returns database results                 │
       └─────────────────────────────────────────────┘


┌──────────────┐
│  Final Answer│  (LLM has the data it needed)
└──────────────┘

The critical insight: the LLM never executes the function. It decides what to call and with what arguments. Your code executes the actual function and returns the result back to the model.


Defining Tools (Anthropic Claude)

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

// Define the tools the model can call
const tools: Anthropic.Tool[] = [
  {
    name: 'search_products',
    description: 'Search the product catalog. Use when the user asks about product availability, pricing, or specifications.',
    input_schema: {
      type: 'object' as const,
      properties: {
        query: {
          type: 'string',
          description: 'The search query. Be specific.',
        },
        category: {
          type: 'string',
          enum: ['electronics', 'clothing', 'books', 'all'],
          description: 'Product category to filter by.',
        },
        maxPrice: {
          type: 'number',
          description: 'Maximum price filter in USD.',
        },
      },
      required: ['query'],
    },
  },
  {
    name: 'get_order_status',
    description: 'Get the current status of a customer order. Requires an order ID.',
    input_schema: {
      type: 'object' as const,
      properties: {
        orderId: {
          type: 'string',
          description: 'The order ID (format: ORD-XXXXXXXX)',
        },
      },
      required: ['orderId'],
    },
  },
  {
    name: 'create_support_ticket',
    description: 'Create a customer support ticket. Use when the user has an issue that requires human review.',
    input_schema: {
      type: 'object' as const,
      properties: {
        subject: { type: 'string' },
        description: { type: 'string' },
        priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
      },
      required: ['subject', 'description', 'priority'],
    },
  },
];

Implementing the Agentic Loop

The agentic loop runs until the model produces a final text response (no more tool calls):

// lib/tool-executor.ts
type ToolInput = Record<string, unknown>;

async function executeToolCall(toolName: string, toolInput: ToolInput): Promise<string> {
  switch (toolName) {
    case 'search_products': {
      const { query, category = 'all', maxPrice } = toolInput as {
        query: string;
        category?: string;
        maxPrice?: number;
      };
      const products = await db.query(
        `SELECT id, name, price, category FROM products
         WHERE name ILIKE $1
         AND ($2 = 'all' OR category = $2)
         AND ($3 IS NULL OR price <= $3)
         LIMIT 5`,
        [`%${query}%`, category, maxPrice ?? null]
      );
      return JSON.stringify(products.rows);
    }

    case 'get_order_status': {
      const { orderId }= toolInput as { orderId: string };
      const order= await db.query(
        'SELECT id, status, estimated_delivery, items FROM orders WHERE id= $1',
        [orderId]
      );
      if (order.rows.length= 0) return JSON.stringify({ error: 'Order not found' });
      return JSON.stringify(order.rows[0]);
    }

    case 'create_support_ticket': {
      const { subject, description, priority }= toolInput as {
        subject: string;
        description: string;
        priority: string;
      };
      const ticket= await createTicketInHelpDesk({ subject, description, priority });
      return JSON.stringify({ ticketId: ticket.id, message: 'Ticket created successfully' });
    }

    default:
      return JSON.stringify({ error: `Unknown tool: ${toolName}` });
  }
}

// The complete agentic loop
async function runAgentWithTools(userMessage: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userMessage },
  ];

  let iterationCount = 0;
  const MAX_ITERATIONS = 10; // Prevent infinite loops

  while (iterationCount < MAX_ITERATIONS) {
    iterationCount++;

    const response= await client.messages.create({
      model: 'claude-sonnet-4-5',
      max_tokens: 4096,
      system: 'You are a helpful e-commerce assistant. Use the available tools to answer customer questions accurately.',
      messages,
      tools,
    });

    // If the model is done, return the text response
    if (response.stop_reason= 'end_turn') {
      const textBlock= response.content.find(b=> b.type= 'text') as Anthropic.TextBlock;
      return textBlock?.text ?? 'No response generated.';
    }

    // Handle tool use
    if (response.stop_reason= 'tool_use') {
      // Add the assistant's tool call to the conversation
      messages.push({ role: 'assistant', content: response.content });

      // Execute all tool calls (the model can call multiple at once)
      const toolResults: Anthropic.ToolResultBlockParam[]= [];

      for (const block of response.content) {
        if (block.type = 'tool_use') continue;

        const result= await executeToolCall(block.name, block.input as ToolInput);
        toolResults.push({
          type: 'tool_result',
          tool_use_id: block.id,
          content: result,
        });
      }

      // Add tool results to conversation
      messages.push({ role: 'user', content: toolResults });
    }
  }

  return 'Maximum iterations reached. Please try again with a simpler request.';
}

Real-World Example

// Usage in an API route
export async function POST(request: Request) {
  const { message } = await request.json();

  const response = await runAgentWithTools(message);

  return NextResponse.json({ response });
}

// Test the pipeline
const result = await runAgentWithTools(
  "I ordered something last week with order ID ORD-12345678 — where is it?"
);

// The agent will:
// 1. Call get_order_status({ orderId: 'ORD-12345678' })
// 2. Receive order data from your database
// 3. Respond with the current status and estimated delivery date
// in natural language — without you writing any of that routing logic.

Parallel Tool Calls

Claude and GPT-4 can call multiple tools in a single response when the calls are independent:

// If the user asks: "Search for laptops under $1000 and check my order ORD-999"
// The model may return BOTH tool calls simultaneously:
response.content = [
  { type: 'tool_use', name: 'search_products', input: { query: 'laptop', maxPrice: 1000 } },
  { type: 'tool_use', name: 'get_order_status', input: { orderId: 'ORD-999' } },
];

// Execute both in parallel — this halves latency
const [searchResult, orderResult] = await Promise.all([
  executeToolCall('search_products', { query: 'laptop', maxPrice: 1000 }),
  executeToolCall('get_order_status', { orderId: 'ORD-999' }),
]);

Safety Guardrails

Always validate tool inputs before executing them:

import { z } from 'zod';

const SearchSchema = z.object({
  query: z.string().min(1).max(200),
  category: z.enum(['electronics', 'clothing', 'books', 'all']).optional().default('all'),
  maxPrice: z.number().positive().max(100000).optional(),
});

async function executeToolCall(toolName: string, toolInput: unknown): Promise<string> {
  if (toolName === 'search_products') {
    // Validate before executing — never trust LLM output blindly
    const parsed = SearchSchema.safeParse(toolInput);
    if (!parsed.success) {
      return JSON.stringify({ error: 'Invalid search parameters', details: parsed.error.issues });
    }
    // Safe to execute with parsed.data
  }
}

Conclusion

Function calling is what separates a chatbot from an agent. Once your LLM can call real functions — searching your database, querying APIs, creating records — it becomes a capable collaborator rather than a text prediction machine. The agentic loop is simple to implement correctly when you understand the pattern: let the model decide what to call, you execute the call, feed results back, repeat until done. The safety discipline is equally simple: validate every tool input before executing it. Together, these two practices give you powerful, reliable AI-integrated features.

Recommended Posts