在 AI 应用开发中,让大模型调用外部工具、访问实时数据或执行业务逻辑是常见需求。Spring AI Alibaba 结合阿里云 DashScope(通义千问)提供了简洁的 Tool Calling(函数调用)能力,模型可以自动识别用户意图并调用注册的 Java 方法,再将结果融入对话。
本文以“查询天气”为案例,完整演示在 Spring Boot 项目中集成 DashScope,并分别使用注解(@Tool)、接口(Function)以及FunctionTool.builder + Lambda三种方式定义工具。同时,针对每种工具定义方式,均展示基于ChatModel(底层手动循环) 和ChatClient(自动工具闭环) 的调用实现,共六种组合,并解决ChatClient无法自动注入的问题。
ChatModel#call()只负责和大模型网络通信,不会自动执行工具。如果直接调用,收到模型返回FunctionCall后直接返回JSON结构体,不会执行业务逻辑。想要完整工具调用闭环:- 方案A(推荐):使用
ChatClient,内部ToolCallingAdvisor自动完成工具执行+多轮对话; - 方案B(底层API):使用
ChatModel+DefaultToolCallingManager手写while循环驱动工具调用。
2. 环境准备
2.1 添加依赖
在pom.xml中引入 Spring AI Alibaba 的 DashScope 起步依赖:
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<!-- 请使用最新版本,例如 1.0.0-M3 -->
</dependency>提示:建议在<dependencyManagement>中引入 Spring AI Alibaba BOM 统一管理版本。
2.2 配置 application.properties
server.port=8013
# 设置全局编码格式
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SAA-13ToolCalling
# ====SpringAIAlibaba Config=============
spring.ai.dashscope.api-key=${aliQwen-api}请提前在阿里云开通 DashScope 服务并获取 API Key,设置环境变量aliQwen-api=你的key。
3. 定义工具:三种方式
3.1 方式一:使用 @Tool 注解(声明式工具)
使用@Tool注解标记 Java 方法,Spring AI 会自动解析方法签名、参数和描述,生成可供大模型调用的工具元数据。
import org.springframework.ai.tool.annotation.Tool;
public class WeatherTools {
/**
* 查询指定城市的天气
* returnDirect = false 表示工具结果会再次交给大模型,由大模型组织最终回复
*/
@Tool(description = "查询指定城市的天气情况", returnDirect = false)
public String getWeather(String city) {
// 实际项目中可调用第三方天气 API,这里用模拟数据演示
return String.format("%s:晴,气温 25℃,湿度 40%%", city);
}
}关键参数说明:
description:工具的描述信息,大模型会根据它判断是否以及何时调用该函数。returnDirect:true:工具返回后直接作为最终响应,不再调用大模型。false:工具结果会送回给大模型,由大模型结合上下文生成更自然的回答。
3.2 方式二:实现 Function 接口(编程式工具)
通过实现java.util.function.Function<T, R>接口,并包装为FunctionTool,可以更灵活地控制工具逻辑,适合复杂业务场景,便于做代理、鉴权、单元测试。
① 定义入参 record
public record WeatherRequest(String city) {}② 实现 Function 接口
import org.springframework.stereotype.Component;
import java.util.function.Function;
@Component
public class WeatherTool implements Function<WeatherRequest, String> {
@Override
public String apply(WeatherRequest request) {
// 实际项目中可在此调用天气 API
String city = request.city();
return String.format("%s:多云,气温 22℃,风力 3 级", city);
}
}③ 包装为 FunctionTool
⚠️不推荐直接new FunctionTool(weatherTool)无元数据构造,必须通过builder设置name、description,大模型才能识别工具。
生产最佳实践:不要在Controller方法内每次请求构建FunctionTool,统一在配置类注册为Bean复用。
3.3 方式三:FunctionTool.builder + Lambda 编程构建(进阶动态工具)
这种方式不需要编写注解,也不需要实现Function接口,直接使用 Lambda 表达式定义函数逻辑,并通过FunctionTool.builder构建工具。它最大的优势是灵活:可以动态生成工具、临时定义逻辑,尤其适合需要根据运行时条件生成不同工具的场景。
这里继续复用 3.2 中定义的WeatherRequestrecord 作为入参模型。
import org.springframework.ai.tool.function.FunctionTool;
import java.util.function.Function;
public class WeatherToolLambda {
/**
* 使用 Lambda 定义天气查询逻辑
*/
public static final Function<WeatherRequest, String> WEATHER_FUNCTION = request -> {
String city = request.city();
return String.format("%s:阴,气温 18℃,风力 2 级", city);
};
/**
* 通过 FunctionTool.builder 构建 FunctionTool
*/
public static FunctionTool createWeatherTool() {
return FunctionTool.builder("getWeather", WEATHER_FUNCTION)
.description("查询指定城市的天气情况")
.inputType(WeatherRequest.class)
.returnDirect(false)
.build();
}
}关键参数说明:
name("getWeather"):工具名称,模型返回的函数调用请求会使用该名称。description(...):工具描述,用于模型判断是否调用。inputType(WeatherRequest.class):指定入参类型,Spring AI 会据此生成 JSON Schema。returnDirect(false):工具结果是否直接返回,默认false。
4. 使用 ChatModel 进行 Tool Calling(底层手动循环)
使用底层ChatModel必须引入DefaultToolCallingManager手动驱动工具执行循环;否则只能拿到FunctionCall JSON,不会执行业务工具。
4.1 手动配置 ChatClient(解决自动注入问题)
在当前 Spring AI Alibaba 版本中,ChatClient默认不会自动注入,需要通过@Configuration显式注册 Bean。
同时将Function接口包装后的FunctionTool注册为Bean,Controller直接注入复用,避免每次请求重复构建对象。
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.function.FunctionTool;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaaLLMConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
/**
* 将Function接口实现包装为FunctionTool,注册为单例Bean复用
*/
@Bean
public FunctionTool queryWeatherFunctionTool(WeatherTool weatherTool){
return FunctionTool.builder(weatherTool)
.name("queryWeather")
.description("查询指定城市天气情况")
.build();
}
}4.2 注解方式 + ChatModel(手动循环)
import com.example.study.tools.WeatherTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.manager.DefaultToolCallingManager;
import org.springframework.ai.tool.manager.ToolCallingManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolCallingController {
@Resource
private ChatModel chatModel;
private final ToolCallingManager toolCallingManager = new DefaultToolCallingManager();
@GetMapping("/toolcall/chat-annotation")
public String chatWithAnnotation(@RequestParam(name = "msg", defaultValue = "北京天气怎么样") String msg) {
// 1. 将注解式工具注册到回调数组
ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
// 2. 构建带有工具回调的 ChatOptions
var options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
// 3. 组装 Prompt
Prompt prompt = new Prompt(msg, options);
var response = chatModel.call(prompt);
// 手动驱动工具调用循环,最大循环次数防止死循环
int maxRound = 5;
int round = 0;
while (response.hasToolCalls() && round < maxRound) {
var execResult = toolCallingManager.executeToolCalls(prompt, response);
prompt = execResult.conversationHistory();
response = chatModel.call(prompt);
round++;
}
return response.getResult().getOutput().getText();
}
}4.3 接口方式 + ChatModel(手动循环)
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionTool;
import org.springframework.ai.tool.manager.DefaultToolCallingManager;
import org.springframework.ai.tool.manager.ToolCallingManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolCallingController {
@Resource
private ChatModel chatModel;
// 直接注入配置类构建完成的FunctionTool Bean,不再重复builder
@Resource
private FunctionTool queryWeatherFunctionTool;
private final ToolCallingManager toolCallingManager = new DefaultToolCallingManager();
@GetMapping("/toolcall/chat-function")
public String chatWithFunction(@RequestParam(name = "msg", defaultValue = "上海天气如何") String msg) {
ToolCallback[] tools = new ToolCallback[]{queryWeatherFunctionTool};
var options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
Prompt prompt = new Prompt(msg, options);
var response = chatModel.call(prompt);
int maxRound = 5;
int round = 0;
while (response.hasToolCalls() && round < maxRound) {
var execResult = toolCallingManager.executeToolCalls(prompt, response);
prompt = execResult.conversationHistory();
response = chatModel.call(prompt);
round++;
}
return response.getResult().getOutput().getText();
}
}4.4 Lambda 方式 + ChatModel(手动循环)
import com.example.study.tools.WeatherToolLambda;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionTool;
import org.springframework.ai.tool.manager.DefaultToolCallingManager;
import org.springframework.ai.tool.manager.ToolCallingManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolCallingController {
@Resource
private ChatModel chatModel;
private final ToolCallingManager toolCallingManager = new DefaultToolCallingManager();
@GetMapping("/toolcall/chat-lambda")
public String chatWithLambda(@RequestParam(name = "msg", defaultValue = "杭州天气怎么样") String msg) {
FunctionTool functionTool = WeatherToolLambda.createWeatherTool();
ToolCallback[] tools = new ToolCallback[]{functionTool};
var options = ToolCallingChatOptions.builder()
.toolCallbacks(tools)
.build();
Prompt prompt = new Prompt(msg, options);
var response = chatModel.call(prompt);
int maxRound = 5;
int round = 0;
while (response.hasToolCalls() && round < maxRound) {
var execResult = toolCallingManager.executeToolCalls(prompt, response);
prompt = execResult.conversationHistory();
response = chatModel.call(prompt);
round++;
}
return response.getResult().getOutput().getText();
}
}5. 使用 ChatClient 进行 Tool Calling(自动闭环,推荐)
ChatClient内置ToolCallingAdvisor,自动完成工具调用循环,不需要手动写while循环,支持流式返回。
5.1 注解方式 + ChatClient 调用
import com.example.study.tools.WeatherTools;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ToolCallingController {
@Resource
private ChatClient chatClient; // 注入手动配置的 Bean
@GetMapping("/toolcall/chatclient-annotation")
public Flux<String> chatClientWithAnnotation(@RequestParam(name = "msg", defaultValue = "广州天气怎么样") String msg) {
return chatClient.prompt(msg)
.tools(new WeatherTools()) // 直接传入注解工具对象
.stream() // 启用流式调用
.content(); // 返回文本内容的 Flux
}
}5.2 接口方式 + ChatClient 调用
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.function.FunctionTool;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ToolCallingController {
@Resource
private ChatClient chatClient;
// 直接复用配置类中已经构建好的FunctionTool Bean
@Resource
private FunctionTool queryWeatherFunctionTool;
@GetMapping("/toolcall/chatclient-function")
public Flux<String> chatClientWithFunction(@RequestParam(name = "msg", defaultValue = "深圳天气如何") String msg) {
return chatClient.prompt(msg)
.tools(queryWeatherFunctionTool) // 传入已经构建完成的FunctionTool
.stream()
.content();
}
}5.3 Lambda 方式 + ChatClient 调用
import com.example.study.tools.WeatherToolLambda;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.function.FunctionTool;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ToolCallingController {
@Resource
private ChatClient chatClient;
@GetMapping("/toolcall/chatclient-lambda")
public Flux<String> chatClientWithLambda(@RequestParam(name = "msg", defaultValue = "成都天气如何") String msg) {
FunctionTool functionTool = WeatherToolLambda.createWeatherTool();
return chatClient.prompt(msg)
.tools(functionTool) // 传入 FunctionTool
.stream()
.content();
}
}6. 测试与效果
启动项目后,分别测试六个接口。
底层ChatModel接口返回完整文本;ChatClient系列接口为SSE流式输出,浏览器直接访问即可看到逐字输出。
① ChatModel + 注解工具
curl "http://localhost:8013/toolcall/chat-annotation?msg=北京天气怎么样"响应示例:
北京:晴,气温 25℃,湿度 40%② ChatModel + 接口工具
curl "http://localhost:8013/toolcall/chat-function?msg=上海天气如何"响应示例:
上海:多云,气温 22℃,风力 3 级③ ChatModel + Lambda 工具
curl "http://localhost:8013/toolcall/chat-lambda?msg=杭州天气怎么样"响应示例:
杭州:阴,气温 18℃,风力 2 级④ ChatClient + 注解工具(流式 SSE)
浏览器打开:
http://localhost:8013/toolcall/chatclient-annotation?msg=广州天气怎么样会看到文本逐渐输出,例如:
广州:晴,气温 25℃,湿度 40%⑤ ChatClient + 接口工具(流式 SSE)
浏览器打开:
http://localhost:8013/toolcall/chatclient-function?msg=深圳天气如何会看到文本逐渐输出,例如:
深圳:多云,气温 22℃,风力 3 级⑥ ChatClient + Lambda 工具(流式 SSE)
浏览器打开:
http://localhost:8013/toolcall/chatclient-lambda?msg=成都天气如何会看到文本逐渐输出,例如:
成都:阴,气温 18℃,风力 2 级注意:以上示例中工具返回结果后,因为returnDirect = false(注解方式默认)或FunctionTool默认行为,大模型会再次加工生成自然语言回复。若需直接返回工具结果,可调整配置或使用returnDirect选项。
7. 原理与关键点解析
7.1 Tool Calling 工作流程
- 用户提问→ 携带已注册工具的元数据发送给 DashScope 大模型。
- 模型判断→ 如果需要调用某个工具,返回一个“函数调用请求”,包含工具名和参数。
- 框架执行→ Spring AI 根据返回的工具名找到对应 Java 方法并执行,获取结果。
👉 ChatClient:Advisor自动执行;原始ChatModel:必须通过ToolCallingManager手动执行。 - 二次生成→ 若
returnDirect = false,框架将工具返回结果重新提交给模型,模型结合上下文生成最终回复;若为true,则直接返回工具结果。
7.2 三种工具定义方式对比
| 特性 | @Tool 注解方式 | Function 接口方式 | Lambda + FunctionTool.builder |
|---|---|---|---|
| 定义方式 | 在方法上添加注解 | 实现Function<T, R>接口 | Lambda 表达式 + builder 构建 |
| 参数传递 | 方法参数自动映射 | 通过 record 封装入参 | 通过 record 封装入参(inputType指定) |
| 灵活度 | 简单快速,适合单一方法 | 更灵活,适合复杂业务逻辑、代理鉴权、单元测试 | 最灵活,可动态构建,无需类定义 |
| 注册方式 | ToolCallbacks.from(obj)或.tools(obj) | 配置类Bean注册,Controller直接注入复用 | FunctionTool.builder(...).build()或.tools(functionTool) |
| 推荐场景 | 轻量级工具,快速接入 | 需要依赖注入、复杂过滤或自定义逻辑 | 动态工具、临时 Lambda、避免编写类 |
7.3 为什么 ChatClient 不能自动注入?
目前 Spring AI Alibaba 的自动配置还未将ChatClient纳入标准 Bean 管理,因此需要我们在@Configuration类中手动创建并返回。随着版本迭代,这个问题很可能会被解决,留意官方更新即可。
7.4 returnDirect 的选择
- 需要大模型润色结果(例如:“北京今天天气晴朗,温度 25℃,建议穿短袖”) → 设为
false。 - 工具结果已是最终答案(例如:查询用户余额后直接返回数字) → 设为
true,可以节省一次模型调用成本。
对于Function接口方式,通过builder设置returnDirect:
FunctionTool functionTool = FunctionTool.builder(weatherTool)
.name("queryWeather")
.description("查询指定城市天气情况")
.returnDirect(true)
.build();对于FunctionTool.builder + Lambda方式,可在 builder 中设置:
FunctionTool functionTool = FunctionTool.builder("getWeather", WEATHER_FUNCTION)
.description("查询指定城市的天气情况")
.inputType(WeatherRequest.class)
.returnDirect(true) // returnDirect = true
.build();8. 总结
本文以查询天气为例,完整演示了 Spring AI Alibaba 中 Tool Calling 的六种实现组合:
- 注解工具 + ChatModel(底层手动循环)
- 接口工具 + ChatModel(底层手动循环)
- Lambda 工具 + ChatModel(底层手动循环)
- 注解工具 + ChatClient(自动闭环,流式)
- 接口工具 + ChatClient(自动闭环,流式)
- Lambda 工具 + ChatClient(自动闭环,流式)
同时解决了ChatClient无法自动注入的问题,并说明了流式返回的实现方法。
生产优化点:静态工具对象不要在Controller接口方法内重复构建,统一在@Configuration注册单例Bean复用,减少对象创建开销。
- 业务开发优先选择
ChatClient,避免手写工具循环; - 只有需要完全接管工具执行流程、自定义鉴权拦截、需要用户确认后再执行工具场景,才使用原始
ChatModel + DefaultToolCallingManager; - 编程式Function工具优先用builder,不要直接无元参数构造FunctionTool;
- 安全:不要只在工具内部鉴权,优先外层动态裁剪ToolCallback,工具内部做兜底校验。
@Tool注解适合快速接入简单工具;Function接口适合需要依赖注入或复杂业务逻辑的场景;FunctionTool.builder + Lambda适合动态构建、临时定义工具,避免编写额外类。
希望这篇教程能帮助你快速上手 Spring AI Alibaba 的函数调用功能,为构建智能体应用打下坚实基础。