
从“聊天”到“执行”,三行代码让大模型调用你的Java方法
前言:当AI只会“动嘴”的时候
前两篇文章,我们教会了AI说话——它能回答你的问题、按你的要求生成代码。但仔细想想,这跟雇了一个只会纸上谈兵的顾问有什么区别?你问它“北京今天天气怎么样”,它倒是能侃侃而谈“北京属于温带季风气候,四季分明”,但就是给不出今天的实时温度。
为什么?因为大模型的训练数据是有截止日期的。它根本不知道今天是几号,更不知道此时此刻北京是晴是雨。
Tool Calling(工具调用)要解决的问题,就是让AI不再只是“动嘴”,而是能“动手”。当用户问天气时,AI会主动调用你写的天气查询方法,拿到真实数据后再组织成回答返回给用户。
整个过程对用户来说,体验就像AI真的“知道”天气一样——但背后,是你的Java代码在干活。
Spring AI 2.0对Tool Calling机制进行了彻底重构,把它从底层实现提升到了Advisor链中的一等公民。今天这篇文章,我们就从零开始,用阿里百炼(通义千问) 为例,一步步实现一个能查天气的AI应用。
核心概念:Tool Calling到底是怎么工作的?
在动手之前,先搞清楚一件事:大模型本身不执行任何工具——它只负责“决定”要不要调用工具、以及传什么参数。真正干活的,是你的Java代码。
整个流程是这样的:
用户:北京今天天气怎么样?
↓
大模型:判断需要调用“天气工具”
↓
Spring AI:执行你的Java方法(getWeather("北京"))
↓
大模型:拿到结果“晴,28°C”,组织成自然语言
↓
用户:收到“北京今天晴,气温28°C”
Tool Calling的核心价值在于:大模型充当“大脑”做决策,你的Java方法充当“手脚”去执行。大脑负责判断“该干什么”,手脚负责“具体怎么干”。
Spring AI 2.0相比1.x最大的变化是:工具调用循环从模型内部实现,提升到了Advisor链,开发者可以观察、拦截、组合每一个中间步骤。ToolCallingAdvisor会负责完整的工具执行生命周期:发现工具、执行、循环直到模型不再请求工具为止。
第一步:项目配置(基于阿里百炼)
沿用前两篇文章的基础配置,在pom.xml中加入Spring AI 2.0的BOM和OpenAI Starter(兼容阿里百炼):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
application.yml配置阿里百炼的接入地址:
spring:
ai:
openai:
api-key: ${DASH_SCOPE_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-plus
⚠️ 注意:通义千问(qwen-plus/qwen-max)完全支持Tool Calling功能。如果使用的是其他模型,请确认模型是否支持工具调用。
第二步:定义工具——用@Tool注解
Spring AI 2.0提供了声明式的工具定义方式——@Tool注解。你只需要在一个普通方法上加上@Tool注解,Spring AI会自动生成JSON Schema并注册到模型。
先写一个模拟的天气服务(真实场景中这里会调用第三方天气API):
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class WeatherTools {
/**
* 模拟的天气数据源——实际项目中替换为真实API调用
*/
private static final Map<String, String> WEATHER_MOCK = Map.of(
"北京", "晴,28°C,湿度45%",
"上海", "多云,26°C,湿度65%",
"深圳", "阵雨,30°C,湿度80%",
"杭州", "晴,27°C,湿度50%"
);
@Tool(name = "getWeather", description = "获取指定城市的实时天气信息")
public String getWeather(
@ToolParam(description = "城市名称,如:北京、上海、深圳") String city) {
// 模拟调用外部API——真实场景替换为HTTP请求
String weather = WEATHER_MOCK.getOrDefault(city, "未知城市,请确认城市名称");
return String.format("城市:%s,天气:%s", city, weather);
}
}
代码解析:
@Tool(name="getWeather"):工具名称,大模型通过这个名字来调用@ToolParam(description="..."):参数描述,帮助大模型理解该传什么值方法的返回值会直接作为工具结果返回给大模型
第三步:注册工具到ChatClient
Spring AI 2.0中,工具通过.tools()方法显式注册:
@Configuration
public class AiConfiguration {
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
WeatherTools weatherTools) {
return builder
.defaultTools(weatherTools) // 注册工具
.build();
}
}
如果你不想全局注册,也可以在每次调用时临时注册:
@RestController
public class ChatController {
private final ChatClient chatClient;
private final WeatherTools weatherTools;
public ChatController(ChatClient chatClient, WeatherTools weatherTools) {
this.chatClient = chatClient;
this.weatherTools = weatherTools;
}
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.tools(weatherTools) // 本次调用注册工具
.call()
.content();
}
}
第四步:测试——让AI调用天气工具
启动应用,访问:
http://localhost:8080/api/ai/chat?message=北京今天天气怎么样
Tool called: getWeather("北京")返回结果:
北京今天晴,气温28°C,湿度45%。天气不错,适合出门活动。发生了什么:
大模型收到“北京今天天气怎么样”
模型判断需要调用
getWeather工具,参数city="北京"Spring AI执行
WeatherTools.getWeather("北京")工具结果返回给模型
模型基于结果生成最终回答
如果问一个没注册工具的问题,比如“讲个笑话”,模型不会调用任何工具,直接回答——工具是按需调用的,不是每次必调。
实战进阶:支持多参数的工具
天气查询只需要一个参数。但真实场景中,工具往往需要多个参数——比如订票:
@Component
public class BookingTools {
@Tool(description = "预订机票,根据出发地、目的地和日期查询航班并预订")
public String bookFlight(
@ToolParam(description = "出发城市") String origin,
@ToolParam(description = "目的地城市") String destination,
@ToolParam(description = "出行日期,格式:YYYY-MM-DD") String date) {
// 模拟订票逻辑——真实场景调用航司API
return String.format("已为您预订 %s → %s 的机票,日期:%s,订单号:FL-%d",
origin, destination, date, System.currentTimeMillis() % 100000);
}
}
多个参数时,Spring AI会自动生成对应的JSON Schema,大模型会根据用户的问题自动提取所有参数值。用户问“帮我订一张从北京到上海的机票,明天出发”,模型会自动提取origin=北京、destination=上海、date=明天(转换为具体日期)。
进阶:理解ToolCallingAdvisor
在Spring AI 2.0中,工具调用是由ToolCallingAdvisor驱动的。它是一个递归Advisor——会反复进入下游Advisor链,直到模型不再请求工具为止。
DefaultChatClient会自动将ToolCallingAdvisor添加到Advisor链中,它负责完整的工具执行生命周期:
提取工具定义:从
@Tool注解中提取工具的名称、描述、输入参数的JSON Schema注入上下文:将工具定义与用户问题、系统提示一起发送给LLM
循环执行:
LLM返回包含工具调用的响应 → 执行工具 → 将结果追加到对话历史 → 继续
LLM返回不含工具调用的响应 → 结束循环,返回最终答案
阻塞模式(.call())和流式模式(.stream())都完全支持。
💡 了解这个机制的意义:当你需要观察工具调用的中间步骤、记录日志、或者实现自定义的循环逻辑时,就知道该从哪里介入了。
⚠️ 避坑指南
坑1:用旧版的FunctionCallback API
Spring AI 2.0已经废弃了FunctionCallback,全面转向ToolCallback。如果你从1.x迁移过来,注意以下变化:
FunctionCallback→ToolCallback.functions()→.tools()@Function→@Tool
坑2:工具参数用复杂对象
工具方法的参数尽量用简单类型(String、int、double等)。复杂对象容易导致JSON解析问题。如果确实需要多参数,用多个简单类型参数,每个都加上@ToolParam(description="...")。
坑3:忘记给工具加描述
@Tool的description和@ToolParam的description不是注释,是给大模型看的指令。描述越清晰,模型判断“什么时候该调用这个工具”就越准确。
坑4:认为工具是“自动发现”的
Spring AI 2.0中,工具必须显式注册——通过defaultTools()或.tools()传入。光在类上写@Tool注解不够,还得告诉ChatClient这个工具的存在。
坑5:工具执行时间过长
大模型等待工具返回时有超时限制。如果工具方法执行时间过长(比如调用外部API超时),整个请求会失败。建议:
给外部调用设置合理的超时时间
考虑异步处理 + 轮询的模式
小结
Tool Calling让大模型从“聊天”升级到“执行” ——模型决策,Java代码执行
@Tool注解是定义工具的最简单方式,Spring AI自动生成JSON Schema@ToolParam为每个参数添加描述,帮助模型准确提取参数值工具通过
.tools()或defaultTools()显式注册到ChatClientToolCallingAdvisor驱动整个工具调用循环,自动处理多轮调用从1.x迁移注意:
FunctionCallback→ToolCallback,.functions()→.tools()
工具调用是构建AI Agent的基石——有了它,AI才能真正“动手”帮你做事。不只是查天气,查数据库、发邮件、创建订单、执行代码——任何你能用Java做的事,现在都可以让AI来“指挥”完成。
下一篇预告:RAG(检索增强生成)——让AI基于你的私有文档回答问题。当用户问“我们公司的报销流程是什么”,AI不再胡说八道,而是从你上传的《员工手册》中检索答案。敬请期待!
夜雨聆风