一、概述

在现代应用开发中,如何高效地整合内部和外部服务是开发者面临的一个重要挑战。Spring AI 的 Tool Calling 功能提供了一种优雅的解决方案,通过将工具和服务抽象化,开发者可以轻松地调用外部 API 或内部服务,而无需复杂的集成逻辑。

Tool Calling 的核心优势在于:

  1. 简化开发流程:通过统一的工具调用接口,开发者可以专注于业务逻辑,而无需处理繁琐的 HTTP 请求和响应解析。
  2. 增强扩展性:工具可以动态注册和调用,支持快速集成新服务。
  3. 提升用户体验:通过工具调用,应用可以实时获取外部数据(如天气、假期等),为用户提供了一个更智能、更全面的服务体验。

在本篇文章中,我们将通过一个具体的案例,展示如何使用 Spring AI 的 Tool Calling 功能来调用天气服务和 OA 服务,帮助开发者快速上手这一强大功能。

二、功能介绍

Spring AI 的 Tool Calling 提供了以下核心功能:

  1. 工具注册与管理:支持动态注册工具,并通过注解或配置文件定义工具的描述和参数。
  2. 工具调用链:通过方法链式调用,开发者可以轻松组合多个工具,实现复杂业务逻辑。

在本案例中,我们将实现以下功能:

  • 调用外部天气 API,获取指定城市的天气预报。
  • 调用内部 OA 服务,查询员工剩余假期和提交请假申请。
  • 对比两种实现方式(无工具调用 vs 工具调用),展示 Tool Calling 的优势。

三、环境准备

在开始之前,请确保你的环境满足以下要求:

  • 操作系统:Windows 11
  • Java 版本:JDK 17+(请注意 Spring Boot 3.4.4 的兼容性)
  • 依赖管理:Maven 3.8.3+
  • 天气服务:需要注册天气服务 API,可以免费用一段时间,确保我们的验证是没问题的 天气服务账号申请
  • 阿里云百炼平台账号申请 后,可以查看到以下模型的选择
    在这里插入图片描述

四、Spring AI 集成:完整代码实现

1. 代码结构

│─src
│    └─main
│        ├─java
│        │  └─chat
│        │      │  ChatApplication.java
│        │      ├─common
│        │      │      ChatInit.java
│        │      ├─controller
│        │      │      ToolCallController.java
│        │      │      
│        │      └─service
│        │              OaService.java
│        │              WeatherService.java
│        │              
│        └─resources
│                application.yml
└─pom.xml

说明:项目结构遵循标准的 Spring Boot 项目布局,分为控制器、服务和配置等模块,便于代码的组织和维护。

2. 初始化类(ChatInit.java)

@Slf4j
@Configuration
@RequiredArgsConstructor
public class ChatInit {

    private final ChatModel chatModel;

    @Value("${spring.ai.toolcalling.weather.api-key}")
    private String WEATHER_API_KEY;

    @Bean
    public WebClient webClient() {
        log.info("WebClient Bean 已经被创建!");
        return WebClient.builder()
                .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
                .defaultHeader("key", WEATHER_API_KEY)
                .build();
    }

    @Bean
    public ChatClient chatClient() {
        return ChatClient.builder(chatModel)
                .defaultSystem("你是一位专业且细致的助手。在遇到不确定或不明确的信息时," +
                        "会主动询问用户以获取更多信息。回答问题时," +
                        "你倾向于使用简洁、条理清晰的语言。如果信息复杂或包含多个部分," +
                        "请确保每个部分都有适当的标题或编号,以创建分层结构。")
                .build();
    }
}

说明

  • @Configuration 注解表明这是一个配置类,用于定义 Spring 容器中的 Bean。
  • WebClient 是 Spring 提供的用于 HTTP 请求的客户端,这里配置了默认的请求头,包括天气 API 的密钥。
  • ChatClient 是 Spring AI 提供的聊天客户端,用于处理与用户的对话逻辑。

3. 控制器(ToolCallController.java)

@Slf4j
@RestController
@RequestMapping("/spring/ai/")
public class ToolCallController {

    @Autowired
    private ChatClient chatClient;
    @Autowired
    private WebClient webClient;

    /**
     * 无工具版聊天接口
     */
    @GetMapping("/chat")
    public String simpleChat(@RequestParam String message) {
        log.info("simpleChat input message --> [{}]", message);
        return chatClient.prompt(message).call().content();
    }

    /**
     * 调用工具版聊天接口 - 使用方法链
     */
    @GetMapping("/chat-tool-method")
    public String chatToolMethod(@RequestParam String message) {
        log.info("chatToolMethod input message --> [{}]", message);
        return chatClient.prompt(message)
                .tools(
                        new WeatherService(webClient),
                        new OaService())
                .call()
                .content();
    }
}

说明

  • @RestController 注解表明这是一个 RESTful 控制器,用于处理 HTTP 请求。
  • simpleChat 方法展示了不使用工具调用的简单聊天接口,直接通过 ChatClient 处理用户消息。
  • chatToolMethod 方法展示了如何通过工具调用链调用多个工具(天气服务和 OA 服务),实现更复杂的业务逻辑。

4. Tool Calling 的 OA 服务和天气服务

OaService.java

@Slf4j
public class OaService {

    private static int LEFT_DAYS = 5;

    @Tool(description = "员工剩余假期查询:查询员工还有几天的假期可以请")
    public String getCurrentRemainingHoliday() {
        return "目前,你还有 【" + LEFT_DAYS + "】 天的假期可以使用";
    }

    @Tool(description = "员工请假,需要传用户id(userId),和需要请假的天数 (days)")
    public String askForLeave(@ToolParam(description = "用户的工号") String userId,
                              @ToolParam(description = "需要请假的天数") String days) {
        if (!StringUtils.isNumeric(days)) {
            throw new IllegalArgumentException("days参数必须是数字");
        }
        int dayInt = Integer.parseInt(days);
        if (dayInt >= LEFT_DAYS) {
            return "你的假期不足,无法请假";
        }
        return "好的,员工【" + userId + "】, 已经请假【" + days + "】天,请好好享受假期";
    }
}

说明

  • @Tool 注解用于定义工具的描述,方便在调用时识别工具的功能。
  • getCurrentRemainingHoliday 方法用于查询员工剩余假期。
  • askForLeave 方法用于处理员工请假申请,包括参数验证和业务逻辑处理。

WeatherService.java

@Slf4j
@Service
public class WeatherService {

    private static final String WEATHER_API_URL = "https://api.weatherapi.com/v1/forecast.json";
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final WebClient webClient;

    public WeatherService(WebClient webClient) {
        this.webClient = webClient;
    }

    @Tool(description = "使用 api.weather 获取天的预报和天气情况.")
    public String getWeatherServiceMethod(@ToolParam(description = "城市名称") String city,
                                          @ToolParam(description = "天气预报的天数。值的范围为1到14") int days) {

        if (StringUtils.isBlank(city)) {
            log.error("无效请求,必须传城市名称");
            return null;
        }
        String location = this.preprocessLocation(city);
        String url = UriComponentsBuilder.fromHttpUrl(WEATHER_API_URL)
                .queryParam("q", location)
                .queryParam("days", days)
                .toUriString();

        log.info("url : {}", url);
        String result = "获取天气数据失败";
        try {
            result = webClient.get()
                    .uri(url)
                    .retrieve()
                    .bodyToMono(String.class)
                    .block();

            log.info("获取天气数据成功: {}", result);
        } catch (Exception e) {
            log.error("获取天气数据失败: {}", e.getMessage(), e);
        }
        return result;
    }

    /**
     * 处理中文地名
     * @param location
     * @return
     */
    public String preprocessLocation(String location) {
        if (containsChinese(location)) {
            return PinyinUtil.getPinyin(location, "");
        }
        return location;
    }

    /**
     * 判断是否包含中文
     * @param str
     * @return
     */
    public boolean containsChinese(String str) {
        return str.matches(".*[\u4e00-\u9fa5].*");
    }
}

说明

  • @Service 注解表明这是一个服务类,用于处理业务逻辑。
  • getWeatherServiceMethod 方法通过调用外部天气 API 获取指定城市的天气预报,包括参数验证和错误处理。
  • preprocessLocation 方法用于处理中文地名,将其转换为拼音以适应 API 的要求。
  • containsChinese 方法用于判断输入字符串是否包含中文。

5. 启动类(ChatApplication.java)

@SpringBootApplication
public class ChatApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChatApplication.class, args);
    }
}

说明

  • @SpringBootApplication 是 Spring Boot 的主注解,用于标记应用的入口类。
  • main 方法是应用的启动入口,通过 SpringApplication.run 启动 Spring 容器。

6. 配置文件(application.yml)

server:
  port: 8080
logging:
  level:
    org:
      springframework: info
spring:
  application:
    name: Hello-Spring-AI
  ai:
    dashscope:
      # 注意这个是使用阿里云百炼平台的API-KEY
      api-key: sk-xxxxxxxxxxxxxxxxxx
      model: qwq-plus
    toolcalling:
      time:
        enabled: true
      weather:
        enabled: true
        # 注意这个是weather.api的key
        api-key: xxxxxxxxxxxxxxxxxxxxxx

说明

  • 配置文件定义了应用的基本设置,包括服务器端口、日志级别和 Spring AI 的相关配置。
  • spring.ai 配置块中包含了阿里云百炼平台的 API 密钥和模型选择。
  • toolcalling 配置块中启用了天气工具,并提供了天气 API 的密钥。

7. POM 文件

<properties>
    <java.version>23</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>3.4.4</spring-boot.version>
    <spring-ai.version>1.0.0-M6</spring-ai.version>
    <spring-alibaba.version>1.0.0-M6.1</spring-alibaba.version>
    <hutool.version>5.8.35</hutool.version>
    <pinyin4j.version>2.5.1</pinyin4j.version>
    <maven.compiler.version>3.11.0</maven.compiler.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>${spring-boot.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter</artifactId>
        <version>${spring-alibaba.version}</version>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>${hutool.version}</version>
    </dependency>
    <dependency>
        <groupId>com.belerweb</groupId>
        <artifactId>pinyin4j</artifactId>
        <version>${pinyin4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.32</version>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.17.0</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven.compiler.version}</version>
            <configuration>
                <release>${java.version}</release>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>1.18.32</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

<repositories>
    <repository>
        <id>alimaven</id>
        <name>aliyun maven</name>
        <url>https://maven.aliyun.com/repository/public</url>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <releases>
            <enabled>false</enabled>
        </releases>
    </repository>
</repositories>

说明

  • POM 文件定义了项目的依赖和构建配置。
  • 依赖部分包括 Spring Boot、Spring AI、工具库(如 Hutool 和 Pinyin4j)等。
  • dependencyManagement 用于管理依赖的版本,确保一致性。
  • build 部分配置了编译器插件和 Spring Boot 打包插件。
  • repositories 配置了 Maven 的仓库地址,以便获取所需的依赖。

五、验证与效果对比

为了验证 Spring AI 的 Tool Calling 功能是否能够正确解析用户需求并调用多个工具,我们设计了一个具体的测试场景,并对比了两种实现方式的效果。

验证目标

  1. 工具调用的准确性:验证工具调用接口是否能够正确解析用户问题,并调用天气服务和 OA 服务。
  2. 对比两种实现方式:对比无工具调用接口和工具调用接口在处理复杂需求时的表现。
  3. AI 生成的稳定性:提醒读者注意 AI 生成内容的潜在不稳定性,并提供应对策略。

测试场景

我们向两个接口发送相同的请求:

我计划明天从广州到北京,帮我看下这2个地方的天气?再帮我看下我还剩几天假期?

验证过程

1. 无工具调用接口

请求地址http://127.0.0.1:8080/spring/ai/chat

请求参数

  • message: 我计划明天从广州到北京,帮我看下这2个地方的天气?再帮我看下我还剩几天假期?

响应结果
在这里插入图片描述

说明

  • 无工具调用接口只能基于预设的逻辑进行简单回复,无法调用外部服务获取实时数据。
  • 这种方式适合处理简单的聊天逻辑,但对于复杂需求(如实时天气查询和假期管理)显得力不从心。

2. 工具调用接口

请求地址http://127.0.0.1:8080/spring/ai/chat-tool-method

请求参数

  • message: 我计划明天从广州到北京,帮我看下这2个地方的天气?再帮我看下我还剩几天假期?

理想响应结果
在这里插入图片描述

说明

  • 工具调用接口通过调用天气服务和 OA 服务,能够提供更全面和准确的响应。
  • 这种方式适合处理复杂的业务逻辑,但需要确保工具的稳定性和正确性。

验证效果总结

对比维度 无工具调用接口 工具调用接口
实时数据支持 不支持 支持(通过调用外部服务获取实时数据)
复杂需求处理 仅能提供简单回复 能够解析复杂需求并调用多个工具
扩展性 低(无法动态集成新服务) 高(支持动态注册和调用新工具)
用户体验 有限(无法提供全面信息) 更佳(提供全面且准确的信息)

通过对比可以看出,工具调用接口在处理复杂需求时具有明显优势,但需要开发者注意 AI 生成的不稳定性,并采取相应的应对措施。

六、再扯几句

Spring AI 的 Tool Calling 功能为开发者提供了一种高效、灵活的方式来整合内部和外部服务。通过工具调用,开发者可以:

  1. 快速集成多种服务,减少重复开发。
  2. 提升应用的智能化水平,为用户提供了一个更全面的服务体验。
  3. 简化复杂业务逻辑的实现,降低开发和维护成本。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐