SpringAI(GA):Tool工具整合—快速上手
原文链接:SpringAI(GA):Tool工具整合—快速上手
教程说明
说明:本教程将采用2025年5月20日正式的GA版,给出如下内容
- 核心功能模块的快速上手教程
- 核心功能模块的源码级解读
- Spring ai alibaba增强的快速上手教程 + 源码级解读
版本:JDK21 + SpringBoot3.4.5 + SpringAI 1.0.0 + SpringAI Alibaba最新
将陆续完成如下章节教程。本章是第三章(tool整合)的快速上手
代码开源如下:https://github.com/GTyingzi/spring-ai-tutorial
第三章:tool 整合—快速上手
[!TIP]
Tool 工具允许模型与一组 API 或工具进行交互,增强模型功能
以下实现了工具的典型案例:Method 版、Function 版实现、internalToolExecutionEnabled 设置
实战代码可见:https://github.com/GTyingzi/spring-ai-tutorial 下的 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: 8080spring:application:name: tool-callingai:openai:api-key: ${DASHSCOPEAPIKEY}base-url: https://dashscope.aliyuncs.com/compatible-modechat:options:model: qwen-max// 启动配置的time、weather的工具toolcalling:time:enabled: trueweather:enabled: trueapi-key: ${WEATHERAPIKEY}
天气预测 API 接入文档:https://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 ZoneIdZoneId zid = ZoneId.of(zoneId);// Get the current time in this time zoneZonedDateTime zonedDateTime = ZonedDateTime.now(zid);// Defining a formatterDateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");// Format ZonedDateTime as a stringString 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);@Overridepublic 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);}@Overridepublic 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,实现获取天气