跳到主要内容

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) {}

小结

本章我们学习了:

  1. Tool Calling 概念:让 AI 调用外部工具
  2. 定义工具:使用 @Tool@ToolParam 注解
  3. 注册工具:在 ChatClient 中启用工具
  4. 实际应用:数据库查询、HTTP 请求、邮件发送
  5. 最佳实践:描述、校验、错误处理、安全考虑

练习

  1. 实现一个天气查询助手
  2. 创建数据库查询工具
  3. 构建一个能发送邮件的 AI 助手

参考资源