Merit ACClaude architecture
Guide · architecture

Claude tool use and function calling architecture

Tool use lets Claude act as well as talk, and the mechanism is simple. A reliable tool integration differs from a confusing one almost entirely in design: how the tools are described, how failure is reported back, and how authority is enforced independently of what the model asks for. This guide stays at that design level.

1. The mechanism

Tool use has four parts. You describe the available tools to Claude as named, typed definitions: what each does and what parameters it accepts. Given a task and the conversation so far, the model decides whether a tool is relevant and, if so, requests a call to a specific tool with argument values it chooses. Your code executes that call against the target system. The result returns to the model in the next turn, and the model uses it in whatever it says or does next.

The message format varies across APIs and versions. The stable shape matches the companion guide on the agentic loop: a model turn requesting a named tool with structured input, and a following turn supplying the result correlated to that request. Nothing here depends on specific field names. What holds regardless is that the model never touches the outside world. It only asks; your code decides whether and how to act.

Most of this guide follows from that. If the model only asks, then whether the ask is well-specified, whether it's allowed, and what happens when it fails are all things your system designs.

2. Descriptions as a design surface

A tool's description is easy to treat as a formality, a sentence written quickly and rarely revisited. It's the tool's entire interface to the model: the only information about when the tool applies, what its parameters mean, and how it differs from similar tools. A vague description produces a confident, well-formed call to the wrong tool, or to the right tool with wrong arguments, because the model fills the gap with its best guess.

This is the same discipline as writing a docstring for a function a colleague will call without reading the implementation. Given def process(data, mode) and nothing else, a colleague will guess what mode accepts and sometimes get it wrong, because the interface didn't say enough. A model facing an underspecified tool is in the same spot, except it can't ask a follow-up question. It must commit to an argument value based on whatever the description and conversation implied.

The failure is most common when two tools are similar but differ in scope, which happens once a tool set grows past a handful. A model easily conflates search_customers(query) and search_orders(query) if both descriptions say "searches for records matching a query," because neither tells it which applies when a user asks about "the customer's recent purchase." Say what the tool searches, what the query matches against (name and email, versus order ID and product name), and what it does not cover. That gives the model what it needs to choose correctly, and gives a human reviewing the call from a log the same information.

Write a tool description like a docstring for a careful, literal-minded developer who can't see your code or ask you a question mid-task: what the tool does, what each parameter means and accepts, what it returns, and (often the most important part) what it does not do, especially compared with any similarly named or scoped tool nearby.

3. Parallel vs. sequential calls

Some tasks need several independent tool calls, none of which needs another's result. Others need a dependency chain, where one call's output is the next call's input. Treating one case as the other costs you either way.

Serializing independent calls (waiting for each result before issuing the next when nothing depends on it) costs latency for no benefit. To answer "which of these three cities has the best forecast this weekend," you don't need city two's weather before requesting city three's. A harness that issues several tool calls from one model turn and runs them concurrently answers in roughly the time of one call instead of three.

Parallelizing calls that have a dependency costs correctness, and it's the more dangerous mistake. If step two needs step one's output (look up a customer's account ID, then fetch their order history with it), running both at once because the harness defaults to parallel means step two runs with a missing or stale argument. This is the default failure of any system that treats "the model requested two tool calls in one turn" as permission to run them concurrently without checking whether one's input is the other's output.

So the harness needs a way to tell the cases apart; neither the model nor a blanket policy can do it. Sometimes it's structural: if a call's arguments reference nothing from another pending call's result, it's likely independent. Sometimes it has to be explicit: a tool definition can expose a dependency by requiring, as an argument, the kind of value another tool outputs. An order-lookup tool that requires a customer_id makes its dependency on a customer-lookup tool obvious from the type signature.

4. Error handling as part of the tool contract

What a tool returns on failure matters as much as what it returns on success, and it's the part most often left as an afterthought: write the happy path carefully, let failures throw whatever the library throws. The model pays for that shortcut, because it has to decide what to do next with only an opaque error.

An opaque failure (a raw stack trace, a bare "Error: request failed", an HTTP status code with no context) tells the model only that something went wrong. It can't tell whether a retry might help (a transient network issue), whether different arguments would help (invalid input, a permission scope too narrow, a resource that doesn't exist under that name), or whether the approach needs to change (the tool doesn't do what the model assumed). Under that uncertainty, a model often retries the identical call, the unproductive loop the agentic loop guide's stopping conditions exist to catch.

A structured, specific failure lets the model act. Compare a bare exception with a result saying, in effect, "invalid argument: region must be one of ['us-east-1', 'us-west-2', 'eu-west-1'], got 'us-east'". The second gives the model a correction it can apply on the next call. You make that design choice once, in the tool, and the model doesn't have to re-solve it at every failure.

# pseudocode — illustrative shape
# Opaque failure: the model has nothing to act on but "something broke."
def get_forecast(city):
    response = weather_api.fetch(city)   # raises on a bad city name
    return response.json()

# Structured failure: the model can correct its next call.
def get_forecast(city):
    if city not in KNOWN_CITIES:
        return {"error": "unknown_city", "message": f"'{city}' not recognized",
                "did_you_mean": closest_matches(city, KNOWN_CITIES)}
    response = weather_api.fetch(city)
    if not response.ok:
        return {"error": "upstream_unavailable", "retryable": True}
    return {"result": response.json()}

Design in the difference between a retryable failure (a transient upstream outage, where retrying after a short delay is reasonable) and a non-retryable one (an invalid argument or a missing resource, where the same arguments will never succeed). A contract that marks which kind occurred saves the model from guessing. It also saves your stopping-condition logic from having to work out, from the outside, whether an agent is making progress through legitimate retries or is stuck on something that cannot succeed.

5. Scoping and authorization

The model choosing to call a tool does not authorize the call. Treating the model's decision as sufficient authority is the mistake this site's governed agentic DevSecOps guide names in its foundational rule for coding agents: keep reasoning separate from authority. The principle applies to tool use generally. A model can reason well about what action would accomplish the task and still have no say over whether it's allowed. Different code answers each question. Conflating them is how a well-behaved model making a reasonable-looking request produces an unauthorized action.

The tool's own implementation enforces scope. Neither the model nor the fact that it was given the tool's definition does. A delete_file(path) tool that deletes anything the process's credentials can reach has handed its authorization decision to whatever the model requests, so its permission boundary is "whatever the process can do." Prompting the model to delete only safe things doesn't fix that. The tool must check the requested path against an allowed scope before acting, and refuse with a structured, specific error (per the previous section) when the request falls outside it, however reasonable the model's justification sounds.

This separation also lets you write descriptions (section 2) generously. The description only helps the model choose and phrase a call; it isn't the security boundary. You can describe a tool's full intended capability clearly, because the tool's enforcement code holds the boundary no matter how the request was phrased.

6. Structured output vs. true tool use

A close relative of tool use does a different architectural job, though the two get used interchangeably in conversation. True tool use lets the model trigger an action with a side effect: send an email, write a database row, call an external API that changes something. Structured output uses the same tool-call mechanism only to force the response into a machine-parseable schema, with no side effect. The "tool" is a schema definition, and calling it produces data in the shape you asked for.

The two have different requirements. A structured-output "tool" that extracts fields from a document into fixed JSON needs none of the authorization enforcement from section 5, because there's no action to authorize. Its error handling is a validation check on output shape, not a real-world failure like an upstream timeout. Giving it the full design weight of an action-taking tool wastes effort. Treating an action-taking tool as structured output (skipping authorization because "it's just calling a function") is the opposite mistake and the more dangerous one.

Test each new tool: what happens in the world if the call executes? If nothing outside this conversation changes, it's structured output, and the design effort goes into the schema. If a system, a record, or a person outside the model call is affected, it's true tool use, and sections 2 through 5 apply in full.

7. Worked example: revising a tool description

Task: an internal support agent needs a tool to look up a customer's subscription status to answer billing questions. A quick first draft:

{
  "name": "get_customer_info",
  "description": "Gets information about a customer.",
  "parameters": {
    "id": "string"
  }
}

This works in the sense that the model can call it and get a response. It under-specifies almost everything the model needs. "Information about a customer" doesn't say which fields come back, so the model can't tell whether "is this customer's plan active" is in scope without calling it. "id: string" doesn't say what kind of ID (account number, email address, internal database key). If the conversation only has the customer's email, the model has to guess whether that's valid input or whether it needs another tool first to resolve the email. Nothing distinguishes this tool from a get_customer_orders or get_customer_billing_history that might exist alongside it.

The revision closes each gap:

{
  "name": "get_subscription_status",
  "description":
    "Looks up a customer's current subscription plan, billing status, and renewal date. " +
    "Does NOT return order history, support ticket history, or payment method details — " +
    "use get_order_history or get_payment_methods for those. " +
    "Returns an error if the customer has no active or past subscription on record.",
  "parameters": {
    "customer_email": {
      "type": "string",
      "description":
        "The customer's account email address, exactly as they use it to log in. " +
        "Not their support-ticket contact email if different — use the account email."
    }
  }
}

Renaming get_customer_info to get_subscription_status tells the model what's inside without reading the definition. The "does NOT return" list prevents the conflation failure from section 2: asked about a customer's last order, the model has a clear signal to use a different tool. Naming the parameter customer_email instead of id, and saying which of the customer's email addresses is meant, removes a guess the model used to make silently. It also removes a class of wrong calls that look plausible in a log (syntactically valid, semantically wrong argument) but return data for the wrong lookup or fail with an unhelpful error.

The implementation could be identical between the two versions. The revision changes model behavior entirely through the description and parameter contract. For a model that only sees a tool through its definition, the definition is the tool for deciding when and how to use it.

For how these calls fit into a full agent loop, including where an approval gate belongs and how tool-execution logs support the stopping conditions this guide's error-handling section relies on, see Building agents with Claude: the agentic loop.

Keep reading