大模型开发 - 28 Tool Calling:Spring AI 工具调用执行外部操作实战
本文介绍了如何利用Spring AI的Tool Calling机制扩展大语言模型(LLM)的能力边界。通过@Tool注解和ChatClient API,开发者可以轻松实现以下功能:1)让LLM调用预定义的Java方法获取实时信息(如当前时间);2)执行外部操作(如设置闹钟)。文章详细讲解了工具调用的核心原理,包括模型与应用的解耦设计、安全边界控制等,并通过两个实用案例(获取当前时间和设置闹钟)演示
文章目录
Pre
大模型开发 - 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 中,工具调用的核心思想是将模型的“意图”与应用程序的“执行”解耦。
- 模型角色:模型负责理解用户请求,并决定是否需要调用工具。如果需要,它会生成一个结构化的请求,包含工具名称和参数。
- 应用角色:应用程序(即你的 Spring Boot 服务)负责接收这个请求,找到对应的工具(一个 Java 方法),执行它,并将结果返回给模型。
- 安全边界:模型永远不会直接访问你的 API 或数据库。它只能“请求”调用,由你的应用代码来安全地执行,这是一个至关重要的安全设计。
Spring AI 通过 @Tool
注解和 ChatClient
API 极大地简化了这一过程,开发者只需关注业务逻辑的实现。
案例说明
我们将实现两个工具来演示两种主要用途:
- 信息检索工具 (
getCurrentDateTime
):用于获取用户时区的当前日期和时间。这解决了模型无法知晓“现在是什么时候”的问题。 - 执行操作工具 (
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
注解为参数提供了清晰的说明,帮助模型生成正确的参数格式。ChatClient
的tools()
方法是注册工具的关键。Spring AI 会自动扫描该对象上所有带@Tool
注解的方法,并将其暴露给模型。- 整个工具调用过程对开发者是透明的。你只需提供工具和提问,
ChatClient
会自动处理模型的工具调用请求、执行工具、并将结果回传给模型以生成最终答案。
时序图
下面的 Mermaid 时序图清晰地展示了“设置10分钟后闹钟”这个复杂请求的完整交互流程。
时序图解读:
- 用户发起一个复杂请求。
- 模型意识到需要先知道“现在”的时间,于是发起第一次工具调用。
- 应用程序执行
getCurrentDateTime
并返回结果。 - 模型拿到当前时间后,计算出10分钟后的具体时间点。
- 模型发起第二次工具调用,请求设置闹钟。
- 应用程序执行
setAlarm
并确认操作。 - 最后,模型综合所有信息,生成一个流畅的自然语言回答给用户。
这个过程可能涉及多轮工具调用,但对用户和开发者来说,整个体验是无缝的。
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 的两大短板:缺乏实时数据和无法执行操作。
核心优势
- 开发简单:只需添加注解,无需复杂的配置
- 类型安全:基于Java方法,编译时检查
- 自动管理:Spring AI自动处理工具注册和调用
- 灵活扩展:可以轻松添加新的工具和功能
- 安全可控:工具执行完全在应用控制下
应用场景
- 信息检索:获取实时数据(时间、天气、股价等)
- 操作执行:发送邮件、设置提醒、更新数据库
- 计算服务:复杂计算、数据分析、格式转换
- 系统集成:调用外部API、访问数据库、文件操作
通过工具调用,我们可以构建真正智能的AI应用,让大语言模型不仅能够理解和生成文本,还能够与现实世界进行交互,执行具体的业务逻辑。
更多推荐
所有评论(0)