Tools (Function Calling)

一些LLMs,除了生成文本外,还可以触发操作。

存在一个称为“工具”或“函数调用”的概念。它允许LLM在必要时调用一个或多个可用的工具,通常由开发者定义。工具可以是任何东西:网络搜索、调用外部 API 或执行特定的代码等。LLMs实际上不能调用工具;相反,它们在回复中表达调用特定工具的意图(而不是以纯文本形式回复)。作为开发者,我们应该执行这个工具并报告工具执行的结果。

例如,我们知道LLMs本身在数学方面并不擅长。如果你的用例涉及偶尔的数学计算,你可能想为LLM提供一个“数学工具”。通过在请求LLM时声明一个或多个工具,它就可以决定是否调用其中一个。给定一个数学问题和一组数学工具,LLM可能会决定为了正确回答问题,它应该首先调用提供的某个数学工具。

让我们看看这在实践中是如何工作的(带工具和不带工具的情况):

  • 没有工具的示例消息交换:
Request:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?

Response:
- AiMessage:
    - text: The square root of 475695037565 is approximately 689710.

差不多,但并不正确。
  • 以下工具的示例消息交换:
@Tool("Sums 2 given numbers")
double sum(double a, double b) {
    return a + b;
}

@Tool("Returns a square root of a given number")
double squareRoot(double x) {
    return Math.sqrt(x);
}
---------------------------------------------------------
Request 1:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?
- tools:
    - sum(double a, double b): Sums 2 given numbers
    - squareRoot(double x): Returns a square root of a given number

Response 1:
- AiMessage:
    - toolExecutionRequests:
        - squareRoot(475695037565)
------------------------------------------------------------------
Request 2:
- messages:
    - UserMessage:
        - text: What is the square root of 475695037565?
    - AiMessage:
        - toolExecutionRequests:
            - squareRoot(475695037565)
    - ToolExecutionResultMessage:
        - text: 689706.486532

Response 2:
- AiMessage:
    - text: The square root of 475695037565 is 689706.486532.

如您所见,当LLM有权访问工具时,它可以在适当的时候调用其中一个。

这是一个非常强大的功能。在这个简单的例子中,我们提供了LLM原始数学工具,但想象一下,如果我们提供了例如 googleSearch 和 sendEmail 工具,以及一个查询“我的朋友想知道 AI 领域的最新新闻。请将摘要发送到 friend@email.com”,那么它可以使用 googleSearch 工具找到最新新闻,然后进
行总结,并通过 sendEmail 工具发送摘要。

注意:为了增加LLM正确调用工具并传递正确参数的机会,我们应该提供清晰且明确的:

  • name of the tool
    工具的名称
  • description of what the tool does and when it should be used
    工具的功能描述以及何时应该使用它
  • description of every tool parameter
    每个工具参数的描述

一个好的经验法则:如果人类能够理解工具的用途以及如何使用它,那么LLM很可能也能。

LLMs专门针对何时调用工具以及如何调用工具进行了微调。一些模型甚至可以同时调用多个工具,例如 OpenAI。

  • 请注意,并非所有模型都支持工具。
  • 请注意,工具/函数调用与 JSON 模式不同。

2 levels of abstraction (2 层抽象)

LangChain4j 为使用工具提供了两层抽象:

  • 低级,使用 ChatLanguageModel 和 ToolSpecification API
  • 高级,使用 AI 服务和 @Tool -注解的 Java 方法

Low Level Tool API 低级工具 API

在低级,你可以使用 ChatLanguageModel 的 chat(ChatRequest) 方法。类似的函数也存在于 StreamingChatLanguageModel 中。
创建 ChatRequest 时,你可以指定一个或多个 ToolSpecification 。
ToolSpecification 是一个包含工具所有信息的对象:

  • The name of the tool
    工具的 name
  • The description of the tool
    工具的描述
  • The parameters of the tool and their descriptions
    工具的 parameters 及其描述

建议尽可能提供关于工具的详细信息:一个清晰的名字、一个全面的描述以及每个参数的描述等。

创建 ToolSpecification 有两种方式:

  1. Manually 手动
ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("getWeather")
    .description("Returns the weather forecast for a given city")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("city", "The city for which the weather forecast should be returned")
        .addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
        .required("city") // the required properties should be specified explicitly
        .build())
    .build();

您可以在这里找到更多关于 JsonObjectSchema 的信息。
https://docs.langchain4j.dev/tutorials/structured-outputs/#jsonobjectschema

  1. Using helper methods: 使用辅助方法:
  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools { 
  
    @Tool("Returns the weather forecast for a given city")
    String getWeather(
            @P("The city for which the weather forecast should be returned") String city,
            TemperatureUnit temperatureUnit
    ) {
        ...
    }
}

List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);

一旦你有了 List ,你可以调用模型:

ChatRequest request = ChatRequest.builder()
    .messages(UserMessage.from("What will the weather be like in London tomorrow?"))
    .toolSpecifications(toolSpecifications)
    .build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();

如果LLM决定调用工具,返回的 AiMessage 将包含在 toolExecutionRequests 字段中的数据。在这种情况下, AiMessage.hasToolExecutionRequests() 将返回 true 。根据LLM,它可以包含一个或多个 ToolExecutionRequest 对象(一些LLMs支持并行调用多个工具)。
每个占位符 ToolExecutionRequest 应包含:

  • The id of the tool call (some LLMs do not provide it)
    工具调用的占位符 id (某些LLMs不提供)
  • The name of the tool to be called, for example: getWeather
    要调用的工具的占位符 name ,例如: getWeather
  • The arguments, for example: { “city”: “London”, “temperatureUnit”: “CELSIUS” }
    占位符 arguments ,例如: { “city”: “London”, “temperatureUnit”: “CELSIUS” }

您需要手动使用来自 ToolExecutionRequest (s) 的信息执行工具(们)。

如果您想将工具执行的输出结果发送回 LLM,您需要创建一个 ToolExecutionResultMessage (每个 ToolExecutionRequest 一个)并将其与所有之前的消息一起发送:


String result = "It is expected to rain in London tomorrow.";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
        .messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
        .toolSpecifications(toolSpecifications)
        .build();
ChatResponse response2 = model.chat(request2);

High Level Tool API 高级工具 API

在高级抽象层面,您可以为任何 Java 方法添加 @Tool 注解,并在创建 AI 服务时指定它们。

AI 服务将自动将这些方法转换为 ToolSpecification ,并在与LLM的每次交互中包含它们。当LLM决定调用工具时,AI 服务将自动执行相应的方法,并将方法的返回值(如果有)发送回LLM。您可以在 DefaultToolExecutor 中找到实现细节。

几个工具示例:

@Tool("Searches Google for relevant URLs, given the query")
public List<String> searchGoogle(@P("search query") String query) {
    return googleSearchService.search(query);
}

@Tool("Returns the content of a web page, given the URL")
public String getWebPageContent(@P("URL of the page") String url) {
    Document jsoupDocument = Jsoup.connect(url).get();
    return jsoupDocument.body().text();
}

Tool Method Limitations 工具方法的局限性

被 @Tool 注解的方法可以是静态的或非静态的

  • 可以是静态的或非静态的
  • 可具有任何可见性(公共、私有等)。

Tool Method Parameters 工具方法参数

使用 @Tool 注解的方法可以接受任意数量和类型的参数:

  • 基本类型: int , double ,等等
  • 对象类型: String , Integer , Double ,等等
  • 自定义 POJOs(可以包含嵌套 POJOs)
  • 枚举
  • List / Set 其中 T 是上述提到的类型之一
  • Map<K,V> (您需要在参数描述中手动指定 K 和 V 的类型,并用 @P 表示)
  • 支持不带参数的方法。
Required and Optional 必需和可选

默认情况下,所有工具方法参数都被视为必需。这意味着LLM将必须为这样的参数提供一个值。可以通过使用 @P(required = false) 来使参数变为可选:

@Tool
void getTemperature(String location, @P(required = false) Unit unit) {
    ...
}

复杂参数的字段和子字段默认也被视为必需。您可以通过使用 @JsonProperty(required = false) 来使字段变为可选:
record User(String name, @JsonProperty(required = false) String email) {}

@Tool
void add(User user) {

}
请注意,当与结构化输出一起使用时,所有字段和子字段默认为可选。

递归参数(例如,具有 Set children 字段的 Person 类)目前仅由 OpenAI 支持。

Tool Method Return Types 工具方法返回类型

使用 @Tool 注解的方法可以返回任何类型,包括 void 。如果方法具有 void 返回类型,且方法返回成功,则向 LLM 发送 “Success” 字符串。

如果方法有 String 返回类型,则返回值将原样发送到 LLM,不进行任何转换。

对于其他返回类型,返回值将在发送到 LLM 之前转换为 JSON 字符串。

Exception Handling 异常处理

如果一个被 @Tool 注解的方法抛出 Exception ,则 Exception ( e.getMessage() )的消息将被发送到LLM,作为工具执行的结果。这允许LLM纠正其错误并重新尝试,如果它认为有必要的话。

@Tool

任何在构建 AI 服务时被 @Tool 注解并明确指定的 Java 方法都可以由LLM执行:

interface MathGenius {
    
    String ask(String question);
}

class Calculator {
    
    @Tool
    double add(int a, int b) {
        return a + b;
    }

    @Tool
    double squareRoot(double x) {
        return Math.sqrt(x);
    }
}

MathGenius mathGenius = AiServices.builder(MathGenius.class)
    .chatLanguageModel(model)
    .tools(new Calculator())
    .build();

String answer = mathGenius.ask("What is the square root of 475695037565?");

System.out.println(answer); // The square root of 475695037565 is 689706.486532.

当调用 ask 方法时,将发生 2 次与LLM的交互,如前文所述。在这两次交互之间,会自动调用 squareRoot 方法。

@Tool 注解有 2 个可选字段:

  • name :工具的名称。如果未提供,则方法名称将作为工具的名称。
  • value : 工具的描述。

根据工具的不同,LLM 可能即使没有任何描述也能很好地理解(例如, add(a, b) 是显而易见的),但通常最好提供清晰且具有意义的名称和描述。这样,LLM 就有更多信息来决定是否调用给定的工具,以及如何调用。

@P

方法参数可以可选地用 @P 进行注解。

@P 注解有 2 个字段

  • value : 参数描述。必填字段。
  • required : 参数是否必须,默认为 true 。可选字段。

@Description

类和字段的描述可以使用 @Description 注解来指定:

@Description("Query to execute")
class Query {

  @Description("Fields to select")
  private List<String> select;

  @Description("Conditions to filter on")
  private List<Condition> where;
}

@Tool
Result executeQuery(Query query) {
  ...
}

请注意,在 enum 值上放置的 @Description 没有效果,并且不会包含在生成的 JSON 模式中:

enum Priority {

    @Description("Critical issues such as payment gateway failures or security breaches.") // this is ignored
    CRITICAL,
    
    @Description("High-priority issues like major feature malfunctions or widespread outages.") // this is ignored
    HIGH,
    
    @Description("Low-priority issues such as minor bugs or cosmetic problems.") // this is ignored
    LOW
}

@ToolMemoryId

如果您的 AI 服务方法有一个参数被 @MemoryId 注解,您还可以注解 @Tool 方法的一个参数为 @ToolMemoryId 。提供给 AI 服务方法的值将自动传递给 @Tool 方法。此功能在您有多个用户和/或每个用户有多个聊天/记忆时非常有用,可以在 @Tool 方法内部区分它们。

Accessing Executed Tools 访问已执行的工具

如果您希望在调用 AI 服务期间访问已执行的工具,您可以通过将返回类型包装在 Result 类中来轻松实现:

interface Assistant {

    Result<String> chat(String userMessage);
}

Result<String> result = assistant.chat("Cancel my booking 123-456");

String answer = result.content();
List<ToolExecution> toolExecutions = result.toolExecutions();

在流式模式下,您可以通过指定 onToolExecuted 回调来实现:

interface Assistant {

    TokenStream chat(String message);
}

TokenStream tokenStream = assistant.chat("Cancel my booking");

tokenStream
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
    .onPartialResponse(...)
    .onCompleteResponse(...)
    .onError(...)
    .start();

Specifying Tools Programmatically 以编程方式指定工具

在使用 AI 服务时,工具也可以通过编程方式指定。这种方法提供了很大的灵活性,因为工具可以从数据库和配置文件等外部来源加载。

工具名称、描述、参数名称和描述都可以使用 ToolSpecification 进行配置:

ToolSpecification toolSpecification = ToolSpecification.builder()
        .name("get_booking_details")
        .description("Returns booking details")
        .parameters(JsonObjectSchema.builder()
                .properties(Map.of(
                        "bookingNumber", JsonStringSchema.builder()
                                .description("Booking number in B-12345 format")
                                .build()
                ))
                .build())
        .build();

对于每个 ToolSpecification ,需要提供一个 ToolExecutor 实现,该实现将处理由LLM生成的工具执行请求

ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
    Map<String, Object> arguments = fromJson(toolExecutionRequest.arguments());
    String bookingNumber = arguments.get("bookingNumber").toString();
    Booking booking = getBooking(bookingNumber);
    return booking.toString();
};

一旦我们有一个或多个( ToolSpecification , ToolExecutor )对,我们就可以在创建 AI 服务时指定它们:

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(chatLanguageModel)
    .tools(Map.of(toolSpecification, toolExecutor))
    .build();

Specifying Tools Dynamically 动态指定工具

当使用 AI 服务时,工具也可以在每次调用时动态指定。可以为每次调用 AI 服务配置一个 ToolProvider ,该 ToolProvider 将在每次调用 AI 服务时被调用,并提供应包含在当前请求中的工具给LLM。 ToolProvider 接受一个包含 UserMessage 和聊天记忆 ID 的 ToolProviderRequest ,并返回一个包含工具的 ToolProviderResult ,这些工具以 Map 从 ToolSpecification 到 ToolExecutor 的形式存在。

下面是一个示例,说明如何在用户的消息包含“预订”一词时仅添加 get_booking_details 工具:

ToolProvider toolProvider = (toolProviderRequest) -> {
    if (toolProviderRequest.userMessage().singleText().contains("booking")) {
        ToolSpecification toolSpecification = ToolSpecification.builder()
            .name("get_booking_details")
            .description("Returns booking details")
            .parameters(JsonObjectSchema.builder()
                .addStringProperty("bookingNumber")
                .build())
            .build();
        return ToolProviderResult.builder()
            .add(toolSpecification, toolExecutor)
            .build();
    } else {
        return null;
    }
};

Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(model)
    .toolProvider(toolProvider)
    .build();

人工智能服务可以在同一调用中同时使用程序性和动态指定的工具。

Tools Hallucination Strategy 工具幻觉策略

可能会出现LLM在工具调用上出现幻觉,换句话说,就是请求使用一个不存在的工具名称。在这种情况下,LangChain4j 默认会抛出一个异常来报告问题,但可以配置不同的行为,为 AI 服务提供在这种情况下使用的策略。

该策略是实现一个 Function<ToolExecutionRequest, ToolExecutionResultMessage> 的实例,定义了对于包含请求调用一个不可用工具的 ToolExecutionRequest 应该产生什么样的 ToolExecutionResultMessage 作为结果。例如,可以配置 AI 服务使用一个策略,向LLM返回一个可能促使它重试不同工具调用的响应,知道之前所需的工具不存在,如下例所示:

AssistantHallucinatedTool assistant = AiServices.builder(AssistantHallucinatedTool.class)
        .chatLanguageModel(chatLanguageModel)
        .tools(new HelloWorld())
        .hallucinatedToolNameStrategy(toolExecutionRequest -> ToolExecutionResultMessage.from(
                toolExecutionRequest, "Error: there is no tool called " + toolExecutionRequest.name()))
        .build();

Model Context Protocol (MCP) 模型上下文协议(MCP)

您也可以从 MCP 服务器导入工具。更多相关信息请在此处查看。

Related Tutorials 相关教程

来自“罐子边的传说”(Ken Kousen)关于工具的精彩指南

  • https://www.youtube.com/watch?v=cjI_6Siry-s
  • https://www.youtube.com/@talesfromthejarside

Examples 示例

  • https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/ServiceWithToolsExample.java
  • https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/ServiceWithDynamicToolsExample.java
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐