JAVA + SPRING AI — ADVANCED
Multiple Tools, Return Types, Error Handling & Tools vs. Advisors
The Intermediate level’s endpoint registered exactly one tool and assumed it always ran cleanly. Real applications usually register several tools at once, need structured data back rather than plain strings, and have to handle the moment a tool call fails — plus an honest look at how tool calling actually relates to the Advisor-based RAG augmentation from the previous topic.
The Quick Answer
Multiple tools register together in a single .tools(...)call, and the model picks among them by matching the question against each tool’s name and description — the same general mechanism Tool Calling Intermediate already covers, with nothing Spring-AI-specific added on top of it. A tool’s return value can be nearly any serializable type — not just String — and gets serialized to JSON automatically. A thrown RuntimeExceptionis, by default, converted into an error result the model can react to, rather than crashing the request. And tool calling and the previous topic’s QuestionAnswerAdvisor genuinely share the same underlying Advisor mechanism — verified below — though this page is careful about which conclusions that shared mechanism actually supports.
Multiple Tools on One Call
.tools(...) accepts more than one tool object at once — every @Tool-annotated method across all of them becomes available to the model in that single request:
String answer = chatClient.prompt()
.tools(orderTools, weatherTools)
.user("What's the status of order 4471, and what's the weather like in Pune?")
.call()
.content();Spring AI’s current documentation is explicit on one requirement this raises: a tool’s name (the @Toolattribute from the Intermediate level, defaulting to the method name) “must be unique across all the tools available to the model” for that specific request — two different tool classes, each with their own method happening to be named the same thing, would collide if both were registered together.
As for how the model actually decides which tool (if any) is relevant to a given question: this is exactly the same tool-selection process Tool Calling Intermediate already covers on this site, conceptually — the model matches the request against each available tool’s description to judge relevance. Spring AI’s own documentation doesn’t describe a separate, Spring-AI-specific selection algorithm on top of that — it supplies the tool schemas to the model and reads back whatever structured request the model produces, the same underlying mechanism regardless of language or framework. This page doesn’t claim anything more specific about the model’s internal decision process than that.
What a Tool Can Return
Every tool shown so far in this pillar returned a plain String. Spring AI’s current documentation is direct that this isn’t a requirement: a @Toolmethod can return “most types” — primitives, POJOs, enums, lists, arrays, maps, voidincluded — as long as the type is serializable, since “the result will be serialized and sent back to the model.”
public record OrderStatus(String orderId, String status, String eta) {}
@Tool(description = "Get the current shipping status of a customer's order, by order ID")
public OrderStatus getOrderStatus(@ToolParam(description = "The order ID") String orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
return new OrderStatus(order.getId(), order.getShippingStatus(), order.getEstimatedArrival());
}Returning OrderStatusdirectly, rather than hand-building a formatted string inside the method, means the tool’s own logic stays focused on producing the right data — Spring AI’s default DefaultToolCallResultConverter serializes it to JSON (via Jackson) before it goes back to the model, which is what actually turns it into text the model can read. That default converter is itself replaceable — the resultConverter attribute on @Tool accepts a custom ToolCallResultConverter implementation for the less common case where the default JSON shape isn’t the right fit.
When a Tool Throws: Error Handling
Every tool method so far has assumed success. Verified against Spring AI’s current documentation, a thrown exception is handled differently depending on its type, by default:
RuntimeExceptionIts message is sent back to the model as an error result — not silence, not a crash — so the model can react to it directly: apologize to the user, explain the failure, or (if the tool’s description gave it enough to work with) retry with corrected arguments.
Checked exceptions and
Error(e.g.IOException,OutOfMemoryError)Always thrown rather than converted into a model-facing error — these represent failures serious enough, or unexpected enough, that Spring AI’s current default behavior is to let them propagate to the caller instead of quietly feeding them back into the conversation.
@Tool(description = "Get the current shipping status of a customer's order, by order ID")
public OrderStatus getOrderStatus(@ToolParam(description = "The order ID") String orderId) {
return orderRepository.findById(orderId)
.map(this::toOrderStatus)
.orElseThrow(() -> new IllegalArgumentException("No order found with ID " + orderId));
// IllegalArgumentException is a RuntimeException — its message is sent back
// to the model by default, which can then explain to the user that this
// order ID doesn't exist, rather than the whole request failing outright.
}This whole default is itself one configuration property away from being reversed: setting spring.ai.tools.throw-exception-on-error to true makes tool errors throw for the calling code to handle directly, instead of being converted into a message for the model — worth reaching for if a specific application would rather fail loudly than let the model attempt to talk its way around a failure.
The Loop, Visually
Every piece covered across both levels of this topic fits into one loop, run by ToolCallingAdvisor every time a tool call happens:
The dashed loop back to the model is exactly the “recursive advisor” behavior Spring AI’s own documentation describes: ToolCallingAdvisor re-enters the same advisor chain after a tool result comes back, rather than the loop being a separate mechanism bolted on beside it.
Tool Calling vs. RAG: What’s Actually Verified, and What Isn’t
The previous topic’s Advanced level kept the same cautious posture this pillar maintains throughout: rather than asserting a specific relationship between tool calling and QuestionAnswerAdvisor-based RAG, it simply noted both extend what a single ChatClientcall can do and left it there, without going further than that. This topic can confirm one specific, verifiable fact that’s consistent with that same caution, without overstating it: both are implemented through the exact same underlying mechanism. Spring AI’s current reference documentation is explicit that ToolCallingAdvisor— the advisor behind everything covered across this topic — is “registered in the advisor chain,” the identical extension point QuestionAnswerAdvisor plugs into for retrieval.
What that shared mechanism does notestablish, and what this page isn’t claiming, is that Spring AI’s own documentation goes on to explicitly describe tool calling and RAG as two named alternatives for the same job, or as interchangeable in any sense. They still solve genuinely different problems — retrieval hands the model text it couldn’t otherwise see before it answers; tool calling hands the model a way to request a real action or a real, current piece of data mid-conversation. What follows is this pillar’s own synthesis, clearly separated from the verified fact above it:
| Aspect | RAG (QuestionAnswerAdvisor) | Tool calling (ToolCallingAdvisor) |
|---|---|---|
| What it supplies | Relevant text, retrieved before the model ever answers. | A way to request a real action or a live piece of data, mid-conversation. |
| Who decides to use it | The advisor always runs its retrieval step, on every call it’s attached to. | The model itself decides whether a registered tool is even relevant to this specific question. |
| Underlying data source | A VectorStore — documents embedded and searched ahead of time. | Whatever the registered Java method actually does — a database call, a live API, arbitrary logic. |
| Shared mechanism (verified) | Both are Advisors, registered in the same ChatClient advisor chain. | |
Nothing above rules out combining both on the same call — a ChatClient.prompt() chain can carry a QuestionAnswerAdvisor in .advisors(...) and a tool class in .tools(...)at the same time, since they’re independent additions to the same chain rather than mutually exclusive choices — this page just hasn’t found, and isn’t asserting, an official Spring AI framing that goes further than the shared-mechanism fact above.
Where This Goes Next
Every tool across both levels of this topic — getOrderStatus, getCurrentWeather— has been a local, in-process Java method call: Spring AI invokes it directly, inside the same running application, and gets its result back essentially instantly. The next topic in this pillar, Microservices Meet LLMs & AI Agents, explores what changes when a tool’s own implementation is itself a network call to a different microservice — not just a method inside the same process, but a real request across the network, with its own latency, its own chance of failing, and its own database on the other end.
Fun Fact
Spring AI’s current documentation notes that a tool doesn’t have to be declared statically up front, either — an advisor running earlier in the chain can inject additional tools at runtime, dynamically, rather than every possible tool being fixed at the moment a ChatClient call is written. This pillar teaches the static, .tools(...)-at-call-time shape throughout, since it’s the foundation the dynamic case still builds on.
Test Yourself
What must be true of tool names when multiple tools are registered on the same ChatClient call?
What can a @Tool-annotated method return, according to Spring AI’s current documentation?
What happens by default when a @Tool method throws a RuntimeException?
How does this page characterize the relationship between tool calling and Advisor-based RAG (from the previous topic)?
What does this page say changes in the next topic’s treatment of tool calling within microservices-spanning agents?