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 参考资源


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

相关推荐
记忆停留w10 分钟前
从单体到微服务:Redis 协同 MySQL、Milvus、MinIO 搭建企业级RAG/AI Agent脚手架架构
大数据·人工智能·redis·微服务·ai·架构·milvus
承受失22 分钟前
Cloud Agent 开发笔记(4):Skill 与 MCP 集成、项目后记
人工智能·prompt
狼爷23 分钟前
从入门到进阶:全方位拆解RAG落地优化方案,彻底解决幻觉与检索短板
ai编程
进击的横打26 分钟前
【人工智能】AI基础知识(第七节:RAG)
人工智能
KaMeidebaby28 分钟前
卡梅德生物技术快报|抗体亲和力成熟工业化调控新机制:差异性浆细胞增殖工艺优化思路
java·开发语言·人工智能·算法·机器学习·架构·spark
tmlx3I0811 小时前
高光谱拼接算法(六)RANSAC 误匹配剔除
人工智能·算法·机器学习
Mininglamp_27182 小时前
Claude Code 封禁中国开发者之后:本地 AI 编程工具的替代方案实测
开发语言·人工智能·windows·开源软件·ai-native
硅谷秋水2 小时前
World Engine:迈向自动驾驶的后训练时代
人工智能·深度学习·机器学习·计算机视觉·语言模型·自动驾驶
RobinDevNotes2 小时前
PentAGI:全自主 AI 渗透测试代理系统
人工智能
明理的信封2 小时前
AI 基础设施的“去 Python 化“:Rust 与 C# 的两条替代路径
人工智能·python·rust