Tool Calling 工具调用
Tool Calling(也称 Function Calling)是让 LLM 具备"行动能力"的核心机制,是构建 AI Agent 的基础。
什么是 Tool Calling?
LLM 本身只能处理文本,无法直接访问外部系统。Tool Calling 让模型能够:
- 调用 REST API 获取实时数据
- 执行数据库查询
- 操作文件系统
- 调用业务系统接口
- 执行代码
用户:"北京今天天气怎么样,适合出门吗?"
│
▼
LLM 分析:需要调用天气工具
│
▼
Tool Call: getWeather(city="北京")
│
▼
工具执行:调用天气 API → "晴,25°C,空气质量优"
│
▼
LLM 综合:基于天气数据生成回答
│
▼
"北京今天天气晴朗,25°C,非常适合出门!"@Tool 注解驱动开发
Spring AI Alibaba 提供了最简洁的工具定义方式:
基础工具定义
java
@Component
public class WeatherTools {
@Tool(description = "获取指定城市的实时天气信息")
public WeatherInfo getWeather(
@ToolParam(description = "城市名称,如:北京、上海、广州") String city
) {
// 调用真实天气 API
return weatherApiClient.getWeather(city);
}
@Tool(description = "获取未来 N 天的天气预报")
public List<WeatherForecast> getWeatherForecast(
@ToolParam(description = "城市名称") String city,
@ToolParam(description = "预报天数,1-7 之间") int days
) {
return weatherApiClient.getForecast(city, days);
}
}
// 返回值类型(自动序列化为 JSON)
record WeatherInfo(
String city,
String condition,
int temperature,
int humidity,
String airQuality
) {}数据库查询工具
java
@Component
public class DatabaseTools {
@Autowired
private JdbcTemplate jdbcTemplate;
@Tool(description = "根据用户 ID 查询用户信息")
public UserInfo getUserById(
@ToolParam(description = "用户 ID") Long userId
) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new BeanPropertyRowMapper<>(UserInfo.class),
userId
);
}
@Tool(description = "查询订单列表,支持按状态和时间范围筛选")
public List<Order> queryOrders(
@ToolParam(description = "订单状态:PENDING/PAID/SHIPPED/COMPLETED") String status,
@ToolParam(description = "开始日期,格式 yyyy-MM-dd") String startDate,
@ToolParam(description = "结束日期,格式 yyyy-MM-dd") String endDate
) {
// 查询逻辑
return orderRepository.findByStatusAndDateRange(status, startDate, endDate);
}
}代码执行工具
java
@Component
public class CodeExecutionTools {
@Tool(description = "执行 Java 代码片段并返回结果(沙箱环境)")
public CodeResult executeJava(
@ToolParam(description = "要执行的 Java 代码") String code
) {
// 在沙箱中执行代码
return sandboxExecutor.execute(code, "java");
}
@Tool(description = "执行 SQL 查询(只读)")
public QueryResult executeSQL(
@ToolParam(description = "SELECT 查询语句") String sql
) {
// 安全检查:只允许 SELECT
if (!sql.trim().toUpperCase().startsWith("SELECT")) {
throw new IllegalArgumentException("只允许 SELECT 查询");
}
return jdbcTemplate.query(sql, new ColumnMapRowMapper());
}
}在 ChatClient 中使用工具
java
// 方式一:传入工具实例
String answer = chatClient.prompt()
.user("北京今天天气怎么样?")
.tools(new WeatherTools())
.call()
.content();
// 方式二:传入 Spring Bean(推荐)
@Autowired
private WeatherTools weatherTools;
@Autowired
private DatabaseTools databaseTools;
String answer = chatClient.prompt()
.user(question)
.tools(weatherTools, databaseTools) // 多个工具
.call()
.content();
// 方式三:默认工具(全局注册)
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultTools(weatherTools, databaseTools)
.build();
}工具调用流程详解
1. 用户发送消息
"查询用户 123 的最近 3 笔订单"
2. ChatClient 将工具定义(JSON Schema)注入 Prompt
{
"tools": [
{
"type": "function",
"function": {
"name": "queryOrders",
"description": "查询订单列表...",
"parameters": {
"type": "object",
"properties": {
"userId": {"type": "integer"},
"limit": {"type": "integer"}
}
}
}
}
]
}
3. LLM 决定调用工具,返回 ToolCall
{
"tool_calls": [{
"id": "call_abc123",
"function": {
"name": "queryOrders",
"arguments": "{\"userId\": 123, \"limit\": 3}"
}
}]
}
4. Spring AI 自动执行工具方法
queryOrders(123, 3) → [Order1, Order2, Order3]
5. 将工具结果注入对话,再次调用 LLM
ToolResponseMessage: [订单数据 JSON]
6. LLM 基于工具结果生成最终回答
"用户 123 的最近 3 笔订单为:..."并行工具调用
当一个问题需要多个工具时,LLM 可以并行调用:
java
// 问题:"比较北京和上海的天气,并查询两地的机票价格"
// LLM 会同时调用:
// - getWeather("北京")
// - getWeather("上海")
// - searchFlights("北京", "上海")
// - searchFlights("上海", "北京")
// Spring AI 自动处理并行调用,无需额外配置
String result = chatClient.prompt()
.user("比较北京和上海的天气,并查询两地的机票价格")
.tools(weatherTools, flightTools)
.call()
.content();工具返回值处理
java
// 返回简单类型
@Tool(description = "获取当前时间戳")
public long getCurrentTimestamp() {
return System.currentTimeMillis();
}
// 返回复杂对象(自动 JSON 序列化)
@Tool(description = "搜索商品")
public List<Product> searchProducts(String keyword) {
return productService.search(keyword);
}
// 返回字符串(直接传递给 LLM)
@Tool(description = "执行 Shell 命令")
public String executeShell(String command) {
// 安全检查...
return shellExecutor.execute(command);
}
// 工具执行失败处理
@Tool(description = "调用外部 API")
public ApiResult callExternalApi(String endpoint) {
try {
return httpClient.get(endpoint);
} catch (Exception e) {
// 返回错误信息,让 LLM 决定如何处理
return new ApiResult(false, "API 调用失败:" + e.getMessage(), null);
}
}工具权限控制
java
@Component
public class SecureTools {
@Autowired
private SecurityContext securityContext;
@Tool(description = "删除指定记录(需要管理员权限)")
public DeleteResult deleteRecord(
@ToolParam(description = "记录 ID") Long id
) {
// 权限检查
if (!securityContext.hasRole("ADMIN")) {
throw new AccessDeniedException("需要管理员权限");
}
return recordService.delete(id);
}
@Tool(description = "查询当前用户的数据")
public List<UserData> getMyData() {
// 自动获取当前用户,无需参数
Long userId = securityContext.getCurrentUserId();
return dataService.findByUserId(userId);
}
}ToolCallback 底层接口
如果需要更精细的控制,可以直接实现 ToolCallback:
java
public class DynamicTool implements ToolCallback {
@Override
public ToolDefinition getToolDefinition() {
return ToolDefinition.builder()
.name("dynamicSearch")
.description("动态搜索工具")
.inputSchema("""
{
"type": "object",
"properties": {
"query": {"type": "string"},
"maxResults": {"type": "integer", "default": 10}
},
"required": ["query"]
}
""")
.build();
}
@Override
public String call(String toolInput) {
// toolInput 是 JSON 字符串
Map<String, Object> params = JsonUtils.parse(toolInput);
String query = (String) params.get("query");
int maxResults = (int) params.getOrDefault("maxResults", 10);
List<SearchResult> results = searchEngine.search(query, maxResults);
return JsonUtils.toJson(results);
}
}最佳实践
工具设计原则
- 描述要精准:
description是 LLM 选择工具的依据,要清晰描述功能和适用场景 - 参数要明确:每个参数的
description要说明格式、范围、示例 - 单一职责:每个工具只做一件事,避免过于复杂的工具
- 幂等设计:工具应尽量设计为幂等的,避免重复调用产生副作用
- 错误处理:工具内部捕获异常,返回结构化错误信息而非抛出异常
安全注意事项
- 对写操作工具(删除、修改)添加权限检查
- SQL 工具只允许 SELECT,防止 SQL 注入
- Shell 工具需要严格的命令白名单
- 敏感数据工具要做脱敏处理