Tool Calling in Spring AI 2.0: A Composable, Agentic Architecture

TL;DR · AI 摘要
Spring AI 2.0 重构了工具调用机制,通过 advisor 链实现可组合的代理架构,提升 AI 系统的灵活性和可扩展性。
核心要点
- Spring AI 2.0 将工具调用提升为 advisor 链中的核心组件,支持递归调用和组合行为。
- 使用 `@Tool` 注解可以轻松定义工具方法,并通过 `.tools()` 显式传递给 `ChatClient`。
- 工具调用循环由 `ToolCallingAdvisor` 实现,直到模型生成无工具调用的响应时停止。
结构提纲
按章节快速跳转。
- §引言
工具调用是构建代理 AI 系统的核心机制,Spring AI 2.0 重构了这一机制。
1.x 版本中,每个聊天模型实现都包含私有的工具执行循环,无法组合或扩展。
2.0 版本将工具循环提升为 advisor 链中的可组合组件,支持递归调用和多种循环机制。
- ›定义工具
通过 `@Tool` 注解可以定义工具方法,并自动生成 JSON 参数模式。
`ToolCallingAdvisor` 是一个递归顾问,用于实现工具调用循环,直到模型生成无工具调用的响应。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Spring AI 2.0 工具调用架构
- 核心改进
- advisor 链机制
- 工具调用循环
- 工具定义
- @Tool 注解
- JSON 参数模式
- 工具调用循环实现
- ToolCallingAdvisor
金句 / Highlights
值得收藏与分享的关键句。
Spring AI 2.0 将工具循环提升为 advisor 链中的可组合组件,支持递归调用和多种循环机制。
通过 `@Tool` 注解可以轻松定义工具方法,并通过 `.tools()` 显式传递给 `ChatClient`。
`ToolCallingAdvisor` 是一个递归顾问,用于实现工具调用循环,直到模型生成无工具调用的响应时停止。
标题:Spring AI 2.0 中的工具调用:可组合的智能代理架构
URL 来源:https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling
Markdown 内容: 工具调用 —— 即 AI 模型能够调用应用定义的函数并根据结果采取行动 —— 是智能代理系统的基本构建块。
能够发现信息、采取行动并循环直到达成目标的模型即为代理。
Spring AI 2.0 从底层重新设计了工具调用。在 1.x 版本中,每个聊天模型的实现都包含其私有的工具执行循环 —— 虽然功能齐全,但隐藏较深。没有方法可以接入它、观察中间步骤或与其他行为组合。你可以调用工具,但你无法在工具调用的基础上构建。
2.0 版本将工具循环提升为 advisor 链 中的一等、可组合组件。ChatClient 会将每个请求通过一个有序的 advisor 链运行,并支持循环,允许 advisor 重新进入下游链。相同的机制驱动了工具调用循环、结构化输出重试循环和评估循环。
- * *
[定义工具](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#defining-tools)
定义工具最简单的方式是使用 @Tool 注解在任意方法上:
class WeatherTools {
@Tool(description = "获取给定城市的当前天气")
public String getWeather(String city) {
return weatherService.fetch(city);
}
@Tool(description = "在给定日期预订两个城市之间的航班")
public BookingConfirmation bookFlight(
String origin,
String destination,
@ToolParam(description = "格式为 YYYY-MM-DD 的日期") String date) {
return flightService.book(origin, destination, date);
}
}Spring AI 会自动生成输入参数的 JSON 模式。@ToolParam 为每个参数添加描述和可选/必填提示。标注了 @Nullable 的参数默认被视为可选。
工具通过 .tools() 显式传递给 ChatClient:
String response = ChatClient.create(chatModel)
.prompt("阿姆斯特丹的天气如何?如果天气晴朗,请预订从伦敦出发的航班。")
.tools(new WeatherTools())
.call()
.content();参考文档详细介绍了 所有工具定义选项,包括编程式 MethodToolCallback 和 FunctionToolCallback API。
- * *
[工具调用循环:`ToolCallingAdvisor`](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#the-tool-calling-loop-toolcallingadvisor)
ToolCallingAdvisor 是一个递归的 advisor —— 一种会反复重新进入下游链,直到满足停止条件的 advisor。在此情况下,停止条件是模型生成一个不包含工具调用的响应。DefaultChatClient 会自动将其添加到 advisor 链中 —— 任何时候只能存在一个 ToolAdvisor —— 从那时起,它将拥有完整的工具执行生命周期:

工具通过 @Tool、@McpTool1、java.util.Function 或 ToolCallback 定义 —— 顾问提取它们的名称、描述和输入模式,并将生成的 工具定义 与用户的问题和系统提示一起注入到初始上下文中。
在每次迭代中,累积的 对话历史(用户消息、AI 工具调用请求以及前几轮的工具响应)会与当前上下文合并并发送给 LLM。LLM 生成一个完成结果,顾问会检查该结果:
- 如果包含工具调用:
ToolCallingManager会找到并执行引用的工具,将工具响应追加到对话历史中,然后循环回去。 - 如果不包含工具调用:最终答案将返回给用户。
阻塞(.call())和流式(.stream())模式都得到了全面支持。
顾问相对于 ToolCallingAdvisor 的位置(默认顺序 HIGHEST_PRECEDENCE + 300)决定了它是否只看到最终结果(外部)或每一轮迭代(内部)——下一节将通过内存展示这一点。
- * *
内存与工具循环
你将 MessageChatMemoryAdvisor 放置在 ToolCallingAdvisor 相对于的位置决定了内存存储捕获的对话上下文数量。
在循环外部(默认 —— 顺序 HIGHEST_PRECEDENCE + 200):内存顾问在循环开始前加载一次历史记录,并仅持久化最终的用户和助手消息。工具请求和响应消息不会写入存储。这对于每个 ChatMemoryRepository 实现都是安全的,并且与 Spring AI 1.x 的行为一致,其中工具循环在聊天模型内部运行,内存无法观察到工具消息。
在循环内部(顺序大于 ToolCallingAdvisor.DEFAULT_ORDER):内存顾问在每次迭代中都会被调用,并持久化完整的工具请求/响应记录。这为 LLM 提供了更丰富的上下文 —— 它可以推理出已经尝试过的内容、调用了哪些工具以及它们返回了什么。
为了避免重复写入,当内存顾问位于循环内部时,必须禁用 ToolCallingAdvisor 的内部对话历史。对于自动注册的 `ToolCallingAdvisor`,这是自动完成的 —— DefaultChatClient 会检测到任何放置在循环内部的 MemoryAdvisor,并自动禁用内部历史,无需额外配置。如果你手动构建 ToolCallingAdvisor,请自行在构建器上调用 .disableInternalConversationHistory()。

并非所有的 ChatMemoryRepository 都能够持久化工具消息。仓库需要知道如何序列化 ToolResponseMessage 以及工具调用请求,同时还要处理普通用户和助手的对话轮次 —— 而大多数当前的实现只建模了后者。从 2.0 版本开始,支持完整消息集的内置仓库包括 InMemoryChatMemoryRepository、RedisChatMemoryRepository 和 Neo4jChatMemoryRepository —— 这些仓库在循环中使用都是安全的。
对于需要完整工具消息支持的 JDBC 持久化 —— 加上事件源历史记录、轮次感知的压缩以及多代理分支隔离 —— 请使用新的社区项目 Spring-AI-Session。该项目专为此场景设计,并计划在 Spring AI 2.1 版本中包含。
- * *
扩展到数百个工具:`ToolSearchToolCallingAdvisor`

标准的 ToolCallingAdvisor 在每次请求时会将所有注册的工具定义发送给模型。对于工具库较小的情况,这没有问题。但在有 30 个以上工具的情况下 —— 或者在多服务器 MCP 设置中,单个会话可能聚合数百个工具定义时 —— 会导致上下文膨胀、准确性下降和不必要的令牌成本。
ToolSearchToolCallingAdvisor 是 ToolCallingAdvisor 的一种即插即用的替代方案,它实现了 _渐进式工具披露_ 模式:而不是一开始就发送所有工具定义,它会按需逐步将工具暴露给模型。在会话开始时,它会索引完整的工具集;在每次迭代中,它只注入一个内置的 toolSearchTool,模型可以使用它通过自然语言查询来检索相关工具。只有发现的工具才会包含在后续请求中。
通过以下属性启用它:
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector # regex (默认), lucene, 或 vector由于工具索引是按会话范围限定的,调用方必须在每次请求中提供一个 会话 ID —— 顾问使用它来隔离不同对话和租户之间的索引。默认情况下,会话 ID 从顾问上下文中的 ChatMemory.CONVERSATION_ID 读取:
chatClient.prompt()
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "user-42-session"))
.user("Help me plan my trip to Amsterdam")
.call()
.content();如果已经使用了不同的名称传递会话标识符,可以通过 spring.ai.chat.client.tool-search-advisor.session-id-key-name 配置键名。
有三种 ToolIndex 策略可用:regex(轻量级,无需额外依赖,默认)、lucene(关键字搜索,包含在 starter 中)和 vector(基于嵌入的语义搜索,需要 VectorStore bean)。有关完整的配置表面,请参阅 Tool Search Tool 参考文档。
我们的2025年12月文章深入探讨了这一模式,包括基准测试结果,显示在OpenAI、Anthropic和Gemini模型上实现了34–64%的token减少,以及多步骤发现流程的工作示例。该顾问从社区毕业,成为Spring AI核心的一部分,作为2.0版本的一部分。
- * *
[工具参数增强](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#tool-argument-augmentation)
Spring AI允许你动态地向工具的输入模式中添加额外的参数,而无需修改工具的实现。模型会看到增强后的模式并填写额外的字段;你的代码通过消费者接收它们;原始工具只接收到自己的参数,保持不变。
主要的使用场景是内部思考:强制模型在执行工具之前阐述其推理过程,这可以提高可追溯性,并可以存储在长期记忆中或用于评估。

使用AugmentedToolCallbackProvider包装你的工具:
public record AgentThinking(
@ToolParam(description = "调用此工具的理由")
String innerThought) {}
AugmentedToolCallbackProvider<AgentThinking> toolProvider =
AugmentedToolCallbackProvider.<AgentThinking>builder()
.toolObject(new WeatherTools()) // 包装原始工具
.argumentType(AgentThinking.class) // 增强模式类型
.argumentConsumer(event -> log.info( // 可选的增强内容消费者
"工具: {} | 推理: {}", event.toolDefinition().name(), event.arguments().innerThought()))
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(toolProvider)
.build();有关完整的API,请参阅工具参数增强参考。
- * *
[MCP工具](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#mcp-tools)
MCP(Model Context Protocol)工具从两个方向与Spring AI的工具调用架构集成:你的应用程序可以消费远程MCP服务器暴露的工具,也可以将它自己的Spring管理的工具暴露给MCP客户端。
[使用MCP服务器工具](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#consuming-mcp-server-tools)
添加MCP客户端启动器并在application.properties中配置连接到一个或多个MCP服务器:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>spring.ai.mcp.client.stdio.connections.my-server.command=npx
spring.ai.mcp.client.stdio.connections.my-server.args=-y,@modelcontextprotocol/server-everything自动配置会连接到所有配置的MCP服务器,发现它们的工具,并将它们作为单个SyncMcpToolCallbackProvider bean(或AsyncMcpToolCallbackProvider用于异步客户端类型)暴露。MCP提供者故意不会自动注册到ChatClient中——它们实现了ToolCallbackProvider,但急于列出工具会强制在启动时向每个连接的MCP服务器发起网络往返。相反,你注入提供者并显式地连接它:
@Autowired SyncMcpToolCallbackProvider mcpTools;// 作为默认工具用于每个请求
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(mcpTools)
.build();
// 或者每次调用时指定
chatClient.prompt()
.user("在网页上搜索最新的 Spring AI 发布说明")
.tools(mcpTools)
.call()
.content();工具回调的自动配置默认是启用的,可以通过设置 spring.ai.mcp.client.toolcallback.enabled=false 来关闭。当连接到多个可能暴露相同名称工具的 MCP 服务器时,会自动应用 DefaultMcpToolNamePrefixGenerator 以避免冲突。有关配置属性、传输选项和工具过滤的完整列表,请参阅 MCP 客户端参考文档。
将 Spring 工具作为 MCP 服务器公开
反过来,将你的 Spring Bean 作为 MCP 工具公开,只需将 @Tool 替换为 @McpTool 并添加 MCP 服务器启动器:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>@Component
public class WeatherTools {
@McpTool(description = "获取给定城市的当前天气")
public String getWeather(
@McpToolParam(description = "城市名称") String city) {
return weatherService.fetch(city);
}
}MCP 服务器的自动配置会扫描带有 @McpTool 注解的 Bean,为它们的参数生成 JSON 模式,并将其注册到 MCP 服务器中 —— 不需要额外的连接。
有关传输配置、安全性和可观测性选项的详细信息,请参阅 MCP 服务器参考文档。
组合本地和 MCP 工具
一旦注册,本地 @Tool 方法和远程 MCP 工具共享相同的 ToolCallback 接口 —— 模型和 ToolCallingAdvisor 不会区分它们。.tools(...) 和 .defaultTools(...) 是异构的,可以在一次调用中接受两种类型,因此你可以自由混合使用:
chatClient.prompt()
.tools(new LocalTools(), mcpTools)
.call()
.content();在混合设置中需要注意以下几点:
- 名称冲突仅在 MCP 端处理。
DefaultMcpToolNamePrefixGenerator会在 MCP 服务器之间对重复项进行前缀处理,但它并不知道本地的@Tool方法。如果本地工具和远程 MCP 工具名称相同,你需要自己重命名其中一个,或者使用McpToolFilter来排除远程工具。 - 限制暴露的内容。 MCP 工具来自外部源,你无法完全控制它们的表面。一个
McpToolFilterBean 允许你根据服务器身份、工具名称或描述来选择哪些工具进入命名空间 —— 这对于限制嘈杂或不可信的 MCP 服务器的影响范围非常有用。
- * *
扩展循环:构建你自己的 `ToolAdvisor`
ToolAdvisor 是一个标记接口:任何自定义的工具调用顾问都必须实现它,这样 DefaultChatClient 才能识别它、强制单顾问约束,并将其注册为默认 ToolCallingAdvisor 的替代者。
ToolSearchToolCallingAdvisor 并不是某种特殊的框架魔法 —— 它是 ToolCallingAdvisor(实现了 ToolAdvisor)的一个子类,它重写了若干受保护的钩子方法,以在明确定义的点上拦截循环:
| 钩子 | 触发时机 | | --- | --- | | doInitializeLoop / doInitializeLoopStream | 在第一次迭代之前,只触发一次 | | doBeforeCall / doBeforeStream | 在每次迭代之前 | | doAfterCall / doAfterStream | 在每次迭代之后 | | doFinalizeLoop / doFinalizeLoopStream | 在循环结束后,只触发一次 |
ToolSearchToolCallingAdvisor 使用 doInitializeLoop 来索引工具集并增强系统消息,使用 doBeforeCall 来注入到目前为止发现的工具。任何自定义的 ToolCallingAdvisor 子类都遵循相同的模式。
自动配置集成
自定义的 ToolCallingAdvisor 实现可以插入到自动配置系统中,而无需任何手动的 ChatClient 连接。扩展点是 ToolCallingAdvisor.Builder<?> Bean。
ChatClientAutoConfiguration 声明了一个默认的 ToolCallingAdvisor.Builder<?> Bean,由 @ConditionalOnMissingBean 保护。要替换它,可以在一个在 ChatClientAutoConfiguration 之前运行的自动配置中注册你自己的 ToolCallingAdvisor.Builder<?> Bean —— 类型为基类 ToolCallingAdvisor.Builder<?>:
@AutoConfiguration(beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
@ConditionalOnProperty(prefix = "my.advisor", name = "enabled", havingValue = "true")
public class MyToolAdvisorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ToolCallingAdvisor.Builder<?> toolCallingAdvisorBuilder(ToolCallingManager toolCallingManager) {
return MyCustomToolCallingAdvisor.builder()
.toolCallingManager(toolCallingManager);
}
}然后,ChatClient.Builder 使用你自定义的构建器,透明地自动注册你的顾问。
ToolSearchToolCallingAdvisor 正是使用了这种机制 —— 它的自动配置注册了一个类型为 ToolCallingAdvisor.Builder<?> 的 ToolSearchToolCallingAdvisor.Builder,这正是 DefaultChatClient 为了替换默认顾问而自动注册它所需要的全部内容。
- * *
用户控制的工具执行
自动注册的循环涵盖了大多数情况,但有些场景确实需要你来掌控每一次迭代:在外部批准步骤上限制工具执行、将中间进度转发到 SSE 或 WebSocket 端点、在回合之间应用条件逻辑,或根据侧信道信号停止循环。
退出自动注册是一个按调用切换的选项 —— 在请求上设置 AdvisorParams.toolCallingAdvisorAutoRegister(false),你将负责在 ChatResponse 中检测工具调用,并通过 ToolCallingManager 执行它们。
ChatClient chatClient = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();ToolCallback[] tools = ToolCallbacks.from(new WeatherTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
String question = "What is the weather in Amsterdam and Paris?";
// ToolCallingAdvisor 已被禁用 —— 不会自动运行工具循环
ChatClientResponse response = chatClient.prompt()
.user(question)
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
Prompt prompt = new Prompt(List.of(new UserMessage(question)), chatOptions);
// 手动控制循环 —— 每次迭代都可以被观察和中断
while (response.chatResponse() != null && response.chatResponse().hasToolCalls()) {
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, response.chatResponse());
prompt = new Prompt(result.conversationHistory(), chatOptions);
response = chatClient.prompt()
.messages(result.conversationHistory())
.options(chatOptions)
.advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
.call()
.chatClientResponse();
}相同的模式也适用于流式 API,其中每次迭代的 Flux 块可以转发给订阅者,同时通过 ChatClientMessageAggregator 进行聚合。有关流式版本和完整执行模型的详细信息,请参阅 用户控制的工具执行参考。
- * *
从 Spring AI 1.x 升级
##### FunctionToolCallback 豆替换 Function 豆和 .functions()
SpringBeanToolCallbackResolver 和 toolNames() API —— 通过名称从裸 Function/Supplier/Consumer 豆解析工具 —— 已被移除。现在工具必须注册为显式的 ToolCallback 豆。对于函数风格的工具,请使用 FunctionToolCallback.builder():
// 之前(1.x) —— 通过名称解析裸 Function 豆
@Bean
@Description("Get the weather in location")
Function<WeatherRequest, WeatherResponse> currentWeather() {
return weatherService::getWeather;
}
chatClient.prompt().toolNames("currentWeather"); // 不再存在
// 之后(2.0) —— 显式的 ToolCallback 豆
@Bean
ToolCallback currentWeather() {
return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
.description("Get the weather in location")
.inputType(WeatherRequest.class)
.build();
}
// 注入豆并将其传递给 ChatClient —— 名称解析已被移除
@Autowired ToolCallback currentWeather;
chatClient.prompt()
.user("What's the weather in Copenhagen?")
.tools(currentWeather)
.call()
.content();##### internalToolExecutionEnabled 已被移除
internalToolExecutionEnabled 选项和对应的配置属性已被移除。每个模型的内部工具执行不再存在 —— ToolCallingAdvisor 是唯一的执行路径。请从代码中删除所有对 .internalToolExecutionEnabled(...) 的调用。
对于用户控制的执行(之前通过设置 internalToolExecutionEnabled(false) 实现),请改用 AdvisorParams.toolCallingAdvisorAutoRegister(false)。
##### [](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#toolcalladvisor-renamed-to-toolcallingadvisor)ToolCallAdvisor 重命名为 ToolCallingAdvisor
如果你直接引用了 ToolCallAdvisor,请更新为 ToolCallingAdvisor。
##### [](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#streamtoolcallresponses-removed-from-advisor-builders)streamToolCallResponses 从 advisor 构建器中移除
ToolCallingAdvisor.Builder 和 ToolSearchToolCallingAdvisor.Builder 上的 .streamToolCallResponses(...) 选项已被移除。该选项实际上存在缺陷:当启用时,它会将模型的 _工具请求_ 消息传递到下游,但由 advisor 本身生成的 _工具响应_ 消息仍留在循环中。任何位于工具调用 advisor 之外的 advisor 只能观察到每次工具交互的一半 —— 一个请求但没有对应的响应 —— 这比什么也看不到更糟糕。
为了避免提供一个不完整功能,我们选择将其移除。若要观察每个工具请求和响应,请将你的 advisor 置于 工具循环内部 —— 给它一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的顺序,这样它将在每次迭代中被调用,并能够访问完整的请求/响应历史。如果这还不够 —— 例如,你还需拦截模型响应和工具执行之间 —— 请退回到 用户控制的工具执行,并自行控制循环。
##### [](https://spring.io/blog/2026/06/15/spring-ai-composable-tool-calling#options-are-now-immutable)选项现在是不可变的
ChatOptions#copy() 和 [*]Options#fromOptions() 已被移除。使用 .mutate() 来创建现有选项实例的修改副本。
完整的变更列表请参见 升级说明。FunctionCallback 到 ToolCallback 的迁移指南 详细介绍了从函数到工具的重命名。
- * *
总结
Spring AI 2.0 的工具调用架构设计旨在与你一同成长:从一个简单的 @Tool 注解开始,随着应用的成熟逐步添加内存和可观测性,当跨服务时插入 MCP 工具,当工具集扩展时将默认循环替换为 ToolSearchToolCallingAdvisor,当你的领域需要时扩展循环本身。
所有这些都通过一个机制组合在一起:advisor 的顺序。advisor 相对于工具循环的位置 —— 在外部(安全的默认设置,除非你选择加入否则用于内存)或在内部(内存可以捕获完整的工具记录) —— 同样控制着可观测性、重试和你添加的任何自定义 advisor。
- * *
参考资料
- Tools API 参考
- 递归顾问参考
- 升级说明
- FunctionCallback → ToolCallback 迁移指南
- MCP 客户端启动器参考
- MCP 服务端启动器参考
- 智能工具选择:通过动态工具发现节省 34–64% 的令牌
- Spring AI 递归顾问
- spring-ai-session: 结构化对话记忆
- * *