来源:互联网 更新时间:2026-07-01 08:31
在搞大语言模型应用的时候,最让人挠头的点是什么?AI固然聪明伶俐,但它本质上是个“瞎子”和“哑巴”——看不见外面的世界,也没法动手干活。你问它一句“今儿个北京天气怎么样”,它只能凭借训练数据里的“记忆”来给你瞎蒙一顿,根本摸不着实时数据。
Function Calling这个机制,恰好就是来治这个病的。它能让AI光明正大地调用你写好的Ja va方法,拿到一手实时数据,然后底气十足地给你一个靠谱的答案。下面就是我在实际项目里折腾Spring AI Function Calling攒下来的经验,有走过的弯路,也有能落地的最佳实践。
假设我们要整一个智能助手,用户可能会这么问:
用户:帮我查一下北京今天的天气
AI(无Function Calling):根据我的知识,北京今天可能是...
AI(有Function Calling):调用getWeather("北京") → 获取实时数据 → 北京今天晴,25°C
你看,有和没有,差距一下就出来了。
整个过程的执行链条其实并不复杂:
用户提问
↓
AI分析:需要调用工具吗?
↓ 是
AI决定调用哪个方法 + 参数
↓
Spring AI执行对应的Ja va方法
↓
将方法返回值返回给AI
↓
AI基于返回数据生成回答
↓
返回给用户
简单来说,就是AI内部其实做了这几步:先分析你的问题,判断需不需要调用外部工具;如果需要,就选一个最合适的Ja va方法,顺便把参数也组装好;Spring AI拿到指令后,找到对应的Bean执行;方法返回的结果再喂回给AI;最后AI基于实时数据生成一段自然的回答。
Spring AI当前支持三种定义工具方法的方式,各有各的适用场景:
| 方式 | 说明 | 推荐度 |
|---|---|---|
@Tool注解 | 最简洁,Spring AI 1.1+ | ⭐⭐⭐⭐⭐ |
Function接口 | 传统方式,兼容性好 | ⭐⭐⭐ |
ToolCallback | 更灵活,支持动态工具 | ⭐⭐⭐⭐ |
想省事、代码又干净,首选就是@Tool注解。如果对灵活性要求高,比如工具需要动态组装,那ToolCallback就更合适。
先把依赖整上。这里的核心是spring-ai-alibaba-spring-boot-starter,版本用1.0.0-M3,别忘了加上Spring的里程碑仓库。
配置也在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 场景温度要低
重点来了,怎么定义一个工具方法?其实就是一个普通的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
工具定义好了之后,需要在构造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;
}
}
最后写一个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方法拿到实时数据后再回答的效果。
单一的天气查询场景只是个入门,实际生产环境中,一个AI助手可能需要同时掌管多个领域的知识。比如企业内部需要一个智能助理,它能:
一个工具类显然不够,我们得准备多个工具类了。
这里定义一个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
注册多个工具类和注册一个工具类没有本质区别,直接在.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会自动判断该调用哪个工具来回答。
上一种写法是把所有工具都固定注册进去,但有些场景下,并不是所有用户都配拥有所有工具的权限。比如管理员可以重启服务,普通用户不能。或者根据当前会话状态,某些工具需要动态加载或隐藏。这时候就需要动态工具机制了。
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
这样做的好处很明显:按需加载、权限管控灵活。缺点也很明显:每次请求都创建ChatClient,性能上需要注意。不过对于大多数场景,这种开销完全可以接受。
理论说完了,下面盘点一下在实际落地过程中踩过的硬坑。
问题现象:明明用户问的问题明显需要调用工具,AI却自己瞎编了一个答案,工具就这么被晾在一边。
原因分析:
Temperature参数设得太高,AI过于“自由发挥”。解决方案:
// 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能自己猜出工具的用途,你得把话说明白。
问题现象: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,自己转换。这算是经验和教训并存的策略——简洁、可靠。
问题现象:工具方法执行时间太长,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引导用户去查结果。如果全程等待,那用户体验会很差。
问题现象:工具调用成功了,但返回的数据量太大,导致Token一下子超限,响应失败。
解决方案:
@Tool(description = "查询用户列表(分页)")
public String getUsers(@ToolParam(description = "页码,从1开始") String pageStr) {
int page = Integer.parseInt(pageStr);
// 限制返回数量
List
记住一条原则:工具方法返回给AI的内容要尽量精炼,不要什么都往里塞。详细的完整信息可以留给AI引导用户去查看详情。
问题现象:注册了好几个功能相似的工具,AI搞不清究竟该用哪一个,有时甚至会错选。
解决方案:
// 明确区分工具的职责
@Tool(description = """获取单个城市的天气(用于查询一个城市的天气)""")
public String getWeather(String city) { ... }
@Tool(description = """对比多个城市的天气(用于对比两三个城市的天气差异)
当用户问"北京和上海天气怎么样"时调用此工具。""")
public String compareWeather(String cities) { ... }
工具的可区分度越高,AI做选择的准确性就越高。描述里甚至可以加上什么情况该用哪个工具。
总结下来,工具设计有五个关键原则:
1. 单一职责:每个工具只做一件事
2. 明确描述:description要详细,说明何时调用
3. 参数简单:优先用String,避免复杂对象
4. 快速返回:工具执行时间控制在3秒内
5. 安全校验:所有参数都要校验
涉及到系统操作或者敏感数据时,权限校验不能少。
@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;
}
}
这条红线不能碰:任何执行系统命令或修改数据的工具,必须做权限校验+命令白名单。
工具调用了多少次?哪个工具最快、哪个最慢?失败率怎么样?这些数据必须记录下来,方便排查问题。
@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);
}
}
}
有了这些数据,你才能知道哪个工具需要优化,哪个工具经常出问题。
系统难免会有异常,降级机制能让工具即便在故障状态下依然返回一个友好的提示,而不是直接让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 "库存查询暂时不可用,请稍后重试";
}
}
}
在容错面前,数据时效性可以适当让步。返回一个不那么精确的缓存数据,比直接报错要好得多。
贴一下完整的项目结构,方便你对照搭建。
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
启动类没什么特殊的,加个@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");
}
}
以下两个测试用例可以帮助你快速验证工具是否正常工作。
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);
}
}
有些数据(比如天气)的时效性要求没有那么高,但查询成本却不低。加个缓存可以大大减轻后端压力。
@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的优先级正确,否则缓存可能不会生效。
如果工具需要查询多个维度的数据,可以考虑用CompletableFuture并发执行,减少总的等待时间。
@Tool(description = "批量查询天气")
public String getMultipleWeather(String cities) {
List
注意线程池的配置,别让并发查询把系统资源打满了。
@Tool注解,description要写清楚什么场景下调用这个工具。| 场景 | 说明 | 示例 |
|---|---|---|
| 实时数据查询 | 天气、股票、新闻等 | 天气查询助手 |
| 数据库操作 | 根据用户意图查询数据库 | 智能客服 |
| 系统运维 | 执行运维命令、查询状态 | 运维助手 |
| 业务操作 | 下单、退款、审批等 | 企业助手 |
archiveofourown 实战指南:常见用法整理
币安Binance虚拟货币交易平台 币安官方APP安卓苹果下载入口
客单价碾压宝马奥迪!极氪5月交付新车34377辆:连续4个月双增长
折后价近千元 澳洲一店主将真老鼠缝到内裤上当时尚单品卖
电视剧《小欢喜》剧情介绍
如何在夸克浏览器中开启网页视频的倍速播放功能?
美好的简约网名男生(精选100个)
植物娘大战僵尸电脑端与手机端存档转移的方法
《梦幻西游》159五开五门怎么搭配-159五开五门常见搭配
欧易OKX官方网站直达入口 2026欧易官方App安卓版v7.1.0下载安装
腾讯元宝怎么用来分析股票基金的基本面信息?
盖乐世社区怎么删除帖子?盖乐世社区个人发布内容撤回步骤
wallpaper壁纸声音怎么开启
独家/李宰旭入伍前「登上孤岛服役」 惊见前辈裸体:忍不住笑了
国际贵金属走低,现货黄金价格跌0.49%
短剧《嫡女她是山大王》剧情介绍
问题:CIA币好不?Cia Protocol币今日上线:价格预测、代币经济学和未来潜力
看韩漫的APP推荐 2026免费韩漫阅读软件大全
OpenAI 调整手机端 ChatGPT,提示词可提前选 AI 响应档位
免费观看国外短视频的app有哪些 观看国外短视频的软件下载
手机号码测吉凶
本站所有软件,都由网友上传,如有侵犯你的版权,请发邮件haolingcc@hotmail.com 联系删除。 版权所有 Copyright@2012-2013 haoling.cc