工具调用(Tool Calling)

        工具调用(亦称函数调用)是 AI 应用的常见模式,允许模型通过与一组 API(即工具)交互来扩展其能力。工具可以是任何东西:网页搜索、对外部 API 的调用,或特定代码的执行等。

        LLM 本身不能实际调用工具,工具调用逻辑是由客户端应用程序提供的。模型只能请求工具调用并提供输入参数,而应用程序负责根据输入参数执行工具调用并返回结果。

一、概述

        Spring AI 通过一组灵活的抽象机制支持工具调用功能,这些抽象允许你以统一的方式定义、解析和执行工具。

  1. 当我们想让模型使用某个工具时,我们会在聊天请求中包含该工具的定义。每个工具的定义都包括名称、描述和输入参数的模式

  2. 当模型决定调用工具时,它会发送包含工具名称及符合预定义模式的输入参数的响应。

  3. 应用程序负责根据工具名称识别对应工具,并使用提供的输入参数执行该工具。

  4. 工具调用的结果由应用程序进行处理。

  5. 应用程序将工具调用结果返回至模型。

  6. 模型最终利用工具调用结果作为附加上下文生成响应。

        Tool 是工具调用的基础构建单元,通过 ToolCallback 接口进行建模。Spring AI 内置支持从方法和函数生成 ToolCallback,同时你始终可以自定义 ToolCallback 实现以满足更多使用场景。

工具主要应用于以下场景:

  • 信息检索。此类工具可用于从外部源检索信息,例如数据库、网络服务、文件系统或网络搜索引擎。其目的是增强模型的知识,使其能够回答原本无法回答的问题。因此,它们可用于检索增强生成(RAG)场景。例如,可以使用工具检索给定位置的当前天气、检索最新新闻文章或查询数据库中的特定记录。

  • 执行操作。此类工具可用于在软件系统中执行操作,例如发送电子邮件、在数据库中创建新记录、提交表单或触发工作流。其目的是自动化那些原本需要人工干预或显式编程的任务。例如,可以使用工具为与聊天机器人交互的客户预订航班、填写网页上的表单,或在代码生成场景中基于自动化测试(TDD)实现 Java 类。

        Spring AI 支持两种工具调用的定义:方法工具 和 函数工具,ChatClient 和 ChatModel 均可接收 ToolCallback 对象列表,既向模型提供可用工具

一、方法工具

Spring AI 为方法转工具(即 ToolCallback)提供两种内置支持方式:

  • 声明式:通过 @Tool 注解实现。

  • 编程式:通过底层的 MethodToolCallback 实现。

这里我们只看最常用的声明式:

1.声明式配置:@Tool 注解方案

1.1 @Tool

你只需为方法添加 @Tool 注解,即可将其转换为工具。

@Tool 注解允许你配置以下关键工具信息:

  • name工具名称。若不指定,默认使用方法名称。AI 模型通过此名称识别调用工具,因此不允许在同一类中存在同名工具。模型处理单个聊天请求时,所有可用工具的名称必须保持全局唯一

  • description工具描述,用于指导模型判断何时及如何调用该工具。若未指定,默认使用方法名称作为工具描述。但强烈建议提供详细描述,因为这对模型理解工具用途和使用方式至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具。

  • returnDirect控制工具结果直接返回客户端(true)还是传回模型(false),默认false。

  • resultConverter工具调用结果的转化器,默认使用 DefaultToolCallResultConverter,将结果转换为字符串。

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();
    }

}

1.2 @ToolParam

        Spring AI 将自动为 @Tool 注解方法的输入参数生成 JSON Schema。该 Schema 供模型理解如何调用工具及准备工具请求。你可使用 @ToolParam 注解为输入参数提供额外信息(如描述、是否必需等),默认情况下所有输入参数均为必需参数。

        你可以为方法定义任意数量的参数(包括无参数),支持大多数类型(基本类型、POJO、枚举、List、数组、Map 等)。同样,方法可以返回大多数类型,包括 void。若方法有返回值,则返回类型必须是可序列化类型,因为结果将被序列化并发送回模型。

@ToolParam 注解允许你配置工具参数的关键信息:

  • description参数描述,用于帮助模型更准确地理解如何使用该参数。例如:参数格式要求、允许取值范围等。

  • required指定参数是否为必需项(默认值:true,即所有参数默认必需)。

@Component
public class TimeService {

    @Tool(description = "Get time by zone id")
    public String getTimeByZoneId(@ToolParam(description = "Time zone id, such as Asia/Shanghai")
                                  String zoneId) {
        ZoneId zid = ZoneId.of(zoneId);
        ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
        return zonedDateTime.format(formatter);
    }
}

2.示例:获取当前时间

说明:AI 模型无法获取实时信息。任何需要知晓当前日期或天气预报等实时信息的问题,模型都无法直接回答。但我们可以提供能够检索这类信息的工具,当需要访问实时数据时,让模型调用这些工具。

定义并注册工具类:

@Component
public class TimeService {

    @Tool(description = "Get time by zone id")
    public String getTimeByZoneId(@ToolParam(description = "Time zone id, such as Asia/Shanghai")
                                  String zoneId) {
        ZoneId zid = ZoneId.of(zoneId);
        ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
        return zonedDateTime.format(formatter);
    }
}
@Configuration
public class ToolConfig {

    @Bean
    @Description("时间查询工具")
    public TimeService timeService() {
        return new TimeService();
    }
}

调用工具:

        使用ChatClient 或 ChatModel 均可接收,可在配置时通过 defaultTools 或 调用时通过 tools方法传入工具类实例。

@Configuration
public class ChatClientConfig {

    @Autowired
    private TimeService timeService;

    @Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
        
        SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
                request -> "请求内容: " + request.prompt().getUserMessage().getText(),
                response -> "响应内容: " + response.getResult().getOutput().getText(),
                0
        );

        return ChatClient.builder(ollamaChatModel).
                defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
                defaultTools(timeService).
                defaultAdvisors(
                    simpleLoggerAdvisor
                )
                .build();
    }
   
}

controller:

    @Autowired
    @Qualifier("ollamaChatClient")
    private ChatClient ollamaChatClient;

    @GetMapping("/chat1")
    public String chat(@RequestParam(value = "question") String question) {
        log.info("question: {}", question);
        String content = ollamaChatClient.prompt(question)
                .system(s -> s.param("tone", "邓超"))
                .call()
                .content();
        return content;
    }

调用接口:

        单LLM是无法获取时间或天气等信息的。

二、函数工具

        Spring AI 内置支持通过函数定义工具,既可通过底层的 FunctionToolCallback 实现以编程方式配置,也能作为运行时解析的 @Bean 动态注册。

1.编程式规范:FunctionToolCallback

1.1 Function

        你可以通过编程方式构建 FunctionToolCallback,将函数式类型(FunctionSupplierConsumer 或 BiFunction)转换为工具。

public class TimeFunction implements
        Function<TimeFunction.Request, TimeFunction.Response> {

    @JsonClassDescription("Request to get time by zone id")
    public record Request(@JsonProperty(required = true, value = "zoneId")
                              @JsonPropertyDescription("Time zone id, such as Asia/Shanghai") String zoneId) {
    }

    @JsonClassDescription("Response to get time by zone id")
    public record Response(@JsonPropertyDescription("time") String time) {
    }

    @Override
    public Response apply(Request request) {
        ZoneId zid = ZoneId.of(request.zoneId());
        ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
        return new Response(zonedDateTime.format(formatter));
    }
}

1.2 FunctionToolCallback.Builder

        FunctionToolCallback.Builder 允许你构建 FunctionToolCallback 实例并提供以下关键工具信息:

  • name:工具名称。AI 模型通过此名称识别调用工具,因此同一上下文中不允许存在同名工具。对于特定聊天请求,模型可用的所有工具名称必须保持全局唯一。(必需项)

  • toolFunction:表示工具方法的函数式对象(FunctionSupplierConsumer 或 BiFunction)。(必需项)

  • description:工具描述,用于帮助模型判断何时及如何调用该工具。若未提供,将使用方法名称作为工具描述。但强烈建议提供详细描述,这对模型理解工具用途及使用方法至关重要。若描述不充分,可能导致模型在该调用工具时未调用,或错误调用工具。

  • inputType:函数输入类型。(必需项)

  • inputSchema:工具输入参数的 JSON Schema。若未提供,将基于 inputType 自动生成 Schema。你可使用 @ToolParam 注解提供输入参数的额外信息(如描述、是否必需等),默认情况下所有输入参数均为必需参数。

  • toolMetadata:定义额外设置的 ToolMetadata 实例(如是否将结果直接返回客户端、使用的结果转换器等),可通过 ToolMetadata.Builder 类构建。

  • toolCallResultConverter:用于将工具调用结果转换为 String 对象并返回 AI 模型的 ToolCallResultConverter 实例(未配置时默认使用 DefaultToolCallResultConverter)。

ToolCallback toolCallback = FunctionToolCallback
    .builder("currentWeather", new WeatherService())
    .description("Get the weather in location")
    .inputType(WeatherRequest.class)
    .build();

        函数输入和输出可以是 Void 或 POJO。输入和输出的 POJO 必须是可序列化的,因为结果将被序列化并发送回模型。函数及输入输出类型必须是 public 的。

1.3 示例:获取当前时间

        在调用 ChatClient 时通过 .toolCallBacks() 或者在实例化 ChatClient 对象的时候通过 .defalutToolCallBacks() 传递 FunctionToolCallBack 对象:

实现Function接口:

package com.hl.springdemo.function;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;

public class TimeFunction implements
        Function<TimeFunction.Request, TimeFunction.Response> {

    @JsonClassDescription("Request to get time by zone id")
    public record Request(@JsonProperty(required = true, value = "zoneId",defaultValue = "Asia/Shanghai")
                          @JsonPropertyDescription("Time zone id, such as Asia/Shanghai") String zoneId) {
    }

    @JsonClassDescription("Response to get time by zone id")
    public record Response(@JsonPropertyDescription("time") String time) {
    }

    @Override
    public Response apply(Request request) {
        ZoneId zid = ZoneId.of(request.zoneId());
        ZonedDateTime zonedDateTime = ZonedDateTime.now(zid);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
        return new Response(zonedDateTime.format(formatter));
    }
}

        实例化 ChatClient 对象的时候通过 .defalutToolCallBacks() 传递 FunctionToolCallBack 对象:

@Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
        
        // defaultToolCallbacks
        FunctionToolCallback<TimeFunction.Request, TimeFunction.Response> timeFunction = FunctionToolCallback
                .builder("timeFunction", new TimeFunction())
                .description("Get time by zone id")
                .inputType(TimeFunction.Request.class)
                .build();
        SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
                request -> "请求内容: " + request.prompt().getUserMessage().getText(),
                response -> "响应内容: " + response.getResult().getOutput().getText(),
                0
        );

        return ChatClient.builder(ollamaChatModel).
                defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
                defaultToolCallbacks(timeFunction).
                defaultAdvisors(
                    simpleLoggerAdvisor
                )
                .build();
    }

调用接口:

2.动态规范:@Bean

       开发者可以把任意实现 Function 接口的对象,定义为 Bean ,并通过 .toolNames() 或 .defaultToolNames() 传递给 ChatClient 对象。

@Configuration
public class TestAutoConfiguration {

    @Bean
    @Description("Get time by zone id")
    public TimeFunction getTimeByZoneId() {
        return new TimeFunction();
    }
}

        在调用 ChatClient 时,通过.toolNames() 传递函数工具的 Bean 名称,或者在实例化 ChatClient 对象的时候通过 .defalutToolNames() 方法传递函数工具:

@Bean
    public ChatClient ollamaChatClient(@Qualifier("ollamaChatModel") OllamaChatModel ollamaChatModel) {
        
        SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor(
                request -> "请求内容: " + request.prompt().getUserMessage().getText(),
                response -> "响应内容: " + response.getResult().getOutput().getText(),
                0
        );

        return ChatClient.builder(ollamaChatModel).
                defaultSystem("你是一个乐于助人的智能助手,请以{tone}的语气回答用户的问题。").
                defaultToolNames("getTimeByZoneId").
                defaultAdvisors(
                    simpleLoggerAdvisor
                )
                .build();
    }

三、返回值转换

        Spring AI 框架中,工具调用的结果会通过 ToolCallResultConverter 进行处理,然后回传给 AI 模型。ToolCallResultConverter 接口提供了将工具调用结果转换为字符串对象的方法。Spring AI 默认使用 DefaultToolCallResultConverter,将返回结果对象使用 Jackson 库转化为 JSON 字符串ToolCallResultConverter 接口的定义为:

@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);

}

        定义方法工具时,可以通过 @Tool 注解的 resultConverter 参数提供 ToolCallResultConverter 的实现类;定义方法工具和函数工具时可以通过 MethodToolCallBack.Builder 和 FunctionToolCallBack.Builder 的 resultConverter() 方法设置ToolCallResultConverter 的实现类。

四、工具上下文

        Spring AI 支持通过 ToolContext API 向工具传递额外的上下文信息。该特性允许提供补充数据,比如用户身份信息。这些数据将与 AI 模型传递的工具参数结合使用。

        

1.ToolContext(工具上下文)

        用于在函数调用(工具调用)场景下,封装和传递工具执行所需的上下文信息。它保证上下文数据不可变,便于多线程安全地传递和使用

  • Map<String, Object> context:于存储和获取工具调用的消息历史

示例:

public class UserInfoTools {
    @Tool(description = "get current user name")
    public String getUserName(ToolContext context) {
        String userId = context.getContext().get("userId").toString();
        if (!StringUtils.hasText(userId)) {
            return "null";
        }
        // 模拟数据
        return userId + "user";
    }
}

在调用 ChatClient 时,通过 .toolContext() 方法传递工具上下文:

String response = chatClient.prompt("获取我的用户名")
    .tools(new UserInfoTools())
    .toolContext(Map.of("userId", "12345"))
    .call()
    .content();

五、工具调用直接返回

        默认情况下,工具调用的返回值会再次回传到 AI 模型进一步处理。但在一些场景中需要将结果直接返回给调用方而非模型,比如数据搜索。

        每个 ToolCallback 实现均可定义工具调用结果应直接返回调用方还是传回模型。默认结果为传回模型,但你可按工具单独修改此行为。ToolCallingManager 负责管理工具执行生命周期,其会处理工具的 returnDirect 属性。若该属性设为 true,工具调用结果将直接返回调用方;否则结果将传回模型。

  1. 当需要向模型提供工具时,我们将其定义包含在聊天请求中。若希望工具执行结果直接返回调用方,则将 returnDirect 属性设为 true

  2. 当模型决定调用工具时,它会发送一个响应,其中包含工具名称和根据定义的模式建模的输入参数。

  3. 应用程序负责使用工具名称来识别并使用提供的输入参数执行工具。

  4. 工具调用的结果由应用程序处理。

  5. 应用程序将工具调用结果直接返回调用方,而非传回模型。

1.方法直接返回

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

class CustomerTools {

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

}

2.函数直接返回

        使用编程式方法从函数构建工具时,你可通过 ToolMetadata 接口(描述工具的元数据信息,目前仅用来控制是否直接将工具结果返回给 AI 模型)设置 returnDirect 属性,并将其传递给 FunctionToolCallback.Builder

        调用这段代码将直接返回 TimeFunction 返回的JSON对象,而不再经过大模型加工处理。

String response = chatClient.prompt("获取北京时间")
    .toolCallbacks(FunctionToolCallback
        .builder("getTimeByZoneId", new TimeFunction())
        .toolMetadata(ToolMetadata.builder()
            .returnDirect(true)
            .build())
        .description("Get time by zone id")
        .inputType(TimeFunction.Request.class)
        .build())
    .call()
    .content();

在现代软件开发和系统设计中,**function calling(函数调用)** 和 **tool calling工具调用)** 是两个相关但有区别的概念。它们通常用于不同的目的,并在不同上下文中被使用。 ### 三、函数调用(Function Calling) 函数调用是编程中最基本的操作之一,指的是程序执行过程中调用一个定义好的函数以完成特定任务的行为。函数通常是代码中的模块化单元,具有明确的输入参数和返回值[^1]。 例如,在 Python 中可以这样定义并调用一个函数: ```python def add(a, b): return a + b result = add(3, 5) ``` 函数调用的优势在于: - 可重用性:同一个函数可以在多个地方被调用。 - 封装性:隐藏实现细节,只暴露接口。 - 易于维护:修改函数逻辑只需更新一次。 函数调用通常发生在程序内部,不需要依赖外部服务或运行时环境。 ### 四、工具调用Tool Calling工具调用则更广泛地指调用某种“外部”功能或服务,这些功能可能是由其他系统、API 或命令行工具提供的。工具调用常用于自动化流程、集成测试、部署脚本等场景[^2]。 例如,在一个包含多个工具的字典中选择并调用某个工具函数: ```python import functools names_to_functions = { "retrieve_payment_status": functools.partial(retrieve_payment_status, df=df), "retrieve_payment_date": functools.partial(retrieve_payment_date, df=df), } function_result = names_to_functions[tool_function.name](**args) ``` 工具调用的特点包括: - 外部依赖:通常需要与外部系统交互,如数据库、HTTP API、CLI 工具等。 - 状态管理:可能涉及上下文保存、历史记录(如 `chat_history.append(tool_msg)`)。 - 动态路由:可以根据运行时条件动态决定调用哪个工具[^2]。 工具调用适用于构建可扩展的应用程序架构,尤其是在微服务、插件系统或 AI Agent 架构中非常常见。 ### 五、区别总结 | 特性 | 函数调用 | 工具调用 | |------------------|------------------------------|--------------------------------------| | 调用对象 | 内部定义的函数 | 外部系统或服务 | | 是否需外部依赖 | 否 | 是 | | 调用方式 | 直接通过函数名调用 | 通过配置、路由或插件机制调用 | | 应用场景 | 通用逻辑处理 | 集成第三方服务、动态行为控制 | | 示例 | `add(3, 5)` | `names_to_functions[tool_function.name](**args)` | ### 六、典型使用方法对比 - **函数调用示例**: ```python def calculate_discount(price, discount_rate): return price * (1 - discount_rate) final_price = calculate_discount(100, 0.1) ``` - **工具调用示例**: ```python from langchain.runnables import OpenAIFunctionsRouter router = OpenAIFunctionsRouter([func1, func2]) response = router.invoke({"query": "get user info"}) ``` 工具调用通常会结合 `Runnable` 接口来实现异步、链式调用,支持更多高级特性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值