Tool Use & Function Calling Mechanics in Claude

9 min readYaseen Khatib · MERN + AI Architect

Ask Claude for today's order count and a naive integration gets back a confident, specific, completely invented number. The model has no database — it has a probability distribution over plausible-sounding answers. Tool use is how you close that gap: instead of hallucinating, the model pauses and asks your runtime to go get the real value. Get the request/result loop right and the same mechanism turns a chat model into an agent that can read databases, hit APIs, and run code.

Core Architectural Concepts & Trade-offs

Tool use is a turn-based negotiation, not a callback. You send the model a list of tools — each a name, a description, and a JSON Schema for its inputs. When the model decides it needs one, it doesn't execute anything; it ends its turn with stop_reason: "tool_use" and emits one or more tool_use blocks naming the tool and its arguments. Your code runs the actual function, then sends the output back as a tool_result block referencing the call's id. The model picks up where it left off. The loop continues until a turn comes back without a tool request.

The schema and the description are the entire interface, and they do more work than developers expect. The model chooses tools and fills arguments based almost entirely on the natural-language description — a vague one produces wrong tool selection and malformed inputs no amount of prompting fixes. Treat tool descriptions as the most load-bearing prose in your system: state what the tool does, when to use it, what each parameter means, and what it returns. The JSON Schema constrains shape; the description drives the decision.

Claude can request multiple tools in a single turn, and that's a performance lever, not a curiosity. When the model needs the weather in three cities, it emits three tool_use blocks at once — you run them concurrently with Promise.all and return all results together, collapsing three sequential round-trips into one. Designing tools to be independent and parallel-safe is how you keep agent latency tolerable as the toolset grows.

The trade-offs are real. Every tool definition lives in the context window (Lesson 1) and is re-sent each turn, so a sprawling toolset is a standing token tax — cache the definitions (Lesson 9) and prune what the model doesn't need. And tools are an execution surface: a run_sql tool with raw query access is a SQL-injection vector driven by a probabilistic caller. Constrain at the boundary — parameterize, allowlist, scope credentials — because the model is an untrusted planner, not a trusted executor.

The Agent Loop

The whole pattern is one loop: call, check the stop reason, execute any requested tools in parallel, feed results back, repeat.

agent-loop.ts
// The model plans; your runtime executes. Loop until it stops asking.
import Anthropic from "@anthropic-ai/sdk"
import type { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages"

const anthropic = new Anthropic()

const tools: Tool[] = [{
  name: "get_order_count",
  description: "Count orders for a given ISO date (YYYY-MM-DD). Use for any "
    + "question about how many orders were placed on a specific day.",
  input_schema: {
    type: "object",
    properties: { date: { type: "string", description: "ISO date" } },
    required: ["date"],
  },
}]

// Real implementations — parameterized, never string-built SQL.
const handlers: Record<string, (input: any) => Promise<unknown>> = {
  get_order_count: ({ date }) => db.orders.countByDate(date),
}

export async function runAgent(prompt: string) {
  const messages: MessageParam[] = [{ role: "user", content: prompt }]

  while (true) {
    const res = await anthropic.messages.create({
      model: "claude-opus-4-8",
      max_tokens: 1024,
      tools,
      messages,
    })
    messages.push({ role: "assistant", content: res.content })

    if (res.stop_reason !== "tool_use") return res   // model is done

    const calls = res.content.filter((b) => b.type === "tool_use")
    // Independent tools run concurrently — N round-trips collapse to one.
    const results = await Promise.all(calls.map(async (c: any) => ({
      type: "tool_result" as const,
      tool_use_id: c.id,
      content: JSON.stringify(await handlers[c.name](c.input)),
    })))
    messages.push({ role: "user", content: results })
  }
}

Every branch matters: appending the assistant turn before answering keeps the transcript valid, matching tool_use_id wires each result to its request, and the stop_reason check is the only thing ending the loop.

One Turn of the Loop

Claudeplanner (untrusted)Your Runtimeexecutor (trusted)stop_reason: tool_use → { date }db.countByDate()parameterized · scopedtool_result: { count: 1432 }final answerloop repeats until a turn has no tool_use block
Request → execute in your runtime → result → answer. The model plans; it never executes.

With the loop understood, the next question is how to expose tools to many agents without re-wiring each one — the Model Context Protocol. Or see the full roadmap.

Looking to architect a similar system?

Let's ship it at AI-speed.

Start a conversation →