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.
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:
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?
How does an agent decide which tool to use?
What is tool composition?
How should tools handle errors so the agent can recover?
Related tutorials
- Agent Architecture PatternsThe core agent architecture patterns explained with Java: ReAct, plan-and-execute, reflection, orchestrator-worker and routing — when to use each, and why simpler is usually better.
- Planning & Reasoning in AgentsHow agents plan and reason: task decomposition, hierarchical planning, chain-of-thought and tree-of-thought — with Java examples and honest guidance on when planning helps.
- What Is Agentic AI? A Complete GuideA clear, hype-free explanation of agentic AI: how it differs from generative AI, the five components of an agent, when autonomy is worth it, and when a plain workflow is the better engineering choice.
- Memory Systems for AgentsHow agent memory works beyond a chat window: working, episodic and semantic memory, vector-based recall, memory consolidation, and implementing persistent agent memory in Java.