1. Spring AI 2.0中的Tool/Function Calling基础概念
在AI应用开发中,Tool Calling(也称为Function Calling)是一种关键模式,它允许AI模型与外部API或工具进行交互。Spring AI 2.0对这一模式提供了全面的支持,让开发者能够更灵活地构建智能应用。
1.1 什么是Tool Calling
Tool Calling本质上是一种让AI模型能够调用外部功能的机制。想象一下,当你在与智能助手对话时,询问"明天上海的天气如何?",助手需要调用天气API来获取实时数据,这就是Tool Calling的典型应用场景。
在Spring AI中,Tool Calling通过ToolCallback接口实现,它包含三个核心部分:
- 工具定义(ToolDefinition):描述工具的名称、功能和输入参数
- 工具元数据(ToolMetadata):配置工具的行为特性
- 工具执行逻辑:实际执行工具调用的代码
1.2 为什么需要Function Calling
传统AI模型的局限性在于它们只能基于训练数据生成响应。通过Function Calling,我们可以:
- 扩展模型能力:让模型能够访问实时数据(如天气、股票)
- 执行具体操作:如发送邮件、更新数据库
- 集成现有系统:与企业内部API对接
Spring AI 2.0的独特之处在于它提供了多种工具定义方式,既支持基于Java方法的声明式定义,也支持函数式编程风格的工具创建。
2. 工具定义的两种核心方式
2.1 基于方法的工具定义(MethodToolCallback)
这是Spring AI中最直观的工具定义方式,允许你将现有的Java方法直接暴露为AI可调用的工具。
public class DateTimeTools { @Tool(description = "获取指定时区的当前时间") public static String getCurrentTime( @ToolParam(description = "时区ID,如Asia/Shanghai") String zoneId) { return ZonedDateTime.now(ZoneId.of(zoneId)).toString(); } }定义方法工具时需要注意:
- 方法可以是静态或实例方法
- 支持各种可见性(public/protected/private)
- 参数和返回值类型需要可序列化
- 可以使用@ToolParam注解增强参数描述
2.1.1 方法工具的注册方式
// 通过反射获取方法 Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentTime"); // 构建工具回调 ToolCallback toolCallback = MethodToolCallback.builder() .toolDefinition(ToolDefinitions.builder(method) .name("getTime") // 自定义工具名称 .build()) .toolMethod(method) .build();2.2 基于函数的工具定义(FunctionToolCallback)
对于更喜欢函数式编程的开发者,Spring AI提供了FunctionToolCallback:
public class WeatherService implements Function<WeatherRequest, WeatherResponse> { public WeatherResponse apply(WeatherRequest request) { // 调用天气API的实现 return weatherApi.fetch(request.location(), request.unit()); } } // 注册函数工具 ToolCallback weatherTool = FunctionToolCallback.builder() .name("currentWeather") .description("获取指定位置的天气信息") .inputType(WeatherRequest.class) .toolFunction(new WeatherService()) .build();函数工具的特点:
- 支持Function、Supplier、Consumer等函数式接口
- 输入输出必须是POJO或Void
- 需要显式指定输入类型和schema
3. 工具的高级配置与使用
3.1 参数schema的精细化控制
Spring AI会自动生成工具的JSON Schema,但我们可以通过注解进行精细控制:
public class CustomerService { @Tool(description = "更新客户信息") public void updateCustomer( @ToolParam(description = "客户ID", required = true) Long id, @Nullable String name, // 标记为可选参数 @ToolParam(description = "邮箱格式校验", required = false) @Pattern(regexp = "^.+@.+\\..+$") String email) { // 实现逻辑 } }支持的注解包括:
- @ToolParam:Spring AI专用注解
- @Schema:Swagger注解
- @JsonProperty:Jackson注解
- @Nullable:标记可选参数
3.2 工具执行上下文(ToolContext)
有时工具执行需要额外的上下文信息,而这些信息不应该暴露给AI模型:
public class OrderService { @Tool(description = "查询订单详情") public Order getOrder(Long orderId, ToolContext context) { String tenantId = (String) context.get("tenantId"); return orderRepository.findByOrderIdAndTenant(orderId, tenantId); } } // 调用时传入上下文 ChatClient.create(chatModel) .prompt("查询订单12345的详情") .tools(new OrderService()) .toolContext(Map.of("tenantId", "company_A")) .call();上下文的特点:
- 不会发送给AI模型
- 可以合并默认和运行时上下文
- 适合传递用户身份、租户信息等敏感数据
3.3 直接返回结果(Return Direct)
默认情况下,工具执行结果会被送回AI模型处理。但某些场景下,我们可能希望直接返回原始结果:
@Tool(description = "获取原始数据", returnDirect = true) public DataTable getRawData(String query) { return dataService.executeQuery(query); }适用场景包括:
- 结果不需要AI再加工
- 需要保持数据原始格式
- 性能敏感型操作
4. 工具执行的生命周期管理
4.1 框架控制的自动执行(推荐)
使用ChatClient时,Spring AI会自动处理整个工具调用生命周期:
String result = ChatClient.create(chatModel) .prompt("获取北京和上海的天气对比") .tools(weatherTool) .call() .content();执行流程:
- 发送用户问题和工具定义给模型
- 模型返回工具调用请求
- 框架执行工具并返回结果
- 模型生成最终响应
4.2 顾问控制的半自动执行
对于需要自定义流程的场景,可以显式配置ToolCallingAdvisor:
ToolCallingAdvisor advisor = ToolCallingAdvisor.builder() .toolCallingManager(toolCallingManager) .advisorOrder(300) .build(); ChatClient client = ChatClient.builder(chatModel) .defaultAdvisors(advisor) .build();这种模式下,你可以:
- 控制工具调用顺序
- 添加自定义拦截逻辑
- 集成对话历史管理
4.3 完全手动的执行控制
最高级别的控制权,适合特殊场景:
Prompt prompt = new Prompt("查询订单状态", options); ChatResponse response = chatModel.call(prompt); while (response.hasToolCalls()) { // 手动执行工具 ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response); // 构建新prompt prompt = new Prompt(result.conversationHistory(), options); response = chatModel.call(prompt); }手动控制的典型用例:
- 需要流式处理中间结果
- 实现自定义的审批流程
- 特殊的错误处理需求
5. 实战技巧与最佳实践
5.1 工具设计的黄金法则
单一职责原则:每个工具应该只做一件事
- 反例:一个工具既查询天气又发送邮件
- 正例:分离为getWeather和sendEmail两个工具
描述即文档:工具和参数的description要详细准确
@Tool(description = "发送邮件到指定地址。支持HTML内容。") public void sendEmail( @ToolParam(description = "收件人邮箱,多个地址用逗号分隔") String to, @ToolParam(description = "邮件主题,不超过100字符") String subject, @ToolParam(description = "邮件内容,支持HTML") String body) { // 实现 }输入验证:在工具内部进行严格验证
@Tool(description = "预订会议室") public BookingResult bookRoom( @ToolParam(description = "会议室ID") String roomId, @ToolParam(description = "开始时间,ISO8601格式") String startTime) { if (!isValidRoom(roomId)) { throw new ToolExecutionException("无效的会议室ID"); } // ... }
5.2 性能优化技巧
延迟加载:对于重量级工具
@Lazy @Component public class ReportGenerator { @Tool(description = "生成年度报表") public byte[] generateAnnualReport() { // 耗时操作 } }缓存常用结果:
@Tool(description = "获取城市信息") public CityInfo getCityInfo(String cityName) { return cache.get(cityName, () -> { return cityService.fetchFromDB(cityName); }); }批量处理支持:
@Tool(description = "批量查询用户信息") public List<UserInfo> getUsers(List<Long> userIds) { return userService.batchGet(userIds); }
5.3 安全最佳实践
权限控制:
@Tool(description = "删除用户") public void deleteUser(Long userId, ToolContext context) { if (!hasPermission(context.get("userRole"), "DELETE_USER")) { throw new SecurityException("权限不足"); } userService.delete(userId); }敏感数据过滤:
@Tool(description = "查询用户详情") public UserInfo getUser(Long userId) { User user = userRepository.findById(userId); return new UserInfo( user.getId(), user.getName(), null, // 不返回密码 maskEmail(user.getEmail()) ); }访问日志:
@Aspect @Component public class ToolLoggingAspect { @Around("@annotation(org.springframework.ai.tool.annotation.Tool)") public Object logToolCall(ProceedingJoinPoint joinPoint) throws Throwable { // 记录调用信息 Object result = joinPoint.proceed(); // 记录结果 return result; } }
5.4 调试与问题排查
工具调用日志:
logging.level.org.springframework.ai.tool=DEBUGSchema验证工具:
ToolDefinition definition = toolCallback.getToolDefinition(); System.out.println(JsonSchemaValidator.validate( definition.inputSchema(), toolInputJson ));模拟测试:
@SpringBootTest class WeatherToolTests { @Autowired ToolCallingManager toolCallingManager; @Test void testWeatherTool() { ToolCallback tool = getWeatherTool(); String input = "{\"location\":\"Shanghai\"}"; String result = tool.call(input, null); assertNotNull(result); } }
6. 高级应用场景
6.1 动态工具注册
某些场景下,我们需要根据运行时条件动态注册工具:
ChatClient client = ChatClient.create(chatModel); if (user.isPremium()) { client.tools(premiumTools); } else { client.tools(basicTools); } String response = client.prompt(query).call().content();6.2 工具组合与编排
通过组合多个工具实现复杂逻辑:
@Tool(description = "行程规划") public Itinerary planTrip( @ToolParam(description = "出发城市") String from, @ToolParam(description = "目的地") String to, @ToolParam(description = "出发日期") String date) { // 调用多个子工具 Weather weather = weatherTool.getWeather(to, date); Flight flight = flightTool.searchFlight(from, to, date); Hotel hotel = hotelTool.findHotel(to, date); return new Itinerary(weather, flight, hotel); }6.3 领域特定语言(DSL)集成
将工具与DSL结合,实现更自然的交互:
@Tool(description = "执行数据查询") public QueryResult runQuery( @ToolParam(description = "使用自然语言描述查询需求") String query) { // 将自然语言转换为SQL String sql = dslParser.parse(query); return dbClient.execute(sql); }6.4 长流程事务管理
对于需要多步骤的事务型操作:
@Tool(description = "电子商务订单流程") public OrderResult handleOrder( @ToolParam(description = "操作类型") String action, @ToolParam(description = "订单ID") Long orderId, ToolContext context) { Transaction tx = beginTransaction(); try { if ("create".equals(action)) { // 调用多个子工具 inventoryTool.reserve(items); paymentTool.charge(amount); shippingTool.schedule(order); } tx.commit(); } catch (Exception e) { tx.rollback(); throw e; } }