SpringAI——函数工具(Function Calling)

该文章已生成可运行项目,

        Function Calling允许大型语言模型(LLM)在必要时调用一个或多个可用的工具,这些工具通常由开发者定义。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。开发者定义好函数工具,将工具注册给大模型,大模型通过工具的描述来决定是否调用或者调用哪一个工具,从用户输入中提取函数工具的参数,从而成功调用函数。调用的函数返回结果将作为大模型回答结果的一部分。

接下来我们学习如何实现函数工具的定义与注册

首先我写了一个简单的天气的Service类

@Slf4j
@Service
public class WeatherService {

  
    /**
     * 获取某城市某天的天气
     * @param city
     * @param date
     * @return
     */
    public String getWeatherByDate(String city,String date) {
        log.info("调用函数工具: 城市:{} 时间:{}", city, date);
        //TODO 调用天气查询接口
        String weather = "阴天";
        return date + city + "的天气情况为:" + weather;
    }
    
}

接下来就是函数工具类的定义了,其格式如下:

函数定义如下:
@Configuration
public class FunctionToolConfig {
    @Bean
    @Description("函数描述....") //AI大模型就是通过该描述来决定是否调用,如何调用函数的
    public Function< T request, R response>                                                                               
         weatherFunction1() {
                return //实现;
}

接下来我们来解释一下函数定义的关键:

 @Description("函数描述....") :AI大模型就是通过该描述来决定是否调用,如何调用函数的,必须要,而且很关键,关系到函数是否调用成功

Function< T request, R response> :含有两个泛型的一个接口

        T request:表示的是AI大模型调用函数传入的参数,可以是任何对象类型,会把所有的参数封装转化成T类型

        R response:函数响应的类型,也就是return 返回的类型。

 函数名:weatherFunction1,这个就是我们将定义好的函数注册到AI大模型的函数名称,比如

 public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultFunctions("weatherFunction1",... )
                .build();

    }

那我就想,AI大模型怎么知道我们需要哪些参数,就算它根据函数描述可以知道要传入哪些参数,那又怎么知道哪个参数对应哪个参数呢?参数顺序是怎样的呢?

Java 16 引入的 record(记录类),用于定义不可变的数据载体类(data class / POJO),具体细节可以看其他博主的介绍,这里就不多解释:

我可以在WeatherService里面定义一个类Request,用于封装传入的参数,同时解释参数,用于AI大模型的对应传参。

@Slf4j
@Service
public class WeatherService {
   
    public record Request(
            @JsonProperty(value = "city", required = true) @JsonPropertyDescription("城市名称") String city,
            @JsonProperty(value = "date", required = true) @JsonPropertyDescription("日期") String date) {

    }

    /**
     * 获取某城市某天的天气
     * @param city
     * @param date
     * @return
     */
    public String getWeatherByDate(String city,String date) {
        log.info("调用函数工具: 城市:{} 时间:{}", city, date);
        //TODO 调用天气查询接口
        String weather = "阴天";
        return date + city + "的天气情况为:" + weather;
    }

}

 其实上面record的写法就相当于为weatherService创建了一个静态内部类

@Slf4j
@Service
public class WeatherService {

    /**
     * 获取某城市某天的天气
     * @param city
     * @param date
     * @return
     */
    public String getWeatherByDate(String city,String date) {
        log.info("调用函数工具: 城市:{} 时间:{}", city, date);
        //TODO 调用天气查询接口
        String weather = "阴天";
        return date + city + "的天气情况为:" + weather;
    }
    @Data
    public static class Request {
        @JsonProperty(value = "city", required = true)
        @JsonPropertyDescription("城市名称")
        private String city;
        @JsonProperty(value = "date", required = true)
        @JsonPropertyDescription("日期")
        private String date;
    }

}

当然也不需要一定是内部类,外部也可以,将Request单独创建一个外部类也是可以的。 

那么大模型如何通过定义 Request 类来保证大模型传入正确参数呢?

1、Spring AI 使用 Request 类的字段信息(包括字段名、类型、是否必填、描述等),自动生成一个 JSON Schema。

例如会将上述的Requestl类信息转换成JSON Schema

{
  "type": "object",
  "properties": {
    "city": { "type": "string", "description": "城市名称" },
    "date": { "type": "string", "description": "日期" }
  },
  "required": ["city","date"]
}

 2、告诉大模型:这个函数需要哪些参数?
当模型收到用户提问,比如:“2025年7月15日深圳天气怎么样?”
它会查看所有可用的函数工具及其 JSON Schema,发现你需要一个包含 "city" 和 "date" 的参数对象,并且 "city" ,"date"是必填项。于是模型会尝试构造如下 JSON:

{
  "name": "getWeatherByDate",
  "arguments": {
    "city": "深圳",
    "date": "2025-07-15"
  }
}

3、Spring AI 自动将 JSON 转换为 Java 对象

Spring AI 接收到模型返回的 JSON 后,会使用 Jackson 或其他序列化库,将该 JSON 转换为你的 Request 

接下来我们来完整的定义一个查询天气的工具函数:


@Configuration
public class FunctionToolConfig {

    @Bean
    @Description("天气查询城市某一天的天气情况")
    public Function<WeatherService.Request, String> getWeatherByDate(WeatherService weatherService) {
       return request->weatherService.getWeatherByDate(request.city(), request.date());
    }
}

然后就是函数工具注册给ChatClient:

  @GetMapping("/simple/getWeather")
    public String simpleChat(String query) {
        String response =  chatClient.prompt()
                .user(query)
                .functions("getWeatherByDate")
                .call()
                .content();
        return response;
    }

然后我们调用一下接口,最好开启DEBUG日志,这样可以打印请求与响应的过程,方便观察调试

结果为:

控制台日志输出为:

 如果函数没有或者只有一个参数,那我们就可以不需要创建一个Request类了

@Service
public class WeatherService {

    /**
     * 获取今天的时间
     * @return
     */
    public String getToday(){
        LocalDate today = LocalDate.now();
        log.info("调用函数工具: 今天的日期{}", today);
        return "今天是" + today+ "哦,嘿嘿";
    }

    /**
     * 获取某城市今天的天气
     * @param city
     * @return
     */
    public String getTodayWeather(String city) {
           log.info("调用函数工具: {}", city);
           LocalDate today = LocalDate.now();
           //TODO 调用天气查询接口
           String weather = "晴天";
           return "今天是" + today + "," + city + "的天气情况为:" + weather;
    }
}

定义函数时我们可以:

@Configuration
public class FunctionToolConfig {
    @Bean
    @Description("获取今日的日期")
    public Function<Void, String> getToday(WeatherService weatherService) {
        return v -> weatherService.getToday();
    }
    @Bean
    @Description("天气查询城市今天的天气")
    public Function<String, String> getTodayWeather(WeatherService weatherService) {
        return weatherService::getTodayWeather;
    }
}

当然函数的请求参数可以封装传入,那函数的响应参数也是可以封装响应的 

@Slf4j
@Service
public class WeatherService {
   
    public record Request(
            @JsonProperty(value = "city") @JsonPropertyDescription("城市名称") String city,
            @JsonProperty(value = "date") @JsonPropertyDescription("日期") String date) {

    }

    public record Response(
            @JsonProperty(value = "date") @JsonPropertyDescription("日期") String date,
            @JsonProperty(value = "city") @JsonPropertyDescription("城市名称") String city,
            @JsonProperty(value = "weather") @JsonPropertyDescription("天气情况") String weather) {
    }


    /**
     * 获取某城市某天的天气
     * @param city
     * @param date
     * @return
     */
    public Response getWeatherByDate(String city,String date) {
        log.info("调用函数工具: 城市:{} 时间:{}", city, date);
        //TODO 调用天气查询接口
        String weather = "阴天";
        Response response = new Response(date, city, weather);
        log.info("返回结果:{}", response);
        return response;
    }

}
@Configuration
public class FunctionToolConfig {
  
    @Bean
    @Description("天气查询城市某一天的天气情况")
    public Function<WeatherService.Request, WeatherService.Response> getWeatherByDate(WeatherService weatherService) {
       return request->weatherService.getWeatherByDate(request.city(), request.date());
    }
}

接下来我们来学习另一种 函数工具的定义FunctionCallback

@Component
public class GetWeatherByDateFunction implements FunctionCallback {

    private final WeatherService weatherService;
    private static final ObjectMapper mapper = new ObjectMapper();

    public GetWeatherByDateFunction(WeatherService weatherService) {
        this.weatherService = weatherService;
    }

    /**
     * 定义函数名称
     * @return
     */
    @Override
    public String getName() {
        return "getWeatherByDate1";
    }

    /**
     * 函数描述
     * @return
     */
    @Override
    public String getDescription() {
        return "根据城市和日期查询天气信息";
    }

    /**
     * 函数调用
     * @param functionInput
     * @return
     */
    @Override
    public String call(String functionInput) {
        try {
            Request request = mapper.readValue(functionInput, Request.class);
            return weatherService.getWeatherByDate(request.getCity(), request.getDate()).toString();
        } catch (Exception e) {
            throw new RuntimeException("解析函数调用参数失败", e);
        }
    }

    /**
     * 输入参数的 JSON Schema
     * @return
     */
    @Override
    public String getInputTypeSchema() {
        try {
            return mapper.writeValueAsString(mapper.generateJsonSchema(Request.class));
        } catch (Exception e) {
            throw new RuntimeException("生成 JSON Schema 失败", e);
        }
    }

}

两个实体类:

@Data
public class Request {
        @JsonProperty(value = "city", required = true)
        @JsonPropertyDescription("城市名称")
        private  String city;
        @JsonProperty(value = "date", required = true)
        @JsonPropertyDescription("日期")
        private  String date;
}
@Data
public class Response {
    @JsonProperty(value = "date")
    @JsonPropertyDescription("日期")
    private String date;
    @JsonProperty(value = "city")
    @JsonPropertyDescription("城市名称")
    private String city;
    @JsonProperty(value = "weather")
    @JsonPropertyDescription("天气情况")
    private String weather;
}

Function<T t,R r>和 FunctionCallback实现函数定义的区别:

function<T t,R r>:

优点:

  • 简洁直观,适合快速开发。
  • 不需要额外类或接口。
  • 易于测试和集成。

❌ 缺点:

  • 无法提供详细的 JSON Schema,模型可能无法准确理解参数结构。
  • 不支持复杂参数类型(如嵌套对象)
  • 无法自定义错误处理、参数转换逻辑
  • 不便于扩展功能(如记录调用日志、权限校验)

 FunctionCallback:

✅ 优点:

  • 可以定义完整的函数元信息(名称、描述、参数结构)
  • 支持 JSON Schema 自动生成
  • 支持多种调用方式(Map、JSON 字符串)
  • 可扩展性强,便于封装通用逻辑(如日志、异常处理)
  • 更适用于多平台(OpenAI / DashScope / 自定义模型)

❌ 缺点:

  • 相对繁琐,需要实现多个接口方法。
  • 对新手来说学习成本略高。

 Function<T, R> 更轻量,适合快速开发;FunctionCallback 更强大,适合生产环境。两者都可用于注册函数工具,但 FunctionCallback 提供了更强的控制能力和兼容性

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值