在大模型应用开发中,一个核心挑战是如何让语言模型“走出”自身的知识边界,与外部系统进行交互。这就是 工具调用(Tool Calling) 的意义所在。

工具调用 是指语言模型根据用户输入判断是否需要调用某个外部 API 或执行某项操作,并生成结构化的调用请求(包含工具名和参数),由客户端应用程序负责执行该调用并将结果返回给模型,最终由模型整合信息并生成自然语言响应。

它主要应用于两大场景:

  1. 信息检索:从数据库、网络服务、文件系统等外部来源获取实时或私有数据,扩展模型的知识能力。
  2. 采取行动:触发系统内的具体操作,如发送邮件、设置提醒、创建任务等,实现自动化流程。

本文将结合完整的代码示例,带你深入掌握 Spring AI 中实现工具调用的两种方式 —— 方法作为工具函数作为工具,并通过一个天气查询系统的实战案例,全面解析其工作原理与最佳实践。

虽然我们常说“模型调用了工具”,但实际上,模型本身并不执行任何外部逻辑。真正的执行责任在于客户端应用程序。整个过程是一个循环的协作流程:

  1. 应用程序构建 ChatRequest,并将可用的 工具定义(Tool Definition) 注入其中;
  2. 请求被发送至大模型;
  3. 模型分析用户意图,若需调用工具,则返回一个结构化指令(含工具名称和参数);
  4. 客户端接收到响应后,解析出工具调用请求;
  5. 工具调度器根据工具名称查找对应的工具实现并执行;
  6. 执行结果被封装并再次发送回模型;
  7. 模型结合工具返回的结果生成最终的人类可读回复;
  8. 若模型仍需调用其他工具,则重复上述流程,直到完成所有步骤。

这个过程可能涉及多次往返通信(multi-turn),确保复杂任务的顺利完成。

方法一:使用 @Tool 注解定义工具

Spring AI 支持通过注解的方式将普通 Java 方法转化为可被模型识别的“工具”。

示例:时间与闹钟工具

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.context.i18n.LocaleContextHolder;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class LocalDateTimeTool {

    @Tool(description = "Get the current date and time in the user's timezone")
    public String getCurrentDateTime() {
        System.out.println("Get the current date!");
        return LocalDateTime.now()
                .atZone(LocaleContextHolder.getTimeZone().toZoneId())
                .toString();
    }

    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    public void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }
}
注解说明:
注解 参数 说明
@Tool name 工具名称,默认为方法名
description 必须提供!帮助模型理解何时调用此工具
returnDirect 是否直接返回结果给用户(默认 false
resultConverter 自定义结果转字符串逻辑
@ToolParam description 参数描述,强烈建议填写
required 是否必填
控制器调用示例:
@GetMapping("/annotate/setAlarm")
public String setAlarm(String userInput) {
    return chatClient.prompt()
            .tools(new LocalDateTimeTool()) // 注册整个类中的 @Tool 方法
            .user(userInput)
            .call()
            .content();
}

测试请求

GET /tool/annotate/setAlarm?userInput=请帮我设置10分钟后的闹钟

模型会自动提取时间并格式化为 ISO-8601 字符串传入 setAlarm() 方法。

使用限制

当使用方法作为工具时,以下类型不能用于参数或返回值:

  • Optional<T>
  • 异步类型(如 CompletableFuture, Future
  • 响应式类型(如 Mono, Flux, Flow
  • 函数式接口(如 Function, Supplier, Consumer

方法二:使用函数式编程定义工具

Spring AI 还支持以 Function<I, O> 形式定义工具,更适合无状态的服务逻辑。

示例:天气查询服务

public class WeatherService implements Function<WeatherService.WeatherRequest, WeatherService.WeatherResponse> {

    public record WeatherRequest(String location, Unit unit) {}
    public record WeatherResponse(double temp, Unit unit) {}
    public enum Unit { C, F }

    @Override
    public WeatherResponse apply(WeatherRequest request) {
        System.out.println("获取{" + request.location + "}的天气");
        double temperature = Math.random() * 50; // 模拟随机温度
        return new WeatherResponse(temperature, Unit.C);
    }
}

方式 A:手动构建 FunctionToolCallback

适用于灵活注册、动态管理工具场景。

@GetMapping("/method/getWeather")
public String getWeather(String userInput) {
    FunctionToolCallback<WeatherService.WeatherRequest, WeatherService.WeatherResponse> callback =
        FunctionToolCallback.builder("currentWeather", new WeatherService())
            .description("Get the weather in location")
            .inputType(WeatherService.WeatherRequest.class)
            .build();

    return chatClient.prompt()
            .toolCallbacks(callback)
            .user(userInput)
            .call()
            .content();
}

方式 B:通过 @Bean 注册工具(推荐)

利用 Spring IoC 容器统一管理工具实例,更符合我们的开发习惯。

@Configuration(proxyBeanMethods = false)
public class WeatherTools {

    public static final String CURRENT_WEATHER_TOOL = "currentWeather";

    @Bean(CURRENT_WEATHER_TOOL)
    @Description("Get the weather in location")
    public Function<WeatherRequest, WeatherResponse> currentWeather() {
        return new WeatherService();
    }
}

注意:@Description 注解用于提供工具描述,若不加则默认使用 Bean 名称作为描述。

调用控制器:
@GetMapping("/spring/getWeather")
public String getWeatherBySpring(String userInput) {
    return chatClient.prompt()
            .toolNames(WeatherTools.CURRENT_WEATHER_TOOL) // 仅传入 Bean 名称
            .user(userInput)
            .call()
            .content();
}

测试请求

GET /tool/spring/getWeather?userInput=上海今天热吗?

模型会正确解析出地点“上海”,调用 currentWeather 工具,并结合返回温度生成人性化回答。

函数工具的类型限制

以下类型不支持作为函数工具的输入或输出:

  • 原始类型(primitive types,如 int, double
  • Optional<T>
  • 集合类型(如 List, Map, Set, 数组)
  • 异步/响应式类型

✅正确做法是使用 RecordPOJO 封装参数和返回值,如上文的 WeatherRequestWeatherResponse

总结对比

特性 方法工具(@Tool) 函数工具(Function)
定义方式 类中带注解的方法 Function<I, O> 实现
注册方式 .tools(instance) .toolCallbacks().toolNames(beanName)
适合场景 多个相关功能组合(如时间处理) 单一职责服务(如天气、翻译)
管理方式 手动实例化 可交由 Spring 容器管理(@Bean)
类型限制 不支持异步/响应式等 不支持原始类型、集合等

最佳实践建议

  1. 优先使用 @Bean + Function 模式:便于依赖注入、单元测试和统一管理;
  2. 务必填写 description:高质量的描述能显著提升模型选择工具的准确性;
  3. 合理命名工具和参数:语义清晰有助于模型理解和推理;
  4. 避免副作用过重的操作:工具应尽量保持幂等性和安全性;
  5. 记录日志便于调试:观察模型是否准确识别了工具调用时机;
  6. 考虑错误处理机制:工具执行失败时应有兜底策略。

结语

Spring AI 对工具调用的抽象极大简化了大模型与现实世界的连接。无论是将已有方法快速暴露为工具,还是构建新的函数式服务,Spring AI 都提供了简洁而强大的支持。

本文使用 @Tool 注解创建了时间工具和闹钟功能,利用 Function + @Bean 构建了可复用的天气查询服务,介绍了工具调用背后的多轮交互机制和常见类型限制陷阱。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐