skip to content

How do you define a tool with @Tool / @ToolParam and register it with ChatClient, and what does the execution loop look like?

level: middleimportance: must knowfreq 50%

answer

  1. @Tool description + @ToolParam schema
  2. .tools() per request vs .defaultTools()
  3. ToolCallbacks.from(bean)
  4. loop: request -> execute -> response -> re-call
  5. return value serialized to JSON

basics

~20 s

Put @Tool(description=...) on a method and @ToolParam on its arguments. Register the containing object with ChatClient via .tools(new MyTools()) per request or .defaultTools(...) on the builder. Spring advertises the schema, runs the method when the model asks, and loops until a final answer.

solid answer

~50 s

You annotate a method with @Tool, giving a clear description; @ToolParam documents each argument and can mark it required. Spring AI reflects over the method to build a JSON schema (name, description, parameter types) and sends it to the model. Registration happens either per-request with ChatClient.prompt().tools(new MyTools()) or globally on the builder with .defaultTools(...); you can also register singletons as beans and pass ToolCallbacks. The execution loop: the model returns a tool-call request, Spring's ToolCallingManager binds the JSON args to your method parameters, invokes it, serializes the return value into a tool-response message, and re-calls the model. This repeats for multiple/parallel tool calls until the model produces text with no further calls. Method return types are serialized to JSON; void tools return a generic done signal. Descriptions and parameter names strongly influence whether and how the model calls the tool.

code

java · 18 lines
java
record OrderStatus(String orderId, String state, String eta) {}

class OrderTools {
    private final OrderService orderService;
    OrderTools(OrderService orderService) { this.orderService = orderService; }

    @Tool(description = "Look up the delivery status of a customer order")
    OrderStatus orderStatus(@ToolParam(description = "Order ID, e.g. ORD-123") String orderId) {
        return orderService.status(orderId);
    }
}

// Register globally so every call can use it
ChatClient client = ChatClient.builder(chatModel)
        .defaultTools(new OrderTools(orderService))
        .build();

String reply = client.prompt("Has order ORD-123 shipped yet?").call().content();

go deeper

for a junior

Recognize @Tool/@ToolParam and that .tools()/.defaultTools() register them.

for a middle

Explain schema generation via reflection and the full request→execute→response→re-call loop, including multiple/parallel calls.

for a senior

Discuss registration strategies (per-request vs default vs ToolCallbacks.from bean), return serialization, and validation of model-chosen args.

for a principal

Weigh token cost of returned payloads, naming/ambiguity hygiene, and error-to-message conversion policy.

## Declaring a tool `@Tool` (package `org.springframework.ai.tool.annotation`) turns any method — on a POJO or a Spring bean — into a model-callable tool. Its attributes: - `name` — defaults to the method name; override to give the model a clearer handle. - `description` — the natural-language explanation the model reads to decide *when* to call. This is the single most important field. - `returnDirect` — if `true`, the tool's result is returned straight to the caller instead of being sent back to the model (see below). `@ToolParam` (same package) documents each argument: - `description` — what the parameter means. - `required` — whether the model must supply it (defaults to true). ```java class OrderTools { @Tool(description = "Look up the delivery status of a customer order") OrderStatus orderStatus( @ToolParam(description = "The order ID, e.g. ORD-123") String orderId) { return orderService.status(orderId); } } ``` Spring reflects over parameter types and any `@ToolParam` metadata to generate a **JSON Schema** describing the inputs. That schema plus the description is what the model sees. ## Registering tools Three common ways: 1. **Per request** — most explicit: ```java chatClient.prompt("Where is order ORD-123?") .tools(new OrderTools()) .call().content(); ``` 2. **Default on the builder** — applied to every call: ```java ChatClient.builder(chatModel) .defaultTools(new OrderTools()) .build(); ``` 3. **Programmatic ToolCallbacks** — build `ToolCallback[]` explicitly (e.g. via `ToolCallbacks.from(myBean)`) and pass with `.tools(callbacks)` / `.toolCallbacks(...)`. Useful when tools come from beans or are built functionally. ## The execution loop in detail 1. Request is sent with the tool schemas attached (via `ToolCallingChatOptions`). 2. The model may respond with one or more **tool calls** — each a `{name, arguments}` pair. Providers like OpenAI can request several in parallel. 3. `ToolCallingManager` matches each call to a registered `ToolCallback`, deserializes the JSON arguments onto the method parameters, and invokes it. 4. The return value is serialized to JSON and wrapped in a **tool-response message** appended to the conversation. 5. The model is invoked again with the responses. It either calls more tools or emits a final answer. The loop terminates when there are no more tool calls (or `returnDirect=true`). By default this loop runs **internally** and automatically — you just call `.content()` and get the final text. ## Return-value handling - Objects/records → serialized to JSON. - `String` → passed through. - `void` → a generic success indicator is returned to the model. - Throwing an exception surfaces as an error; you can convert it to a message the model can reason about via a `ToolExecutionExceptionProcessor`. ## Gotchas - **Overloaded methods / same tool name** cause ambiguity — keep names unique. - The model picks arguments; **validate** them defensively (nulls, ranges, injection). - Large return payloads re-enter the prompt as tokens — trim what you return. - A missing/weak `description` makes the model ignore or misuse the tool. - Method-based tools must be **instantiable/reachable** at call time; per-request registration passes a live instance.

  • What is the difference between .tools(new MyTools()) and .defaultTools(...)?
    .tools(...) on a prompt registers tools for that single request; .defaultTools(...) on the builder registers them for every request made through that ChatClient. Per-request tools add to (don't replace) the defaults.
  • How does Spring build the schema the model sees for a tool?
    It reflects over the annotated method: the @Tool name/description, the parameter types, and any @ToolParam metadata become a JSON Schema plus a textual description, which Spring sends to the model as the tool definition.
  • Can the model call more than one tool for a single prompt?
    Yes. Providers can return multiple tool calls (even in parallel), and the loop can span several rounds — the model may call tools repeatedly, chaining results, before producing the final answer.

saying these in an interview costs you the question

  • Thinking you must manually parse the tool-call request and re-invoke the model yourself (the internal loop does it)
  • Claiming @ToolParam is required for every parameter
  • Assuming only one tool call per prompt is possible

context