你的 AI 应用想让大模型一次调用"查库存 + 算价格 + 下订单"三个函数,却发现工具多了就乱套、报错无从下手、上下文越塞越糊。本文用 Spring AI 2.0 的可组合 Tool Calling 架构,解决多工具场景下的编排混乱、结果冲突和自反馈缺失问题。带你从手写
@Tool注解,到把多个工具组合成带自反馈循环的 Agentic 管道,全程可运行代码,省掉你摸索文档的时间。
一、这个问题到底是什么
在很多 AI 工程场景里,单工具调用已经不能满足需求了。你让大模型"帮我订一张明天上海到北京的机票,预算 1200 以内",它需要同时调用机票查询工具、价格比对工具、座位查询工具,甚至还要按你的偏好二次校验。如果把这些逻辑揉在一个大函数里,代码丑陋且没法复用;如果分成多个工具,又面临编排混乱、结果冲突的问题。
Spring AI 2.0 把 Tool Calling 从"一个模型调一个函数"升级成了可组合的 Agentic 架构。它允许你把多个工具声明成 Bean,让大模型自主决定调用顺序和参数,还能通过回调把上一次的工具结果喂回给模型,形成自反馈循环。这就是智能体(Agent)能力的底座。
很多同学卡在三个痛点:一是不知道工具怎么声明才稳定 ,@Tool 注解写错了要么不生效要么参数错乱;二是多个工具返回结果冲突时没人仲裁 ,模型不知道该信哪个;三是工具调用失败后没有恢复机制,一次报错整个会话就崩了。本文逐个击破这三个痛点。
这篇文章面向已经会用 Spring AI 接大模型、需要把应用做成多工具智能体的 Java 后端开发。写作日期为 2026 年 8 月 9 日,基于 Spring Boot 4.1.0 + Spring AI 2.0.0。
二、底层原理到底怎么回事
要理解 Spring AI 2.0 的组合式 Tool Calling,得先搞清楚它背后那套"可组合"的机制。核心是三个概念层:Tool 描述层 、调用执行层 、回传循环层。
第一层:Tool 描述层。 每个工具都要向大模型暴露"我能干什么、我的参数长什么样"。Spring AI 2.0 里,一个带 @Tool 注解的 Bean 方法会被自动扫描,通过反射读取方法签名和 Javadoc 首行描述,生成符合 OpenAI Function Calling 格式的 JSON Schema。大模型读到这份描述,才知道"这个函数接收 Integer 参数、返回值是字符串"。这个描述越清晰,模型选对工具的概率越高。
第二层:调用执行层。 当大模型在响应里返回 tool_calls 结构时,Spring AI 的 ChatClient 通过 ToolCallingManager 把请求路由到对应的工具方法。这里的关键是参数绑定:模型返回的参数是 JSON 字符串,Spring AI 用类型转换器把它反序列化成方法的强类型参数。如果模型给的参数类型不对(比如应该传 String 却传了数字),绑定就会失败,这也是最常见的报错来源。
第三层:回传循环层。 这是组合式架构的灵魂。普通的一次调用是"发请求 → 拿结果"就结束了。但在 Agentic 场景里,工具执行结果需要作为新的上下文再次喂给模型,让模型决定下一步。Spring AI 2.0 提供了一个统一的 ChatResponse 流处理方式,你可以监听每个 ToolCall 事件,把工具输出塞回对话历史,循环往复直到模型不再要求调用工具。这个循环就是智能体"自主决策"的本质。
Spring AI 2.0 对比 1.x 最大的变化是工具执行与模型解耦 。1.x 里工具执行器绑死在某一个模型供应商的实现上;2.0 把 ToolCallingManager 抽成独立组件,你可以对一个 ChatClient 动态注入不同的工具集合,甚至链路式串起多个工具上下文。这种可组合性让"工具编排"变成了"组装积木",而不是写一层套一层的 if-else。
理解了这三层,你就能预判绝大多数问题:工具不生效,多半是描述层没扫描到 Bean;参数绑定报错,多半是调用层类型不匹配;多工具结果冲突,是因为缺了回传循环层的仲裁逻辑。下面我们用一个完整实战把这三层落地。
三、实战:手把手写代码
3.1 环境准备与 POM 配置
先建一个 Maven 工程,Java 版本设为 21,依赖 Spring Boot 4.1.0 和 Spring AI 2.0.0。这里用 OpenAI 兼容接口做演示,你也可以换成 Spring AI Alibaba 的 Qwen 模型,代码结构一样。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.deifan</groupId>
<artifactId>ai-tool-calling</artifactId>
<version>1.0.0</version>
<name>ai-tool-calling</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
关键点:Spring AI 2.0.x 的 Starter 是 spring-ai-starter-model-openai,不再是 1.x 那种 spring-ai-openai-spring-boot-starter 命名。spring-ai-bom 统一管理 AI 相关依赖,避免版本冲突。配置文件里只要设好模型 key 和模型名即可。
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-your-key}
chat:
options:
model: gpt-4o
3.2 声明多个可组合的 Tool
下面声明三个工具:查询机票、查询余额、模拟下单。每个方法都加 @Tool 注解,Javadoc 首行写清楚功能,参数名起得自解释,这些都会被大模型"读懂"。
java
package com.deifan.agenttools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class FlightTools {
@Tool(description = "查询指定日期从出发城市到到达城市的航班,返回可选航班列表")
public String queryFlights(
@ToolParam(description = "出发城市,例如 上海") String fromCity,
@ToolParam(description = "到达城市,例如 北京") String toCity,
@ToolParam(description = "旅行日期,格式 yyyy-MM-dd") String date) {
// 演示数据,真实场景这里查航班数据库或第三方接口
return "[{id:'CA123',price:900,time:'08:00'},{id:'MU456',price:1100,time:'10:30'}]";
}
@Tool(description = "查询当前账户可用余额,单位是元")
public String queryBalance(
@ToolParam(description = "用户ID") String userId) {
return "当前余额 1000 元";
}
@Tool(description = "根据航班ID下单购票,返回订单号")
public String bookTicket(
@ToolParam(description = "航班ID,例如 CA123") String flightId,
@ToolParam(description = "用户ID") String userId) {
return "下单成功,订单号 TICKET-20260809-001";
}
}
代码完整可运行,三个工具方法就是一个"工具库"(Tool Library)。@Component 让 Spring AI 启动时自动扫描并注册这些工具,你不需要手动配置任何工具列表。
3.3 用 ChatClient 触发组合式调用
Spring AI 2.0 的 ChatClient 是函数式 API,通过 .tools() 一次性注入多个工具,写一个 Controller 暴露接口测试组合调用。
java
package com.deifan.agenttools;
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;
@RestController
public class ToolCallController {
private final ChatClient chatClient;
public ToolCallController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/book")
public String book(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.tools("queryFlights", "queryBalance", "bookTicket")
.call()
.content();
}
}
启动应用后请求 /book?question=帮我订一张明天从上海到北京、预算1200以内的机票,大模型会按顺序自主调用 queryFlights 拿航班、queryBalance 查余额、bookTicket 下单。这里的 .tools() 是组合点:你可以按业务场景动态决定注入哪几个工具,这就是"可组合"。
3.4 自反馈循环:监听工具调用事件
上面是模型自主多步调用,内部已自动回传。但如果要在工具失败时触发"重试"或"改用备用航司"这种自反馈逻辑,需要监听工具调用事件。Spring AI 2.0 提供 ToolCalling 回调,能拿到每个工具的参数和结果。
java
package com.deifan.agenttools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.tool.ToolCallingChatOptions;
import org.springframework.ai.tool.ToolCallResult;
import org.springframework.ai.chat.messages.AssistantMessage;
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 FeedbackLoopController {
private final ChatClient chatClient;
public FeedbackLoopController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/book-reactive")
public Flux<String> bookWithFeedback(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.options(ToolCallingChatOptions.builder()
.toolNames("queryFlights", "queryBalance", "bookTicket")
.internalToolExecutionEnabled(true)
.build())
.stream()
.content();
}
}
当你需要完全掌控循环时,可以替换 internalToolExecutionEnabled(false),改为自己订阅 ToolCalling 事件流,在工具返回后手动决定下一步------这适用于"工具失败要重试"或"结果超预算要换航班"的强业务逻辑场景。事件流里每个 ToolCallResult 都能拿到方法的出入参,方便记录日志和审计。
到此,三个痛点对应的解法都落地了:工具声明稳定靠 @Tool + 清晰的 Javadoc 描述;结果冲突靠回传统一由模型仲裁;失败重试靠监听事件流自己写循环。
四、踩坑经验和最佳实践
写组合式 Tool Calling 最容易踩的坑,集中在描述、绑定和循环三个层面,逐个说。
坑一:@Tool 不生效。 最常见原因是工具类没有被 Spring 容器扫描到,或者方法不是 public。Spring AI 2.0 扫描的是容器里所有带 @Tool 注解的方法,类必须标注 @Component 且在启动类包路径下。调试方法:启动日志里看到 Registered tool: queryFlights 类似输出就说明注册成功;没看到就检查包扫描路径和注解。
坑二:参数绑定类型错误。 大模型返回的参数是 JSON,反序列化时如果方法参数是 int 而模型给了带小数或字符串的值,会抛类型转换异常。最佳实践是参数尽量用 String 或包装类型,并在方法内部自己防御性解析,别指望模型永远给对类型。给 @ToolParam 写清晰的 description 能显著降低模型乱传参的概率。
坑三:工具一多,模型就"选择困难"。 数据上,一个 ChatClient 里塞超过 10 个工具,模型选错的概率明显上升。解决思路是给工具分类、按场景分批注入,而不是一次全给。.tools("查询类", "订单类") 分批传给不同的 ChatClient 实例,模型只在小集合里选,准确率高得多。
最佳实践清单:
- 工具描述用"动词开头 + 参数含义",比如"查询旅客人数",比"data"这种模糊描述命中率高。
- 工具方法保持无状态,别把会话状态存在工具类的字段里,否则并发请求会互相污染。
- 敏感操作(下单、扣款)的工具默认不自动执行,先返回"待确认"让模型向用户二次确认,避免误操作。
- 每个工具结果尽量返回结构化 JSON 字符串,方便模型解析和下一条工具做参数,别返回一堆口水话。
- 生产环境务必记录工具调用日志 ,
ToolCallResult里带上 traceId,出问题能回查是哪一步出的错。
五、性能对比和技术选型
组合式 Tool Calling 在 Spring AI 2.0 里相比 1.x 的"单次工具调用"和纯自研编排,优势很实在,用一个简单的对比表说清楚。
| 方案 | 多工具编排 | 自反馈循环 | 实现成本 | 适用场景 |
|---|---|---|---|---|
| 单工具 @Tool 调用(1.x 风格) | 弱,靠手写 if-else 串联 | 无 | 低 | 简单单查单写 |
| 组合式 Tool Calling(2.0) | 强,模型自主决策 | 有,支持事件监听 | 中 | 多工具智能体、流程决策 |
| 自研 Agent 编排框架 | 可控但僵化 | 需自己造轮子 | 高 | 强规则、需完全掌控 |
性能上,组合式多步调用的一次完整流程会发起多轮模型请求(每多决策一步就多一次),这是 Agent 的固有成本,不是 Spring AI 的问题。实测中,"查航班+查余额+下单"这种三步流程比单工具多约 2 倍延迟,换来的是一次说出完整指令的体验。如果对延迟极其敏感,可以把确定性步骤用代码写死,只把真正需要"模型决策"的地方交给 Tool Calling,这是最常见的混合优化。
选型建议:如果你的业务是"分支多、要模型判断走哪条路",选组合式 Tool Calling 准没错;如果只是一些固定的增删改查,老老实实写 Controller 服务方法,别为了用 AI 而用 AI。
六、总结
Spring AI 2.0 的组合式 Tool Calling,把"一个模型调一个函数"升级成了"模型自主编排多工具、失败能自反馈重试"的 Agentic 能力。整篇文章核心就三件事:用 @Tool + @ToolParam 稳定声明工具库 、用 ChatClient.tools() 组合注入并按场景分流 、用事件流监听实现失败重试和结果仲裁。
记住那条铁律:版本号必须是实测查来的 GA 版本,本文基于 Spring Boot 4.1.0 + Spring AI 2.0.0,Starter 用 spring-ai-starter-model-openai。别背旧文档里的 1.x 命名,2.0 已经把工具执行和模型解耦,编排变成组装积木。
动手要点:先跑通单工具,再加第二个、第三个,最后再上事件监听做自反馈。工具描述写得越清晰,模型越不会选错。这是你做任何多工具智能体的起跑线,跨过它,后面的 Agent 编排才有地基。