Spring AI 实战系列 | 第 2 篇
Tool Calling:让 AI 调用外部函数
系列说明:本文为《Spring AI 实战系列 入门篇》第 2 篇
前置知识:完成第 1 篇(基础概念与快速上手)
预计阅读时间:15 分钟
📖 目录
一、为什么需要 Tool Calling?
1.1 AI 模型的局限性
尽管大语言模型(LLM)很强大,但它们有天然的缺陷:
| 局限性 | 示例 |
|---|---|
| ❌ 知识陈旧 | GPT-4 训练数据截至 2023 年,不知道今天的天气 |
| ❌ 无法访问实时信息 | 无法查询实时股价、新闻、库存 |
| ❌ 无法执行外部操作 | 不能发邮件、下订单、控制 IoT 设备 |
| ❌ 计算能力有限 | 数学计算可能出错 |
1.2 Tool Calling 解决什么问题?
Tool Calling = 扩展 AI 的能力边界
┌─────────────────────────────────────────────────────────┐
│ 没有 Tool Calling │
├─────────────────────────────────────────────────────────┤
│ 用户:"北京今天多少度?" │
│ AI:"我没有实时天气数据,无法回答。"(❌) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 有 Tool Calling │
├─────────────────────────────────────────────────────────┤
│ 用户:"北京今天多少度?" │
│ AI:调用 getWeather("北京") │
│ ↓ │
│ 工具返回:25°C,晴 │
│ ↓ │
│ AI:"北京今天天气晴朗,温度25°C。"(✅) │
└─────────────────────────────────────────────────────────┘
1.3 典型使用场景
| 场景 | 工具示例 |
|---|---|
| 🌤️ 天气查询 | getWeather(city) |
| 📅 日历管理 | createEvent(title, time) |
| 📧 邮件处理 | sendEmail(to, subject, body) |
| 📊 数据库查询 | query(sql) |
| 🔍 网络搜索 | search(query) |
| 💰 支付处理 | createPayment(amount) |
二、工作原理
2.1 执行流程
┌────────────────────────────────────────────────────────────┐
│ Tool Calling 完整流程 │
├────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 注册工具 │
│ ┌─────────────────┐ │
│ │ 天气工具 │ │
│ │ - name: get_weather │
│ │ - desc: 获取城市天气 │
│ │ - param: city │ │
│ └────────┬─────────┘ │
│ ↓ │
│ 2️⃣ 用户提问 │
│ "北京今天多少度?" │
│ ↓ │
│ 3️⃣ 模型决策 │
│ 模型判断需要调用 get_weather,参数 city="北京" │
│ ↓ │
│ 4️⃣ 执行工具 │
│ 应用调用 getWeather("北京") → 返回 "25°C, 晴" │
│ ↓ │
│ 5️⃣ 返回结果 │
│ 工具结果发送给模型 │
│ ↓ │
│ 6️⃣ 生成回答 │
│ "北京今天天气晴朗,气温25°C。" │
│ │
└────────────────────────────────────────────────────────────┘
2.2 Spring AI 中的核心组件
| 组件 | 作用 |
|---|---|
@Tool | 声明式定义工具 |
ToolCallback | 工具回调接口 |
ToolCallingManager | 工具执行管理器 |
ChatClient | 支持 .tools() 方法 |
三、声明式工具定义:@Tool 注解
3.1 基础用法
最简单的一个工具:
package com.example.demo.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
@Component // 需要注册为 Spring Bean
public class WeatherTools {
@Tool(description = "获取指定城市的当前天气")
public String getWeather(String city) {
// 这里调用真实的天气 API
return switch (city) {
case "北京" -> "25°C,晴";
case "上海" -> "28°C,多云";
case "深圳" -> "30°C,雷阵雨";
default -> "未知城市";
};
}
}
在 ChatClient 中使用:
@RestController
public class ToolController {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public ToolController(ChatClient.Builder builder, WeatherTools weatherTools) {
this.chatClient = builder.build();
this.weatherTools = weatherTools;
}
@GetMapping("/weather")
public String weather(@RequestParam String city) {
return chatClient.prompt()
.user("北京今天多少度?")
.tools(weatherTools) // 注入工具
.call()
.content();
}
}
3.2 带参数描述
使用 @ToolParam 为参数添加描述,帮助模型理解:
public class AdvancedTools {
@Tool(description = "设置闹钟")
public void setAlarm(
@ToolParam(description = "闹钟时间,格式:yyyy-MM-dd HH:mm") String time
) {
// 设置闹钟逻辑
System.out.println("闹钟已设置:" + time);
}
@Tool(description = "计算两个数的运算结果")
public double calculate(
@ToolParam(description = "第一个数字") double a,
@ToolParam(description = "运算符:add/subtract/multiply/divide") String operator,
@ToolParam(description = "第二个数字") double b
) {
return switch (operator) {
case "add" -> a + b;
case "subtract" -> a - b;
case "multiply" -> a * b;
case "divide" -> b != 0 ? a / b : 0;
default -> 0;
};
}
}
3.3 多工具组合
一个类可以定义多个工具:
@Component
public class DateTimeTools {
@Tool(description = "获取当前日期时间")
public String getCurrentDateTime() {
return java.time.LocalDateTime.now().toString();
}
@Tool(description = "计算指定天数后的日期")
public String addDays(
@ToolParam(description = "起始日期,yyyy-MM-dd格式") String startDate,
@ToolParam(description = "要增加的天数") int days
) {
// 日期计算逻辑
return "2026-04-01"; // 返回计算结果
}
@Tool(description = "获取用户所在时区")
public String getTimeZone() {
return java.time.ZoneId.systemDefault().toString();
}
}
使用多个工具:
@GetMapping("/ask")
public String ask(@RequestParam String question) {
DateTimeTools dateTimeTools = new DateTimeTools();
return chatClient.prompt()
.user(question)
.tools(dateTimeTools) // 一个实例包含多个工具
.call()
.content();
}
3.4 默认工具 vs 运行时工具
// 方式1:运行时工具(仅当前请求有效)
chatClient.prompt()
.user("今天几号?")
.tools(new DateTimeTools()) // 只在这个请求中使用
.call();
// 方式2:默认工具(所有请求共享)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new CommonTools()) // 所有请求都可用
.build();
四、编程式工具定义
当 @Tool 注解不够用时,可以用 MethodToolCallback 手动创建:
import org.springframework.ai.tool.support.MethodToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
@Service
public class CustomToolService {
// 需要手动包装的方法
public String searchProducts(String keyword, int limit) {
// 搜索产品逻辑
return "找到 3 个商品:商品A、商品B、商品C";
}
@Bean
public ToolCallback searchToolCallback() {
return MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder()
.name("search_products")
.description("搜索商品列表")
.inputSchema("""
{
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "搜索关键词"},
"limit": {"type": "integer", "description": "返回数量限制"}
},
"required": ["keyword"]
}
""")
.build())
.toolMethod(getMethod("searchProducts", String.class, int.class))
.toolObject(this)
.build();
}
private Method getMethod(String name, Class<?>... paramTypes) {
try {
return getClass().getMethod(name, paramTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
五、实战案例
5.1 案例一:AI 日程助手
@Component
public class CalendarTools {
private final List<Map<String, String>> events = new ArrayList<>();
@Tool(description = "创建日程事件")
public String createEvent(
@ToolParam(description = "事件标题") String title,
@ToolParam(description = "事件时间,ISO格式") String datetime,
@ToolParam(description = "事件描述") String description
) {
Map<String, String> event = Map.of(
"title", title,
"datetime", datetime,
"description", description
);
events.add(event);
return "日程已创建:" + title + ",时间:" + datetime;
}
@Tool(description = "查询指定日期的日程")
public String getEvents(
@ToolParam(description = "查询日期,yyyy-MM-dd格式") String date
) {
List<String> dayEvents = events.stream()
.filter(e -> e.get("datetime").startsWith(date))
.map(e -> "- " + e.get("title") + " (" + e.get("datetime") + ")")
.toList();
return dayEvents.isEmpty()
? date + " 没有日程安排"
: String.join("\n", dayEvents);
}
@Tool(description = "删除日程")
public String deleteEvent(
@ToolParam(description = "要删除的事件标题") String title
) {
boolean removed = events.removeIf(e -> e.get("title").equals(title));
return removed ? "已删除:" + title : "未找到日程:" + title;
}
}
@RestController
@RequestMapping("/calendar")
public class CalendarController {
private final ChatClient chatClient;
private final CalendarTools calendarTools;
public CalendarController(ChatClient.Builder builder, CalendarTools tools) {
this.chatClient = builder.build();
this.calendarTools = tools;
}
@GetMapping("/ask")
public String ask(@RequestParam String question) {
return chatClient.prompt()
.system("你是一个智能日程助手,可以帮用户创建、查询和删除日程。")
.user(question)
.tools(calendarTools)
.call()
.content();
}
}
测试:
# 创建日程
curl "http://localhost:8080/calendar/ask?question=帮我安排一个明天上午10点的会议"
# 查询日程
curl "http://localhost:8080/calendar/ask?question=明天有什么安排?"
# 删除日程
curl "http://localhost:8080/calendar/ask?question=取消明天的会议"
5.2 案例二:AI 计算助手
@Component
public class MathTools {
@Tool(description = "进行数学运算")
public double calculate(
@ToolParam(description = "第一个数") double a,
@ToolParam(description = "运算符:add/sub/mult/div/pow/sqrt") String op,
@ToolParam(description = "第二个数") double b
) {
return switch (op) {
case "add" -> a + b;
case "sub" -> a - b;
case "mult" -> a * b;
case "div" -> b != 0 ? a / b : Double.NaN;
case "pow" -> Math.pow(a, b);
case "sqrt" -> Math.sqrt(a);
default -> 0;
};
}
@Tool(description = "获取圆周率")
public double getPi() {
return Math.PI;
}
}
@RestController
@RequestMapping("/math")
public class MathController {
private final ChatClient chatClient;
public MathController(ChatClient.Builder builder, MathTools tools) {
this.chatClient = builder.build();
}
@GetMapping("/solve")
public String solve(@RequestParam String question) {
return chatClient.prompt()
.system("你是一个数学计算助手,用户提出数学问题时,使用 calculate 工具来精确计算。")
.user(question)
.tools(new MathTools())
.call()
.content();
}
}
测试:
curl "http://localhost:8080/math/solve?question=15的平方根是多少?"
curl "http://localhost:8080/math/solve?question=100加200等于多少?"
六、常见问题与最佳实践
6.1 常见问题
Q1: 模型不调用工具?
检查以下几点:
- 工具描述是否清晰明确?
- 工具参数是否有
@ToolParam描述? - 提示词是否引导模型使用工具?
- 模型训练参数太少,我本地用的qwen2.5:7b,偶尔就会出现AI回复了,但不调用工具。换成有免费额度的大模型就好
Q2: 工具返回结果格式?
@Tool
public String getData() {
return "返回的字符串"; // 返回 String,会自动转为 JSON
}
Q3: 工具执行失败怎么办?
@Tool
public String riskyOperation() {
try {
// 正常逻辑
return "成功";
} catch (Exception e) {
// 返回错误信息,模型会解释给用户
return "操作失败:" + e.getMessage();
}
}
6.2 最佳实践
| 实践 | 说明 |
|---|---|
| ✅ 详细描述 | @Tool(description = "获取...") 要写清楚 |
| ✅ 参数命名 | 参数名要有意义,如 cityName 而非 arg0 |
| ✅ 错误处理 | 工具内部做好 try-catch |
| ✅ 单工具类 | 相关工具放一个类,复杂应用多个类 |
| ✅ Spring Bean | 工具类加 @Component 自动注入 |
6.3 @Tool 注解参数
| 参数 | 说明 | 默认值 |
|---|---|---|
name | 工具名称 | 方法名 |
description | 工具描述(最重要) | 方法名 |
returnDirect | 是否直接返回结果 | false |
@Tool(
name = "get_weather",
description = "获取指定城市的当前天气,包括温度和天气状况",
returnDirect = false
)
public String getWeather(String city) { ... }
七、系列预告与回顾
7.1 本篇小结
- ✅ 理解了 Tool Calling 的价值与原理
- ✅ 掌握了
@Tool声明式定义 - ✅ 学会了多工具组合使用
- ✅ 完成了两个实战案例
7.2 系列回顾
| 篇目 | 内容 | 状态 |
|---|---|---|
| 第 1 篇 | 核心概念 + 快速上手 | ✅ 已完成 |
| 第 2 篇 | Tool Calling + 工具调用 | ✅ 本文 |
| 第 3 篇 | VectorStore + RAG | 🔜 下一篇 |
| 第 4 篇 | 结构化输出 | 🔜 待编写 |
| 第 5 篇 | Advisors 中间件 | 🔜 待编写 |
| 第 6 篇 | 国产模型集成 | 🔜 待编写 |
7.3 下篇预告
第 3 篇:VectorStore + RAG:构建私有知识库
- 向量数据库选型与配置
- 文档 ETL 管道
- Embedding 模型使用
- RAG 完整实现
📚 参考资料
-
Spring AI Tool Calling 官方文档
-
MethodToolCallback 文档
📌 引用说明:本文核心概念与技术描述参考自 Spring AI 官方文档(docs.spring.io/spring-ai/r…
关注公众号「AI日撰」,点击菜单「获取源码」获取完整代码(Gitee 仓库)。
系列:《Spring AI 实战系列 入门篇》第 2 篇(共 6 篇)