Spring AI Alibaba Function Calling:外部工具集成与业务函数注册

Spring AI Alibaba Function Calling:外部工具集成与业务函数注册

导读:Function Calling(工具调用)是让 AI 从"纸上谈兵"走向"真正干活"的关键能力。本文深入讲解如何用 Spring AI Alibaba 1.1 版让大模型调用你的 Java 方法、查询数据库、访问第三方 API,并覆盖并行调用、参数校验、权限控制等生产级细节。


一、Function Calling 的本质

Function Calling 解决的问题很具体:大模型的知识有截止日期,也无法直接访问你的系统。但通过 Function Calling,你可以告诉模型"有哪些工具可以用",当它判断需要某个工具时,会输出一个结构化的函数调用请求,你的代码执行后把结果返回给模型,模型再基于结果给出最终答案。

复制代码
用户:现在上海的天气怎么样?

[无 Function Calling]:
模型:"我无法获取实时天气信息,我的训练数据截止到..."

[有 Function Calling]:
模型:"需要调用天气查询工具"
        ↓
  你的代码:调用天气 API,得到"上海 25°C,晴"
        ↓
  模型:"目前上海天气晴朗,气温 25°C,适合户外活动..."

这个能力让 AI 真正成为能执行任务的智能体,而不仅仅是"知识检索器"。


二、依赖与配置

Function Calling 能力包含在 spring-ai-alibaba-starter-dashscope 中,无需额外依赖:

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>

<!-- 参数校验(JSR-303) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

三、三种函数注册方式

3.1 方式一:@Bean + Function 接口(推荐)

这是最标准的注册方式,函数实现与 Spring 容器解耦:

java 复制代码
package com.example.ai.tools;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;

@Slf4j
@Configuration
public class ToolsConfiguration {

    /**
     * 工具一:获取当前时间
     * 模型会在需要知道当前时间时调用此工具
     */
    @Bean
    @Description("获取当前的日期和时间,当用户询问时间相关问题时调用")
    public Function<CurrentTimeRequest, CurrentTimeResponse> getCurrentTime() {
        return request -> {
            log.info("工具调用:getCurrentTime,时区:{}", request.timezone());
            String now = LocalDateTime.now()
                    .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return new CurrentTimeResponse(now, "Asia/Shanghai");
        };
    }

    /**
     * 工具二:天气查询(模拟实现)
     */
    @Bean
    @Description("查询指定城市的实时天气信息。当用户询问某城市天气时调用此工具。")
    public Function<WeatherRequest, WeatherResponse> getWeather() {
        return request -> {
            log.info("工具调用:getWeather,城市:{}", request.city());
            // 实际项目:调用天气 API(如 OpenWeatherMap、和风天气等)
            return simulateWeather(request.city());
        };
    }

    private WeatherResponse simulateWeather(String city) {
        // 模拟数据,实际需调用真实 API
        return new WeatherResponse(city, "晴", 22.5, 65, "适宜户外活动");
    }

    // ---- 请求/响应 DTO(JSON 描述是 Prompt 工程的关键!)----

    public record CurrentTimeRequest(
            @JsonProperty("timezone")
            @JsonPropertyDescription("时区,如 Asia/Shanghai")
            String timezone
    ) {}

    public record CurrentTimeResponse(
            @JsonProperty("current_time")
            @JsonPropertyDescription("当前时间,格式:yyyy-MM-dd HH:mm:ss")
            String currentTime,
            @JsonProperty("timezone")
            String timezone
    ) {}

    public record WeatherRequest(
            @JsonProperty("city")
            @JsonPropertyDescription("城市名称,如:上海、北京、广州")
            String city
    ) {}

    public record WeatherResponse(
            String city,
            String condition,
            double temperature,
            int humidity,
            String suggestion
    ) {}
}

3.2 方式二:@Tool 注解(1.1版推荐的简化写法)

Spring AI 1.1 版引入了 @Tool 注解,可以直接标注方法,更简洁:

java 复制代码
package com.example.ai.tools;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

/**
 * 数据库查询工具类
 * 使用 @Tool 注解,无需额外的 Function Bean 注册
 */
@Component
public class DatabaseTools {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

    public DatabaseTools(OrderRepository orderRepository,
                          ProductRepository productRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
    }

    /**
     * 查询订单状态
     * 模型描述决定了模型何时会调用此工具
     */
    @Tool(description = "根据订单号查询订单的状态、金额、收货地址等详细信息")
    public String queryOrderStatus(
            @JsonPropertyDescription("订单号,格式如:ORD20250320001")
            String orderId) {

        log.info("[Tool] 查询订单:{}", orderId);
        Order order = orderRepository.findById(orderId)
                .orElse(null);

        if (order == null) {
            return String.format("未找到订单号为 %s 的订单,请确认订单号是否正确", orderId);
        }

        return String.format("订单 %s 状态:%s,金额:%.2f 元,下单时间:%s,收货地址:%s",
                orderId, order.getStatus(), order.getAmount(),
                order.getCreatedAt(), order.getShippingAddress());
    }

    /**
     * 查询商品库存
     */
    @Tool(description = "查询指定商品的当前库存数量,用于回答用户关于商品是否有货的问题")
    public String queryProductStock(
            @JsonPropertyDescription("商品 SKU 编码或商品名称")
            String productIdentifier) {

        log.info("[Tool] 查询库存:{}", productIdentifier);
        // 先尝试按 SKU 查询,再按名称模糊查询
        Product product = productRepository.findBySku(productIdentifier)
                .orElseGet(() -> productRepository.findByNameContaining(productIdentifier)
                        .stream().findFirst().orElse(null));

        if (product == null) {
            return "未找到商品:" + productIdentifier;
        }

        return String.format("商品 [%s] 当前库存:%d 件,状态:%s",
                product.getName(),
                product.getStock(),
                product.getStock() > 0 ? "有货" : "缺货");
    }
}

3.3 在 ChatClient 中注册工具

java 复制代码
@Service
@RequiredArgsConstructor
@Slf4j
public class ToolChatService {

    private final ChatClient chatClient;

    // 注入 @Tool 注解的工具类
    private final DatabaseTools databaseTools;

    // 通过 @Bean 注册的工具
    @Qualifier("getCurrentTime")
    private final Function<ToolsConfiguration.CurrentTimeRequest,
                           ToolsConfiguration.CurrentTimeResponse> getCurrentTime;

    @Qualifier("getWeather")
    private final Function<ToolsConfiguration.WeatherRequest,
                           ToolsConfiguration.WeatherResponse> getWeather;

    /**
     * 带工具的对话:模型自主决定是否调用工具
     */
    public String chatWithTools(String userMessage) {
        log.info("用户消息:{}", userMessage);

        return chatClient.prompt()
                .user(userMessage)
                // 方式一:注册 @Bean Function 工具
                .tools(
                        FunctionToolCallback.builder("getCurrentTime", getCurrentTime)
                                .description("获取当前时间")
                                .inputType(ToolsConfiguration.CurrentTimeRequest.class)
                                .build(),
                        FunctionToolCallback.builder("getWeather", getWeather)
                                .description("查询城市天气")
                                .inputType(ToolsConfiguration.WeatherRequest.class)
                                .build()
                )
                // 方式二:注册 @Tool 注解的工具类(Spring AI 1.1)
                // .tools(databaseTools)  // 自动发现类中所有 @Tool 方法
                .call()
                .content();
    }

    /**
     * 智能客服场景:使用数据库查询工具
     */
    public String customerService(String question) {
        return chatClient.prompt()
                .system("""
                        你是一个智能客服助手,可以帮助用户查询订单状态和商品信息。
                        当用户询问订单时,调用查询工具获取实时信息。
                        回答要友好、简洁,并在适当时主动提供帮助。
                        """)
                .user(question)
                .tools(databaseTools)  // 注入数据库查询工具
                .call()
                .content();
    }
}

四、完整工具调用链分析

理解调用链有助于排查问题:

复制代码
用户请求 → Spring Controller
              |
              v
        [ChatClient]
              |
    (携带工具描述的 Prompt)
              |
              v
        [DashScope API]
        模型判断:需要调用工具
              |
              v
        模型输出:
        {
          "tool_calls": [{
            "function": {
              "name": "getWeather",
              "arguments": {"city": "上海"}
            }
          }]
        }
              |
              v
        [Spring AI 内部处理]
        自动解析工具调用
              |
              v
        [你的 Java 函数]
        WeatherResponse {city="上海", condition="晴", ...}
              |
              v
        [结果回传模型]
        模型收到工具结果
              |
              v
        [模型生成最终回答]
        "上海今天天气晴朗,气温约 22.5°C..."
              |
              v
        [返回给用户]

五、MyBatis 数据库查询集成

工具调用最常见的场景之一是让 AI 查询数据库。以下是结合 MyBatis 的完整示例:

java 复制代码
@Component
@Slf4j
public class MyBatisQueryTools {

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 查询用户积分余额
     */
    @Tool(description = "查询用户的积分余额,当用户询问自己有多少积分时调用")
    public String getUserPoints(
            @JsonPropertyDescription("用户 ID 或用户名")
            String userIdentifier) {

        try {
            User user = userMapper.findByIdOrName(userIdentifier);
            if (user == null) {
                return "未找到用户:" + userIdentifier;
            }
            return String.format("用户 %s 当前积分:%d 分,等级:%s",
                    user.getName(), user.getPoints(), user.getLevel());
        } catch (Exception e) {
            log.error("查询用户积分失败", e);
            return "查询失败,请稍后重试";
        }
    }

    /**
     * 查询历史订单列表
     */
    @Tool(description = "查询用户最近的历史订单列表(最近10条),当用户询问历史购买记录时调用")
    public String getRecentOrders(
            @JsonPropertyDescription("用户 ID")
            String userId) {

        List<Order> orders = orderMapper.findRecentByUserId(userId, 10);
        if (orders.isEmpty()) {
            return "该用户暂无订单记录";
        }

        StringBuilder sb = new StringBuilder("最近订单:\n");
        orders.forEach(o -> sb.append(String.format(
                "- 订单号:%s,商品:%s,金额:%.2f元,状态:%s\n",
                o.getId(), o.getProductName(), o.getAmount(), o.getStatus())));

        return sb.toString();
    }
}

六、并行工具调用

当一次对话需要调用多个工具时,模型可以并行发起多个调用,Spring AI 会并行执行并聚合结果:

java 复制代码
/**
 * 并行调用示例:同时查询多个数据源
 * 用户:"帮我看看北京和上海的天气,还有告诉我现在几点了"
 */
public String parallelToolsDemo(String message) {
    return chatClient.prompt()
            .user(message)
            // 注册多个工具,模型可以并行调用
            .tools(
                    FunctionToolCallback.builder("getWeather", getWeather)
                            .description("查询城市天气,支持并行调用多个城市")
                            .inputType(WeatherRequest.class)
                            .build(),
                    FunctionToolCallback.builder("getCurrentTime", getCurrentTime)
                            .description("获取当前时间")
                            .inputType(CurrentTimeRequest.class)
                            .build()
            )
            .call()
            .content();

    // 模型会一次性输出:
    // tool_calls: [
    //   {name: "getWeather", args: {city: "北京"}},
    //   {name: "getWeather", args: {city: "上海"}},
    //   {name: "getCurrentTime", args: {timezone: "Asia/Shanghai"}}
    // ]
    // Spring AI 并行执行三个工具调用,再将结果聚合返回给模型
}

七、安全控制:权限白名单与参数校验

工具调用涉及真实的业务操作,必须有安全防护:

7.1 参数校验(JSR-303)

java 复制代码
@Component
public class SecureQueryTools {

    @Tool(description = "查询指定日期范围内的销售报表")
    public String getSalesReport(
            @NotBlank(message = "开始日期不能为空")
            @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message = "日期格式必须为 yyyy-MM-dd")
            @JsonPropertyDescription("开始日期,格式:yyyy-MM-dd")
            String startDate,

            @NotBlank(message = "结束日期不能为空")
            @JsonPropertyDescription("结束日期,格式:yyyy-MM-dd")
            String endDate) {

        // 业务逻辑校验
        LocalDate start = LocalDate.parse(startDate);
        LocalDate end = LocalDate.parse(endDate);

        if (end.isBefore(start)) {
            throw new IllegalArgumentException("结束日期不能早于开始日期");
        }

        if (ChronoUnit.DAYS.between(start, end) > 90) {
            throw new IllegalArgumentException("查询范围不能超过 90 天");
        }

        // 执行查询...
        return "查询成功,日期范围:" + startDate + " 至 " + endDate;
    }
}

7.2 工具执行超时控制

java 复制代码
@Component
public class TimeoutSafeTools {

    private final ExecutorService executor = Executors.newFixedThreadPool(10);

    @Tool(description = "调用外部 API 获取数据(含超时保护)")
    public String callExternalApi(String apiName, String params) {
        Future<String> future = executor.submit(() -> {
            // 实际的 API 调用逻辑
            return externalApiService.call(apiName, params);
        });

        try {
            // 工具执行超时:5秒
            return future.get(5, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            future.cancel(true);
            log.warn("工具 [{}] 执行超时", apiName);
            return "查询超时,请稍后重试";
        } catch (Exception e) {
            log.error("工具执行失败:{}", e.getMessage());
            return "执行失败:" + e.getMessage();
        }
    }
}

八、工具描述的 Prompt 工程

工具描述(Description)的质量直接决定模型何时调用工具,这是被很多人忽视的"Prompt 工程":

java 复制代码
// ❌ 差的描述:模糊不清
@Description("查询信息")
public Function<Request, Response> queryInfo() { ... }

// ✅ 好的描述:明确触发条件 + 输入输出说明
@Description("查询指定城市的实时天气信息,包括温度、天气状况、湿度等。" +
             "当用户询问某城市天气、是否需要带雨伞、户外活动是否合适等问题时调用此工具。" +
             "输入城市名称(支持中文城市名),返回当前天气信息。")
public Function<WeatherRequest, WeatherResponse> getWeather() { ... }

好的工具描述需要包含:

  1. 工具做什么(功能说明);
  2. 什么时候调用(触发条件);
  3. 输入什么(参数说明);
  4. 返回什么(输出说明)。

九、总结

Function Calling 是 AI 应用从"对话机器人"进化为"智能助手"的关键能力:

  1. 三种注册方式@Bean + Function 接口(标准)、@Tool 注解(1.1版简化)、手动 FunctionToolCallback
  2. 调用链理解:模型 → 工具调用请求 → Spring AI 执行 → 结果回传模型 → 最终回答;
  3. 数据库集成:结合 MyBatis,让 AI 直接查询业务数据库;
  4. 并行调用:多工具并行执行,结果自动聚合;
  5. 安全防护:JSR-303 参数校验 + 超时控制 + 权限白名单。

下一篇将深入 AI 应用的可观测性:Token 消耗监控、链路追踪、Prometheus 指标接入,打造生产级运维能力。


参考资料

相关推荐
SuniaWang1 小时前
《Spring AI + 大模型全栈实战》学习手册系列 · 专题四:《Ollama 模型管理与调优:让 AI 模型在低配服务器上流畅运行》
人工智能·学习·spring
逆境不可逃1 小时前
LeetCode 热题 100 之 33. 搜索旋转排序数组 153. 寻找旋转排序数组中的最小值 4. 寻找两个正序数组的中位数
java·开发语言·数据结构·算法·leetcode·职场和发展
码界奇点2 小时前
基于Spring Boot的医院药品管理系统设计与实现
java·spring boot·后端·车载系统·毕业设计·源代码管理
anscos_yumi2 小时前
Altair OptiStruct:重构结构研发逻辑,引领工业仿真与优化新纪元
人工智能·科技·软件工程
小旭95272 小时前
Spring MVC :从入门到精通(下)
java·后端·spring·mvc
夏语灬2 小时前
MySQL大小写敏感、MySQL设置字段大小写敏感
java
毕设源码-郭学长2 小时前
【开题答辩全过程】以 某地红十字会门户网站为例,包含答辩的问题和答案
java
林夕sama2 小时前
多线程基础(四)
java·开发语言
市象2 小时前
小红书盯上“AI版郑州帮”
人工智能·网络安全·传媒