Spring AI Function Calling:让AI调用你的Java方法

Spring AI Function Calling:让AI调用你的Java方法

前言

在大语言模型的应用中,有一个核心问题:AI虽然聪明,但它无法感知外部世界,也无法执行实际操作

比如你问"今天北京的天气怎么样",AI只能根据训练数据猜测,而无法获取实时天气。

Function Calling(函数调用)解决了这个问题:让AI能够调用你提供的Java方法,获取实时数据,然后基于这些数据回答问题。

本文分享我在实际项目中使用Spring AI Function Calling的经验,包括踩过的坑和最佳实践。


一、Function Calling原理

1.1 问题场景

假设我们要实现一个智能助手,用户会问:

javascript 复制代码
用户:帮我查一下北京今天的天气
AI(无Function Calling):根据我的知识,北京今天可能是...
AI(有Function Calling):调用getWeather("北京") → 获取实时数据 → 北京今天晴,25°C

1.2 工作流程

markdown 复制代码
用户提问
    ↓
AI分析:需要调用工具吗?
    ↓ 是
AI决定调用哪个方法 + 参数
    ↓
Spring AI执行对应的Java方法
    ↓
将方法返回值返回给AI
    ↓
AI基于返回数据生成回答
    ↓
返回给用户

1.3 支持的方法类型

Spring AI支持以下几种方式定义工具方法:

方式 说明 推荐度
@Tool注解 最简洁,Spring AI 1.1+ ⭐⭐⭐⭐⭐
Function接口 传统方式,兼容性好 ⭐⭐⭐
ToolCallback 更灵活,支持动态工具 ⭐⭐⭐⭐

二、基础实战:天气查询助手

2.1 环境准备

xml 复制代码
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>function-calling-demo</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>17</java.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
            <version>1.0.0-M3</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
</project>

2.2 配置

yaml 复制代码
# application.yml
server:
  port: 8080

spring:
  ai:
    alibaba:
      api-key: ${ALI_API_KEY}
      chat:
        options:
          model: qwen-plus
          temperature: 0.1  # Function Calling 场景温度要低

2.3 定义工具方法

java 复制代码
package com.example.demo.tool;

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

import java.time.LocalDate;
import java.util.Map;
import java.util.Random;

/**
 * 天气查询工具
 * 实际项目中应调用真实的天气API(如和风天气、OpenWeather等)
 */
@Component
public class WeatherTool {
    
    private static final Map<String, String> CITY_WEATHER = Map.of(
            "北京", "晴,25°C,空气质量良好",
            "上海", "多云,22°C,空气质量优",
            "广州", "雷阵雨,28°C,空气质量良",
            "深圳", "多云转晴,27°C,空气质量优",
            "杭州", "小雨,20°C,空气质量良"
    );
    
    private final Random random = new Random();
    
    @Tool(description = "获取指定城市的天气信息。支持北京、上海、广州、深圳、杭州等城市。")
    public String getWeather(
            @ToolParam(description = "城市名称,如:北京、上海、广州等") 
            String city) {
        
        System.out.println("[Tool Called] getWeather(city=" + city + ")");
        
        // 模拟网络延迟
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        return CITY_WEATHER.getOrDefault(city, 
                "抱歉," + city + "的天气数据暂不可用");
    }
    
    @Tool(description = "获取多个城市的天气对比信息")
    public String getWeatherCompare(
            @ToolParam(description = "城市名称列表,用逗号分隔") 
            String cities) {
        
        System.out.println("[Tool Called] getWeatherCompare(cities=" + cities + ")");
        
        StringBuilder result = new StringBuilder();
        String[] cityArray = cities.split("[,,]");
        
        for (String city : cityArray) {
            city = city.trim();
            String weather = CITY_WEATHER.getOrDefault(city, "数据暂不可用");
            result.append(city).append(":").append(weather).append("\n");
        }
        
        return result.toString();
    }
    
    @Tool(description = "获取今天的日期")
    public String getTodayDate() {
        return LocalDate.now().toString();
    }
}

2.4 注册工具并调用

java 复制代码
package com.example.demo.service;

import com.example.demo.tool.WeatherTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class WeatherAssistantService {
    
    private final ChatClient chatClient;
    private final WeatherTool weatherTool;
    
    public WeatherAssistantService(
            ChatClient.Builder chatClientBuilder,
            WeatherTool weatherTool) {
        this.chatClient = chatClientBuilder
                .defaultTools(weatherTool)  // 注册工具
                .build();
        this.weatherTool = weatherTool;
        log.info("WeatherAssistantService initialized with tools");
    }
    
    /**
     * 智能天气查询
     */
    public String queryWeather(String userMessage) {
        log.info("User message: {}", userMessage);
        
        String response = chatClient.prompt()
                .user(userMessage)
                .call()
                .content();
        
        log.info("AI response generated");
        return response;
    }
}

2.5 Controller

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.WeatherAssistantService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
public class WeatherController {
    
    private final WeatherAssistantService weatherService;
    
    @PostMapping("/query")
    public String query(@RequestBody QueryRequest request) {
        if (request.message() == null || request.message().isBlank()) {
            throw new IllegalArgumentException("消息不能为空");
        }
        return weatherService.queryWeather(request.message());
    }
    
    /**
     * 测试场景
     */
    @GetMapping("/test")
    public String test() {
        return weatherService.queryWeather("北京今天天气怎么样?和上海比怎么样?");
    }
}

record QueryRequest(String message) {}

三、进阶实战:企业知识库助手

3.1 场景说明

在企业内部,我们可能需要一个助手,能够:

  1. 查询员工信息
  2. 查询项目进度
  3. 查询系统状态
  4. 执行简单的运维操作

3.2 定义多个工具

java 复制代码
package com.example.demo.tool;

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

import java.util.List;
import java.util.Map;

/**
 * 企业系统工具集
 */
@Component
public class EnterpriseTools {
    
    @Tool(description = "查询员工信息,包括姓名、部门、职位等")
    public String getEmployeeInfo(
            @ToolParam(description = "员工姓名或工号") 
            String employeeId) {
        
        System.out.println("[Tool Called] getEmployeeInfo(employeeId=" + employeeId + ")");
        
        // 模拟数据库查询
        Map<String, String> employees = Map.of(
                "张三", "工号:E001,部门:技术部,职位:高级工程师",
                "李四", "工号:E002,部门:产品部,职位:产品经理",
                "王五", "工号:E003,部门:运维部,职位:SRE工程师"
        );
        
        return employees.getOrDefault(employeeId, "未找到员工:" + employeeId);
    }
    
    @Tool(description = "查询项目进度,返回项目当前状态和完成百分比")
    public String getProjectStatus(
            @ToolParam(description = "项目编号或项目名称") 
            String projectId) {
        
        System.out.println("[Tool Called] getProjectStatus(projectId=" + projectId + ")");
        
        Map<String, String> projects = Map.of(
                "项目A", "状态:进行中,完成度:65%,预计完成时间:2026-06-30",
                "项目B", "状态:已上线,完成度:100%,上线时间:2026-03-15",
                "项目C", "状态:需求评审中,完成度:10%,预计完成时间:2026-08-15"
        );
        
        return projects.getOrDefault(projectId, "未找到项目:" + projectId);
    }
    
    @Tool(description = "查询系统运行状态,包括CPU、内存、磁盘使用率")
    public String getSystemStatus() {
        System.out.println("[Tool Called] getSystemStatus()");
        
        // 实际项目中应调用监控系统API
        return """
               系统运行状态:
               - CPU使用率:45%
               - 内存使用率:62%
               - 磁盘使用率:78%
               - 网络状态:正常
               - 活跃连接数:342
               """;
    }
    
    @Tool(description = "重启指定服务。注意:此操作会中断服务,请谨慎使用!")
    public String restartService(
            @ToolParam(description = "服务名称,如:user-service、order-service") 
            String serviceName) {
        
        System.out.println("[Tool Called] restartService(serviceName=" + serviceName + ")");
        
        // 实际项目中应调用运维系统API
        return "服务 " + serviceName + " 重启指令已发送,预计30秒后恢复";
    }
}

3.3 多工具注册

java 复制代码
package com.example.demo.service;

import com.example.demo.tool.EnterpriseTools;
import com.example.demo.tool.WeatherTool;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class EnterpriseAssistantService {
    
    private final ChatClient chatClient;
    
    public EnterpriseAssistantService(
            ChatClient.Builder chatClientBuilder,
            WeatherTool weatherTool,
            EnterpriseTools enterpriseTools) {
        
        // 注册多个工具类
        this.chatClient = chatClientBuilder
                .defaultTools(weatherTool, enterpriseTools)
                .build();
        
        log.info("EnterpriseAssistantService initialized with multiple tool classes");
    }
    
    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

四、动态工具:根据上下文加载

4.1 场景说明

在某些场景下,工具需要根据用户权限、当前会话等动态加载。

4.2 实现动态工具

java 复制代码
package com.example.demo.service;

import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class DynamicToolService {
    
    private final ChatClient.Builder chatClientBuilder;
    
    public DynamicToolService(ChatClient.Builder chatClientBuilder) {
        this.chatClientBuilder = chatClientBuilder;
    }
    
    /**
     * 根据用户角色动态注册工具
     */
    public String chatWithDynamicTools(String message, UserRole role) {
        
        List<ToolCallback> tools = new ArrayList<>();
        
        // 基础工具:所有用户可用
        tools.add(FunctionToolCallback.builder("getWeather", this::getWeather)
                .description("获取天气信息")
                .inputType(String.class)
                .build());
        
        // 管理员专属工具
        if (role == UserRole.ADMIN) {
            tools.add(FunctionToolCallback.builder("restartService", this::restartService)
                    .description("重启服务(管理员专属)")
                    .inputType(String.class)
                    .build());
        }
        
        // 构建ChatClient
        ChatClient client = chatClientBuilder
                .defaultTools(tools.toArray(new ToolCallback[0]))
                .build();
        
        return client.prompt()
                .user(message)
                .call()
                .content();
    }
    
    // 工具方法
    private String getWeather(String city) {
        return city + "今天天气晴,25°C";
    }
    
    private String restartService(String serviceName) {
        return "服务 " + serviceName + " 正在重启...";
    }
    
    public enum UserRole {
        USER, ADMIN
    }
}

五、踩坑记录与解决方案

5.1 工具未被调用

问题现象: 即使用户问题明显需要调用工具,AI也没有调用,而是直接回答。

原因分析

  1. 工具描述不够清晰,AI不理解何时调用
  2. Temperature参数过高,AI过于"创造性"
  3. 模型不支持Function Calling(检查模型版本)

解决方案

java 复制代码
// 1. 改进工具描述
@Tool(description = """
        获取指定城市的实时天气信息。
        当用户询问天气、气温、是否下雨等问题时,必须调用此工具。
        支持的城市:北京、上海、广州、深圳、杭州。
        """)
public String getWeather(String city) { ... }

// 2. 降低temperature
spring.ai.alibaba.chat.options.temperature=0.1

// 3. 使用支持Function Calling的模型
// qwen-plus以上版本支持

5.2 参数解析错误

问题现象

复制代码
AI尝试调用工具,但参数类型错误,导致调用失败

原因分析: AI生成的参数与Java方法签名不匹配。

解决方案

java 复制代码
// 1. 使用@ToolParam明确参数描述
@Tool(description = "查询员工信息")
public String getEmployeeInfo(
        @ToolParam(description = "员工姓名,如:张三、李四") 
        String name) { ... }

// 2. 参数类型使用String(最灵活)
// 避免用int、boolean等,让AI生成String再由你转换

// 3. 添加参数校验
@Tool(description = "根据年龄查询用户")
public String getUsersByAge(
        @ToolParam(description = "年龄,整数") 
        String ageStr) {
    
    int age;
    try {
        age = Integer.parseInt(ageStr);
    } catch (NumberFormatException e) {
        return "年龄参数错误,请提供数字";
    }
    
    // 查询逻辑...
    return "找到 " + age + " 岁的用户共10人";
}

5.3 工具执行超时

问题现象: 工具方法执行时间过长,导致AI调用超时。

解决方案

java 复制代码
@Tool(description = "执行长时间任务")
public String longRunningTask(String param) {
    
    // 方案1:异步执行,立即返回任务ID
    String taskId = submitAsyncTask(param);
    return "任务已提交,任务ID:" + taskId + ",请稍后查询结果";
    
    // 方案2:设置超时
    // 在application.yml中配置
    // spring.ai.alibaba.chat.options.timeout=60000
}

5.4 工具返回数据过大

问题现象: 工具返回了大量数据,导致Token超限。

解决方案

java 复制代码
@Tool(description = "查询用户列表(分页)")
public String getUsers(
        @ToolParam(description = "页码,从1开始") 
        String pageStr) {
    
    int page = Integer.parseInt(pageStr);
    
    // 限制返回数量
    List<User> users = queryUsersByPage(page, 10);  // 每页10条
    
    // 只返回摘要,不要完整对象
    return users.stream()
            .map(u -> u.getName() + "(" + u.getAge() + "岁)")
            .collect(Collectors.joining("\n"));
}

5.5 多个工具冲突

问题现象: 注册了多个功能相似的工具,AI不知道调用哪个。

解决方案

java 复制代码
// 明确区分工具的职责
@Tool(description = """
        获取单个城市的天气(用于查询一个城市的天气)
        """)
public String getWeather(String city) { ... }

@Tool(description = """
        对比多个城市的天气(用于对比两三个城市的天气差异)
        当用户问"北京和上海天气怎么样"时调用此工具。
        """)
public String compareWeather(String cities) { ... }

六、生产级最佳实践

6.1 工具设计原则

markdown 复制代码
1. 单一职责:每个工具只做一件事
2. 明确描述:description要详细,说明何时调用
3. 参数简单:优先用String,避免复杂对象
4. 快速返回:工具执行时间控制在3秒内
5. 安全校验:所有参数都要校验

6.2 安全防护

java 复制代码
@Component
public class SecureTools {
    
    @Tool(description = "执行系统命令(仅管理员)")
    public String executeCommand(
            @ToolParam(description = "命令内容") 
            String command,
            UserContext userContext) {  // 从上下文获取用户信息
        
        // 权限校验
        if (!userContext.hasRole("ADMIN")) {
            return "权限不足,需要管理员权限";
        }
        
        // 命令白名单
        if (!isCommandAllowed(command)) {
            return "此命令不在允许列表中";
        }
        
        // 执行命令
        return executeSystemCommand(command);
    }
    
    private boolean isCommandAllowed(String command) {
        String[] allowed = {"ls", "pwd", "df -h", "top -b -n 1"};
        for (String allowedCmd : allowed) {
            if (command.startsWith(allowedCmd)) {
                return true;
            }
        }
        return false;
    }
}

6.3 监控与日志

java 复制代码
@Component
@Slf4j
public class MonitoredTools {
    
    private final MeterRegistry meterRegistry;
    
    public MonitoredTools(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
    
    @Tool(description = "查询用户订单")
    public String getUserOrders(String userId) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行查询
            String result = queryOrdersFromDb(userId);
            
            // 记录成功
            meterRegistry.counter("tools.user_orders.calls.success").increment();
            
            return result;
            
        } catch (Exception e) {
            // 记录失败
            meterRegistry.counter("tools.user_orders.calls.failure").increment();
            log.error("查询用户订单失败", e);
            return "查询失败:" + e.getMessage();
            
        } finally {
            // 记录耗时
            long duration = System.currentTimeMillis() - startTime;
            meterRegistry.timer("tools.user_orders.duration").record(duration, java.util.concurrent.TimeUnit.MILLISECONDS);
            log.info("getUserOrders executed in {} ms", duration);
        }
    }
}

6.4 优雅降级

java 复制代码
@Tool(description = "查询实时库存")
public String getInventory(String productId) {
    
    try {
        // 主数据源
        return queryFromMainDatabase(productId);
        
    } catch (Exception e) {
        log.warn("主数据源查询失败,尝试备用方案", e);
        
        try {
            // 降级:读缓存
            return queryFromCache(productId);
            
        } catch (Exception e2) {
            log.error("备用方案也失败", e2);
            
            // 返回友好提示
            return "库存查询暂时不可用,请稍后重试";
        }
    }
}

七、完整项目示例

7.1 项目结构

css 复制代码
function-calling-demo/
├── src/main/java/com/example/demo/
│   ├── DemoApplication.java
│   ├── config/
│   │   └── AiConfig.java
│   ├── controller/
│   │   ├── WeatherController.java
│   │   └── AssistantController.java
│   ├── service/
│   │   ├── WeatherAssistantService.java
│   │   ├── EnterpriseAssistantService.java
│   │   └── DynamicToolService.java
│   ├── tool/
│   │   ├── WeatherTool.java
│   │   └── EnterpriseTools.java
│   └── model/
│       └── QueryRequest.java
├── src/main/resources/
│   └── application.yml
└── pom.xml

7.2 启动类

java 复制代码
package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class DemoApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        log.info("Function Calling Demo started!");
        log.info("Test URL: http://localhost:8080/api/weather/test");
    }
}

7.3 测试用例

java 复制代码
package com.example.demo;

import com.example.demo.service.WeatherAssistantService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class FunctionCallingTest {
    
    @Autowired
    private WeatherAssistantService weatherService;
    
    @Test
    void testWeatherQuery() {
        String response = weatherService.queryWeather("北京今天天气怎么样?");
        
        assertNotNull(response);
        assertTrue(response.contains("北京") || response.contains("天气") || response.contains("°C"));
        
        System.out.println("AI回答:" + response);
    }
    
    @Test
    void testMultiCityComparison() {
        String response = weatherService.queryWeather("北京和上海天气对比");
        
        assertNotNull(response);
        assertTrue(response.length() > 10);
        
        System.out.println("AI回答:" + response);
    }
}

八、性能优化

8.1 工具结果缓存

java 复制代码
@Component
public class CachedTools {
    
    @Cacheable(value = "weather", key = "#city", unless = "#result == null")
    @Tool(description = "获取天气(带缓存)")
    public String getWeather(String city) {
        System.out.println("【执行查询】" + city);
        // 实际调用天气API
        return callWeatherApi(city);
    }
}

8.2 并发工具调用

java 复制代码
@Tool(description = "批量查询天气")
public String getMultipleWeather(String cities) {
    
    List<CompletableFuture<String>> futures = Arrays.stream(cities.split(","))
            .map(city -> CompletableFuture.supplyAsync(() -> queryWeather(city.trim())))
            .toList();
    
    return futures.stream()
            .map(CompletableFuture::join)
            .collect(Collectors.joining("\n"));
}

九、总结

9.1 核心要点

  1. Function Calling让AI能够调用Java方法,获取实时数据
  2. 工具定义 使用@Tool注解,描述要清晰明确
  3. 参数设计优先使用String类型,避免复杂对象
  4. 安全防护必须做权限校验和命令白名单
  5. 监控日志记录工具调用情况,便于排查问题

9.2 适用场景

场景 说明 示例
实时数据查询 天气、股票、新闻等 天气查询助手
数据库操作 根据用户意图查询数据库 智能客服
系统运维 执行运维命令、查询状态 运维助手
业务操作 下单、退款、审批等 企业助手

9.3 参考资源


如果有帮助,欢迎点赞、收藏、关注!如有问题,欢迎在评论区交流。

相关推荐
Agent手记1 小时前
空运智能装箱规划自动化、落地方法与合规适配:2026年Agent矩阵驱动的技术演进与实操指引
运维·人工智能·ai·矩阵·自动化
七牛开发者1 小时前
不写框架、不用 npm,我用 AI Coding 做了一个家庭记忆站
前端·人工智能·npm
FelixZhang0281 小时前
工业时序工况识别项目复盘:从深度学习探索到 LightGBM/CatBoost 落地
人工智能·深度学习·机器学习·gru·lstm·边缘计算·boosting
智能相对论1 小时前
应用“深水区”正在被攻克,轮足机器人迎来拐点时刻
大数据·人工智能·机器人
专利观察员1 小时前
用AI进行专利智能检索分析:拆解人形机器人半马跑赢的秘密/跑崩的解法(科技行业专利检索、专利分析实例)
人工智能·科技·机器人
得一录1 小时前
TradingAgents金融股票分析的最小实现
开发语言·数据库·人工智能·python
俊基科技1 小时前
AR1105 声源定位模组 矿场智能安全监测与设备全生命周期运维技术方案
人工智能·声源定位·语音模组
Zzj_tju1 小时前
视觉语言模型技术指南:LLaVA、Qwen-VL、MiniCPM-V 等主流方案差别在哪?
人工智能·语言模型·自然语言处理