在这里插入图片描述

Pre

大模型开发 - 01 Spring AI 核心特性一览

大模型开发 - 02 Spring AI Concepts

大模型开发 - 03 QuickStart_借助DeepSeekChatModel实现Spring AI 集成 DeepSeek

大模型开发 - 04 QuickStart_DeepSeek 模型调用流程源码解析:从 Prompt 到远程请求

大模型开发 - 05 QuickStart_接入阿里百炼平台:Spring AI Alibaba 与 DashScope SDK

大模型开发 - 06 QuickStart_本地大模型私有化部署实战:Ollama + Spring AI 全栈指南

大模型开发 - 07 ChatClient:构建统一、优雅的大模型交互接口

大模型开发 - 08 ChatClient:构建智能对话应用的流畅 API

大模型开发 - 09 ChatClient:基于 Spring AI 的多平台多模型动态切换实战

大模型开发 - 10 ChatClient:Advisors API 构建可插拔、可组合的智能对话增强体系

大模型开发 - 11 ChatClient:Advisor 机制详解:拦截、增强与自定义 AI 对话流程

大模型开发 - 12 Prompt:Spring AI 中的提示(Prompt)系统详解_从基础概念到高级工程实践

大模型开发 - 13 Prompt:提示词工程实战指南_Spring AI 中的提示设计、模板化与最佳实践

大模型开发 - 14 Chat Memory:实现跨轮次对话上下文管理

大模型开发 - 15 Tool Calling :从入门到实战,一步步构建智能Agent系统

大模型开发 - 16 Chat Memory:借助 ChatMemory + PromptChatMemoryAdvisor轻松实现大模型多轮对话记忆

大模型开发 - 17 Structured Output Converter:结构化输出转换器_从文本到结构化数据的可靠桥梁

大模型开发 - 18 Chat Memory:集成 JdbcChatMemoryRepository 实现大模型多轮对话记忆

大模型开发 - 19 Chat Memory:集成 BaseRedisChatMemoryRepository实现大模型多轮对话记忆

大模型开发 - 20 Chat Memory:多层次记忆架构_突破大模型对话中的 Token 上限瓶颈

大模型开发 - 21 Structured Output Converter:结构化输出功能实战指南

大模型开发 - 22 Multimodality API:多模态大模型与 Spring AI 的融合

大模型开发 - 23 Chat Model API:深入解析 Spring AI Chat Model API_构建统一、灵活、可扩展的 AI 对话系统

大模型开发 - 24 Embeddings Model API:深入解析 Spring AI Embeddings Model API_构建语义理解的基石

大模型开发 - 25 Image Model API:深入解析 Spring AI Image Model API_构建统一、灵活的 AI 图像生成系统

大模型开发 - 26 Origin Tools: Spring AI 结构化多聊天客户端实战

大模型开发 - 27 Tool Calling:Spring AI 中的工具调用指南

引言

大语言模型(LLM)虽然强大,但存在两个关键限制:无法访问实时信息(如当前时间、天气)和无法直接执行外部操作(如发送邮件、设置闹钟)。为了解决这些问题,Spring AI 引入了 工具调用(Tool Calling) 机制,允许模型在需要时调用开发者预定义的 Java 方法,从而扩展其能力边界。

接下来从零开始,构建一个支持工具调用的 Spring AI 应用,并通过清晰的代码和时序图,揭示其内部工作原理。

理论背景

在 Spring AI 中,工具调用的核心思想是将模型的“意图”与应用程序的“执行”解耦

  1. 模型角色:模型负责理解用户请求,并决定是否需要调用工具。如果需要,它会生成一个结构化的请求,包含工具名称和参数。
  2. 应用角色:应用程序(即你的 Spring Boot 服务)负责接收这个请求,找到对应的工具(一个 Java 方法),执行它,并将结果返回给模型。
  3. 安全边界:模型永远不会直接访问你的 API 或数据库。它只能“请求”调用,由你的应用代码来安全地执行,这是一个至关重要的安全设计。

Spring AI 通过 @Tool 注解和 ChatClient API 极大地简化了这一过程,开发者只需关注业务逻辑的实现。

案例说明

我们将实现两个工具来演示两种主要用途:

  1. 信息检索工具 (getCurrentDateTime):用于获取用户时区的当前日期和时间。这解决了模型无法知晓“现在是什么时候”的问题。
  2. 执行操作工具 (setAlarm):用于在指定时间设置一个闹钟。这展示了模型如何生成一个计划(“10分钟后设闹钟”),并委托应用去执行。

场景流程

  • 用户提问:“明天是几号?” → 模型调用 getCurrentDateTime 工具 → 应用返回当前时间 → 模型计算并回答。
  • 用户提问:“能帮我设置一个10分钟后的闹钟吗?” → 模型先调用 getCurrentDateTime 获取当前时间 → 模型计算出10分钟后的具体时间 → 模型调用 setAlarm 工具 → 应用打印设置成功的日志。

Java代码

首先,我们需要创建一个包含这两个工具的类,并使用 @Tool 注解进行标记。

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.context.i18n.LocaleContextHolder;

/**
 * 日期时间工具类,提供给AI模型调用的工具。
 */
public class DateTimeTools {

    /**
     * 信息检索工具:获取用户时区的当前日期和时间。
     * 
     * @return ISO-8601 格式的当前日期时间字符串。
     */
    @Tool(description = "Get the current date and time in the user's timezone")
    public String getCurrentDateTime() {
        // 使用Spring的LocaleContextHolder获取用户时区
        ZoneId userZoneId = LocaleContextHolder.getTimeZone().toZoneId();
        return LocalDateTime.now().atZone(userZoneId).toString();
    }

    /**
     * 执行操作工具:设置一个闹钟。
     * 
     * @param time ISO-8601 格式的时间字符串,例如 "2025-10-18T15:30:00"
     */
    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    public void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        // 在实际应用中,这里可能会调用系统API或发送消息
        System.out.println("【闹钟已设置】将在 " + alarmTime + " 响铃!");
    }
}

接下来,我们编写一个简单的测试类来演示如何使用 ChatClient 调用这些工具。

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;

public class ToolCallingDemo {

    public static void main(String[] args) {
        // 1. 创建一个ChatModel实例(这里以Ollama为例)
        ChatModel chatModel = OllamaChatModel.builder()
                .ollamaApi(new OllamaApi("http://localhost:11434")) // 确保Ollama服务已启动
                .modelName("llama3.1") // 使用支持工具调用的模型
                .build();

        // 2. 创建工具实例
        DateTimeTools dateTimeTools = new DateTimeTools();

        // --- 案例1: 信息检索 ---
        String response1 = ChatClient.create(chatModel)
                .prompt("What day is tomorrow?")
                .tools(dateTimeTools) // 将工具注册给本次对话
                .call()
                .content();
        System.out.println("【模型回答1】: " + response1);

        // --- 案例2: 执行操作 ---
        String response2 = ChatClient.create(chatModel)
                .prompt("Can you set an alarm 10 minutes from now?")
                .tools(dateTimeTools) // 将工具注册给本次对话
                .call()
                .content();
        System.out.println("【模型回答2】: " + response2);
        // 注意:setAlarm方法的输出会直接打印在控制台
    }
}

代码解读

  • @Tool 注解的 description 属性至关重要,它告诉模型何时以及如何使用这个工具。
  • @ToolParam 注解为参数提供了清晰的说明,帮助模型生成正确的参数格式。
  • ChatClienttools() 方法是注册工具的关键。Spring AI 会自动扫描该对象上所有带 @Tool 注解的方法,并将其暴露给模型。
  • 整个工具调用过程对开发者是透明的。你只需提供工具和提问,ChatClient 会自动处理模型的工具调用请求、执行工具、并将结果回传给模型以生成最终答案。

时序图

下面的 Mermaid 时序图清晰地展示了“设置10分钟后闹钟”这个复杂请求的完整交互流程。

用户 ChatClient AI模型 应用程序 (DateTimeTools) 提问: “设置10分钟后闹钟” 发送提问 + 工具定义 请求调用 getCurrentDateTime() 执行 getCurrentDateTime() 返回当前时间 (e.g., 2025-10-18T14:20:00) 发送工具执行结果 请求调用 setAlarm(time="2025-10-18T14:30:00") 执行 setAlarm("2025-10-18T14:30:00") (void, 但打印日志) 发送工具执行结果 生成最终回答 回答: “好的,已为您设置10分钟后的闹钟!” 用户 ChatClient AI模型 应用程序 (DateTimeTools)

时序图解读

  1. 用户发起一个复杂请求。
  2. 模型意识到需要先知道“现在”的时间,于是发起第一次工具调用。
  3. 应用程序执行 getCurrentDateTime 并返回结果。
  4. 模型拿到当前时间后,计算出10分钟后的具体时间点。
  5. 模型发起第二次工具调用,请求设置闹钟。
  6. 应用程序执行 setAlarm 并确认操作。
  7. 最后,模型综合所有信息,生成一个流畅的自然语言回答给用户。

这个过程可能涉及多轮工具调用,但对用户和开发者来说,整个体验是无缝的。

Code

项目结构

05-tool-calling/
├── src/main/java/com/artisan/toolcalling/
│   ├── Application.java                  # 主应用类
│   ├── controller/
│   │   └── ToolsController.java          # Web控制器
│   ├── service/
│   │   ├── DateTimeTools.java            # 日期时间工具类
│   │   ├── ToolService.java              # 原有工具服务
│   │   └── MockTicketService.java        # 模拟票务服务
│   └── demo/
│       └── ToolCallingDemo.java          # 工具调用演示类(当前为注释状态)
├── src/main/resources/
│   └── application.properties            # 配置文件
├── src/test/java/com/artisan/toolcalling/
│   └── ApplicationTests.java             # 测试类
├── README.md                             # 项目说明
└── pom.xml                               # Maven配置

核心实现

工具定义

我们创建了 DateTimeTools 类,包含多个工具方法:

@Service
public class DateTimeTools {

    /**
     * 信息检索工具:获取用户时区的当前日期和时间
     */
    @Tool(description = "Get the current date and time in the user's timezone")
    public String getCurrentDateTime() {
        ZoneId userZoneId = LocaleContextHolder.getTimeZone().toZoneId();
        return LocalDateTime.now().atZone(userZoneId).toString();
    }

    /**
     * 执行操作工具:设置一个闹钟
     */
    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    public void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("【闹钟已设置】将在 " + alarmTime + " 响铃!");
    }

    /**
     * 获取指定时区的当前时间
     */
    @Tool(description = "Get the current date and time in a specific timezone")
    public String getCurrentDateTimeInTimezone(@ToolParam(description = "Timezone ID, e.g., Asia/Shanghai, America/New_York") String timezone) {
        try {
            ZoneId zoneId = ZoneId.of(timezone);
            return LocalDateTime.now().atZone(zoneId).toString();
        } catch (Exception e) {
            return "无效的时区ID: " + timezone + ",请使用标准时区格式如 Asia/Shanghai";
        }
    }

    /**
     * 计算两个时间之间的差值
     */
    @Tool(description = "Calculate the time difference between two timestamps")
    public String calculateTimeDifference(
            @ToolParam(description = "Start time in ISO-8601 format") String startTime,
            @ToolParam(description = "End time in ISO-8601 format") String endTime) {
        try {
            LocalDateTime start = LocalDateTime.parse(startTime, DateTimeFormatter.ISO_DATE_TIME);
            LocalDateTime end = LocalDateTime.parse(endTime, DateTimeFormatter.ISO_DATE_TIME);
            
            long minutes = java.time.Duration.between(start, end).toMinutes();
            long hours = minutes / 60;
            long remainingMinutes = minutes % 60;
            
            return String.format("时间差:%d小时%d分钟", hours, remainingMinutes);
        } catch (Exception e) {
            return "时间格式错误,请使用ISO-8601格式";
        }
    }
}

关键注解说明

  • @Tool:标记方法为可被AI调用的工具
  • @ToolParam:为参数提供描述,帮助AI理解参数用途
  • description:工具描述至关重要,它告诉AI何时以及如何使用这个工具

Web接口实现

ToolsController 提供了RESTful API接口:

@RestController
public class ToolsController {

    private final ChatClient dateTimeChatClient;

    public ToolsController(ChatClient.Builder chatClientBuilder,
                           DateTimeTools dateTimeTools,
                           ChatModel chatModel) {
        this.dateTimeChatClient = chatClientBuilder
                .defaultSystem("""
                        # 角色
                        你是一个智能时间助手,可以帮助用户获取时间信息、设置闹钟、计算时间差等。
                        ## 要求
                        1. 当用户询问时间相关问题时,优先使用提供的工具
                        2. 设置闹钟时,请确保时间格式正确
                        3. 计算时间差时,请提供准确的结果
                        """)
                .defaultTools(dateTimeTools)
                .build();
    }

    /**
     * 日期时间工具调用接口
     */
    @RequestMapping("/datetime")
    public String dateTimeTool(@RequestParam(value = "message", defaultValue = "现在几点了?")
                                   String message) {
        return dateTimeChatClient.prompt()
                .user(message)
                .call().content();
    }

    /**
     * 获取当前时间
     */
    @RequestMapping("/time")
    public String getCurrentTime() {
        return dateTimeChatClient.prompt()
                .user("现在几点了?")
                .call().content();
    }

    /**
     * 设置闹钟
     */
    @RequestMapping("/alarm")
    public String setAlarm(@RequestParam(value = "minutes", defaultValue = "10") int minutes) {
        return dateTimeChatClient.prompt()
                .user("帮我设置" + minutes + "分钟后的闹钟")
                .call().content();
    }
}

配置说明

Maven依赖

<dependencies>
    <!-- 阿里百炼 DashScope -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>

    <!-- 聊天记忆支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-chat-memory</artifactId>
    </dependency>

    <!-- Web支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

应用配置

# Spring AI 工具调用配置

# 应用配置
spring.application.name=spring-ai-tools-demo
server.port=8080

# 阿里百炼 DashScope 配置
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}

# 日志配置
logging.level.com.artisan=DEBUG 
logging.level.org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor=DEBUG

API测试

启动应用后,可以通过以下接口测试:

# 获取当前时间
curl "http://localhost:8080/time"

# 设置10分钟后的闹钟
curl "http://localhost:8080/alarm?minutes=10"

# 自定义时间查询
curl "http://localhost:8080/datetime?message=纽约现在几点?"

# 运行演示
curl "http://localhost:8080/demo"

最佳实践

1. 工具描述的重要性

@Tool(description = "Get the current date and time in the user's timezone")
public String getCurrentDateTime() {
    // 实现
}
  • 描述要清晰、具体
  • 说明工具的用途和使用场景
  • 避免模糊或过于宽泛的描述

2. 参数验证和错误处理

@Tool(description = "Get the current date and time in a specific timezone")
public String getCurrentDateTimeInTimezone(@ToolParam(description = "Timezone ID") String timezone) {
    try {
        ZoneId zoneId = ZoneId.of(timezone);
        return LocalDateTime.now().atZone(zoneId).toString();
    } catch (Exception e) {
        return "无效的时区ID: " + timezone;
    }
}

3. 工具分类和组织

  • 按功能领域组织工具类
  • 使用清晰的方法命名
  • 提供完整的JavaDoc文档

4. 安全考虑

  • 工具方法应该验证输入参数
  • 避免直接暴露敏感操作
  • 实现适当的权限控制

5. 性能优化

  • 避免在工具方法中执行耗时操作
  • 考虑异步执行长时间任务
  • 合理使用缓存机制

结论

Spring AI 的工具调用功能是构建强大、实用 AI 应用的关键。通过本文的案例,我们看到:

  • @Tool 注解是定义工具的最简单方式,将普通 Java 方法转变为 AI 可调用的能力。
  • ChatClient 自动管理了整个工具调用的生命周期,极大地简化了开发。
  • 工具调用机制完美地解决了 LLM 的两大短板:缺乏实时数据无法执行操作

核心优势

  1. 开发简单:只需添加注解,无需复杂的配置
  2. 类型安全:基于Java方法,编译时检查
  3. 自动管理:Spring AI自动处理工具注册和调用
  4. 灵活扩展:可以轻松添加新的工具和功能
  5. 安全可控:工具执行完全在应用控制下

应用场景

  • 信息检索:获取实时数据(时间、天气、股价等)
  • 操作执行:发送邮件、设置提醒、更新数据库
  • 计算服务:复杂计算、数据分析、格式转换
  • 系统集成:调用外部API、访问数据库、文件操作

通过工具调用,我们可以构建真正智能的AI应用,让大语言模型不仅能够理解和生成文本,还能够与现实世界进行交互,执行具体的业务逻辑。

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐