Spring AI Function Calling — Give the AI Access to Your Java Methods
Function calling (also called tool calling) allows an LLM to invoke your Java methods when it needs real-world information. Instead of the AI hallucinating an answer, it tells Spring AI "I need to call the weather function" and Spring AI executes your actual method and feeds the result back to the model. This is how AI agents interact with external systems.
How Function Calling Works
1. User asks: "What's the weather in London and should I bring an umbrella?"
2. Spring AI sends question + tool definitions to LLM:
Tools available:
- getWeather(city: String) → Returns current temperature and conditions
- getForecast(city: String, days: int) → Returns N-day weather forecast
3. LLM responds: "Call getWeather('London'), then getForecast('London', 1)"
4. Spring AI executes your Java methods:
getWeather("London") → "18°C, Partly cloudy"
getForecast("London",1) → "Tomorrow: 14°C, Heavy rain expected"
5. Results fed back to LLM
6. LLM responds: "It's currently 18°C and partly cloudy in London.
Tomorrow will be 14°C with heavy rain — bring an umbrella!"
Defining Tools with @Description
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Component;
@Component
public class WeatherTools {
@Tool(description = "Get the current weather conditions for a given city. Returns temperature in Celsius and sky conditions.")
public String getCurrentWeather(String city) {
// In a real app, call a weather API here
Map<String, String> weatherData = Map.of(
"London", "18°C, Partly cloudy",
"New York", "22°C, Sunny",
"Tokyo", "28°C, Humid and overcast",
"Mumbai", "31°C, Hot and sunny"
);
return weatherData.getOrDefault(city, "Weather data unavailable for " + city);
}
@Tool(description = "Get the weather forecast for a city for the next N days. Returns daily summary.")
public String getWeatherForecast(String city, int days) {
return "Forecast for %s next %d days: Temperatures between 14-22°C, expect rain on day 2.".formatted(city, days);
}
}
Using Tools in ChatClient
@Service
public class WeatherAssistantService {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public WeatherAssistantService(ChatClient.Builder builder, WeatherTools weatherTools) {
this.weatherTools = weatherTools;
this.chatClient = builder
.defaultSystem("You are a helpful weather assistant. Use tools to get real weather data.")
.defaultTools(weatherTools) // register tools globally
.build();
}
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
REST Controller
@RestController
@RequestMapping("/weather")
public class WeatherController {
private final WeatherAssistantService assistant;
public WeatherController(WeatherAssistantService assistant) {
this.assistant = assistant;
}
@GetMapping("/ask")
public String ask(@RequestParam String q) {
return assistant.ask(q);
}
}
Output
GET /weather/ask?q=What's the weather in London and do I need a jacket?
The current weather in London is 18°C with partly cloudy skies. I'd recommend
bringing a light jacket — the temperature is mild but can feel cooler with cloud
cover. The forecast shows temperatures dropping to around 14°C with rain
expected in the coming days, so a waterproof layer would be wise.
Database Query Tools
@Component
public class DatabaseTools {
private final UserRepository userRepo;
private final OrderRepository orderRepo;
public DatabaseTools(UserRepository userRepo, OrderRepository orderRepo) {
this.userRepo = userRepo;
this.orderRepo = orderRepo;
}
@Tool(description = "Find a user by their email address. Returns user ID, name, and registration date.")
public String findUserByEmail(String email) {
return userRepo.findByEmail(email)
.map(u -> "ID: %d, Name: %s, Registered: %s"
.formatted(u.getId(), u.getName(), u.getCreatedAt()))
.orElse("No user found with email: " + email);
}
@Tool(description = "Get total order count and revenue for a user ID.")
public String getUserOrderSummary(Long userId) {
long count = orderRepo.countByUserId(userId);
double revenue = orderRepo.sumRevenueByUserId(userId);
return "User %d has placed %d orders totaling $%.2f".formatted(userId, count, revenue);
}
}
// Usage in service:
this.chatClient = builder
.defaultTools(databaseTools)
.defaultSystem("You are a customer support agent. Use tools to look up real customer data.")
.build();
Multiple Tools Together
@Service
public class CustomerSupportService {
private final ChatClient chatClient;
public CustomerSupportService(ChatClient.Builder builder,
DatabaseTools dbTools,
OrderTools orderTools,
EmailTools emailTools) {
this.chatClient = builder
.defaultSystem("""
You are a customer support agent.
Use the provided tools to look up customer information and process requests.
Always verify customer identity before performing any actions.
""")
.defaultTools(dbTools, orderTools, emailTools)
.build();
}
public String handle(String customerId, String request) {
return chatClient.prompt()
.user("Customer ID: " + customerId + "\nRequest: " + request)
.call()
.content();
}
}
Tool with Complex Return Type
public record StockInfo(String symbol, double price, double change, String trend) {}
@Component
public class StockTools {
@Tool(description = "Get current stock price and daily change for a ticker symbol.")
public StockInfo getStockPrice(String symbol) {
// Call real stock API here
return new StockInfo(symbol, 185.43, 2.17, "UP");
}
}
Key Points
- The
@Tooldescription is what the LLM reads to decide whether to call that method — write it clearly and precisely - Tool methods must be on a Spring bean — Spring AI uses the bean to invoke them
- The LLM can call multiple tools in sequence or in parallel depending on the model and request
- Tools can return any Java type — Spring AI serializes it to JSON before passing it back to the model
- Never expose destructive operations (DELETE, DROP) as tools without confirmation steps — the AI might call them unexpectedly
Comments