概述
Tool Calling(工具调用)是 Spring AI 框架中让 AI 模型与外部世界交互的核心能力。它允许 AI 模型在对话过程中识别需要调用外部工具的时机,生成结构化的调用参数,执行工具后将结果融入对话。
💡 提示: 与 Function Calling 的关系
Tool Calling 是 Spring AI 对各大模型厂商 Function Calling 能力的统一封装,提供了更加 Spring 友好的编程模型。
核心架构
整体流程
1 2 3 4 5 6 7 8 9 10
| ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 用户请求 │────►│ ChatClient │────►│ ChatModel │────►│ AI 模型 │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Tools │◄────│ToolCalling │◄────│ 工具调用 │ │ (工具实现) │ │ Manager │ │ 意图 │ └─────────────┘ └─────────────┘ └─────────────┘
|
关键组件
| 组件 |
职责 |
@Tool |
注解,将方法标记为可被 AI 调用的工具 |
@ToolParam |
注解,描述工具参数的元信息 |
ToolCallback |
接口,工具执行的核心抽象 |
ToolDefinition |
工具定义(名称、描述、输入 Schema) |
ToolCallingManager |
管理工具执行生命周期 |
ChatClient |
客户端,注册和调用工具的入口 |
Spring AI 提供两种定义工具的方式:Declarative(声明式) 和 Programmatic(编程式)。
对比总结
| 维度 |
Declarative(声明式) |
Programmatic(编程式) |
| 实现方式 |
@Tool + @ToolParam 注解 |
MethodToolCallback / FunctionToolCallback |
| Schema 生成 |
自动(基于方法签名) |
手动定义 inputSchema |
| 灵活性 |
低(依赖注解属性) |
高(完全控制所有配置) |
| 适用场景 |
大多数常规场景 |
动态工具、复杂定制需求 |
| 代码量 |
少 |
多 |
无论哪种方式,最终都会生成 ToolDefinition,这是 AI 模型理解工具的核心:
1 2 3 4 5
| public interface ToolDefinition { String name(); String description(); String inputSchema(); }
|
定义工具的三种方式
最简洁的声明式方式,Spring AI 自动生成 JSON Schema。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component;
@Component public class WeatherService {
@Tool(description = "获取指定城市的天气信息") public String getWeather( @ToolParam(description = "城市名称,如:北京、上海") String city, @ToolParam(description = "温度单位:celsius 或 fahrenheit") String unit) { return "当前 " + city + " 天气:晴,温度 25°C"; } }
|
⚠️ 重要: @Tool 注解属性
description:工具描述,AI 模型根据此决定何时调用(必填)
returnDirect:是否直接返回结果给用户,跳过模型后处理(默认 false)
当需要更精细控制时使用,可完全自定义 ToolDefinition。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import org.springframework.ai.tool.MethodToolCallback; import org.springframework.ai.tool.ToolDefinition; import org.springframework.ai.tool.ToolDefinitions; import org.springframework.util.ReflectionUtils;
Method method = WeatherService.class.getMethod("getWeather", String.class, String.class);
ToolCallback toolCallback = MethodToolCallback.builder() .toolDefinition(ToolDefinition.builder() .name("getWeather") .description("获取指定城市的天气信息") .inputSchema(""" { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["city"] } """) .build()) .toolMethod(method) .toolObject(new WeatherService()) .build();
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime"); ToolDefinition toolDefinition = ToolDefinitions.builder(method) .name("currentDateTime") .description("获取用户时区的当前日期时间") .build();
|
使用 Function、Supplier、Consumer、BiFunction 等函数式接口定义工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public record WeatherRequest(String location, Unit unit) {} public record WeatherResponse(double temp, Unit unit) {} public enum Unit { C, F }
public class WeatherFunction implements Function<WeatherRequest, WeatherResponse> { @Override public WeatherResponse apply(WeatherRequest request) { return new WeatherResponse(25.0, Unit.C); } }
ToolCallback weatherTool = FunctionToolCallback.builder("getWeather", new WeatherFunction()) .description("获取指定位置的天气信息") .inputType(WeatherRequest.class) .build();
ChatClient chatClient = ChatClient.builder(chatModel) .defaultTools(weatherTool) .build();
|
适合封装 MCP 工具或构建复杂的 Agent 应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class CustomToolCallback implements ToolCallback {
@Override public ToolDefinition getToolDefinition() { return ToolDefinition.builder() .name("customTool") .description("自定义工具") .inputSchema("{...}") .build(); }
@Override public ToolMetadata getToolMetadata() { return ToolMetadata.builder() .returnDirect(false) .build(); }
@Override public String call(String toolInput) { return "执行结果"; }
@Override public String call(String toolInput, ToolContext toolContext) { return call(toolInput); } }
|
注册和使用工具
方式一:Builder 级别注册(全局默认)
所有通过该 ChatClient 发起的请求都可使用这些工具。
1 2 3 4 5 6 7 8 9 10
| @Configuration public class AiConfig {
@Bean public ChatClient chatClient(ChatModel chatModel, WeatherService weatherService) { return ChatClient.builder(chatModel) .defaultTools(weatherService) .build(); } }
|
方式二:请求级别注册(按需使用)
针对特定请求注册工具,会覆盖默认工具。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Service public class ChatService {
private final ChatClient chatClient; private final WeatherService weatherService; private final StockService stockService;
public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .tools(weatherService, stockService) .call() .content(); } }
|
方式三:通过名称引用(需预先注册)
1 2 3 4 5 6 7 8 9 10 11
| ChatClient chatClient = ChatClient.builder(chatModel) .defaultToolCallbacks(weatherToolCallback) .build();
String response = chatClient.prompt() .user("北京天气如何?") .toolNames("getWeather") .call() .content();
|
参数处理
1 2 3 4 5 6 7 8 9 10 11 12
| public record BookingRequest( @ToolParam(description = "出发城市") String from, @ToolParam(description = "目的城市") String to, @ToolParam(description = "出发日期,格式:yyyy-MM-dd") String date, @ToolParam(description = "舱位类型", required = false) String cabinClass ) {}
@Tool(description = "查询航班信息") public List<Flight> searchFlights(BookingRequest request) { return flightService.search(request); }
|
📝 注意: 参数规则
- 默认所有参数为必需,除非显式标记
required = false
- Spring AI 自动根据 Java 类型生成 JSON Schema
- 支持基本类型、对象、集合等
自动 Schema 生成
Spring AI 根据方法签名自动生成输入 Schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Tool(description = "Set a user alarm") void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time)
{ "type": "object", "properties": { "time": { "type": "string", "description": "Time in ISO-8601 format" } }, "required": ["time"] }
|
returnDirect 机制
默认行为
工具执行结果返回给 AI 模型,模型整合后再响应用户。
1
| 用户 → 模型 → 调用工具 → 工具结果 → 模型整合 → 用户
|
returnDirect = true
工具结果直接返回给用户,跳过模型后处理。
1
| 用户 → 模型 → 调用工具 → 工具结果 → 用户(直接返回)
|
适用场景:
- RAG 检索结果无需再加工
- 某些工具作为 Agent 推理循环的终止点
- 减少不必要的 Token 消耗
1 2 3 4
| @Tool(description = "获取图书馆营业时间", returnDirect = true) public String getLibraryHours() { return "营业时间:周一至周五 9:00-20:00,周末 10:00-18:00"; }
|
⚠️ 警告: 多工具调用时的注意事项
如果一次请求调用多个工具,所有工具都必须设置 returnDirect = true 才会直接返回。否则结果仍会发送给模型。
当应用注册了大量工具时(几十甚至上百个),将所有工具定义发送给 LLM 会消耗大量 Token 并影响性能。Spring AI 提供了动态工具发现机制来解决这个问题。
核心思想
1 2 3
| 传统方式:注册 100 个工具 → 全部发送给 LLM → Token 爆炸 💥
动态发现:注册 100 个工具 → 只发送 ToolSearchTool → LLM 按需搜索 → 按需加载
|
工作流程
sequenceDiagram
participant U as 用户
participant C as ChatClient
participant S as ToolSearcher
participant L as LLM
Note over C,S: 启动时:索引所有工具,但不发送给 LLM
U->>C: "阿姆斯特丹天气如何?"
C->>L: Prompt + ToolSearchTool 定义
L->>C: 调用 searchTools("weather")
C->>S: 搜索匹配的工具
S->>C: 返回 getWeather 工具定义
C->>L: 补充 getWeather 工具定义
L->>C: 调用 getWeather("Amsterdam")
C->>U: "阿姆斯特丹当前天气..."
实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @SpringBootApplication public class Application {
@Bean CommandLineRunner demo(ChatClient.Builder builder, ToolSearcher toolSearcher) { return args -> { var advisor = ToolSearchToolCallAdvisor.builder() .toolSearcher(toolSearcher) .build();
ChatClient chatClient = builder .defaultTools(new MyTools()) .defaultAdvisors(advisor) .build();
var answer = chatClient.prompt(""" 帮我规划今天在阿姆斯特丹穿什么。 请推荐现在营业的服装店。 """).call().content();
System.out.println(answer); }; }
static class MyTools { @Tool(description = "获取指定位置和时间的天气") public String weather(String location, @ToolParam(description = "格式:YYYY-MM-DDTHH:mm") String atTime) { }
@Tool(description = "获取指定位置和时间营业的服装店") public List<String> clothing(String location, @ToolParam(description = "格式:YYYY-MM-DDTHH:mm") String openAtTime) { }
@Tool(description = "获取指定位置的当前日期时间") public String currentTime(String location) { }
} }
|
关键组件
| 组件 |
职责 |
ToolSearcher |
索引所有工具,提供搜索能力 |
ToolSearchToolCallAdvisor |
Advisor,拦截请求并启用动态搜索 |
ToolSearchTool |
暴露给 LLM 的元工具,用于搜索其他工具 |
适用场景
- 工具数量多(>10 个)
- 单次对话通常只用少数几个工具
- 需要优化 Token 消耗和响应速度
工具执行生命周期
ToolCallingManager 是管理工具执行的核心接口:
1 2 3 4 5 6 7 8
| public interface ToolCallingManager {
List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions);
ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse); }
|
内部执行 vs 用户控制执行
内部执行(默认)
Spring AI 自动处理工具调用循环:
1 2 3 4 5 6
| String response = chatClient.prompt() .user("北京和上海的天气如何?") .tools(weatherService) .call() .content();
|
用户控制执行
手动管理工具调用,适合需要自定义逻辑的场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ChatOptions chatOptions = ToolCallingChatOptions.builder() .toolCallbacks(new WeatherService()) .internalToolExecutionEnabled(false) .build();
Prompt prompt = new Prompt(List.of(new UserMessage("北京天气如何?")), chatOptions); ChatResponse response = chatModel.call(prompt);
while (response.hasToolCalls()) { ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response); prompt = new Prompt(result.conversationHistory(), chatOptions); response = chatModel.call(prompt); }
|
完整实战示例
场景:智能客服系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| @Component public class CustomerServiceTools {
private final OrderRepository orderRepository; private final ProductRepository productRepository;
@Tool(description = "根据订单号查询订单状态") public OrderInfo queryOrder( @ToolParam(description = "订单号,格式:ORD-XXXXXXXX") String orderId) { return orderRepository.findById(orderId) .map(order -> new OrderInfo(order.getStatus(), order.getEstimatedDelivery())) .orElseThrow(() -> new RuntimeException("订单不存在")); }
@Tool(description = "查询商品库存") public StockInfo checkStock( @ToolParam(description = "商品SKU编码") String sku, @ToolParam(description = "仓库代码", required = false) String warehouse) { return productRepository.checkStock(sku, warehouse); }
@Tool(description = "提交退款申请", returnDirect = true) public String submitRefund( @ToolParam(description = "订单号") String orderId, @ToolParam(description = "退款原因") String reason) { String refundId = refundService.submit(orderId, reason); return "退款申请已提交,退款单号:" + refundId + ",预计 3-5 个工作日到账。"; } }
@RestController @RequestMapping("/api/chat") public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatModel chatModel, CustomerServiceTools tools) { this.chatClient = ChatClient.builder(chatModel) .defaultSystem("你是一个专业的客服助手,请礼貌地帮助用户解决问题。") .defaultTools(tools) .build(); }
@PostMapping public String chat(@RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .call() .content(); } }
|
与其他技术的关系
1 2 3 4 5 6 7
| ┌─────────────────────────────────────────────────────────────┐ │ Spring AI Tool Calling │ │ (Java 生态的 AI 工具调用统一抽象层) │ ├─────────────────────────────────────────────────────────────┤ │ 各厂商 Function Calling │ │ OpenAI / Claude / 通义千问 / Gemini / 文心一言 等 │ └─────────────────────────────────────────────────────────────┘
|
Spring AI Tool Calling 的价值:
- 统一 API:屏蔽各厂商 Function Calling 的实现差异
- Spring 友好:注解驱动、依赖注入、与 Spring 生态无缝集成
- 类型安全:自动 JSON Schema 生成,编译期检查
- 可扩展:通过
ToolCallback 接口对接任意工具来源(如 MCP)
总结
| 要点 |
说明 |
| 定义工具 |
声明式 @Tool 或编程式 MethodToolCallback / FunctionToolCallback |
| 注册工具 |
Builder 级别(全局)或请求级别(按需) |
| 动态发现 |
ToolSearchToolCallAdvisor 实现按需加载,优化 Token |
| 执行控制 |
默认内部自动循环,可切换为手动控制 |
| 返回策略 |
returnDirect=true 可跳过模型后处理 |
| 核心接口 |
ToolCallback 是扩展点,ToolCallingManager 管理生命周期 |
相关链接
- Agent 工具调用技术详解 - Function Calling / MCP / Skills 对比
- WebFlux 简介 - 响应式编程基础(Spring AI 支持响应式)