前面章节介绍了 Spring AI 中工具调用的核心概念,以及如何通过方法和函数定义工具。下面将介绍工具是如何被执行?为了后续演示方便,先使用 @Tool 注解定义一个工具,代码如下:
package com.hxstrive.springai.springai_openai.tools.tools21; import org.springframework.ai.tool.annotation.Tool; import org.springframework.context.i18n.LocaleContextHolder; import java.time.LocalDateTime; public class DateTimeTools { @Tool(description = "获取用户所在时区的当前日期和时间") public String getCurrentDateTime() { System.out.println("->> getCurrentDateTime() 获取用户所在时区的当前日期和时间"); return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString(); } }
上面提供的名为 getCurrentDateTime() 工具将返回用户所在时区的当前日期和时间。
工具执行是指使用提供的输入参数调用工具并返回结果的过程。该过程由 ToolCallingManager 接口处理,该接口负责管理工具执行的完整生命周期。
ToolCallingManager 接口定义如下:
package org.springframework.ai.model.tool; import java.util.List; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.tool.definition.ToolDefinition; /** * 工具调用管理器接口,负责协调AI模型与工具之间的交互流程 * 主要功能包括解析可用工具定义和执行工具调用 */ public interface ToolCallingManager { /** * 解析工具定义列表 * 根据聊天选项确定当前可用的工具集合,供AI模型选择调用 * * @param chatOptions 聊天选项,可能包含工具调用相关的配置参数 * @return 可用的工具定义列表,每个定义描述了工具的基本信息和调用方式 */ List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions); /** * 执行工具调用 * 根据AI模型的响应结果(包含工具调用指令)执行相应的工具,并返回执行结果 * * @param prompt 原始的提示信息,包含用户的查询内容 * @param chatResponse AI模型的响应,可能包含工具调用请求 * @return 工具执行结果,包含执行状态和输出数据 */ ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse); /** * 创建默认工具调用管理器的构建器 * 提供一种便捷的方式来配置和实例化 DefaultToolCallingManager * * @return DefaultToolCallingManager 的构建器实例 */ static DefaultToolCallingManager.Builder builder() { return DefaultToolCallingManager.builder(); } }
上面源码中,ToolDefinition 表示工具定义,工具调用需要用到。工具定义接口,用于描述AI可调用工具的基本信息和输入规范,该接口定义了工具的核心元数据,使AI模型能够理解工具的功能和使用方式。
ToolDefinition 接口定义如下:
package org.springframework.ai.tool.definition; public interface ToolDefinition { /** * 获取工具的名称 * 名称用于唯一标识工具,是AI模型指定调用哪个工具的关键标识 * * @return 工具的名称字符串 */ String name(); /** * 获取工具的描述信息 * 描述详细说明工具的功能和用途,帮助AI模型判断该工具是否适合解决当前问题 * * @return 工具的描述字符串 */ String description(); /** * 获取工具的输入数据模式(Schema) * 通常以JSON Schema格式定义,描述工具所需输入参数的结构、类型和约束条件, * 确保AI模型能够生成符合要求的输入数据 * * @return 输入模式的字符串表示(通常为JSON Schema) */ String inputSchema(); /** * 创建默认工具定义的构建器 * 提供便捷的方式来构建DefaultToolDefinition实例 * * @return DefaultToolDefinition的构建器对象 */ static DefaultToolDefinition.Builder builder() { return DefaultToolDefinition.builder(); } }
如果使用 Spring AI Spring Boot Starter,DefaultToolCallingManager 将作为 ToolCallingManager 接口的自动配置实现。你可通过提供自定义的 ToolCallingManager Bean 来定制工具执行行为。例如:
@Bean ToolCallingManager toolCallingManager() { return ToolCallingManager.builder().build(); }
默认情况下,Spring AI 在每个 ChatModel 实现中透明地管理工具执行生命周期。但你可选择退出此行为,自行控制工具执行。下面将描述这两种场景。
使用默认配置时,Spring AI 会自动拦截模型的工具调用请求,执行工具并将结果返回模型。这些操作均由各 ChatModel 实现通过 ToolCallingManager 透明完成。对于开发人员是完全透明的,不需要进行任何干预。如下图:
上图说明:
① 如果要为 AI 模型提供工具,需把工具定义(含名称、描述、输入模式等信息)包含在聊天请求(Prompt),随后调用 ChatModel API,将该请求发送至 AI 模型。
② 当 AI 模型判定需要调用工具时,会返回包含工具名称以及符合工具输入模式参数的响应(ChatResponse)。
③ ChatModel 把工具调用请求传递给 ToolCallingManager API。
④ ToolCallingManager 依据请求,识别出要调用的工具,并使用给定的输入参数去执行该工具。
⑤ 工具执行完毕后,结果会返回给 ToolCallingManager。
⑥ ToolCallingManager 将工具执行结果回传给 ChatModel。
⑦ ChatModel 把工具执行结果以 ToolResponseMessage 的形式返回给 AI 模型。
⑧ AI 模型以工具调用结果作为额外上下文,生成最终的响应,再通过 ChatClient 将该响应返回给调用方。
注意:目前与模型之间就工具执行所交换的内部消息不会向用户公开。如果您需要访问这些消息,应当采用用户控制的工具执行方式。
问题:Spring AI 是如何判断一个工具调用是否可以执行的呢?
工具调用是否具备执行资格的逻辑由 ToolExecutionEligibilityPredicate 接口处理。默认情况下,通过检查 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性(默认值为 true)及 ChatResponse 是否包含工具调用来判定执行资格。
ToolExecutionEligibilityPredicate 接口定义:
package org.springframework.ai.model.tool; import java.util.function.BiPredicate; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.util.Assert; /** * 用于决定是否需要执行工具调用 * 继承自BiPredicate,支持对聊天选项和模型响应进行条件判断 */ public interface ToolExecutionEligibilityPredicate extends BiPredicate<ChatOptions, ChatResponse> { /** * 判断是否需要执行工具调用 * 该默认方法对输入参数进行非空校验后,调用test方法执行具体的判断逻辑 * * @param promptOptions 聊天选项,包含与本次对话相关的配置信息 * @param chatResponse AI模型的响应结果,可能包含工具调用请求 * @return 如果需要执行工具调用则返回true,否则返回false * @throws IllegalArgumentException 如果任何参数为null时抛出 */ default boolean isToolExecutionRequired(ChatOptions promptOptions, ChatResponse chatResponse) { // 校验输入参数非空,确保后续逻辑的安全性 Assert.notNull(promptOptions, "promptOptions cannot be null"); Assert.notNull(chatResponse, "chatResponse cannot be null"); // 调用BiPredicate的test方法执行具体的判断逻辑 return this.test(promptOptions, chatResponse); } }
Spring AI 为 ToolExecutionEligibilityPredicate 接口提供了一个默认实现 DefaultToolExecutionEligibilityPredicate,默认实现中,仅仅实现了 test() 方法,如下:
package org.springframework.ai.model.tool; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; /** * 工具执行资质的默认判断实现类 * 实现了ToolExecutionEligibilityPredicate接口,提供默认的工具执行必要性判断逻辑 */ public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate { /** * 默认构造函数 */ public DefaultToolExecutionEligibilityPredicate() { } /** * 判断是否需要执行工具调用的核心逻辑 * 当且仅当满足以下所有条件时,才需要执行工具调用: * 1. 工具执行功能已通过聊天选项启用 * 2. 模型响应不为空 * 3. 模型响应中包含工具调用请求 * * @param promptOptions 聊天选项,包含工具执行的配置信息 * @param chatResponse AI模型的响应结果,可能包含工具调用指令 * @return 如果满足所有工具执行条件则返回true,否则返回false */ public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) { // 检查工具执行是否启用,且响应不为空且包含工具调用 return ToolCallingChatOptions.isInternalToolExecutionEnabled(promptOptions) && chatResponse != null && chatResponse.hasToolCalls(); } }
注意:创建 ChatModel Bean 时,你可提供自定义的 ToolExecutionEligibilityPredicate 实现。
某些情况下,你可能希望自行控制工具执行生命周期。此时可将 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性设为 false。
当使用此选项调用 ChatModel 时,工具执行将委托给调用方,由你完全控制工具执行生命周期。你需要检查 ChatResponse 中的工具调用,并使用 ToolCallingManager 执行它们。
以下示例演示了用户控制工具执行方案的最小实现:
package com.hxstrive.springai.springai_openai.tools.tools21.controller; import com.hxstrive.springai.springai_openai.tools.tools21.DateTimeTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolExecutionResult; import org.springframework.ai.support.ToolCallbacks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController class MyController { @Autowired private ChatModel chatModel; // 模型使用 gpt-4-turbo // GPT-4 全面支持工具调用功能,且在处理复杂度、准确性和灵活性上显著优于 GPT-3.5-turbo // http://localhost:8080/ai @GetMapping(value = "/ai",produces = "text/html; charset=UTF-8") public String ai() { // 创建 ToolCallingManager ToolCallingManager toolCallingManager = ToolCallingManager.builder().build(); // 创建聊天选项,禁止自动处理工具调用 ChatOptions chatOptions = ToolCallingChatOptions.builder() .toolCallbacks(ToolCallbacks.from(new DateTimeTools())) .internalToolExecutionEnabled(false) .build(); // 创建提示词 Prompt 对象 Prompt prompt = new Prompt("明天是星期几?", chatOptions); ChatResponse chatResponse = chatModel.call(prompt); // 循环调用,直到完成所有工具调用 while (chatResponse.hasToolCalls()) { // 执行工具 ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse); // 将工具执行结果拼装到提示词 Prompt 中 prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions); // 继续调用AI,AI大模型可能返回需要调用另一个工具的响应,这就是为什么需要用 while 循环 chatResponse = chatModel.call(prompt); } // 响应结果 return chatResponse.getResult().getOutput().getText(); } }
运行示例,效果如下图:
选择用户控制的工具执行方案时,建议使用 ToolCallingManager 管理工具调用操作。这样可充分利用 Spring AI 内置的工具执行支持。当然,你也可完全自行实现工具执行逻辑。
下面的示例展示了结合使用 ChatMemory API 的用户控制工具执行方案的最小实现:
package com.hxstrive.springai.springai_openai.tools.tools21.controller; import com.hxstrive.springai.springai_openai.tools.tools21.DateTimeTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.DefaultToolCallingManager; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolExecutionResult; import org.springframework.ai.support.ToolCallbacks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.UUID; @RestController class MyController { @Autowired private ChatModel chatModel; // 模型使用 gpt-4-turbo // GPT-4 全面支持工具调用功能,且在处理复杂度、准确性和灵活性上显著优于 GPT-3.5-turbo // http://localhost:8080/ai @GetMapping(value = "/ai",produces = "text/html; charset=UTF-8") public String ai() { // 创建默认的工具调用管理器,用于处理工具调用的相关逻辑 ToolCallingManager toolCallingManager = DefaultToolCallingManager.builder().build(); // 创建聊天记忆对象,使用消息窗口模式管理对话历史 ChatMemory chatMemory = MessageWindowChatMemory.builder().build(); // 生成唯一的对话ID,用于标识当前对话会话 String conversationId = UUID.randomUUID().toString(); // 配置聊天选项,设置工具调用相关参数 ChatOptions chatOptions = ToolCallingChatOptions.builder() // 注册工具回调,这里添加了日期时间工具 .toolCallbacks(ToolCallbacks.from(new DateTimeTools())) // 禁用内部工具执行(由用户自定义工具调用) .internalToolExecutionEnabled(false) .build(); // 创建初始提示信息,包含系统消息和用户问题 Prompt prompt = new Prompt( List.of(new SystemMessage("你是AI助手小粒,所有回复均使用中文。"), new UserMessage("明天是星期几?")), chatOptions); // 将初始提示信息添加到聊天记忆,保存对话历史 chatMemory.add(conversationId, prompt.getInstructions()); // 创建包含对话历史的新提示词,用于后续模型调用 Prompt promptWithMemory = new Prompt(chatMemory.get(conversationId), chatOptions); ChatResponse chatResponse = chatModel.call(promptWithMemory); // 将模型响应结果添加到聊天记忆 chatMemory.add(conversationId, chatResponse.getResult().getOutput()); // 循环处理工具调用:如果模型响应包含工具调用请求,则执行工具并继续对话 while (chatResponse.hasToolCalls()) { // 通过工具调用管理器执行工具调用 ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(promptWithMemory, chatResponse); // 将工具执行结果的最后一条记录添加到聊天内存 chatMemory.add(conversationId, toolExecutionResult.conversationHistory() .get(toolExecutionResult.conversationHistory().size() - 1)); // 基于更新后的聊天记忆创建新提示 promptWithMemory = new Prompt(chatMemory.get(conversationId), chatOptions); // 再次调用AI模型,传入包含工具执行结果的新提示词 chatResponse = chatModel.call(promptWithMemory); // 将新的模型响应添加到聊天记忆 chatMemory.add(conversationId, chatResponse.getResult().getOutput()); } // 用户新提问:"我刚刚问了什么?" UserMessage newUserMessage = new UserMessage("我刚刚问了什么?"); chatMemory.add(conversationId, newUserMessage); ChatResponse newResponse = chatModel.call(new Prompt(chatMemory.get(conversationId))); // 返回模型对新问题的回答文本 return newResponse.getResult().getOutput().getText(); } }
运行示例,效果如下图:
当工具调用失败时,异常会以 ToolExecutionException 形式传播,可捕获该异常进行错误处理。
通过 ToolExecutionExceptionProcessor 可处理 ToolExecutionException 异常,异常处理会产生两种结果:要么生成错误消息发送回 AI 模型,要么抛出异常由调用者处理。
ToolExecutionExceptionProcessor 接口定义:
package org.springframework.ai.tool.execution; /** * 用于定义工具执行过程中发生异常时的处理逻辑,将异常转换为合适的字符串表示 */ @FunctionalInterface public interface ToolExecutionExceptionProcessor { /** * 处理工具执行异常 * 当工具调用过程中发生异常时,通过此方法对异常进行处理,返回处理后的字符串结果 * 通常用于生成传递给AI模型的错误信息,以便模型理解工具调用失败的原因。 * * @param exception 工具执行过程中抛出的异常 * @return 处理后的异常信息字符串,通常包含异常详情或友好提示 */ String process(ToolExecutionException exception); }
若使用 Spring AI Spring Boot Starter,DefaultToolExecutionExceptionProcessor 将作为 ToolExecutionExceptionProcessor 接口的自动配置实现。
默认会将错误信息返回 AI 模型。但是,可以通过 DefaultToolExecutionExceptionProcessor 构造函数的 alwaysThrow 参数修改此行为。如果将 alwaysThrow 设置为 true 时,将直接抛出异常而非返回错误信息。例如:
@Bean ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() { return new DefaultToolExecutionExceptionProcessor(true); }
源码如下:
public class DefaultToolExecutionExceptionProcessor implements ToolExecutionExceptionProcessor { private static final Logger logger = LoggerFactory.getLogger(DefaultToolExecutionExceptionProcessor.class); private static final boolean DEFAULT_ALWAYS_THROW = false; private final boolean alwaysThrow; // 控制参数 public DefaultToolExecutionExceptionProcessor(boolean alwaysThrow) { this.alwaysThrow = alwaysThrow; } public String process(ToolExecutionException exception) { Assert.notNull(exception, "exception cannot be null"); if (this.alwaysThrow) { throw exception; // 抛出异常 } else { logger.debug("Exception thrown by tool: {}. Message: {}", exception.getToolDefinition().name(), exception.getMessage()); return exception.getMessage(); // 返回给AI模型的内容 } } //.... }
注意,除了通过代码控制外,你还可通过 spring.ai.tools.throw-exception-on-error 属性控制 DefaultToolExecutionExceptionProcessor Bean 的行为,例如:
属性 | 说明 | 默认值 |
spring.ai.tools.throw-exception-on-error | 若为 true,工具调用错误将作为异常抛出供调用方处理;若为 false,错误将转为消息返回 AI 模型由其处理响应。 | false |
如果要自定义 ToolCallback 实现,请确保在 call() 方法的工具执行逻辑中发生错误时抛出 ToolExecutionException。
ToolExecutionExceptionProcessor 由默认的 ToolCallingManager(DefaultToolCallingManager)内部使用,用于处理工具执行期间的异常。