Tool Calling 工具调用
Tool Calling(工具调用)允许 AI 模型调用外部工具和 API,扩展 AI 的能力边界。本章介绍 Spring AI 的工具调用机制。
什么是 Tool Calling?
Tool Calling 使 AI 模型能够:
- 获取实时数据(天气、股价等)
- 执行操作(发送邮件、调用 API)
- 访问外部系统(数据库、文件系统)
┌─────────────────────────────────────────────────────────────┐
│ Tool Calling 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户请求 │
│ "北京今天的天气怎么样?" │
│ │ │
│ 2. AI 模型决定调用工具 │
│ tool: get_weather, args: {city: "北京"} │
│ │ │
│ 3. 应用执行工具 │
│ 调用天气 API,返回结果 │
│ │ │
│ 4. 将结果返回给 AI │
│ {temp: 25, weather: "晴"} │
│ │ │
│ 5. AI 生成最终回答 │
│ "北京今天天气晴朗,气温25度..." │
│ │
└─────────────────────────────────────────────────────────────┘
基本使用
定义工具
使用 @Tool 注解定义工具:
@Component
public class WeatherTools {
@Tool(description = "获取指定城市的当前天气信息")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,如:北京、上海") String city) {
// 调用天气 API
return weatherService.getWeatherByCity(city);
}
@Tool(description = "获取指定城市未来几天的天气预报")
public List<WeatherForecast> getWeatherForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "预报天数,1-7") int days) {
return weatherService.getForecast(city, days);
}
}
record WeatherInfo(String city, double temperature, String weather, int humidity) {}
record WeatherForecast(String date, String weather, double highTemp, double lowTemp) {}
注册和使用工具
@Service
public class AssistantService {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public AssistantService(ChatClient.Builder builder, WeatherTools weatherTools) {
this.weatherTools = weatherTools;
this.chatClient = builder
.build();
}
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.functions("getWeather", "getWeatherForecast") // 注册工具
.call()
.content();
}
}
自动注册工具
@Configuration
public class ToolConfig {
@Bean
@Description("获取当前时间")
public Function<TimeRequest, TimeResponse> getCurrentTime() {
return request -> {
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new TimeResponse(time);
};
}
@Bean
@Description("计算数学表达式")
public Function<CalcRequest, CalcResponse> calculator() {
return request -> {
// 使用脚本引擎计算表达式
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
try {
Object result = engine.eval(request.expression());
return new CalcResponse(result.toString());
} catch (Exception e) {
return new CalcResponse("Error: " + e.getMessage());
}
};
}
}
record TimeRequest() {}
record TimeResponse(String currentTime) {}
record CalcRequest(String expression) {}
record CalcResponse(String result) {}
实际应用示例
1. 数据库查询工具
@Component
public class DatabaseTools {
@Autowired
private JdbcTemplate jdbcTemplate;
@Tool(description = "执行只读 SQL 查询,用于查询数据库数据")
public String executeQuery(
@ToolParam(description = "SELECT 查询语句") String sql) {
// 安全检查:只允许 SELECT 语句
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
return "错误:只允许执行 SELECT 查询";
}
try {
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
return results.toString();
} catch (Exception e) {
return "查询错误:" + e.getMessage();
}
}
@Tool(description = "获取数据库表结构信息")
public String getTableSchema(
@ToolParam(description = "表名") String tableName) {
try {
String sql = "DESCRIBE " + tableName;
return jdbcTemplate.queryForList(sql).toString();
} catch (Exception e) {
return "获取表结构错误:" + e.getMessage();
}
}
}
2. HTTP 请求工具
@Component
public class HttpTools {
@Autowired
private RestTemplate restTemplate;
@Tool(description = "发送 HTTP GET 请求获取网页内容")
public String httpGet(
@ToolParam(description = "请求 URL") String url) {
try {
return restTemplate.getForObject(url, String.class);
} catch (Exception e) {
return "请求错误:" + e.getMessage();
}
}
@Tool(description = "发送 HTTP POST 请求")
public String httpPost(
@ToolParam(description = "请求 URL") String url,
@ToolParam(description = "请求体 JSON") String body) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(body, headers);
return restTemplate.postForObject(url, entity, String.class);
} catch (Exception e) {
return "请求错误:" + e.getMessage();
}
}
}
3. 邮件发送工具
@Component
public class EmailTools {
@Autowired
private JavaMailSender mailSender;
@Tool(description = "发送电子邮件")
public String sendEmail(
@ToolParam(description = "收件人邮箱") String to,
@ToolParam(description = "邮件主题") String subject,
@ToolParam(description = "邮件内容") String content) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
return "邮件发送成功";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
}
4. 文件操作工具
@Component
public class FileTools {
@Tool(description = "读取文件内容")
public String readFile(
@ToolParam(description = "文件路径") String filePath) {
try {
return Files.readString(Paths.get(filePath));
} catch (Exception e) {
return "读取文件错误:" + e.getMessage();
}
}
@Tool(description = "写入文件内容")
public String writeFile(
@ToolParam(description = "文件路径") String filePath,
@ToolParam(description = "文件内容") String content) {
try {
Files.writeString(Paths.get(filePath), content);
return "文件写入成功";
} catch (Exception e) {
return "写入文件错误:" + e.getMessage();
}
}
@Tool(description = "列出目录内容")
public String listDirectory(
@ToolParam(description = "目录路径") String dirPath) {
try {
return Files.list(Paths.get(dirPath))
.map(Path::toString)
.collect(Collectors.joining("\n"));
} catch (Exception e) {
return "列出目录错误:" + e.getMessage();
}
}
}
工具回调控制
手动控制工具执行
@GetMapping("/chat-with-tools")
public String chatWithTools(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.functions("getWeather", "getCurrentTime", "calculator")
.call()
.content();
}
查看工具调用过程
@GetMapping("/debug-tools")
public Map<String, Object> debugTools(@RequestParam String message) {
ChatResponse response = chatClient.prompt()
.user(message)
.functions("getWeather", "calculator")
.call()
.chatResponse();
Map<String, Object> result = new HashMap<>();
result.put("content", response.getResult().getOutput().getContent());
result.put("toolCalls", response.getMetadata().get("toolCalls"));
result.put("usage", response.getMetadata().getUsage());
return result;
}
工具最佳实践
1. 提供清晰的描述
// 好的描述
@Tool(description = """
获取指定城市的天气信息。
输入:城市名称(中文或英文)
输出:包含温度、天气状况、湿度的天气对象
注意:只支持中国主要城市
""")
public WeatherInfo getWeather(String city) { ... }
// 不好的描述
@Tool(description = "获取天气")
public WeatherInfo getWeather(String city) { ... }
2. 参数校验
@Tool(description = "计算器")
public String calculate(
@ToolParam(description = "数学表达式") String expression,
@ToolParam(description = "精度,小数位数", required = false) Integer precision) {
// 参数校验
if (expression == null || expression.isEmpty()) {
return "错误:表达式不能为空";
}
if (precision != null && (precision < 0 || precision > 10)) {
return "错误:精度必须在0-10之间";
}
// 执行计算
// ...
}
3. 错误处理
@Tool(description = "调用外部API")
public String callExternalApi(String url) {
try {
// 调用 API
return result;
} catch (HttpClientErrorException e) {
// 处理 HTTP 错误
return "API 调用失败:" + e.getStatusCode() + " " + e.getStatusText();
} catch (ResourceAccessException e) {
// 处理网络错误
return "网络连接失败,请稍后重试";
} catch (Exception e) {
// 处理其他错误
return "发生未知错误:" + e.getMessage();
}
}
4. 安全考虑
@Component
public class SecureTools {
// 白名单验证
private static final Set<String> ALLOWED_TABLES = Set.of("users", "products", "orders");
@Tool(description = "查询数据库表")
public String queryTable(
@ToolParam(description = "表名") String tableName,
@ToolParam(description = "筛选条件", required = false) String whereClause) {
// 安全检查
if (!ALLOWED_TABLES.contains(tableName)) {
return "错误:不允许访问该表";
}
// 执行查询
// ...
}
}
综合示例:智能助手
@RestController
@RequestMapping("/api/assistant")
public class AssistantController {
private final ChatClient chatClient;
public AssistantController(ChatClient.Builder builder,
WeatherTools weatherTools,
DatabaseTools databaseTools,
HttpTools httpTools) {
this.chatClient = builder
.defaultSystem("""
你是一个智能助手,可以调用以下工具帮助用户:
- 查询天气
- 查询数据库
- 发送 HTTP 请求
请根据用户需求选择合适的工具。
如果不需要调用工具,直接回答用户问题。
""")
.build();
}
@PostMapping("/chat")
public String chat(@RequestBody ChatRequest request) {
return chatClient.prompt()
.user(request.message())
.functions("getWeather", "executeQuery", "httpGet")
.call()
.content();
}
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.functions("getWeather", "executeQuery", "httpGet")
.stream()
.content();
}
}
record ChatRequest(String message) {}
小结
本章我们学习了:
- Tool Calling 概念:让 AI 调用外部工具
- 定义工具:使用
@Tool和@ToolParam注解 - 注册工具:在 ChatClient 中启用工具
- 实际应用:数据库查询、HTTP 请求、邮件发送
- 最佳实践:描述、校验、错误处理、安全考虑
练习
- 实现一个天气查询助手
- 创建数据库查询工具
- 构建一个能发送邮件的 AI 助手