Spring AI 之工具调用

工具调用(也称为函数调用)是人工智能应用中的一种常见模式,它允许模型与一组应用程序接口(API)或工具进行交互,从而增强其能力。

工具主要用于以下方面:

  • 信息检索。此类工具可用于从外部来源检索信息,例如数据库、网络服务、文件系统或网络搜索引擎。其目标是扩展模型的知识库,使其能够回答原本无法回答的问题。因此,这些工具可用于检索增强生成(RAG)场景。例如,可以使用工具检索特定地点的当前天气、最新新闻文章,或从数据库中查询特定记录。
  • 执行操作。此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流程。其目标是自动化那些原本需要人工干预或明确编程才能完成的任务。例如,在代码生成场景中,可以使用工具为与客户聊天机器人交互的客户预订航班、填写网页表单,或基于自动化测试(测试驱动开发,TDD)来实现 Java 类

尽管通常将工具调用视为模型的一种能力,但实际上,提供工具调用逻辑的是客户端应用程序。模型只能请求调用某个工具并提供输入参数,而应用程序则负责根据这些输入参数执行工具调用,并返回结果。模型永远无法访问作为工具提供的任何应用程序接口(API),这是至关重要的安全考量。

Spring AI 提供了便捷的 API,用于定义工具、解析来自模型的工具调用请求,以及执行这些工具调用。以下部分将概述 Spring AI 中的工具调用功能。

快速入门

接下来实现两个简单的工具:一个用于信息检索,另一个用于执行操作。信息检索工具将用于获取用户所在时区的当前日期和时间。操作工具则用于在指定时间设置闹钟。

信息检索

人工智能模型无法访问实时信息。任何假设了解当前日期或天气预报等信息的提问,模型都无法回答。然而,我们可以提供一个能够检索这些信息的工具,并在需要访问实时信息时,让模型调用该工具。

<font style="color:rgb(6, 7, 31);background-color:rgba(27, 31, 35, 0.05);">DateTimeTools</font> 类中实现一个工具,用于获取用户所在时区的当前日期和时间。该工具不需要任何参数。Spring 框架中的 <font style="color:rgb(6, 7, 31);background-color:rgba(27, 31, 35, 0.05);">LocaleContextHolder</font> 可以提供用户的时区信息。将这个工具定义为一个带有 <font style="color:rgb(6, 7, 31);background-color:rgba(27, 31, 35, 0.05);">@Tool</font> 注解的方法。为了帮助模型理解是否以及何时调用此工具,提供关于该工具功能的详细描述。

java 复制代码
import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

}

接下来,将该工具提供给模型使用。在这个示例中使用 ChatClient 来与模型进行交互。通过 tools() 方法传递一个 DateTimeTools 的实例,将该工具提供给模型。当模型需要知道当前日期和时间时,它会请求调用该工具。在内部,ChatClient 会调用该工具,并将结果返回给模型,然后模型会利用工具调用的结果来生成对原始问题的最终回应。

java 复制代码
ChatModel chatModel = ...

String response = ChatClient.create(chatModel)
        .prompt("What day is tomorrow?")
        .tools(new DateTimeTools())
        .call()
        .content();

System.out.println(response);

上面的输出结果类似:

java 复制代码
2025-06-01

如果不使用工具,输出的结果会类似:

java 复制代码
I am an AI and do not have access to real-time information. Please provide the current date so I can accurately determine what day tomorrow will be.

如果没有这个工具,模型就不知道如何回答这个问题,因为它没有能力确定当前的日期和时间。

执行动作

人工智能模型可用于生成实现特定目标的计划。例如,模型可以生成一份前往丹麦旅行的预订计划。然而,模型本身并没有执行该计划的能力。这时,工具就派上用场了:它们可用于执行模型生成的计划。

在之前的示例中,使用了一个工具来确定当前的日期和时间。在本示例中,将定义第二个工具,用于在特定时间设置闹钟。目标是在从现在开始的10分钟后设置一个闹钟,因此我们需要向模型提供这两个工具来完成这项任务。

将这个新工具添加到之前相同的 DateTimeTools 类中。新工具将接受一个单一参数,即采用 ISO-8601 格式的时间。然后,该工具会在控制台打印一条消息,表明闹钟已设置为给定时间。与之前一样,该工具被定义为一个带有 @Tool 注解的方法,利用这个注解来提供详细描述,以帮助模型理解何时以及如何使用该工具。

java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    void setAlarm(String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }

}

接下来,将这两个工具都提供给模型使用。使用 ChatClient 来与模型进行交互。通过 tools() 方法传递一个 DateTimeTools 的实例,将这两个工具提供给模型。当设置从现在开始的10分钟后的闹钟时,模型首先需要知道当前的日期和时间。然后,它会利用当前的日期和时间来计算闹钟的时间。最后,它会使用闹钟工具来设置闹钟。在内部,ChatClient 会处理模型发出的任何工具调用请求,并将工具调用的执行结果返回给模型,以便模型能够生成最终的回应。

java 复制代码
ChatModel chatModel = ...

String response = ChatClient.create(chatModel)
        .prompt("Can you set an alarm 10 minutes from now?")
        .tools(new DateTimeTools())
        .call()
        .content();

System.out.println(response);

在应用程序日志中,可以检查闹钟是否已在正确的时间设置.

概述

Spring AI 通过一组灵活的抽象机制来支持工具调用,这些机制允许以一致的方式定义、解析和执行工具。本节将概述 Spring AI 中工具调用的主要概念和组件。

  1. 当想要将某个工具提供给模型使用时,将其定义包含在聊天请求中。每个工具定义都包含一个名称、一个描述以及输入参数的模式
  2. 当模型决定调用某个工具时,它会发送一个包含工具名称和按照定义模式建模的输入参数的回应。
  3. 应用程序负责使用工具名称来识别该工具,并使用提供的输入参数来执行它。
  4. 工具调用的结果由应用程序进行处理。
  5. 应用程序将工具调用的结果发送回模型。
  6. 模型利用工具调用的结果作为额外上下文来生成最终回应。

Tools是工具调用的构建块,它们由 ToolCallback 接口进行建模。Spring AI 为从方法和函数中指定 ToolCallback 提供了内置支持,但始终可以定义自己的 ToolCallback 实现,以支持更多的使用场景。

ChatModel 实现会透明地将工具调用请求分派给对应的 ToolCallback 实现,并将工具调用结果发送回模型,模型最终会据此生成最终回应。它们是通过使用 ToolCallingManager 接口来实现这一点的,该接口负责管理工具执行的整个生命周期。

ChatClient 和 ChatModel 都接受一个 ToolCallback 对象列表,以便将这些工具提供给模型使用,同时还接受一个最终会执行这些工具的 ToolCallingManager。

除了直接传递 ToolCallback 对象之外,还可以传递一个工具名称列表,这些工具名称将通过 ToolCallbackResolver 接口进行动态解析。

接下来的章节将详细介绍所有这些概念和 API,包括如何对它们进行自定义和扩展,以支持更多的使用场景。

方法作为工具

Spring AI 提供了两种内置方式来从方法中指定工具(即 ToolCallback):

  • 声明式:使用 @Tool 注解
  • 编程式:使用底层的 MethodToolCallback 实现

声明式指定:@Tool 注解

可以通过使用 @Tool 注解将一个方法转变为一个工具

java 复制代码
class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

}

@Tool 注解允许提供关于工具的关键信息:

  • name(名称):工具的名称。如果未提供,将使用方法名作为工具名称。人工智能模型在调用工具时会使用此名称来识别工具。因此,在同一个类中不允许有两个名称相同的工具。对于特定的聊天请求,该名称在模型可用的所有工具中必须是唯一的。
  • description(描述):工具的描述信息,模型可以使用该描述来了解何时以及如何调用此工具。如果未提供,将使用方法名作为工具的描述。然而,强烈建议提供详细的描述,因为这对于模型理解工具的用途以及如何使用它至关重要。未能提供良好的描述可能导致模型在应该使用工具时未使用,或者使用方式不正确。
  • returnDirect(直接返回):指示工具的结果是否应直接返回给客户端,还是应传回给模型。
  • resultConverter(结果转换器):用于将工具调用的结果转换为字符串对象以发送回人工智能模型的 ToolCallResultConverter 实现。

该方法既可以是静态方法(static method),也可以是实例方法(instance method),并且它可以具有任何访问修饰符(public、protected、包级私有(package-private)或 private)。包含该方法的类既可以是顶层类(top-level class),也可以是嵌套类(nested class),同样,该类也可以具有任何访问修饰符(只要在计划实例化它的地方是可访问的即可)。

Spring AI 为带有 @Tool 注解的方法提供了内置的提前(AOT,Ahead-Of-Time)编译支持,只要包含这些方法的类是一个 Spring Bean(例如,使用 @Component 注解的类)。否则,需要为 GraalVM 编译器提供必要的配置。例如,可以通过使用 @RegisterReflection 注解(并设置 memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)来配置该类

可以为该方法定义任意数量的参数(包括零个参数),参数类型可以是大多数类型(基本类型、普通旧式 Java 对象(POJOs)、枚举、列表、数组、映射等)。同样,该方法也可以返回大多数类型,包括 void。如果该方法返回一个值,那么返回类型必须是可序列化的类型,因为结果会被序列化并发送回模型。

Spring AI 会自动为带有 @Tool 注解的方法的输入参数生成 JSON 模式(schema)。该模式会被模型用来理解如何调用工具以及如何准备工具请求。@ToolParam 注解可用于提供关于输入参数的额外信息,例如参数的描述,或者该参数是必需的还是可选的。默认情况下,所有输入参数都被视为必需的。

java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

class DateTimeTools {

    @Tool(description = "Set a user alarm for the given time")
    void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }

}

@ToolParam 注解允许你提供关于工具参数的关键信息:

  • description(描述):参数的描述信息,模型可以利用该描述来更好地理解如何使用该参数。例如,参数应采用何种格式、允许哪些值等。
  • required(是否必需):指示参数是必需的还是可选的。默认情况下,所有参数都被视为必需的。

如果一个参数被注解为 @Nullable,那么除非使用 @ToolParam 注解明确将其标记为必需的,否则该参数将被视为可选的。

除了 @ToolParam 注解外,还可以使用来自 Swagger 的 @Schema 注解或来自 Jackson 的 @JsonProperty 注解。更多详情请参见 JSON 模式(JSON Schema)相关文档。

向 ChatClient 添加默认工具

在使用声明式指定方法时,可以通过将工具类的实例传递给 defaultTools() 方法,将其添加到 ChatClient.Builder 中作为默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具会被所有基于同一个 ChatClient.Builder 构建的 ChatClient 实例在所有聊天请求中共享。对于在不同聊天请求中经常使用的工具来说,它们非常有用。然而,如果不谨慎使用,它们也可能带来风险,因为可能会导致这些工具在不应该可用的时候变得可用。

java 复制代码
ChatModel chatModel = ...
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(new DateTimeTools())
    .build();
向 ChatModel 添加工具

在使用声明式指定方法时,可以将工具类的实例传递给用于调用 ChatModel 的 ToolCallingChatOptions 的 toolCallbacks() 方法。这样的工具将仅对添加了它们的特定聊天请求可用。

java 复制代码
ChatModel chatModel = ...
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(dateTimeTools)
    .build();
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
chatModel.call(prompt);
向 ChatModel 添加默认工具

在使用声明式指定方法时,可以在创建 ChatModel 时,通过将工具类的实例传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolCallbacks() 方法,来为 ChatModel 添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具会被该 ChatModel 实例所处理的所有聊天请求共享。对于在不同聊天请求中经常使用的工具来说,它们非常有用。然而,如果不谨慎使用,它们也可能带来风险,因为可能会导致这些工具在不应该可用的时候变得可用。

java 复制代码
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .defaultOptions(ToolCallingChatOptions.builder()
            .toolCallbacks(dateTimeTools)
            .build())
    .build();

编程式指定:MethodToolCallback

可以通过编程方式构建一个 MethodToolCallback,从而将一个方法转化为一个工具。

java 复制代码
class DateTimeTools {

    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

}

MethodToolCallback.Builder 允许构建一个 MethodToolCallback 实例,并提供关于该工具的关键信息:

  • toolDefinition:一个 ToolDefinition 实例,用于定义工具的名称、描述和输入模式(schema)。可以使用 ToolDefinition.Builder 类来构建它。此参数是必需的。
  • toolMetadata:一个 ToolMetadata 实例,用于定义额外的设置,例如是否应将结果直接返回给客户端,以及要使用的结果转换器。可以使用 ToolMetadata.Builder 类来构建它。
  • toolMethod:一个 Method 实例,代表工具方法。此参数是必需的。
  • toolObject:包含工具方法的对象实例。如果方法是静态的,可以省略此参数。
  • toolCallResultConverter:一个 ToolCallResultConverter 实例,用于将工具调用的结果转换为 String 对象,以便发送回 AI 模型。如果未提供,将使用默认的转换器(DefaultToolCallResultConverter)。

ToolDefinition.Builder 允许构建一个 ToolDefinition 实例,并定义工具的名称、描述和输入模式(schema):

  • 名称(name):工具的名称。如果未提供,将使用方法名。AI 模型在调用工具时会使用此名称来识别工具。因此,在同一个类中,不允许有两个名称相同的工具。在针对特定聊天请求可用的所有工具中,该名称必须是唯一的。
  • 描述(description):对工具的描述,模型可以使用此描述来了解何时以及如何调用该工具。如果未提供,将使用方法名作为工具的描述。然而,强烈建议提供详细的描述,因为这对于模型理解工具的用途以及如何使用它至关重要。未能提供良好的描述可能会导致模型在应该使用工具时未使用,或者使用不正确。
  • 输入模式(inputSchema):工具输入参数的 JSON 模式。如果未提供,将根据方法参数自动生成模式。可以使用 @ToolParam 注解来提供关于输入参数的额外信息,例如描述或参数是必需的还是可选的。默认情况下,所有输入参数都被视为必需的。有关更多详细信息,请参见 JSON 模式文档

ToolMetadata.Builder 允许你构建一个 ToolMetadata 实例,并为该工具定义额外的设置:

returnDirect(直接返回):指定工具的结果是否应直接返回给客户端,还是传回给模型。

java 复制代码
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder(method)
            .description("Get the current date and time in the user's timezone")
            .build())
    .toolMethod(method)
    .toolObject(new DateTimeTools())
    .build();

该方法可以是静态方法或实例方法,并且可以具有任何可见性(public、protected、包私有(package-private)或 private)。包含该方法的类可以是顶层类或嵌套类,并且也可以具有任何可见性(只要在计划实例化它的地方它是可访问的)。

Spring AI 为工具方法的 AOT(Ahead-Of-Time,提前)编译提供了内置支持,只要包含这些方法的类是一个 Spring Bean(例如,用 @Component 注解)。否则,你需要向 GraalVM 编译器提供必要的配置。例如,可以通过用 @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS) 注解该类来实现。

可以为该方法定义任意数量的参数(包括无参数),参数类型可以是大多数类型(如基本类型、POJO(普通 Java 对象)、枚举、列表、数组、映射等)。同样,该方法可以返回大多数类型,包括 void(无返回值)。如果该方法返回一个值,那么返回类型必须是可序列化的类型,因为结果会被序列化并发送回模型。

如果方法是静态的,你可以省略 toolObject() 方法,因为它不是必需的。

java 复制代码
class DateTimeTools {

    static String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

}
java 复制代码
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder(method)
            .description("Get the current date and time in the user's timezone")
            .build())
    .toolMethod(method)
    .build();

Spring AI 会自动为方法的输入参数生成 JSON 模式(schema)。该模式会被模型用来理解如何调用工具以及准备工具请求。@ToolParam 注解可用于提供关于输入参数的额外信息,例如描述,或者参数是必需的还是可选的。默认情况下,所有输入参数都被视为必需的。

java 复制代码
class DateTimeTools {

    void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }

}

@ToolParam 注解允许为工具参数提供关键信息:

  • description(描述):参数的描述,模型可以利用该描述更好地理解如何使用该参数。例如,参数应采用的格式、允许的值等。
  • required(必需):指定参数是必需的还是可选的。默认情况下,所有参数都被视为必需的。

如果一个参数被 @Nullable 注解标记,那么除非使用 @ToolParam 注解明确将其标记为必需,否则它将被视为可选的。

除了 @ToolParam 注解外,还可以使用 Swagger 的 @Schema 注解或 Jackson 的 @JsonProperty 注解。有关更多详细信息,请参见 JSON 模式相关文档。

将工具添加到 ChatClient 和 ChatModel

当使用编程式规范方法时,可以将 MethodToolCallback 实例传递给 ChatClient 的 tools() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ToolCallback toolCallback = ...
ChatClient.create(chatModel)
    .prompt("What day is tomorrow?")
    .tools(toolCallback)
    .call()
    .content();
将默认工具添加到 ChatClient

当使用编程式规范方法时,可以通过将 MethodToolCallback 实例传递给 ChatClient.Builder 的 defaultTools() 方法,来为 ChatClient 添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具会被所有从同一个 <font style="color:rgb(6, 7, 31);background-color:rgba(27, 31, 35, 0.05);">ChatClient.Builder</font> 构建的 <font style="color:rgb(6, 7, 31);background-color:rgba(27, 31, 35, 0.05);">ChatClient</font> 实例在所有聊天请求中共享。它们对于在不同聊天请求中普遍使用的工具非常有用,但如果使用不当,也可能带来风险,因为这些默认工具可能会在不应该可用的时候被意外提供。

java 复制代码
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(toolCallback)
    .build();
将工具添加到 ChatModel

当使用编程式规范方法时,可以将 MethodToolCallback 实例传递给用于调用 ChatModel 的 ToolCallingChatOptions 的 toolCallbacks() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(toolCallback)
    .build():
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
chatModel.call(prompt);
将默认工具添加到 ChatModel

当使用编程式规范方法时,可以在创建 ChatModel 时,通过将 MethodToolCallback 实例传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolCallbacks() 方法,来为 ChatModel 添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具会被该 ChatModel 实例执行的所有聊天请求共享。它们对于在不同聊天请求中普遍使用的工具非常有用,但如果使用不当,也可能带来风险,因为这些默认工具可能会在不应该可用的时候被意外提供。

java 复制代码
ToolCallback toolCallback = ...
ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .defaultOptions(ToolCallingChatOptions.builder()
            .toolCallbacks(toolCallback)
            .build())
    .build();

方法工具的限制

目前,以下类型不被支持作为用作工具的方法的参数或返回类型:

  • Optional
  • 异步类型(例如 CompletableFuture、Future)
  • 响应式类型(例如 Flow、Mono、Flux)
  • 函数式类型(例如 Function、Supplier、Consumer)
  • 函数式类型可以通过基于函数的工具规范方法来支持。

函数作为工具

Spring AI 提供了内置支持,允许通过函数来指定工具。这既可以通过使用底层的 FunctionToolCallback 实现以编程方式完成,也可以在运行时动态地通过 @Bean 注解来解析完成。

编程式规范:FunctionToolCallback

可以通过编程方式构建一个 FunctionToolCallback,将函数式类型(如 Function、Supplier、Consumer 或 BiFunction)转化为一个工具。

java 复制代码
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
    public WeatherResponse apply(WeatherRequest request) {
        return new WeatherResponse(30.0, Unit.C);
    }
}

public enum Unit { C, F }
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}

FunctionToolCallback.Builder 允许构建一个 FunctionToolCallback 实例,并提供关于该工具的关键信息:

  • name(名称):工具的名称。AI 模型在调用该工具时会使用此名称来识别它。因此,在同一个上下文中,不允许有两个名称相同的工具。在针对特定聊天请求的模型可用的所有工具中,名称必须是唯一的。这是必填项。
  • toolFunction(工具函数):表示工具方法的函数式对象(Function、Supplier、Consumer 或 BiFunction)。这是必填项。
  • description(描述):工具的描述,模型可以使用此描述来理解何时以及如何调用该工具。如果未提供,则方法名将用作工具描述。然而,强烈建议提供详细的描述,因为这对于模型理解工具的目的和使用方式至关重要。如果描述不佳,可能会导致模型在应该使用工具时未使用,或者使用不正确。
  • inputType(输入类型):函数的输入类型。这是必填项。
  • inputSchema(输入模式):工具输入参数的 JSON 模式。如果未提供,将基于 inputType 自动生成模式。你可以使用 @ToolParam 注解来提供关于输入参数的额外信息,例如描述或参数是必需的还是可选的。默认情况下,所有输入参数都被视为必需的。有关更多详细信息,请参见 JSON 模式部分。
  • toolMetadata(工具元数据):定义额外设置的 ToolMetadata 实例,例如是否应将结果直接返回给客户端,以及要使用的结果转换器。你可以使用 ToolMetadata.Builder 类来构建它。
  • toolCallResultConverter(工具调用结果转换器):用于将工具调用结果转换为 String 对象以发送回 AI 模型的 ToolCallResultConverter 实例。如果未提供,将使用默认的转换器(DefaultToolCallResultConverter)。

ToolMetadata.Builder 允许构建一个 ToolMetadata 实例,并为该工具定义额外的设置:

  • returnDirect:决定工具的结果是否应该直接返回给客户端,还是传递回模型。
java 复制代码
ToolCallback toolCallback = FunctionToolCallback
    .builder("currentWeather", new WeatherService())
    .description("Get the weather in location")
    .inputType(WeatherRequest.class)
    .build();

函数的输入和输出可以是 Void 或者普通的 Java 对象(POJOs)。输入和输出的 POJOs 必须是可序列化的,因为结果会被序列化并发送回模型。函数以及输入和输出类型都必须是公开的(public)。

向 ChatClient 添加工具

当使用编程式规范方法时,可以将 FunctionToolCallback 实例传递给 ChatClient 的 tools() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ToolCallback toolCallback = ...
ChatClient.create(chatModel)
    .prompt("What's the weather like in Copenhagen?")
    .tools(toolCallback)
    .call()
    .content();
向 ChatClient 添加默认工具

当使用编程式规范方法时,可以通过将 FunctionToolCallback 实例传递给 ChatClient.Builder 的 defaultTools() 方法,向其中添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具在由同一个 ChatClient.Builder 构建的所有 ChatClient 实例所执行的所有聊天请求中共享。它们对于在不同聊天请求中经常使用的工具非常有用,但如果使用不当,也可能会带来风险,因为可能会导致这些工具在不应该可用的时候被提供。

java 复制代码
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(toolCallback)
    .build();
向 ChatModel 添加工具

当使用编程式规范方法时,可以将 FunctionToolCallback 实例传递给 ToolCallingChatOptions 的 toolCallbacks() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ChatModel chatModel = ...
ToolCallback toolCallback = ...
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(toolCallback)
    .build():
Prompt prompt = new Prompt("What's the weather like in Copenhagen?", chatOptions);
chatModel.call(prompt);
向 ChatModel 添加默认工具

当使用编程式规范方法时,可以在构建 ChatModel 时添加默认工具,方法是将 FunctionToolCallback 实例传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolCallbacks() 方法。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

默认工具在该 ChatModel 实例执行的所有聊天请求中共享。它们对于在不同聊天请求中经常使用的工具非常有用,但如果使用不当,也可能会带来风险,因为可能会导致这些工具在不应该可用的时候被提供。

java 复制代码
ToolCallback toolCallback = ...
ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .defaultOptions(ToolCallingChatOptions.builder()
            .toolCallbacks(toolCallback)
            .build())
    .build();

动态规范:@Bean

除了编程式地指定工具外,还可以将工具定义为 Spring Bean,并让 Spring AI 在运行时通过 ToolCallbackResolver 接口(通过 SpringBeanToolCallbackResolver 实现)动态解析它们。这种方式允许你将任何 Function、Supplier、Consumer 或 BiFunction Bean 用作工具。Bean 的名称将用作工具名称,并且可以使用 Spring 框架中的 @Description 注解为工具提供描述,该描述会被模型用来理解何时以及如何调用该工具。如果没有提供描述,方法名将被用作工具描述。然而,强烈建议提供详细的描述,因为这对于模型理解工具的用途以及如何使用它至关重要。如果未能提供良好的描述,可能会导致模型在应该使用工具时未使用,或者使用不正确。

java 复制代码
@Configuration(proxyBeanMethods = false)
class WeatherTools {

    WeatherService weatherService = new WeatherService();

	@Bean
	@Description("Get the weather in location")
	Function<WeatherRequest, WeatherResponse> currentWeather() {
		return weatherService;
	}

}

工具输入参数的 JSON Schema 将自动生成。可以使用 @ToolParam 注解为输入参数提供额外信息,例如描述或参数是必需的还是可选的。默认情况下,所有输入参数都被视为必需的。有关更多详细信息,请参阅 JSON Schema 相关文档。

java 复制代码
record WeatherRequest(@ToolParam(description = "The name of a city or a country") String location, Unit unit) {}

这种工具规范方法的缺点在于无法保证类型安全,因为工具的解析是在运行时完成的。为了缓解这一问题,可以使用 @Bean 注解显式指定工具名称,并将其值存储在一个常量中,这样就可以在聊天请求中使用该常量,而不是硬编码工具名称。

java 复制代码
@Configuration(proxyBeanMethods = false)
class WeatherTools {

    public static final String CURRENT_WEATHER_TOOL = "currentWeather";

	@Bean(CURRENT_WEATHER_TOOL)
	@Description("Get the weather in location")
	Function<WeatherRequest, WeatherResponse> currentWeather() {
		...
	}

}
向 ChatClient 添加工具

当使用动态规范方法时,可以将工具名称(即函数 Bean 的名称)传递给 ChatClient 的 tools() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ChatClient.create(chatModel)
    .prompt("What's the weather like in Copenhagen?")
    .tools("currentWeather")
    .call()
    .content();
向 ChatClient 添加默认工具

当使用动态规范方法时,可以通过将工具名称传递给 ChatClient.Builder 的 defaultTools() 方法来添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具

java 复制代码
ChatModel chatModel = ...
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools("currentWeather")
    .build();
向 ChatModel 添加工具

当使用动态规范方法时,可以将工具名称传递给你用于调用 ChatModel 的 ToolCallingChatOptions 的 toolNames() 方法。该工具将仅对添加它的特定聊天请求可用。

java 复制代码
ChatModel chatModel = ...
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolNames("currentWeather")
    .build():
Prompt prompt = new Prompt("What's the weather like in Copenhagen?", chatOptions);
chatModel.call(prompt);
向 ChatModel 添加默认工具

当使用动态规范方法时,可以在构建 ChatModel 时,通过将工具名称传递给用于创建 ChatModel 的 ToolCallingChatOptions 实例的 toolNames() 方法来添加默认工具。如果同时提供了默认工具和运行时工具,那么运行时工具将完全覆盖默认工具。

java 复制代码
ChatModel chatModel = OllamaChatModel.builder()
    .ollamaApi(OllamaApi.builder().build())
    .defaultOptions(ToolCallingChatOptions.builder()
            .toolNames("currentWeather")
            .build())
    .build();

函数工具的限制

目前,以下类型不支持用作作为工具的函数的输入或输出类型:

  • 基本类型(Primitive types)
  • Optional 类型
  • 集合类型(例如 List、Map、Array、Set)
  • 异步类型(例如 CompletableFuture、Future)
  • 响应式类型(例如 Flow、Mono、Flux)

基本类型和集合类型可以通过基于方法的工具规范方法来支持。有关更多详细信息,请参阅"将方法用作工具"部分。

工具规范

在 Spring AI 中,工具通过 ToolCallback 接口进行建模。在之前的部分,已经了解了如何使用 Spring AI 提供的内置支持,从方法和函数中定义工具(请参阅"将方法用作工具"和"将函数用作工具")。本节将深入探讨工具规范,以及如何自定义和扩展它以支持更多的使用场景。

工具回调(Tool Callback)

ToolCallback 接口提供了一种方式来定义一个可以被 AI 模型调用的工具,包括工具的定义和执行逻辑。当想要从头开始定义一个工具时,这是需要实现的主要接口。例如,可以从 MCP 客户端(使用模型上下文协议)或 ChatClient(用于构建模块化的智能体应用程序)中定义一个 ToolCallback。

该接口提供了以下方法:

java 复制代码
public interface ToolCallback {

	/**
	 * Definition used by the AI model to determine when and how to call the tool.
	 */
	ToolDefinition getToolDefinition();

	/**
	 * Metadata providing additional information on how to handle the tool.
	 */
	ToolMetadata getToolMetadata();

    /**
	 * Execute tool with the given input and return the result to send back to the AI model.
	 */
	String call(String toolInput);

    /**
	 * Execute tool with the given input and context, and return the result to send back to the AI model.
	 */
	String call(String toolInput, ToolContext tooContext);

}

Spring AI 为工具方法(MethodToolCallback)和工具函数(FunctionToolCallback)提供了内置实现。

工具定义

ToolDefinition 接口为 AI 模型提供了关于工具可用性的必要信息,包括工具名称、描述以及输入架构(schema)。每个 ToolCallback 实现都必须提供一个 ToolDefinition 实例来定义工具。

该接口提供了以下方法:

java 复制代码
public interface ToolDefinition {

	/**
	 * The tool name. Unique within the tool set provided to a model.
	 */
	String name();

	/**
	 * The tool description, used by the AI model to determine what the tool does.
	 */
	String description();

	/**
	 * The schema of the parameters used to call the tool.
	 */
	String inputSchema();

}

ToolDefinition.Builder 允许使用默认实现(DefaultToolDefinition)来构建一个 ToolDefinition 实例。

java 复制代码
ToolDefinition toolDefinition = ToolDefinition.builder()
    .name("currentWeather")
    .description("Get the weather in location")
    .inputSchema("""
        {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string"
                },
                "unit": {
                    "type": "string",
                    "enum": ["C", "F"]
                }
            },
            "required": ["location", "unit"]
        }
    """)
    .build();

方法工具定义

当从方法构建工具时,ToolDefinition 会自动生成。如果希望自己生成 ToolDefinition,可以使用这个方便的构建器。

java 复制代码
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolDefinition toolDefinition = ToolDefinition.from(method);

从方法生成的 ToolDefinition 包含以下信息:将方法名作为工具名,将方法名作为工具描述,以及方法输入参数的 JSON 架构。如果方法上标注了 @Tool 注解,并且设置了工具名和描述,那么将使用注解中指定的工具名和描述。

如果更愿意显式地提供部分或全部属性,可以使用 ToolDefinition.Builder 来构建一个自定义的 ToolDefinition 实例。

java 复制代码
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolDefinition toolDefinition = ToolDefinition.builder(method)
    .name("currentDateTime")
    .description("Get the current date and time in the user's timezone")
    .inputSchema(JsonSchemaGenerator.generateForMethodInput(method))
    .build();

函数工具定义

当从函数构建工具时,ToolDefinition 会自动生成。当使用 FunctionToolCallback.Builder 来构建一个 FunctionToolCallback 实例时,可以提供工具名称、描述以及输入架构(schema),这些信息将用于生成 ToolDefinition。有关更多详细信息,请参阅"将函数用作工具"部分。

JSON Schema

当向 AI 模型提供一个工具时,模型需要知道调用该工具时输入类型的Schema。该Schema用于理解如何调用工具以及准备工具请求。Spring AI 通过 JsonSchemaGenerator 类提供了内置支持,用于为工具的输入类型生成 JSON 架构。该架构作为 ToolDefinition 的一部分提供。

JsonSchemaGenerator 类在底层用于为方法或函数的输入参数生成 JSON 架构,它使用了"将方法用作工具"和"将函数用作工具"部分中描述的任何一种策略。JSON 架构生成逻辑支持一系列注解,你可以在方法和函数的输入参数上使用这些注解来自定义生成的架构。

本节描述了在为工具的输入参数生成 JSON 架构时可以自定义的两个主要选项:描述(description)和必需状态(required status)。

描述

除了为工具本身提供描述外,还可以为工具的输入参数提供描述。描述可以用来提供关于输入参数的关键信息,例如参数应该采用什么格式、允许哪些值等。这对于帮助模型理解输入架构以及如何使用它非常有用。Spring AI 提供了内置支持,可以使用以下注解之一为输入参数生成描述:

  • 来自 Spring AI 的 @ToolParam(description = "...")
  • 来自 Jackson 的 @JsonClassDescription(description = "...")
  • 来自 Jackson 的 @JsonPropertyDescription(description = "...")
  • 来自 Swagger 的 @Schema(description = "...")

这种方法既适用于方法,也适用于函数,并且你可以递归地对嵌套类型使用它。

java 复制代码
class DateTimeTools {

    @Tool(description = "Set a user alarm for the given time")
    void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }

}
必需/可选

默认情况下,每个输入参数都被视为必需的,这要求 AI 模型在调用工具时必须为该参数提供一个值。然而,可以通过使用以下注解之一(按优先级顺序)将输入参数设为可选:

  • 来自 Spring AI 的 @ToolParam(required = false)
  • 来自 Jackson 的 @JsonProperty(required = false)
  • 来自 Swagger 的 @Schema(required = false)
  • 来自 Spring Framework 的 @Nullable

这种方法既适用于方法,也适用于函数,并且可以递归地对嵌套类型使用它。

java 复制代码
class CustomerTools {

    @Tool(description = "Update customer information")
    void updateCustomerInfo(Long id, String name, @ToolParam(required = false) String email) {
        System.out.println("Updated info for customer with id: " + id);
    }

}

为输入参数定义正确的必需状态对于降低产生幻觉的风险以及确保模型在调用工具时提供正确的输入至关重要。在之前的示例中,email 参数是可选的,这意味着模型可以在不提供该参数值的情况下调用工具。如果该参数是必需的,那么模型在调用工具时就必须为该参数提供一个值。而如果没有值存在,模型可能会编造一个值,从而导致产生幻觉。

结果转换

工具调用的结果会使用 ToolCallResultConverter 进行序列化,然后发送回 AI 模型。ToolCallResultConverter 接口提供了一种将工具调用结果转换为 String 对象的方法。

该接口提供了以下方法:

java 复制代码
@FunctionalInterface
public interface ToolCallResultConverter {

	/**
	 * Given an Object returned by a tool, convert it to a String compatible with the
	 * given class type.
	 */
	String convert(@Nullable Object result, @Nullable Type returnType);

}

工具调用的结果必须是可序列化的类型。默认情况下,结果会使用 Jackson(DefaultToolCallResultConverter)序列化为 JSON,但可以通过提供自己的 ToolCallResultConverter 实现来自定义序列化过程。

Spring AI 在方法和函数工具中均依赖于 ToolCallResultConverter

方法工具调用结果转换

当使用声明式方法从方法构建工具时,可以通过设置 @Tool 注解的 resultConverter() 属性,为工具提供一个自定义的 ToolCallResultConverter。

java 复制代码
class CustomerTools {

    @Tool(description = "Retrieve customer information", resultConverter = CustomToolCallResultConverter.class)
    Customer getCustomerInfo(Long id) {
        return customerRepository.findById(id);
    }

}

如果使用编程式方法,可以通过设置 MethodToolCallback.Builder 的 resultConverter() 属性,为工具提供一个自定义的 ToolCallResultConverter。

更多详情请参见"将方法用作工具"部分

函数工具调用结果转换

当使用编程式方法从函数构建工具时,你可以通过设置 FunctionToolCallback.Builder 的 resultConverter() 属性,为工具提供一个自定义的 ToolCallResultConverter。

更多详情请参见"将方法用作工具"部分

工具上下文(Tool Context)

Spring AI 支持通过 ToolContext API 向工具传递额外的上下文信息。借助此功能,可以提供额外的、由用户提供的数据,这些数据可与人工智能模型传递的工具参数一同在工具执行过程中使用。

java 复制代码
class CustomerTools {

    @Tool(description = "Retrieve customer information")
    Customer getCustomerInfo(Long id, ToolContext toolContext) {
        return customerRepository.findById(id, toolContext.get("tenantId"));
    }

}

在调用 ChatClient 时,ToolContext 会被用户提供的数据填充。

java 复制代码
ChatModel chatModel = ...

String response = ChatClient.create(chatModel)
        .prompt("Tell me more about the customer with ID 42")
        .tools(new CustomerTools())
        .toolContext(Map.of("tenantId", "acme"))
        .call()
        .content();

System.out.println(response);

ToolContext 中提供的任何数据都不会被发送给人工智能模型。

同样地,在直接调用 ChatModel 时,也可以定义工具上下文数据。

java 复制代码
ChatModel chatModel = ...
ToolCallback[] customerTools = ToolCallbacks.from(new CustomerTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(customerTools)
    .toolContext(Map.of("tenantId", "acme"))
    .build();
Prompt prompt = new Prompt("Tell me more about the customer with ID 42", chatOptions);
chatModel.call(prompt);

如果在默认选项和运行时选项中都设置了 toolContext 选项,那么最终生成的 ToolContext 将是这两个选项的合并结果,其中运行时选项的优先级高于默认选项。

直接返回结果

默认情况下,工具调用的结果会作为响应发送回模型。然后,模型可以使用该结果继续对话。

但在某些情况下,可能更希望将结果直接返回给调用者,而不是发送回模型。例如,如果构建了一个依赖 RAG(检索增强生成)工具的智能体,可能希望直接将结果返回给调用者,而不是将其发送回模型进行不必要的后处理。或者,可能拥有某些应该终止智能体推理循环的工具。

每个 ToolCallback 实现都可以定义工具调用的结果是否应直接返回给调用者,还是发送回模型。默认情况下,结果会发送回模型。但可以针对每个工具更改此行为。

负责管理工具执行生命周期的 ToolCallingManager 会处理与工具关联的 returnDirect 属性。如果该属性设置为 true,则工具调用的结果会直接返回给调用者。否则,结果会发送回模型。

如果同时请求了多个工具调用,那么为了将结果直接返回给调用者,所有工具的 returnDirect 属性都必须设置为 true。否则,结果将会被发送回模型。

  1. 当想要让模型能够使用某个工具时,将该工具的定义包含在聊天请求中。如果希望工具执行的结果能够直接返回给调用者,将 returnDirect 属性设置为 true。
  2. 当模型决定调用某个工具时,它会发送一个包含工具名称和按照定义模式建模的输入参数的响应。
  3. 应用程序负责使用工具名称来识别并使用提供的输入参数执行该工具。
  4. 工具调用的结果由应用程序进行处理。
  5. 应用程序将工具调用的结果直接发送给调用者,而不是将其发送回模型。
方法直接返回结果

在使用声明式方法从某个方法构建工具时,可以通过将 @Tool 注解的 returnDirect 属性设置为 true,来标记该工具以将结果直接返回给调用者。

java 复制代码
class CustomerTools {

    @Tool(description = "Retrieve customer information", returnDirect = true)
    Customer getCustomerInfo(Long id) {
        return customerRepository.findById(id);
    }

}

如果使用编程式方法,可以通过 ToolMetadata 接口设置 returnDirect 属性,并将其传递给 MethodToolCallback.Builder。

java 复制代码
ToolMetadata toolMetadata = ToolMetadata.builder()
    .returnDirect(true)
    .build();
函数直接返回结果

当使用编程式方法从函数构建工具时,可以通过 ToolMetadata 接口设置 returnDirect 属性,并将其传递给 FunctionToolCallback.Builder。

java 复制代码
ToolMetadata toolMetadata = ToolMetadata.builder()
    .returnDirect(true)
    .build();

工具执行

工具执行是指使用提供的输入参数调用工具并返回结果的过程。工具执行由 ToolCallingManager 接口处理,该接口负责管理工具执行的生命周期。

java 复制代码
public interface ToolCallingManager {

	/**
	 * Resolve the tool definitions from the model's tool calling options.
	 */
	List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions);

	/**
	 * Execute the tool calls requested by the model.
	 */
	ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse);

}

Spring AI Spring Boot Starter(启动器),那么 DefaultToolCallingManager 是 ToolCallingManager 接口的自动配置实现。也可以通过提供自己定义的 ToolCallingManager Bean 来定制工具调用行为。

python 复制代码
@Bean
ToolCallingManager toolCallingManager() {
    return ToolCallingManager.builder().build();
}

默认情况下,Spring AI 会在每个 ChatModel 实现内部透明地管理工具执行的生命周期。但也可以选择不采用这种默认行为,而是自行控制工具的执行。本节将描述这两种场景。

框架控制的工具执行

当使用默认行为时,Spring AI 会自动拦截来自模型的任何工具调用请求,调用工具并将结果返回给模型。所有这些操作都会由每个使用 ToolCallingManager 的 ChatModel 实现透明地完成。

  1. 当希望将某个工具提供给模型使用时,将其定义包含在聊天请求(Prompt)中,并调用 ChatModel API,该 API 会将请求发送给 AI 模型。
  2. 当模型决定调用某个工具时,它会发送一个包含工具名称以及根据已定义模式建模的输入参数的响应(ChatResponse)。
  3. ChatModel 会将工具调用请求发送给 ToolCallingManager API。
  4. ToolCallingManager 负责确定要调用的工具,并使用提供的输入参数来执行该工具。
  5. 工具调用的结果会返回给 ToolCallingManager。
  6. ToolCallingManager 会将工具执行结果返回给 ChatModel。
  7. ChatModel 会将工具执行结果作为 ToolResponseMessage 返回给 AI 模型。
  8. AI 模型会使用工具调用结果作为额外上下文来生成最终响应,并通过 ChatClient 将响应发送回调用方(ChatResponse)。

目前,与模型就工具执行方面进行交换的内部消息并不会向用户公开。如果需要访问这些消息,应当采用用户控制的工具执行方式。

判断工具调用是否具备执行资格的逻辑由 ToolExecutionEligibilityPredicate 接口处理。默认情况下,工具执行资格的判定是通过检查 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性是否设置为 true(默认值),以及 ChatResponse 是否包含任何工具调用来确定的。

python 复制代码
public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate {

	@Override
	public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) {
		return ToolCallingChatOptions.isInternalToolExecutionEnabled(promptOptions) && chatResponse != null
				&& chatResponse.hasToolCalls();
	}

}

在创建 ChatModel Bean 时,可以提供自己自定义的 ToolExecutionEligibilityPredicate 实现。

用户控制的工具执行

在某些情况下,可能更希望自己控制工具执行的生命周期。可以通过将 ToolCallingChatOptions 的 internalToolExecutionEnabled 属性设置为 false 来实现这一点。

当使用此选项调用 ChatModel 时,工具执行将被委托给调用方,从而可以对工具执行的生命周期拥有完全的控制权。你有责任检查 ChatResponse 中的工具调用,并使用 ToolCallingManager 来执行它们。

以下示例展示了用户控制的工具执行方式的最小实现:

python 复制代码
ChatModel chatModel = ...
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(new CustomerTools())
    .internalToolExecutionEnabled(false)
    .build();
Prompt prompt = new Prompt("Tell me more about the customer with ID 42", chatOptions);

ChatResponse chatResponse = chatModel.call(prompt);

while (chatResponse.hasToolCalls()) {
    ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse);

    prompt = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);

    chatResponse = chatModel.call(prompt);
}

System.out.println(chatResponse.getResult().getOutput().getText());

在选择用户控制的工具执行方式时,建议使用 ToolCallingManager 来管理工具调用操作。这样一来,就可以利用 Spring AI 为工具执行提供的内置支持。不过,这并不会阻止你实现自己的工具执行逻辑

接下来的示例展示了结合使用 ChatMemory API 的用户控制的工具执行方式的最小实现:

python 复制代码
ToolCallingManager toolCallingManager = DefaultToolCallingManager.builder().build();
ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
String conversationId = UUID.randomUUID().toString();

ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(ToolCallbacks.from(new MathTools()))
    .internalToolExecutionEnabled(false)
    .build();
Prompt prompt = new Prompt(
        List.of(new SystemMessage("You are a helpful assistant."), new UserMessage("What is 6 * 8?")),
        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);
    chatResponse = chatModel.call(promptWithMemory);
    chatMemory.add(conversationId, chatResponse.getResult().getOutput());
}

UserMessage newUserMessage = new UserMessage("What did I ask you earlier?");
chatMemory.add(conversationId, newUserMessage);

ChatResponse newResponse = chatModel.call(new Prompt(chatMemory.get(conversationId)));

异常处理

当工具调用失败时,会抛出 ToolExecutionException 异常,可通过捕获该异常来处理错误。可以使用 ToolExecutionExceptionProcessor 来处理 ToolExecutionException,处理结果有两种:一是生成一条错误信息,将其返回给人工智能模型;二是抛出异常,交由调用方处理。

python 复制代码
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {

	/**
	 * Convert an exception thrown by a tool to a String that can be sent back to the AI
	 * model or throw an exception to be handled by the caller.
	 */
	String process(ToolExecutionException exception);

}

如果正在使用任何 Spring AI 的 Spring Boot Starter 组件,那么 DefaultToolExecutionExceptionProcessor 就是 ToolExecutionExceptionProcessor 接口的自动配置实现。默认情况下,错误信息会被发送回模型。DefaultToolExecutionExceptionProcessor 的构造函数允许将 alwaysThrow 属性设置为 true 或 false。如果设置为 true,则会抛出异常,而不是将错误信息发送回模型。

python 复制代码
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
    return new DefaultToolExecutionExceptionProcessor(true);
}

如果自定义了 ToolCallback 的实现,请确保在 call() 方法中,当工具执行逻辑出现错误时,抛出 ToolExecutionException 异常。

ToolExecutionExceptionProcessor 由默认的工具调用管理器(DefaultToolCallingManager)在内部使用,用于处理工具执行过程中出现的异常

工具解析(Tool Resolution)

将工具传递给模型的主要方法是在调用 ChatClient 或 ChatModel 时提供 ToolCallback(或多个 ToolCallback),使用"将方法作为工具"和"将函数作为工具"部分中描述的策略之一。

不过,Spring AI 还支持使用 ToolCallbackResolver 接口在运行时动态解析工具。

python 复制代码
public interface ToolCallbackResolver {

	/**
	 * Resolve the {@link ToolCallback} for the given tool name.
	 */
	@Nullable
	ToolCallback resolve(String toolName);

}

使用这种方法时:

  • 在客户端,向 ChatClient 或 ChatModel 提供工具名称,而不是直接提供 ToolCallback(或多个 ToolCallback)。
  • 在服务器端,ToolCallbackResolver 的实现类负责将工具名称解析为对应的 ToolCallback 实例

默认情况下,Spring AI 依赖于一个委托式工具回调解析器(DelegatingToolCallbackResolver),该解析器将工具解析任务委托给一个 ToolCallbackResolver 实例列表:

  • SpringBeanToolCallbackResolver 会从类型为 Function、Supplier、Consumer 或 BiFunction 的 Spring Bean 中解析工具。有关更多详细信息,请参阅"动态规范:@Bean"部分。
  • StaticToolCallbackResolver 会从一个静态的 ToolCallback 实例列表中解析工具。在使用 Spring Boot 自动配置时,该解析器会自动配置应用程序上下文中定义的所有类型为 ToolCallback 的 Bean。

如果依赖 Spring Boot 自动配置,可以通过提供一个自定义的 ToolCallbackResolver Bean 来定制解析逻辑。

python 复制代码
@Bean
ToolCallbackResolver toolCallbackResolver(List<FunctionCallback> toolCallbacks) {
    StaticToolCallbackResolver staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
    return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver));
}

ToolCallbackResolver 由 ToolCallingManager 在内部使用,用于在运行时动态解析工具,同时支持框架控制的工具执行(Framework-Controlled Tool Execution)和用户控制的工具执行(User-Controlled Tool Execution)。

可观测性(Observability)

工具调用包含对可观测性的支持,通过 spring.ai.tool 的观测功能来测量完成时间并传播追踪信息。请参阅"工具调用可观测性"(Tool Calling Observability)部分。

此外,出于敏感性考虑,默认情况下,Spring AI 不会导出工具调用参数和结果作为跨度(span)属性,但可以选择启用此功能。详情请参阅:"工具调用参数和结果数据"(Tool Call Arguments and ResultData)。

日志记录(Logging)

工具调用功能的所有主要操作都会在 DEBUG 级别进行日志记录。可以通过将 org.springframework.ai 包的日志级别设置为 DEBUG 来启用这些日志记录。

相关推荐
NAGNIP7 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab8 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab8 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP12 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年12 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼12 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS12 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区13 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈13 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang14 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx