Course topics

By WebNest Studio

Spring Boot Tutorial

Tool Calling (Function Calling)

A language model on its own only knows what was in its training data. It cannot look up today's exchange rate, check an order's status in your database, or book a meeting. Tool calling changes that: you describe Java methods to the model, the model decides when one would help and asks your application to run it with specific arguments, and your application sends the result back so the model can finish its answer.

Tool calling is the foundation of AI agents. In this lesson you will create tools with @Tool, register them with ChatClient, pass private context to tools safely, return results directly, and understand how Spring AI 2.0 runs the tool loop through the ToolCallingAdvisor.

How the Tool Loop Works

1) Spring AI sends your prompt plus a JSON schema for each available tool. 2) The model replies either with a normal answer or with a tool call request naming a tool and its arguments. 3) Spring AI invokes the matching Java method, 4) adds the result to the conversation as a tool message, and 5) calls the model again. This repeats until the model produces a final answer. The model never runs your code itself — your application stays in control of what is executed.

In Spring AI 2.0 this loop is handled by ToolCallingAdvisor, which is automatically registered with ChatClient and works the same way for every model provider.

Defining Tools with @Tool

Annotate public methods with @Tool(description = ...) and their parameters with @ToolParam(description = ...). The description is the most important part: it is all the model sees when deciding whether and how to use the tool. Explain what the tool does, when to use it and what the parameters mean. Parameters are required unless you set required = false. Return values are converted to JSON for the model.

Registering Tools

Pass tool objects per request with .tools(new DateTimeTools()), or for all requests with defaultTools(...) on the builder. For tools defined as functions or shared across the app, declare ToolCallback beans (for example with FunctionToolCallback.builder(...) or MethodToolCallbackProvider) and pass them explicitly. Spring AI 2.0 removed the old toolNames(...) lookup of bare function beans by name.

ToolContext and Return Direct

ToolContext carries values your tool needs but the model must not see or control — the current user id, tenant id, or request locale. Add them with .toolContext(Map.of(...)) and declare a ToolContext parameter on the tool method. Setting returnDirect = true sends the tool's result straight back to the caller without another model round-trip, useful when the tool output is already the final answer.

Security and Safety

Treat the model like an untrusted user: it can be manipulated by prompt injection into calling tools with harmful arguments. Enforce authorization inside each tool using the real user identity from ToolContext or the SecurityContext, validate every argument, prefer read-only tools, and require human confirmation for destructive actions such as refunds or deletions. Log every tool invocation for auditing.

Examples

A simple date/time tool

Java
public class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's time zone")
    public String currentDateTime() {
        return ZonedDateTime.now(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }
}

String answer = chatClient.prompt()
    .user("What day of the week is it tomorrow?")
    .tools(new DateTimeTools())
    .call()
    .content();
Output
(model requests tool currentDateTime → "2026-09-27T14:05:31+05:30[Asia/Kolkata]")
Tomorrow is Monday, 28 September 2026.

Tools backed by your database, scoped to the current user

Java
@Component
public class OrderTools {

    private final OrderRepository orders;

    public OrderTools(OrderRepository orders) {
        this.orders = orders;
    }

    @Tool(description = "Look up the status and delivery date of one of the current customer's orders by order number")
    public OrderStatusView orderStatus(
            @ToolParam(description = "Order number, for example WN-10231") String orderNumber,
            ToolContext context) {
        String customer = (String) context.getContext().get("customerEmail");
        return orders.findByNumberAndCustomerEmail(orderNumber, customer)
            .map(o -> new OrderStatusView(o.getNumber(), o.getStatus().name(), o.getExpectedDelivery()))
            .orElseThrow(() -> new IllegalArgumentException("No such order for this customer"));
    }

    @Tool(description = "List the current customer's five most recent orders")
    public List<OrderStatusView> recentOrders(ToolContext context) {
        String customer = (String) context.getContext().get("customerEmail");
        return orders.findTop5ByCustomerEmailOrderByCreatedAtDesc(customer).stream()
            .map(o -> new OrderStatusView(o.getNumber(), o.getStatus().name(), o.getExpectedDelivery()))
            .toList();
    }
}

public record OrderStatusView(String number, String status, LocalDate expectedDelivery) {}

@PostMapping("/api/support/chat")
public String chat(@RequestBody String message, Principal principal) {
    return supportClient.prompt()
        .user(message)
        .tools(orderTools)
        .toolContext(Map.of("customerEmail", principal.getName()))  // the model never sees or sets this
        .call()
        .content();
}
Output
User: "Where is my order WN-10231?"
(model calls orderStatus(orderNumber="WN-10231"))
Assistant: Your order WN-10231 has shipped and is expected to arrive on 30 September 2026.

User: "Show me order WN-99999" (belongs to another customer)
Assistant: I couldn't find an order with that number on your account.

A function-style tool registered as a ToolCallback bean

Java
public record WeatherRequest(@ToolParam(description = "City name, e.g. Pune") String city) {}
public record WeatherResponse(String city, double temperatureC, String conditions) {}

@Configuration(proxyBeanMethods = false)
public class WeatherToolConfig {

    @Bean
    ToolCallback currentWeather(WeatherService weatherService) {
        return FunctionToolCallback.builder("currentWeather", weatherService::lookup)
            .description("Get the current weather for a city")
            .inputType(WeatherRequest.class)
            .build();
    }
}

@RestController
public class TravelController {

    private final ChatClient chatClient;
    private final ToolCallback currentWeather;

    public TravelController(ChatClient.Builder builder, ToolCallback currentWeather) {
        this.chatClient = builder.build();
        this.currentWeather = currentWeather;
    }

    @GetMapping("/travel-tip")
    public String tip(@RequestParam String city) {
        return chatClient.prompt()
            .user("Should I carry an umbrella in " + city + " today?")
            .tools(currentWeather)
            .call()
            .content();
    }
}
Output
GET /travel-tip?city=Mumbai
(model calls currentWeather({"city":"Mumbai"}) → {"city":"Mumbai","temperatureC":29.0,"conditions":"heavy rain"})
Yes — Mumbai has heavy rain today, so take an umbrella.

returnDirect and a confirmation step for risky actions

Java
public class RefundTools {

    private final RefundService refunds;

    public RefundTools(RefundService refunds) {
        this.refunds = refunds;
    }

    // The model may only PROPOSE a refund; a human must approve it in the admin UI
    @Tool(description = "Create a refund request for review by staff. Does not refund money directly.",
          returnDirect = true)
    public String requestRefund(@ToolParam(description = "Order number") String orderNumber,
                                @ToolParam(description = "Reason given by the customer") String reason,
                                ToolContext context) {
        String customer = (String) context.getContext().get("customerEmail");
        RefundRequest req = refunds.createPendingRequest(customer, orderNumber, reason);
        return "Refund request " + req.id() + " created. Our team will review it within 2 working days.";
    }
}
Output
User: "I want a refund for WN-10231, the course videos don't play."
Assistant: Refund request RR-5521 created. Our team will review it within 2 working days.
(Returned directly from the tool — no second model call.)

Common Mistakes

  • Writing vague tool descriptions like "gets data" — the model then calls the wrong tool or none at all.
  • Letting the model supply the user id or tenant id as a tool argument; pass identity through ToolContext instead.
  • Exposing destructive tools (delete account, issue refund) that act immediately without authorization checks or human approval.
  • Registering dozens of tools on every request, which inflates prompt size and confuses the model; give each ChatClient only the tools it needs.
  • Using the removed toolNames(...) API from Spring AI 1.x tutorials; in 2.0 pass tool objects or ToolCallback beans to .tools(...).

Key Points to Remember

  • The model requests tool calls; Spring AI executes your Java methods and feeds results back until a final answer is produced.
  • Annotate methods with @Tool and parameters with @ToolParam; descriptions drive the model's decisions.
  • Register tools per request with .tools(...) or globally with defaultTools(...); ToolCallback beans for shared function tools.
  • Use ToolContext for identity and tenant data the model must not control; returnDirect skips the extra model call.
  • Enforce authorization and validation inside tools and keep risky actions behind human approval.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.