LangChain4j 工具调用详解:从基础到高级的实战指南

工具(函数调用) | LangChain4j

一些大型语言模型(LLMs)除了生成文本外,还可以触发操作。

注意:所有支持工具的LLMs都可以在这里找到(查看“工具”列)。

有一个概念被称为“工具”或“函数调用”。它允许LLM在必要时调用一个或多个可用的工具,这些工具通常由开发人员定义。工具可以是任何东西:网络搜索、调用外部API,或者执行一段特定的代码等。

LLMs本身并不能实际调用工具;相反,它们会在响应中表达调用特定工具的意图(而不是以纯文本形式响应)。作为开发人员,我们应该执行这个工具,并将工具执行的结果报告回来。

例如,我们知道LLMs本身在数学方面并不擅长。如果您的用例涉及偶尔的数学计算,您可能希望为LLM提供一个“数学工具”。通过在请求中声明一个或多个工具,LLM可以决定是否调用其中一个工具。

让我们看看在实际中(有工具和没有工具)的情况:

没有工具的消息交换示例:

Request:
- messages:
    - UserMessage:
        - text:475695037565的平方根是多少?

Response:
- AiMessage:
    - text:475695037565的平方根大约是689710。

接近,但不正确。

有以下工具的消息交换示例:

@Tool("对两个给定的数字求和")
double sum(double a, double b) {
    return a + b;
}

@Tool("返回给定数字的平方根")
double squareRoot(double x) {
    return Math.sqrt(x);
}

Request 1:
- messages:
    - UserMessage:
        - text:475695037565的平方根是多少?
- tools:
    - sum(double a, double b):对两个给定的数字求和
    - squareRoot(double x):返回给定数字的平方根

Response 1:
- AiMessage:
    - toolExecutionRequests:
        - squareRoot(475695037565)

... 这里我们使用“475695037565”参数执行squareRoot方法,并得到“689706.486532”作为结果 ...

Request 2:
- messages:
    - UserMessage:
        - text:475695037565的平方根是多少?
    - AiMessage:
        - toolExecutionRequests:
            - squareRoot(475695037565)
    - ToolExecutionResultMessage:
        - text:689706.486532

Response 2:
- AiMessage:
    - text:475695037565的平方根是689706.486532。

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

这是一个非常强大的功能。在这个简单的例子中,我们给LLM提供了原始的数学工具,但想象一下,如果我们给它例如googleSearchsendEmail工具,并且有一个查询如“我的朋友想了解AI领域的最新新闻。请发送简短摘要到[email protected]”,那么它可以用googleSearch工具查找最新新闻,然后总结并使用sendEmail工具通过电子邮件发送摘要。

注意:为了增加LLM调用正确工具和正确参数的可能性,我们应该提供清晰且明确的:

  • 工具的名称
  • 工具的功能和使用时机的描述
  • 每个工具参数的描述

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

LLMs被特别微调以检测何时调用工具以及如何调用它们。一些模型甚至可以同时调用多个工具,例如OpenAI。

注意:并非所有模型都支持工具。要查看哪些模型支持工具,请参阅此页面上的“工具”列。

注意:工具/函数调用与JSON模式不同。

两个抽象级别

LangChain4j为使用工具提供了两个抽象级别:

  • 低级,使用ChatLanguageModelToolSpecification API
  • 高级,使用AI服务和带有@Tool注解的Java方法

在低级中,您可以使用ChatLanguageModelchat(ChatRequest)方法。StreamingChatLanguageModel中也有类似的方法。

创建ChatRequest时可以指定一个或多个ToolSpecification

ToolSpecification是一个包含有关工具所有信息的对象:

  • 工具的名称
  • 工具的描述
  • 工具的参数及其描述

建议尽可能多地提供有关工具的信息:清晰的名称、全面的描述,以及每个参数的描述等。

创建ToolSpecification有两种方式:

  1. 手动
ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("getWeather")
    .description("返回给定城市的天气预报")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("city", "应返回天气预报的城市")
        .addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
        .required("city") // 必需属性应明确指定
        .build())
    .build();

您可以在这里找到有关JsonObjectSchema的更多信息。

  1. 使用辅助方法:
  • ToolSpecifications.toolSpecificationsFrom(Class)
  • ToolSpecifications.toolSpecificationsFrom(Object)
  • ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools {

    @Tool("返回给定城市的天气预报")
    String getWeather(
            @P("应返回天气预报的城市") String city,
            TemperatureUnit temperatureUnit
    ) {
        ...
    }
}

List toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);

一旦您有了一个List,就可以调用模型:

ChatRequest request = ChatRequest.builder()
    .messages(UserMessage.from("明天伦敦的天气会怎样?"))
    .toolSpecifications(toolSpecifications)
    .build();
ChatResponse response = model.chat(request);
AiMessage aiMessage = response.aiMessage();

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

每个ToolExecutionRequest应包含:

  • 工具调用的id(一些LLM不提供此id)
  • 要调用的工具的名称,例如:getWeather
  • 参数,例如:{ "city": "London", "temperatureUnit": "CELSIUS" }

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

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


String result = "预计明天伦敦将下雨。";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
ChatRequest request2 = ChatRequest.builder()
        .messages(List.of(userMessage, aiMessage, toolExecutionResultMessage))
        .toolSpecifications(toolSpecifications)
        .build();
ChatResponse response2 = model.chat(request2);

在高级抽象级别,您可以在创建AI服务时注解任何Java方法为@Tool。AI服务将自动将这些方法转换为ToolSpecification,并将其包含在每次与LLM交互的请求中。当LLM决定调用工具时,AI服务将自动执行相应的方法,并将方法的返回值(如果有)发送回LLM。您可以在DefaultToolExecutor中找到实现细节。

一些工具示例:

@Tool("根据查询在Google中搜索相关URL")
public List searchGoogle(@P("搜索查询") String query) {
    return googleSearchService.search(query);
}

@Tool("根据URL返回网页内容")
public String getWebPageContent(@P("页面URL") String url) {
    Document jsoupDocument = Jsoup.connect(url).get();
    return jsoupDocument.body().text();
}

工具方法限制

带有@Tool注解的方法:

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

工具方法参数

带有@Tool注解的方法可以接受任何数量的各种类型的参数:

  • 基本类型:intdouble
  • 对象类型:StringIntegerDouble
  • 自定义POJO(可以包含嵌套POJO)
  • enum枚举
  • List/ Set,其中T是上述类型之一
  • Map(您需要手动指定K和V的类型,在参数描述中使用@P

也支持无参数的方法。

默认情况下,所有方法参数都被视为必需的/必填的。这意味着LLM必须为这样的参数生成一个值。可以通过用@P(required = false)注解参数来使其成为可选参数。目前不支持将POJO参数的字段声明为可选。

递归参数(例如,一个Person类有一个Set children字段)目前仅被OpenAI支持。

工具方法返回类型

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

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

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

异常处理

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

@Tool

任何带有@Tool注解的Java方法,并且在构建AI服务时明确指定,都可以由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("475695037565的平方根是多少?");

System.out.println(answer); // 475695037565的平方根是689706.486532。

当调用ask方法时,将与LLM进行两次交互,如前面部分所述。在这两次交互之间,将自动调用squareRoot方法。

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

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

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

@P

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

@P注解有两个字段:

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

@Description

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

@Description("要执行的查询")
class Query {

  @Description("要选择的字段")
  private List select;

  @Description("过滤条件")
  private List where;
}

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

@ToolMemoryId

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

访问执行的工具

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

interface Assistant {

    Result chat(String userMessage);
}

Result result = assistant.chat("取消我的预订123-456");

String answer = result.content();
List toolExecutions = result.toolExecutions();

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

interface Assistant {

    TokenStream chat(String message);
}

TokenStream tokenStream = assistant.chat("取消我的预订");

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

程序化指定工具

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

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

ToolSpecification toolSpecification = ToolSpecification.builder()
        .name("get_booking_details")
        .description("返回预订详情")
        .parameters(JsonObjectSchema.builder()
                .properties(Map.of(
                        "bookingNumber", JsonStringSchema.builder()
                                .description("格式为B-12345的预订号")
                                .build()
                ))
                .build())
        .build();

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

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

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

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

动态指定工具

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

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

ToolProvider toolProvider = (toolProviderRequest) -> {
    if (toolProviderRequest.userMessage().singleText().contains("booking")) {
        ToolSpecification toolSpecification = ToolSpecification.builder()
            .name("get_booking_details")
            .description("返回预订详情")
            .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();

模型上下文协议(MCP)

您还可以从MCP服务器导入工具。有关此的更多信息可以在这里找到。

代码示例

  • 带有工具的示例
  • 带有动态工具的示例

你可能感兴趣的:(精品专栏,java,springboot,langchain4j,tools,mcp,ai,llm)