热门搜索:和平精英 原神 街篮2 

您的位置:首页 > > 教程攻略 > ai教程 >Spring AI Function Calling:让AI调用你的Java方法

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

来源:互联网 更新时间:2026-07-01 08:31

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

在搞大语言模型应用的时候,最让人挠头的点是什么?AI固然聪明伶俐,但它本质上是个“瞎子”和“哑巴”——看不见外面的世界,也没法动手干活。你问它一句“今儿个北京天气怎么样”,它只能凭借训练数据里的“记忆”来给你瞎蒙一顿,根本摸不着实时数据。

Function Calling这个机制,恰好就是来治这个病的。它能让AI光明正大地调用你写好的Ja va方法,拿到一手实时数据,然后底气十足地给你一个靠谱的答案。下面就是我在实际项目里折腾Spring AI Function Calling攒下来的经验,有走过的弯路,也有能落地的最佳实践。

一、Function Calling原理

1.1 问题场景

假设我们要整一个智能助手,用户可能会这么问:

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

你看,有和没有,差距一下就出来了。

1.2 工作流程

整个过程的执行链条其实并不复杂:

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

简单来说,就是AI内部其实做了这几步:先分析你的问题,判断需不需要调用外部工具;如果需要,就选一个最合适的Ja va方法,顺便把参数也组装好;Spring AI拿到指令后,找到对应的Bean执行;方法返回的结果再喂回给AI;最后AI基于实时数据生成一段自然的回答。

1.3 支持的方法类型

Spring AI当前支持三种定义工具方法的方式,各有各的适用场景:

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

想省事、代码又干净,首选就是@Tool注解。如果对灵活性要求高,比如工具需要动态组装,那ToolCallback就更合适。

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

2.1 环境准备

先把依赖整上。这里的核心是spring-ai-alibaba-spring-boot-starter,版本用1.0.0-M3,别忘了加上Spring的里程碑仓库。

4.0.0 org.springframework.boot spring-boot-starter-parent 3.3.0 com.example function-calling-demo 1.0.0 17 org.springframework.boot spring-boot-starter-web com.alibaba.cloud.ai spring-ai-alibaba-spring-boot-starter 1.0.0-M3 org.springframework.boot spring-boot-starter-test test spring-milestones Spring Milestones https://repo.spring.io/milestone

2.2 配置

配置也在application.yml里搞定,注意Function Calling场景下temperature建议调低,否则AI太“自由发挥”的话,调用工具的意愿会下降。

# 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 定义工具方法

重点来了,怎么定义一个工具方法?其实就是一个普通的Spring组件,给方法加上@Tool注解,再给参数加上@ToolParam描述就行了。

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 ja va.time.LocalDate; import ja va.util.Map; import ja va.util.Random; /** * 天气查询工具 * 实际项目中应调用真实的天气API(如和风天气、OpenWeather等) */ @Component public class WeatherTool { private static final Map 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 注册工具并调用

工具定义好了之后,需要在构造ChatClient的时候通过.defaultTools()把它注册进去。这样AI才能识别到它。

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

最后写一个Controller对外开放接口,方便测试。

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) {}

这样,一个基础的天气查询助手就搭好了。启动项目,访问/api/weather/test,就能看到AI通过调用getWeather方法拿到实时数据后再回答的效果。

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

3.1 场景说明

单一的天气查询场景只是个入门,实际生产环境中,一个AI助手可能需要同时掌管多个领域的知识。比如企业内部需要一个智能助理,它能:

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

一个工具类显然不够,我们得准备多个工具类了。

3.2 定义多个工具

这里定义一个EnterpriseTools类,把所有企业相关的工具方法都放进去。注意每个方法的description一定要写清楚,这是AI判断何时调用该工具的关键依据。

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 ja va.util.List; import ja va.util.Map; /** * 企业系统工具集 */ @Component public class EnterpriseTools { @Tool(description = "查询员工信息,包括姓名、部门、职位等") public String getEmployeeInfo(@ToolParam(description = "员工姓名或工号") String employeeId) { System.out.println("[Tool Called] getEmployeeInfo(employeeId=" + employeeId + ")"); // 模拟数据库查询 Map 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 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 多工具注册

注册多个工具类和注册一个工具类没有本质区别,直接在.defaultTools()里把所有工具类实例传进去就行。

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(); } }

这样一来,你就能问“张三现在什么职位?”,或者“项目A的进度到哪儿了?”,AI会自动判断该调用哪个工具来回答。

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

4.1 场景说明

上一种写法是把所有工具都固定注册进去,但有些场景下,并不是所有用户都配拥有所有工具的权限。比如管理员可以重启服务,普通用户不能。或者根据当前会话状态,某些工具需要动态加载或隐藏。这时候就需要动态工具机制了。

4.2 实现动态工具

ToolCallback + FunctionToolCallback是最灵活的方式。运行时根据用户角色动态构建工具列表,每次请求都生成一个独立的ChatClient实例。

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 ja va.util.ArrayList; import ja va.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 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 } }

这样做的好处很明显:按需加载、权限管控灵活。缺点也很明显:每次请求都创建ChatClient,性能上需要注意。不过对于大多数场景,这种开销完全可以接受。

五、踩坑记录与解决方案

理论说完了,下面盘点一下在实际落地过程中踩过的硬坑。

5.1 工具未被调用

问题现象:明明用户问的问题明显需要调用工具,AI却自己瞎编了一个答案,工具就这么被晾在一边。

原因分析:

  1. 工具描述不够清晰,AI根本不知道什么时候该用它。
  2. Temperature参数设得太高,AI过于“自由发挥”。
  3. 模型本身不支持Function Calling——这个得先确认。

解决方案:

// 1. 改进工具描述,把触发条件说清楚 @Tool(description = """获取指定城市的实时天气信息。当用户询问天气、气温、是否下雨等问题时,必须调用此工具。支持的城市:北京、上海、广州、深圳、杭州。""") public String getWeather(String city) { ... } // 2. 降低temperature spring.ai.alibaba.chat.options.temperature=0.1 // 3. 使用支持Function Calling的模型 // qwen-plus以上版本支持

描述写得越具体,AI越不容易跑偏。别指望AI能自己猜出工具的用途,你得把话说明白。

5.2 参数解析错误

问题现象:AI确实调用了工具了,但是传进来的参数类型完全不对,导致方法解析失败。

原因分析:AI生成的参数往往跟Ja va方法签名不太匹配,尤其是有多个参数或者参数类型复杂的时候。

解决方案:

// 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人"; }

参数尽量都用String,自己转换。这算是经验和教训并存的策略——简洁、可靠。

5.3 工具执行超时

问题现象:工具方法执行时间太长,AI那边等着超时了,最终返回了一个失败或者空洞的回答。

解决方案:

@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 }

如果工具注定要花很长时间,最好设计成异步模式,返回任务ID让AI引导用户去查结果。如果全程等待,那用户体验会很差。

5.4 工具返回数据过大

问题现象:工具调用成功了,但返回的数据量太大,导致Token一下子超限,响应失败。

解决方案:

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

记住一条原则:工具方法返回给AI的内容要尽量精炼,不要什么都往里塞。详细的完整信息可以留给AI引导用户去查看详情。

5.5 多个工具冲突

问题现象:注册了好几个功能相似的工具,AI搞不清究竟该用哪一个,有时甚至会错选。

解决方案:

// 明确区分工具的职责 @Tool(description = """获取单个城市的天气(用于查询一个城市的天气)""") public String getWeather(String city) { ... } @Tool(description = """对比多个城市的天气(用于对比两三个城市的天气差异) 当用户问"北京和上海天气怎么样"时调用此工具。""") public String compareWeather(String cities) { ... }

工具的可区分度越高,AI做选择的准确性就越高。描述里甚至可以加上什么情况该用哪个工具。

六、生产级最佳实践

6.1 工具设计原则

总结下来,工具设计有五个关键原则:

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

6.2 安全防护

涉及到系统操作或者敏感数据时,权限校验不能少。

@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 监控与日志

工具调用了多少次?哪个工具最快、哪个最慢?失败率怎么样?这些数据必须记录下来,方便排查问题。

@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, ja va.util.concurrent.TimeUnit.MILLISECONDS); log.info("getUserOrders executed in {} ms", duration); } } }

有了这些数据,你才能知道哪个工具需要优化,哪个工具经常出问题。

6.4 优雅降级

系统难免会有异常,降级机制能让工具即便在故障状态下依然返回一个友好的提示,而不是直接让AI卡壳。

@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 项目结构

贴一下完整的项目结构,方便你对照搭建。

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

7.2 启动类

启动类没什么特殊的,加个@SpringBootApplication即可。

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 测试用例

以下两个测试用例可以帮助你快速验证工具是否正常工作。

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 工具结果缓存

有些数据(比如天气)的时效性要求没有那么高,但查询成本却不低。加个缓存可以大大减轻后端压力。

@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); } }

需要注意的一点是:@Cacheable的缓存切面和@Tool注解一起使用时,确保AOP的优先级正确,否则缓存可能不会生效。

8.2 并发工具调用

如果工具需要查询多个维度的数据,可以考虑用CompletableFuture并发执行,减少总的等待时间。

@Tool(description = "批量查询天气") public String getMultipleWeather(String cities) { List> 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能够调用Ja va方法,获取实时数据,打破了AI“闭门造车”的局限。
  2. 工具定义使用@Tool注解,description要写清楚什么场景下调用这个工具。
  3. 参数设计优先使用String类型,自己再转换,避免解析错误。
  4. 安全防护必须做权限校验和命令白名单,敏感操作不能裸奔。
  5. 监控日志记录工具调用情况,便于排查问题。

9.2 适用场景

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

9.3 参考资源

  • Spring AI官方文档:docs.spring.io/spring-ai/…
  • 通义千问Function Calling文档:help.aliyun.com/zh/model-st…
  • OpenAI Function Calling指南:platform.openai.com/docs/guides…
AI自动绘画大师
AI自动绘画大师

类型:益智休闲

大小:5.72MB

语言:简体中文

平台:互联网

游戏下载

热门手游

手机号码测吉凶
本站所有软件,都由网友上传,如有侵犯你的版权,请发邮件haolingcc@hotmail.com 联系删除。 版权所有 Copyright@2012-2013 haoling.cc