Spring AI 工具调用:工具执行

前面章节介绍了 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 透明完成。对于开发人员是完全透明的,不需要进行任何干预。如下图:

Spring AI 工具调用:工具执行

上图说明:

① 如果要为 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 实现方案

当使用此选项调用 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();
    }

}

运行示例,效果如下图:

Spring AI 工具调用:工具执行

ChatMemory 实现方案

选择用户控制的工具执行方案时,建议使用 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();
    }

}

运行示例,效果如下图:

Spring AI 工具调用:工具执行

异常处理

当工具调用失败时,异常会以 ToolExecutionException 形式传播,可捕获该异常进行错误处理。

Spring AI 工具调用:工具执行

通过 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 接口的自动配置实现。

Spring AI 工具调用:工具执行

默认会将错误信息返回 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)内部使用,用于处理工具执行期间的异常。

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号