Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Tool Use & Function Calling

How agents use tools well: designing tool schemas, dynamic tool selection, composing tools into workflows, error recovery, and keeping the tool set small enough to choose from.

Intermediate4 min readUpdated
On this page

Tools are an agent's capabilities — its entire ceiling of what it can do. Getting tool use right is mostly about three things: designing tools the model can select correctly, keeping the set small enough to choose from, and returning errors the agent can recover from. This tutorial covers all three at the agent level.

Key Takeaways

  • The description is the whole interface — write it for a reader who cannot see the code.
  • Keep the tool set small; register a subset per task beyond ~10-15 tools.
  • Design tools to compose — clear inputs and outputs that fit together.
  • Return errors as descriptive values so the agent can recover in-loop.

Tool schemas: what the model actually reads

The model chooses tools based on their name, description and parameter schema — never their implementation. The schema is the contract:

@Tool("""
        Search for orders by customer email address.
        Use when you need to find an order but do not have its ID.
        Returns a list of matching orders with their IDs, or NONE.
        """)
List<OrderSummary> searchOrders(
        @ToolParam(description = "Customer email, e.g. sam@example.com") String email) {
    // ...
}

Every part of that is read by the model: the description tells it when, the parameter description tells it what to put where, and the return type tells it what it will get back.

Keeping the set small

Every tool definition is tokens on every call, and selection accuracy falls as the list grows. Past about ten to fifteen tools, scope them per task:

Task-scoped tools
public String handle(Request request) {
    // Register only the tools this kind of request needs. A billing question
    // does not need the deployment tools in scope, and a tool that is not
    // registered cannot be mis-selected.
    Object[] tools = switch (request.type()) {
        case BILLING -> new Object[] { billingTools, orderTools };
        case TECHNICAL -> new Object[] { diagnosticTools, docsTools };
        case OPS -> new Object[] { deploymentTools, monitoringTools };
    };
 
    Agent agent = AiServices.builder(Agent.class)
            .chatModel(model)
            .tools(tools)
            .build();
    return agent.handle(request.text());
}

Tool composition

Good agents chain tools: the result of one informs the next. You enable this by designing tools whose outputs feed naturally into other tools' inputs.

// These compose: searchOrders returns IDs, getShipment takes an ID,
// estimateDelivery takes a shipment. The agent chains them based on results.
@Tool("Find orders by customer email. Returns order IDs.")
List<OrderSummary> searchOrders(String email) { /* ... */ }
 
@Tool("Get the shipment for an order ID. Returns tracking details.")
Shipment getShipment(String orderId) { /* ... */ }
 
@Tool("Estimate delivery date from tracking details.")
String estimateDelivery(String trackingId) { /* ... */ }

Given "when will Sam's order arrive?", the agent composes all three: search by email, get the shipment for the found ID, estimate delivery from the tracking. You wrote no orchestration — the composability of the tools made it possible.

Error recovery

Tools fail. The difference between a robust agent and a brittle one is whether the failure is a recoverable message or a fatal exception.

@Tool("Get an order's status by ID.")
String orderStatus(String orderId) {
    return orders.findById(orderId)
            .map(Order::statusDescription)
            // A descriptive error keeps the agent in the loop and tells it the
            // next move — ask the customer to re-check the ID.
            .orElse("NOT_FOUND: no order '" + orderId + "'. Ask the customer to verify it.");
}

Dynamic tool selection at scale

When you genuinely have many tools, retrieve the relevant ones per query instead of registering all of them — a "tool RAG" pattern:

// Embed tool descriptions; retrieve the few most relevant to this request;
// register only those. Keeps the prompt small and selection sharp even with a
// large tool catalogue.
List<Object> relevant = toolRetriever.retrieve(request.text(), 5);
Agent agent = AiServices.builder(Agent.class).chatModel(model).tools(relevant).build();

Observability for tool use

Log every tool call — name, arguments, result, duration. When an agent misbehaves, the tool-call sequence is the primary evidence:

log.info("agent={} tool={} args={} durationMs={} outcome={}",
        agentId, toolName, args, duration, outcome);

This trajectory is what you review to understand why an agent reached a wrong conclusion — the final answer alone tells you nothing about which tool returned misleading data. See agent evaluation & testing.

Next

Frequently Asked Questions

How many tools should an agent have?
Fewer than you think. Every tool definition consumes prompt tokens on every call, and selection accuracy degrades as the list grows. Beyond roughly ten to fifteen tools, register a task-appropriate subset per request rather than exposing everything at once. A focused tool set produces a more reliable agent.
How does an agent decide which tool to use?
It reads the tool descriptions and matches them against the current goal. The description is the entire basis for the decision — the model never sees the implementation. Clear descriptions that state when to use each tool, and what distinguishes similar tools, are what make selection reliable.
What is tool composition?
Chaining tools so the output of one feeds another — look up an order, then check its shipment, then estimate delivery. The agent composes them by calling them in sequence based on each result. You enable good composition by designing tools with clear inputs and outputs that fit together, and by returning results the model can act on.
How should tools handle errors so the agent can recover?
Return errors as descriptive values, not exceptions. A returned message like "NOT_FOUND: no order with that ID, ask the customer to verify it" keeps the agent in the loop and tells it what to do next. A thrown exception aborts the whole request and gives the agent nothing to recover from.

Related tutorials