Spring AI 2.0工具调用:可组合智能体架构指南

2026-06-18阅读 0热度 0
智能体

工具调用——也就是AI模型调用应用定义的函数并根据结果行动的能力——是智能体AI系统的基础构建块。一个能自己发现信息、采取行动、反复循环直到达成目标的模型,才称得上智能体。

不过话说回来,Spring AI 2.0对工具调用进行了彻底的重新设计。在1.x版本中,每个聊天模型实现都塞了一个私有的工具执行循环——能用,但完全藏在底层,没法干预。你没法介入循环、观察中间步骤、或者把它和其他行为组合起来。能调用工具,但没法在工具调用之上做文章。

到了2.0,这个循环被提升到了advisor链中,成为了一等公民——可组合、可观察。ChatClient通过一个有序的顾问链处理每个请求,并且支持循环,让顾问可以重新进入下游链。工具调用循环、结构化输出重试循环、以及评估循环,都共享同一个机制。

定义工具

定义工具最直接的方式就是在任何方法上加上@Tool注解:

class WeatherTools {
    @Tool(description = "Get the current weather for a given city")
    public String getWeather(String city) {
        return weatherService.fetch(city);
    }

    @Tool(description = "Book a flight between two cities on a given date")
    public BookingConfirmation bookFlight(String origin, String destination,
            @ToolParam(description = "Date in YYYY-MM-DD format") String date) {
        return flightService.book(origin, destination, date);
    }
}

Spring AI会自动为输入参数生成JSON schema。@ToolParam可以添加参数级别的描述和可选/必填提示。标了@Nullable的参数默认视为可选。

工具通过.tools()显式传递给ChatClient

String response = ChatClient.create(chatModel)
    .prompt("What's the weather in Amsterdam? Book a flight from London if it's sunny.")
    .tools(new WeatherTools())
    .call()
    .content();

参考文档中详细介绍了所有工具定义方式,包括编程式的MethodToolCallbackFunctionToolCallback API。

工具调用循环:ToolCallingAdvisor

ToolCallingAdvisor是一个递归顾问——它能反复进入下游链,直到满足停止条件。在这里,停止条件就是模型生成一个不含工具调用的响应。DefaultChatClient会自动将其添加到顾问链中——任何时候最多只能有一个ToolAdvisor——然后它就全权负责工具执行的整个生命周期:

工具可以通过@Tool@McpTool1ja va.util.FunctionToolCallback来定义——顾问从它们中提取名称、描述和输入schema,然后将生成的工具定义注入到初始上下文中,与用户问题和系统提示一并发送。

每次迭代中,累积的对话历史(用户消息、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场景下,一个会话可能聚合数百个工具定义——就会引起上下文膨胀、准确率下降、不必要的令牌成本。

ToolSearchToolCallingAdvisorToolCallingAdvisor的可替代品,它实现了渐进式工具暴露模式:不是一开始就把所有工具定义都发过去,而是按需增量地暴露工具。在会话启动时,它对整个工具集建立索引;每次迭代中,它只注入一个内置的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)。完整的配置参见工具搜索参考文档。

我们在2025年12月的博客中深入介绍了这个模式,包括基准测试结果——在OpenAI、Anthropic和Gemini模型上实现了34–64%的令牌减少——以及多步发现流程的工作示例。这个顾问从社区升级为核心Spring AI 2.0的一部分。

工具参数增强

Spring AI允许你动态地扩展工具的输入schema,增加额外的参数——而不需要改动工具的实现。模型会看到增强后的schema并填充额外字段;你的代码通过consumer接收这些增强内容;而原始工具只接收到它自己的参数,不受影响。

主要用途是内部思考:强制模型在执行工具之前阐述它的推理过程,这提高了可追溯性,可以存入长期记忆或用于评估。

AugmentedToolCallbackProvider包装你的工具:

public record AgentThinking(
    @ToolParam(description = "Your reasoning for calling this tool")
    String innerThought
) {}

AugmentedToolCallbackProvider toolProvider = AugmentedToolCallbackProvider
    .builder()
    .toolObject(new WeatherTools())   // 包装原始工具
    .argumentType(AgentThinking.class) // 增强schema类型
    .argumentConsumer(event -> log.info( // 可选:消费增强内容
        "Tool: {} | Reasoning: {}", event.toolDefinition().name(), event.arguments().innerThought()))
    .build();

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(toolProvider)
    .build();

完整API参见工具参数增强参考文档。

MCP工具

MCP(Model Context Protocol)工具从两个方向与Spring AI的工具调用架构集成:你的应用可以消费远程MCP服务器暴露的工具,也可以将自己的Spring管理工具暴露给MCP客户端。

消费MCP服务器工具

添加MCP客户端starter,然后在application.properties中配置一个或多个MCP服务器连接:


    org.springframework.ai
    spring-ai-starter-mcp-client
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("Search the web for the latest Spring AI release notes")
    .tools(mcpTools)
    .call()
    .content();

工具回调自动配置默认启用,可以通过spring.ai.mcp.client.toolcallback.enabled=false关闭。当连接多个可能暴露同名工具的MCP服务器时,会自动应用DefaultMcpToolNamePrefixGenerator来避免冲突。完整的配置属性、传输选项和工具过滤参见MCP客户端参考文档。

将Spring工具暴露为MCP服务器

反过来——将你的Spring bean暴露为MCP工具——只需用@McpTool替换@Tool,并添加MCP服务器starter:


    org.springframework.ai
    spring-ai-starter-mcp-server-webmvc
@Component
public class WeatherTools {
    @McpTool(description = "Get the current weather for a given city")
    public String getWeather(@McpToolParam(description = "City name") String city) {
        return weatherService.fetch(city);
    }
}

MCP服务器自动配置会扫描所有标注了@McpTool的bean,为它们的参数生成JSON schema,并向MCP服务器注册——无需额外配置。

传输配置、安全性和可观测性选项参见MCP服务器参考文档。

组合本地工具和MCP工具

注册之后,本地的@Tool方法和远程MCP工具共享同一个ToolCallback接口——模型和ToolCallingAdvisor不会区分它们。.tools(...).defaultTools(...)是异构的,可以在一次调用中接受两种类型,因此你可以自由混合:

chatClient.prompt()
    .tools(new LocalTools(), mcpTools)
    .call()
    .content();

在混合设置中需要注意几点:

  • 名称冲突只在MCP侧处理。 DefaultMcpToolNamePrefixGenerator会为MCP服务器之间的重复工具加前缀,但它不知道本地的@Tool方法。如果本地工具和远程MCP工具名称相同,你需要自己重命名其中一个,或者使用McpToolFilter丢弃远程的。
  • 限制暴露范围。 MCP工具来自外部源,你无法完全控制其表面。一个McpToolFilter bean能让你根据服务器身份、工具名称或描述来选择哪些工具进入命名空间——对于限制一个话多或不可信的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保护。要替换它,注册你自己的ToolCallingAdvisor.Builder bean——类型为基础ToolCallingAdvisor.Builder——在一个早于ChatClientAutoConfiguration运行的自动配置中:

@AutoConfiguration(beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
@ConditionalOnProperty(prefix = "my.advisor", name = "enabled", ha vingValue = "true")
public class MyToolAdvisorAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    ToolCallingAdvisor.Builder toolCallingAdvisorBuilder(ToolCallingManager toolCallingManager) {
        return MyCustomToolCallingAdvisor.builder()
                .toolCallingManager(toolCallingManager);
    }
}

然后ChatClient.Builder就会使用你的自定义构建器,透明地自动注册你的顾问。

ToolSearchToolCallingAdvisor使用的正是这个机制——它的自动配置注册了一个ToolSearchToolCallingAdvisor.Builder,类型为ToolCallingAdvisor.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 bean 替代了 Function bean 和 .functions()

SpringBeanToolCallbackResolvertoolNames() API——它们通过名称从裸 Function/Supplier/Consumer bean 解析工具——已经被移除。工具现在必须注册为显式的 ToolCallback bean。对于函数式工具,使用 FunctionToolCallback.builder()

// 之前 (1.x) —— 通过名称解析裸 Function bean
@Bean
@Description("Get the weather in location")
Function currentWeather() {
    return weatherService::getWeather;
}
chatClient.prompt().toolNames("currentWeather"); // 不再存在

// 之后 (2.0) —— 显式的 ToolCallback bean
@Bean
ToolCallback currentWeather() {
    return FunctionToolCallback.builder("currentWeather", weatherService::getWeather)
        .description("Get the weather in location")
        .inputType(WeatherRequest.class)
        .build();
}
// 注入 bean 并传递给 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)

ToolCallAdvisor 重命名为 ToolCallingAdvisor

如果你直接引用了 ToolCallAdvisor,请更新为 ToolCallingAdvisor

streamToolCallResponses 从顾问构建器中移除

ToolCallingAdvisor.BuilderToolSearchToolCallingAdvisor.Builder 上的 .streamToolCallResponses(...) 选项已经消失。该选项实际上是有问题的:启用后,它将模型的工具请求消息传递到下游,但顾问自身生成的工具响应消息仍然留在循环内部。任何位于工具调用顾问外部的顾问只能观察到每次工具交换的一半——只有请求没有响应——这比什么都看不到更糟糕。

与其提供一个半吊子功能,我们直接移除了它。要观察每次工具请求和响应,请将你的顾问放在工具循环内部——给它一个大于 ToolCallingAdvisor.DEFAULT_ORDER 的顺序,它就会在每次迭代中被调用,并且可以看到完整的请求/响应历史。当这还不够时——例如,你还需要在模型响应和工具执行之间进行拦截——那么退回到用户控制的工具执行,自己驱动循环。

选项现在是不可变的

ChatOptions#copy()[*]Options#fromOptions() 已被移除。使用 .mutate() 创建现有选项实例的修改副本。

完整的破坏性变更列表在升级说明中。FunctionCallback 到 ToolCallback 的迁移指南详细介绍了函数到工具的改名。

总结

Spring AI 2.0 的工具调用架构设计为与你一同成长:从一个简单的 @Tool 注解开始;随着应用成熟,加入内存和可观测性;当需要跨服务时,接入 MCP 工具;当工具集扩大时,将默认循环替换为 ToolSearchToolCallingAdvisor;当你的领域有特殊要求时,扩展循环本身。

所有这一切都通过一个机制来组合:顾问顺序。顾问相对于工具循环的位置——在外面(安全的默认值,用于内存,除非你选择进入)还是里面(内存可以捕获完整的工具记录)——也是控制可观测性、重试以及你添加的任何自定义顾问的同一个旋钮。

参考

  • Tools API 参考
  • 递归顾问参考
  • 升级说明
  • FunctionCallback → ToolCallback 迁移指南
  • MCP 客户端 Boot Starter 参考
  • MCP 服务器 Boot Starter 参考
  • 智能工具选择:使用动态工具发现节省 34–64% 令牌
  • Spring AI 递归顾问
  • spring-ai-session:结构化对话内存
  1. @McpTool 位于远程 MCP 服务器侧——它标记了服务器要作为 MCP 工具暴露的方法。在 Spring AI 客户端侧,SyncMcpToolCallbackProvider(或其异步对应)连接到 MCP 服务器、发现其工具、并创建 ToolCallback 袋里,ToolCallingAdvisor 像调用其他工具一样调用它们。详细信息参见 MCP 工具章节。↩
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策