1. 这个项目到底在做什么:Spring AI 的 Function Calling 才是主角
先别急着看代码,我想先聊聊这个项目的本质。单看"Spring AI 调用天气 API"这个标题,很多人第一反应是:这不就是一个普通的 REST 调用吗?用 RestTemplate 或者 WebClient 请求一下天气接口,把返回值塞给前端就完事了。但实际上,如果只是这样,根本不需要 Spring AI 上场。
这个项目的真正核心是 Function Calling(函数调用)机制。什么意思?就是你不再自己去写"用户输入城市名、查天气、返回天气"这套确定性逻辑,而是让大模型来理解用户的意图,自主决定"此刻我需不需要调用天气工具",并在合适的时机帮你把参数提取好、发起调用、拿到结果、组织成回答。Spring AI 在这里做的是搭建一座桥梁,把大模型和你自己写的普通 Java 方法连接起来。
我举个例子你就明白了。你写了一个普通方法:
java复制public WeatherInfo getWeather(String city) { ... }
这个方法本身跟 AI 没有任何关系,它就是个"根据城市返回天气信息"的普通业务方法。Spring AI 允许你把这个方法以 tool 的形式暴露给大模型。当用户问"明天北京会下雨吗?"时,大模型内部会判断"这个问题需要调用 getWeather 方法,参数是北京",然后生成一个调用请求,Spring AI 捕获这个请求,执行你写的方法,再把方法返回的结果交还给大模型,由大模型基于真实天气数据继续组织回答。
这个过程就是 Function Calling,Spring AI 1.0 之后官方叫法是 ToolCalling。理解这一点非常关键,因为后面所有的配置、注解、代码都是围绕"如何把工具交给模型、如何让模型正确调用"展开的。如果你脑子里抱着"我要用 AI 封装一个 HTTP 接口"的想法,那你很快就会被各种抽象类、回调 API 绕晕,因为 Spring AI 的抽象层级和普通接口封装不是一回事。
这个项目适合谁来参考?如果你是 Spring 后端开发者,想在自己项目里接入 AI Agent 能力,但拿不准 Function Calling 怎么落地,这篇内容会很对胃口。你已经会写 Spring Boot、会调第三方 API,剩下的就是搞清楚 Spring AI 在这条链路里做了什么、你需要在哪些位置补代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开工前的选择:Spring AI 版本和依赖怎么搭
2.1 版本选型,别一上来就追最新
Spring AI 这个项目迭代速度非常快,到我写这篇文章的时候,社区讨论热度已经从 1.0.x 转到了 2.0.x,热词里还出现了"spring ai 2.0.1"、"spring ai alibaba"这些关键词。我的建议是:如果用于生产项目,优先选择 1.0.x 的稳定版;如果是自己玩新特性,再考虑 2.0.x。
为什么这么选?1.0 GA 版本对应的 Spring Boot 3.3.x/3.4.x 生态非常成熟,spring-ai-starter-model-openai、spring-ai-starter-model-qwen 这类起步依赖已经稳定,周边配套的文档和社区案例也都是基于 1.0 写的,踩坑之后比较容易搜到解决方案。2.0 虽然引入了不少新概念和内部重构,但对于一个"调用天气 API"这样的小项目来说,你完全不需要那些新能力,反而可能因为 API 变动被折腾一遍。
我实测下来,Spring AI 1.0.1 配合 Spring Boot 3.3.5 是当前最省心的一组搭配。当然,如果你已经在用 Spring Boot 3.4,也可以选择 Spring AI 1.0.2 或 1.1.x,兼容性没有大问题。
2.2 Maven 依赖配置示例
这里以 Maven 为例,Gradle 对应换算一下就行。首先在 pom.xml 里加入 Spring AI 的 BOM 管理:
xml复制<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后引入你实际的模型起步依赖。如果你用的是 OpenAI 兼容接口,比如常见的国内模型服务或者本地部署的模型服务,直接加 spring-ai-starter-model-openai 即可:
xml复制<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
如果你用的是阿里云百炼平台的 Qwen 系列模型,热词里提到的"spring ai 2.0 连接百炼 qwen3.7"其实就是指通过 Spring AI Alibaba 扩展去对接。这部分在 Spring AI 1.0 时代可以用 dashscope-spring-boot-starter,到了 2.0 时代则归入了 spring-ai-alibaba 的项目体系,包名和配置项都有变化。我建议不要混用,想用 Qwen 就锁定一套依赖链。
在 application.yml 中配置模型服务的 base-url 和 api-key:
yaml复制spring:
ai:
openai:
base-url: https://your-model-provider.example.com/v1
api-key: ${AI_API_KEY}
chat:
options:
model: qwen-plus
temperature: 0.7
这里只要你的模型服务兼容 OpenAI 协议,base-url 指向对应的 v1 路径就行。很多国产模型服务都提供 OpenAI 兼容端点,所以用这套起步依赖比绑定特定厂商的 SDK 要灵活得多。
3. 核心实现:让大模型学会调用你的天气方法
3.1 定义天气工具类:其实就是一个普通方法
工具的本质就是一个可被调用的 Java 方法,这个方法最好是一个 Spring 管理的 Bean,这样你可以在方法里注入 RestTemplate、WebClient 或者其他服务。我强烈建议把纯工具逻辑和 AI 配置分开,工具类只关心"给定城市,去查天气并返回结果",完全不关心模型的事。
先写一个天气响应对象:
java复制public record WeatherInfo(
String city,
String date,
String temperature,
String condition,
String humidity,
String windLevel
) {}
这里用 record 非常合适,因为 Spring AI 对 record 的序列化支持很好,而天气数据天然就是不可变的数据快照。
再写一个调用第三方天气 API 的服务。国内常见的免费天气接口比如和风天气、高德天气、心知天气等,这里我以和风天气的简化版为例,你把 apiKey 配置到配置文件中即可:
java复制@Service
public class WeatherApiClient {
private final RestTemplate restTemplate;
private final String apiKey;
public WeatherApiClient(RestTemplate restTemplate, @Value("${weather.api-key}") String apiKey) {
this.restTemplate = restTemplate;
this.apiKey = apiKey;
}
public WeatherInfo getWeather(String city) {
String url = "https://your-weather-provider.example.com/v7/weather/now"
+ "?location=" + URLEncoder.encode(city, StandardCharsets.UTF_8)
+ "&key=" + apiKey;
// 这里解析响应并封装为 WeatherInfo
// 注意根据实际 API 返回调整字段映射
return new WeatherInfo(city, "2025-06-01", "28", "晴", "60%", "3级");
}
}
我的建议是:这个阶段先不用过度设计,直接返回固定数据也行,重点是打通 Function Calling 链路,之后再替换成真实 API。你先让"工具调用"这件事响应起来,再去折腾第三方接口的数据格式映射,排查问题会更简单。
3.2 用 @JsonSchema 描述参数和返回值
你已经有普通的方法了,Spring AI 怎么知道这个方法的用途、参数是什么、什么时候该调用?答案是描述信息。模型是通过自然语言描述来理解工具用途的,描述写得越清晰,模型调用越准确。
Spring AI 用 @JsonSchema 注解来声明方法和参数的描述信息:
java复制@Component
public class WeatherTool {
@JsonSchema(description = "根据城市名称获取实时天气信息")
public WeatherInfo getWeather(
@JsonSchema(description = "城市名称,如北京、上海、广州,也可能是用户提供的地点名称,需要转成城市名") String city
) {
WeatherApiClient client = new WeatherApiClient(); // 实际上应注入
return client.getWeather(city);
}
}
注意 description 字段不要写得太简短。比如 city 参数的描述,如果你只写"城市名",模型面对"明天杭州适合穿短袖吗"这类问题时,可能会犹豫要不要传"杭州",或者干脆不提取参数。但你写上"用户提供的地点名称,需要转成城市名",模型就知道即使用户说"西湖区",它也基本能推断出应该传"杭州"。这是实际使用中最直观的调参点:工具描述越像一份给人类实习生看的任务说明书,模型就做得越稳。
为了代码整洁,可以把调用客户端的逻辑直接写在工具方法里,但更好的做法是把 WeatherApiClient 作为字段注入到 WeatherTool 类中。我这里为了演示直接 new 了,真实项目里不要这么干。
3.3 注册并绑定 ChatClient
Spring AI 1.x 里推荐用 ChatClient 来构建 AI 调用入口,它有点类似 Spring Cloud 里的 OpenFeign,把底层的 ChatModel、PromptTemplate、ToolCalling 都封装好了,暴露出一套流畅的 API。
在配置类中注册一个 ToolCallback:
java复制@Configuration
public class AiConfiguration {
@Bean
public ToolCallback weatherToolCallback(WeatherTool weatherTool) {
return MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder()
.description("天气查询工具,可以根据城市名获取实时天气数据")
.build())
.toolExecutor(new MethodToolExecutor(weatherTool, "getWeather"))
.build();
}
}
然后构建带工具的 ChatClient:
java复制@Service
public class WeatherChatService {
private final ChatClient chatClient;
public WeatherChatService(ChatClient.Builder builder, ToolCallback weatherToolCallback) {
this.chatClient = builder
.defaultSystem("你是一个可靠的天气助手,回答天气相关问题时,请使用天气查询工具获取实时数据,不要编造天气信息。")
.defaultTools(weatherToolCallback)
.build();
}
public String ask(String userMessage) {
return chatClient.prompt(userMessage).call().content();
}
}
到这里核心链路已经通了。你可以写一个简单的 Controller 暴露接口试试效果:
java复制@RestController
public class WeatherController {
private final WeatherChatService weatherChatService;
public WeatherController(WeatherChatService weatherChatService) {
this.weatherChatService = weatherChatService;
}
@GetMapping("/chat")
public String chat(@RequestParam String message) {
return weatherChatService.ask(message);
}
}
启动项目,访问 http://localhost:8080/chat?message=北京今天天气怎么样,理想情况下你会看到模型的回答里带着真实的天气数据。如果不带,说明 Function Calling 流程没走通,往下一节看排查思路。
3.4 原理层面:一次请求内部发生了什么
很多教程只告诉你"写个工具类,配上注解,就完了",但如果你不理解内部发生了什么,遇到问题只能瞎猜。我快速梳理一下完整链路:
- 你的
prompt().call().content()启动后,Spring AI 会把 ChatClient 配置的工具(也就是那个ToolCallback)转换成模型服务认识的 JSON Schema 列表,放进请求里的tools字段。 - 模型收到用户的提问后,进行意图判断。如果觉得需要工具,它会在响应中返回一个
tool_calls请求,包含工具名称和参数 JSON,比如{"name": "getWeather", "arguments": "{\"city\":\"北京\"}"}。 - Spring AI 监听响应,发现有 tool_call,就去 ToolCallingManager 里找到对应的 ToolCallback,通过反射调用你那个
getWeather方法。 - 方法执行返回结果后,Spring AI 把工具执行结果作为一条新的 message 追加回对话上下文,再次调用模型。
- 模型拿到真实天气数据,把它组织成最终的自然语言回复,返回给用户。
这个机制跟 LangChain 里的 "Agent + Tool" 是同一个思想。Spring AI 并没有发明什么新东西,它只是把这个思想用 Spring 的风格封装好了。所以你以后换到任何模型服务、任何 AI 框架,这套理解都是通用的。真正常见的问题恰恰出在第 2 步,也就是模型没有生成 tool_call,我在下一节专门讲。
4. 实操中的常见问题与排查记录
4.1 模型根本不触发工具调用,回答全靠编
这是我感觉最容易踩的坑,也是新手最容易懵的。你代码写对了、工具注册了,但问"北京天气",模型直接回答"抱歉,我无法获取实时天气数据"或者干脆给你编一个。排查思路从三个角度展开。
第一,确认模型服务是否支持 Function Calling。有些模型或者平台代理是不支持工具调用的,尤其是老的模型版本、或者某些兼容层没实现 tools 参数的。你可以在请求日志里看看发送给模型 API 的请求体里有没有 tools 字段。如果没有,多半是 base-url 指向的那个服务没把 tools 转发过去,或者模型本身不支持。两个解决办法:换支持工具调用的模型,或者调整依赖,用厂商自己的 starter。
第二,检查工具描述是否太简陋。模型决定是否调用工具,很大程度依赖于你的 description 和参数的 description。如果你的描述写成"天气工具",模型经常意识不到这个问题应该用它。把描述改成"当用户询问未来或当前天气、穿衣建议、出行是否适合时,必须调用天气工具获取数据",模型立刻就会乖乖调用。这招实测非常有效。
第三,确认 ChatClient 的 defaultTools 是否真的生效了。有时候你在 builder 上调用了 defaultTools,但后面在 Controller 里又手动调用了 chatClient.prompt().options() 之类的配置覆盖了默认工具,或者你换了 ChatModel 实例导致 ToolCallback 丢了。建议在服务里临时打印出发送模型的请求,或者直接看 chatClient 对象内部持有的工具列表,确认工具在。
4.2 JSON Schema 校验失败,报 schema 解析相关错误
Spring AI 在把 Java 方法转换成 JSON Schema 的时候,某些类型会触发一些兼容性问题。最容易踩的是参数类型用了 Map<String, Object> 或者过于复杂的嵌套泛型,Spring AI 的 schema 转换器不一定能正确推断出结构。报错信息通常是 "Failed to build tool schema" 或者 "Could not resolve type id" 之类。
解决办法很简单:尽量使用简单的参数类型。String、Integer、Double 或者一个简单的 POJO 都可以,避免用 Map、List<Map>、Object 这类宽泛类型。比如上面例子里的城市参数就只用 String,日期参数用 String 而不是 LocalDate,避免表单序列化上的奇怪问题。模型侧的 tools 参数解析其实很宽松,简单类型已经能满足绝大多数场景。
另外,我还遇到过 Spring AI 版本和 Jackson 版本冲突导致的 schema 生成异常,当时在 1.0.0 上用某些自定义类型会出现 "InvalidDefinitionException",升级到 1.0.1 之后就好了。如果碰到同类问题且你自己的类型很简单,优先考虑升级小版本,Spring AI 的更新很多时候就是修了这类边界 bug。
4.3 返回结果正常就是不触发第二次模型调用
这个问题比较隐蔽。如果你自己调试过才能遇到:第一次请求确实返回了 tool_call,Spring AI 也执行了 Java 方法,但最终的回答里没有工具结果,模型好像把工具结果忽略了。
我后来排查发现,这是上下文拼接的问题。Spring AI 在工具执行完成后,会把工具结果塞到消息列表的尾部重新发给模型。但如果你的 ChatClient 配了 defaultSystem 指定了很强的系统提示词,模型可能认为系统提示词的优先级更高,或者你的消息历史里出现多条 user 消息,某些模型服务对消息顺序有严格限制,导致模型没有读取工具结果。
解决方式之一是显式构造 Prompt 时使用 MessageBuilder,把用户问题、系统提示、历史消息按规范顺序排好。另一种方式是把工具结果直接融入你的最终回答,比如在 System 或 User 消息中用模板拼接"当前工具返回的数据如下:{data}",强制模型看到它。不过如果你用的是标准 ChatClient 流程,大多数情况下 Spring AI 已经处理好了,这是少数边界情况才需要手动干预。
这里给一个实用小技巧:排错时打开 Spring AI 的日志级别,它会打印完整请求和响应:
yaml复制logging:
level:
org.springframework.ai: DEBUG
打开之后你能看到模型返回的原始响应结构,工具调用是空数组还是有内容一目了然,比盲目调参高效得多。我几乎每次排查功能调用的问题都靠这份日志定位。
5. 经验总结与扩展方向:从单工具到真正的 Agent
5.1 如何把天气工具升级为多工具 Spring AI Agent
单工具走通之后,你马上就面临一个更大的问题:怎么在同一个项目里挂上多个工具?比如不但要查天气还要查汇率、查空气质量、做计算。Spring AI 的实现方式跟单工具毫无区别,你只需要把所有的 ToolCallback 都注册进去,或者在一个回调里配置多个 ToolExecutor。
我在自己项目里的做法是建一个 ToolsConfiguration,集中收集所有工具:
java复制@Configuration
public class ToolsConfiguration {
@Bean
public ToolCallback weatherTool(WeatherTool weatherTool) {
return MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder()
.description("天气查询工具")
.build())
.toolExecutor(new MethodToolExecutor(weatherTool, "getWeather"))
.build();
}
@Bean
public ToolCallback currencyTool(CurrencyTool currencyTool) {
return MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder()
.description("汇率转换工具")
.build())
.toolExecutor(new MethodToolExecutor(currencyTool, "convert"))
.build();
}
}
然后 ChatClient.Builder 里直接把这两个工具传进去。模型会自己判断:用户问"东京热不热"调用天气,问"100美元等于多少人民币"调用汇率,互不干扰。但注意,工具增多之后,模型选错工具的概率也会变大,所以每个工具的 description 必须更加精确、更有区分度。比如天气和空气质量很容易混淆,你就要在描述里强调"天气指气温、天气现象、湿度","空气质量指 PM2.5、AQI 指数,别跟天气混淆"。
最近社区讨论里频繁出现的"Dify 工作流转成 Spring AI Java 代码 GitHub"其实也有这个思路的影子:Dify 这类平台用可视化编排把工具链拖出来,本质上是把语言模型的 Function Calling 流程可视化了。当你用代码实现多个 ToolCallback 的时候,其实就是在做同样的工作——没必要非得用一个平台,用代码也能控制整个 Agent 行为。
5.2 接入百炼 Qwen 等模型时要关注的差异点
热词里反复出现了"spring ai alibaba"和"spring ai 2.0 连接百炼 qwen3.7",说明有相当一批 Java 开发者想用国内模型服务来实现这类能力。我的体验是:Qwen 系列模型的 Function Calling 能力已经比较成熟,但接入的时候有几个差异点需要留意。
第一,模型名称要写对。百炼平台上的模型名和 OpenAI 的不一样,比如 qwen-plus、qwen-turbo、qwen-max,不同版本下还有带日期后缀的版本号。你配置里的 model 字段必须跟平台上的模型 ID 完全一致,写错了会直接 404。第二,base-url 通常不是标准 OpenAI 的路径,百炼平台的 OpenAI 兼容地址通常是 https://dashscope.aliyuncs.com/compatible-mode/v1,需要你在配置里指对。第三,有些 Qwen 版本对工具参数的支持有细微差别,比如强制某个参数必填、某些类型不支持,遇到模型行为怪异的时候,先改小模型版本试试,而不是盲目改代码。
关于"spring ai alibaba 停更了吗"这个话题,至少到目前这个阶段,Spring AI Alibaba 项目还在积极演进,只是它和 Spring AI 官方的版本节奏不完全同步。我建议不要执着于某一个版本号,直接参考对应仓库 README 里的兼容说明来选版本,然后锁定依赖,不要频繁升级,免得 Function Calling 的行为突然变化。
5.3 我个人实际用下来的几点体会
最后分享几个不算技巧但非常影响体验的经验。
第一个体会是,工具描述真的值得反复打磨。不要怕多写一二十个字,模型对自然语言的理解非常依赖描述质量。同样的工具方法,我一开始写"获取天气数据",模型调用率大概只有一半,改成"获取指定城市当前或预报的天气信息,当用户询问雨伞、穿衣、出行等建议时也要主动调用"之后,调用率接近百分百。
第二个体会是,做这类功能,一定要先把真实的第三方 API 编排做好再对接 AI。我在最初的一个版本里把和风天气的响应字段直接硬编码在工具里调试,等 AI 链路通了之后再去替换真实第三方数据,整个过程非常顺畅。如果你一上来就同时处理第三方 API 认证、限流、字段映射和 Function Calling 的问题,出 bug 的时候你会很难判断是哪个环节出错了。
第三个体会是,Spring AI 的社区迭代很快,API 变动频繁,写博客或者教程的人很可能用的是旧版本。如果你照着老教程做不成功,不要急着怀疑自己,先确认版本号和依赖是否一致。我自己在 1.0.0 上写过的代码到 2.0.1 很多地方都要改,但这不代表大模型工具调用这套思路变了,变的是封装形式。理解了 Function Calling 的本质,换哪个版本你都能很快跟上。
这个项目后续还可以继续扩展:比如把聊天记录持久化到数据库,让模型能基于历史话题继续追问;或者把天气查询的结果缓存起来,避免每问一次都打第三方 API;也可以接入定时任务,让 Agent 每天主动汇报天气。Spring AI 提供了 ChatMemory 之类的组件,配合这些能力能把一个简单的"调用天气 API"场景延伸成完整的 AI 助手功能。我在实际使用中最深刻的感受是:工具本身很简单,真正有意思的是把多个能力组合起来为业务服务,那才是 Agent 真正的价值所在。
