原文链接:SpringAI(GA):Tool工具整合—快速上手
教程说明
说明:本教程将采用2025年5月20日正式的GA版,给出如下内容
- 核心功能模块的快速上手教程
- 核心功能模块的源码级解读
- Spring ai alibaba增强的快速上手教程 + 源码级解读
版本:JDK21 + SpringBoot3.4.5 + SpringAI 1.0.0 + SpringAI Alibaba最新
将陆续完成如下章节教程。本章是第三章(tool整合)的快速上手
代码开源如下:github.com/GTyingzi/sp…
第三章:tool 整合—快速上手
[!TIP] Tool 工具允许模型与一组 API 或工具进行交互,增强模型功能
以下实现了工具的典型案例:Method 版、Function 版实现、internalToolExecutionEnabled 设置
实战代码可见:github.com/GTyingzi/sp… 下的 tool-calling
pom 文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
</dependency>
<!-- 下面这两个依赖是额外引入的工具处理类,不需要可删除-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
</dependencies>
application.yml
server:
port: 8080
spring:
application:
name: tool-calling
ai:
openai:
api-key: ${DASHSCOPEAPIKEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-max
// 启动配置的time、weather的工具
toolcalling:
time:
enabled: true
weather:
enabled: true
api-key: ${WEATHERAPIKEY}
天气预测 API 接入文档:www.weatherapi.com/docs/
时间工具
TimeUtils
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class TimeUtils {
public static String getTimeByZoneId(String zoneId) {
// Get the time zone using ZoneId
ZoneId zid = ZoneId.of(zoneId);
// Get the current time in this time zone
ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
// Defining a formatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
// Format ZonedDateTime as a string
String formattedDateTime = zonedDateTime.format(formatter);
return formattedDateTime;
}
}
TimeTools(Method 版)
public class TimeTools {
private static final Logger logger = LoggerFactory.getLogger(TimeTools.class);
@Tool(description = "Get the time of a specified city.")
public String getCityTimeMethod(@ToolParam(description = "Time zone id, such as Asia/Shanghai") String timeZoneId) {
logger.info("The current time zone is {}", timeZoneId);
return String.format("The current time zone is %s and the current time is " + "%s", timeZoneId,
TimeUtils.getTimeByZoneId(timeZoneId));
}
}
TimeAutoConfiguration(Function 版)
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
@Configuration
@ConditionalOnClass({GetCurrentTimeByTimeZoneIdService.class})
@ConditionalOnProperty(prefix = "spring.ai.toolcalling.time", name = "enabled", havingValue = "true")
public class TimeAutoConfiguration {
@Bean(name = "getCityTimeFunction")
@ConditionalOnMissingBean
@Description("Get the time of a specified city.")
public GetCurrentTimeByTimeZoneIdService getCityTimeFunction() {
return new GetCurrentTimeByTimeZoneIdService();
}
}
GetCurrentTimeByTimeZoneIdService(Function 版)
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.spring.ai.tutorial.toolcall.component.time.TimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.function.Function;
public class GetCurrentTimeByTimeZoneIdService implements Function<GetCurrentTimeByTimeZoneIdService.Request, GetCurrentTimeByTimeZoneIdService.Response> {
private static final Logger logger = LoggerFactory.getLogger(GetCurrentTimeByTimeZoneIdService.class);
@Override
public Response apply(Request request) {
String timeZoneId = request.timeZoneId;
logger.info("The current time zone is {}", timeZoneId);
return new Response(String.format("The current time zone is %s and the current time is " + "%s", timeZoneId,
TimeUtils.getTimeByZoneId(timeZoneId)));
}
@JsonInclude(JsonInclude.Include.NONNULL)
@JsonClassDescription("Get the current time based on time zone id")
public record Request(@JsonProperty(required = true, value = "timeZoneId") @JsonPropertyDescription("Time zone id, such as Asia/Shanghai") String timeZoneId) {
}
public record Response(String description) {
}
}
TimeController
import com.spring.ai.tutorial.toolcall.component.time.method.TimeTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATIONID;
@RestController
@RequestMapping("/chat/time")
public class TimeController {
private final ChatClient chatClient;
private final InMemoryChatMemoryRepository chatMemoryRepository = new InMemoryChatMemoryRepository();
private final int MAXMESSAGES = 100;
private final MessageWindowChatMemory messageWindowChatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(MAXMESSAGES)
.build();
public TimeController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(messageWindowChatMemory)
.build()
)
.build();
}
/**
* 无工具版
*/
@GetMapping("/call")
public String call(@RequestParam(value = "query", defaultValue = "请告诉我现在北京时间几点了") String query) {
return chatClient.prompt(query).call().content();
}
/**
* 调用工具版 - function
*/
@GetMapping("/call/tool-function")
public String callToolFunction(@RequestParam(value = "query", defaultValue = "请告诉我现在北京时间几点了") String query) {
return chatClient.prompt(query).toolNames("getCityTimeFunction").call().content();
}
/**
* 调用工具版 - method
*/
@GetMapping("/call/tool-method")
public String callToolMethod(@RequestParam(value = "query", defaultValue = "请告诉我现在北京时间几点了") String query) {
return chatClient.prompt(query).tools(new TimeTools()).call().content();
}
/**
* call 调用工具版 - method - false
*/
@GetMapping("/call/tool-method-false")
public ChatResponse callToolMethodFalse(@RequestParam(value = "query", defaultValue = "请告诉我现在北京时间几点了") String query) {
ChatClient.CallResponseSpec call = chatClient.prompt(query).tools(new TimeTools())
.advisors(
a -> a.param(CONVERSATIONID, "yingzi")
)
.options(ToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false) // 禁用内部工具执行
.build()
)
.call();
return call.chatResponse();
}
@GetMapping("/messages")
public List<Message> messages(@RequestParam(value = "conversationid", defaultValue = "yingzi") String conversationId) {
return messageWindowChatMemory.get(conversationId);
}
}
效果
无工具版,大模型无法知道当前时间
工具版—Function,通过自动注入对应的工具 Bean,实现获取时间
工具版—Method,通过 @Tool 注解指定工具 Bean,实现获取时间
通过设置工具判断字段 internalToolExecutionEnabled=false(默认为 true),来手动控制工具执行
可结合历史消息记录,用来编写手动控制工具之后的逻辑
天气工具
WeatherUtils
import cn.hutool.extra.pinyin.PinyinUtil;
public class WeatherUtils {
public static String preprocessLocation(String location) {
if (containsChinese(location)) {
return PinyinUtil.getPinyin(location, "");
}
return location;
}
public static boolean containsChinese(String str) {
return str.matches(".*[\u4e00-\u9fa5].*");
}
}
WeatherProperties
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.ai.toolcalling.weather")
public class WeatherProperties {
private String apiKey;
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}
WeatherTools(Method 版)
package com.spring.ai.tutorial.toolcall.component.weather.method;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.ai.tutorial.toolcall.component.weather.WeatherProperties;
import com.spring.ai.tutorial.toolcall.component.weather.WeatherUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public class WeatherTools {
private static final Logger logger = LoggerFactory.getLogger(WeatherTools.class);
private static final String WEATHERAPIURL = "https://api.weatherapi.com/v1/forecast.json";
private final WebClient webClient;
private final ObjectMapper objectMapper = new ObjectMapper();
public WeatherTools(WeatherProperties properties) {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENTTYPE, "application/x-www-form-urlencoded")
.defaultHeader("key", properties.getApiKey())
.build();
}
@Tool(description = "Use api.weather to get weather information.")
public Response getWeatherServiceMethod(@ToolParam(description = "City name") String city,
@ToolParam(description = "Number of days of weather forecast. Value ranges from 1 to 14") int days) {
if (!StringUtils.hasText(city)) {
logger.error("Invalid request: city is required.");
return null;
}
String location = WeatherUtils.preprocessLocation(city);
String url = UriComponentsBuilder.fromHttpUrl(WEATHERAPIURL)
.queryParam("q", location)
.queryParam("days", days)
.toUriString();
logger.info("url : {}", url);
try {
Mono<String> responseMono = webClient.get().uri(url).retrieve().bodyToMono(String.class);
String jsonResponse = responseMono.block();
assert jsonResponse != null;
Response response = fromJson(objectMapper.readValue(jsonResponse, new TypeReference<Map<String, Object>>() {
}));
logger.info("Weather data fetched successfully for city: {}", response.city());
return response;
} catch (Exception e) {
logger.error("Failed to fetch weather data: {}", e.getMessage());
return null;
}
}
public static Response fromJson(Map<String, Object> json) {
Map<String, Object> location = (Map<String, Object>) json.get("location");
Map<String, Object> current = (Map<String, Object>) json.get("current");
Map<String, Object> forecast = (Map<String, Object>) json.get("forecast");
List<Map<String, Object>> forecastDays = (List<Map<String, Object>>) forecast.get("forecastday");
String city = (String) location.get("name");
return new Response(city, current, forecastDays);
}
public record Response(String city, Map<String, Object> current, List<Map<String, Object>> forecastDays) {
}
}
WeatherAutoConfiguration(Function 版)
import com.spring.ai.tutorial.toolcall.component.weather.WeatherProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
@Configuration
@ConditionalOnClass(WeatherService.class)
@EnableConfigurationProperties(WeatherProperties.class)
@ConditionalOnProperty(prefix = "spring.ai.toolcalling.weather", name = "enabled", havingValue = "true")
public class WeatherAutoConfiguration {
@Bean(name = "getWeatherFunction")
@ConditionalOnMissingBean
@Description("Use api.weather to get weather information.")
public WeatherService getWeatherServiceFunction(WeatherProperties properties) {
return new WeatherService(properties);
}
}
WeatherService(Function 版)
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.ai.tutorial.toolcall.component.weather.WeatherProperties;
import com.spring.ai.tutorial.toolcall.component.weather.WeatherUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class WeatherService implements Function<WeatherService.Request, WeatherService.Response> {
private static final Logger logger = LoggerFactory.getLogger(WeatherService.class);
private static final String WEATHERAPIURL = "https://api.weatherapi.com/v1/forecast.json";
private final WebClient webClient;
private final ObjectMapper objectMapper = new ObjectMapper();
public WeatherService(WeatherProperties properties) {
this.webClient = WebClient.builder()
.defaultHeader(HttpHeaders.CONTENTTYPE, "application/x-www-form-urlencoded")
.defaultHeader("key", properties.getApiKey())
.build();
}
public static Response fromJson(Map<String, Object> json) {
Map<String, Object> location = (Map<String, Object>) json.get("location");
Map<String, Object> current = (Map<String, Object>) json.get("current");
Map<String, Object> forecast = (Map<String, Object>) json.get("forecast");
List<Map<String, Object>> forecastDays = (List<Map<String, Object>>) forecast.get("forecastday");
String city = (String) location.get("name");
return new Response(city, current, forecastDays);
}
@Override
public Response apply(Request request) {
if (request == null || !StringUtils.hasText(request.city())) {
logger.error("Invalid request: city is required.");
return null;
}
String location = WeatherUtils.preprocessLocation(request.city());
String url = UriComponentsBuilder.fromHttpUrl(WEATHERAPIURL)
.queryParam("q", location)
.queryParam("days", request.days())
.toUriString();
logger.info("url : {}", url);
try {
Mono<String> responseMono = webClient.get().uri(url).retrieve().bodyToMono(String.class);
String jsonResponse = responseMono.block();
assert jsonResponse != null;
Response response = fromJson(objectMapper.readValue(jsonResponse, new TypeReference<Map<String, Object>>() {
}));
logger.info("Weather data fetched successfully for city: {}", response.city());
return response;
} catch (Exception e) {
logger.error("Failed to fetch weather data: {}", e.getMessage());
return null;
}
}
@JsonInclude(JsonInclude.Include.NONNULL)
@JsonClassDescription("Weather Service API request")
public record Request(
@JsonProperty(required = true, value = "city") @JsonPropertyDescription("city name") String city,
@JsonProperty(required = true,
value = "days") @JsonPropertyDescription("Number of days of weather forecast. Value ranges from 1 to 14") int days) {
}
public record Response(
String city,
Map<String, Object> current,
List<Map<String, Object>> forecastDays) {
}
}
WeatherController
package com.spring.ai.tutorial.toolcall.controller;
import com.spring.ai.tutorial.toolcall.component.weather.WeatherProperties;
import com.spring.ai.tutorial.toolcall.component.weather.method.WeatherTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/chat/weather")
public class WeatherController {
private final ChatClient chatClient;
private final WeatherProperties weatherProperties;
public WeatherController(ChatClient.Builder chatClientBuilder, WeatherProperties weatherProperties) {
this.chatClient = chatClientBuilder.build();
this.weatherProperties = weatherProperties;
}
/**
* 无工具版
*/
@GetMapping("/call")
public String call(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) {
return chatClient.prompt(query).call().content();
}
/**
* 调用工具版 - function
*/
@GetMapping("/call/tool-function")
public String callToolFunction(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) {
return chatClient.prompt(query).toolNames("getWeatherFunction").call().content();
}
/**
* 调用工具版 - method
*/
@GetMapping("/call/tool-method")
public String callToolMethod(@RequestParam(value = "query", defaultValue = "请告诉我北京1天以后的天气") String query) {
return chatClient.prompt(query).tools(new WeatherTools(weatherProperties)).call().content();
}
}
效果
无工具版,大模型无法知道天气情况
工具版—Function,通过自动注入对应的工具 Bean,实现获取天气
工具版—Function,通过 @Tool 注解指定工具 Bean,实现获取天气