 Tools (Function Calling)
Tools (Function Calling)
  # Tools (Function Calling)
某些大语言模型(LLM)除了生成文本,还可以触发一些操作。
支持工具的所有LLM可以在此处 (opens new window)找到(参见“Tools”列)。
有一个概念叫做“工具”(Tools)或“函数调用”(Function Calling), 它允许LLM在必要时调用由开发者定义的一个或多个工具。 工具可以是任何东西,例如网页搜索、调用外部API或执行特定的代码段等。 LLM本身无法直接调用工具,而是通过其响应中表达调用特定工具的意图(而非普通的文本响应)。 开发者需要根据提供的参数执行工具,并将执行结果反馈给LLM。
例如,我们知道LLM在数学方面表现并不佳。 如果您的用例中偶尔需要数学计算,您可以向LLM提供一个“数学工具”。 通过在请求中声明一个或多个工具,LLM可以在认为合适时决定调用其中之一。 面对一个数学问题及一组数学工具,LLM可能会决定为了正确回答问题, 需要首先调用提供的某个数学工具。
以下展示了如何在实践中使用工具(以及不使用工具时的表现):
没有工具的消息交换示例:
请求:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?
响应:
- AiMessage:
    - text: 475695037565的平方根大约是689710。
2
3
4
5
6
7
8
接近,但不正确。
使用以下工具的消息交换示例:
@Tool("计算两个数的和")
double sum(double a, double b) {
    return a + b;
}
@Tool("返回一个数的平方根")
double squareRoot(double x) {
    return Math.sqrt(x);
}
请求1:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?
- tools:
    - sum(double a, double b): 计算两个数的和
    - squareRoot(double x): 返回一个数的平方根
响应1:
- AiMessage:
    - toolExecutionRequests:
        - squareRoot(475695037565)
... 此处我们用参数“475695037565”执行squareRoot方法,并得到结果“689706.486532” ...
请求2:
- messages:
    - UserMessage:
        - text: 475695037565的平方根是多少?
    - AiMessage:
        - toolExecutionRequests:
            - squareRoot(475695037565)
    - ToolExecutionResultMessage:
        - text: 689706.486532
响应2:
- AiMessage:
    - text: 475695037565的平方根是689706.486532。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
正如您所见,当LLM可以访问工具时,它可以在适当的时候决定调用其中一个工具。
这是一个非常强大的功能。 在这个简单的示例中,我们为LLM提供了基础的数学工具, 但可以设想,如果我们为它提供例如googleSearch和sendEmail工具, 并给出一个类似“我的朋友想了解AI领域的最新新闻。请将简要总结发送到friend@email.com”的查询, 它可以使用googleSearch工具查找AI领域的最新新闻, 然后总结内容,并使用sendEmail工具通过电子邮件发送总结。
为了提高LLM调用正确工具和参数的概率,我们应该提供清晰明确的:
- 工具名称
- 工具功能描述以及何时使用的说明
- 每个工具参数的描述
一个好的规则是:如果人类可以理解工具的用途及使用方法,那么LLM也有很大的可能性可以理解。
LLM经过专门调整,可以检测何时调用工具以及如何调用工具。 某些模型甚至可以同时调用多个工具,例如, OpenAI (opens new window)。
请注意,并非所有模型都支持工具。 要查看支持工具的模型,请参阅此页面 (opens new window)的“Tools”列。
请注意,工具/函数调用与JSON模式并不相同。
# 两个抽象级别
LangChain4j为使用工具提供了两个抽象级别:
- 底层,使用ChatLanguageModel和ToolSpecificationAPI
- 高层,使用AI Services和@Tool注解的Java方法
# 底层工具API
在底层,您可以使用ChatLanguageModel的generate(List<ChatMessage>, List<ToolSpecification>)方法。 StreamingChatLanguageModel中也有类似的方法。
ToolSpecification是一个对象,包含工具的所有信息:
- 工具的name
- 工具的description
- 工具的parameters及其描述
建议尽可能多地提供工具信息:清晰的名称、全面的描述以及每个参数的描述等。
创建ToolSpecification有两种方式:
- 手动创建
ToolSpecification toolSpecification = ToolSpecification.builder()
    .name("getWeather")
    .description("返回指定城市的天气预报")
    .parameters(JsonObjectSchema.builder()
        .addStringProperty("city", "需要返回天气预报的城市")
        .addEnumProperty("temperatureUnit", List.of("CELSIUS", "FAHRENHEIT"))
        .required("city") // 必须显式指定必需属性
        .build())
    .build();
2
3
4
5
6
7
8
9
有关JsonObjectSchema的更多信息,请参见此处。
- 使用辅助方法:
- ToolSpecifications.toolSpecificationsFrom(Class)
- ToolSpecifications.toolSpecificationsFrom(Object)
- ToolSpecifications.toolSpecificationFrom(Method)
class WeatherTools { 
  
    @Tool("返回指定城市的天气预报")
    String getWeather(
            @P("需要返回天气预报的城市") String city,
            TemperatureUnit temperatureUnit
    ) {
        ...
    }
}
List<ToolSpecification> toolSpecifications = ToolSpecifications.toolSpecificationsFrom(WeatherTools.class);
2
3
4
5
6
7
8
9
10
11
12
完成后,您可以调用模型:
UserMessage userMessage = UserMessage.from("明天伦敦的天气如何?");
Response<AiMessage> response = model.generate(List.of(userMessage), toolSpecifications);
AiMessage aiMessage = response.content();
2
3
如果LLM决定调用工具,返回的AiMessage将包含在toolExecutionRequests字段中的数据。 此时,AiMessage.hasToolExecutionRequests()将返回true。 根据LLM的不同,ToolExecutionRequest对象可能包含一个或多个(某些LLM支持并行调用多个工具)。
每个ToolExecutionRequest应包含:
- 工具调用的id(某些LLM不提供)
- 要调用的工具名称,例如:getWeather
- arguments,例如:- { "city": "London", "temperatureUnit": "CELSIUS" }
您需要根据ToolExecutionRequest中的信息手动执行工具。
如果希望将工具执行结果发送回LLM,需要为每个ToolExecutionRequest创建一个ToolExecutionResultMessage, 并将其与之前的所有消息一起发送:
String result = "预计明天伦敦将下雨。";
ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from(toolExecutionRequest, result);
List<ChatMessage> messages = List.of(userMessage, aiMessage, toolExecutionResultMessage);
Response<AiMessage> response2 = model.generate(messages, toolSpecifications);
2
3
4
# 高级工具 API
在高层次的抽象中,您可以使用 @Tool 注解标注任何 Java 方法,并在创建 AI 服务 时指定它们。
AI 服务将自动将这些方法转换为 ToolSpecification,并将它们包含在每次与语言模型(LLM)交互的请求中。当 LLM 决定调用工具时,AI 服务将自动执行适当的方法,并将方法的返回值(如果有的话)发送回 LLM。您可以在 DefaultToolExecutor 中找到实现细节。
以下是一些工具示例:
@Tool("根据查询在 Google 中搜索相关 URL")
public List<String> searchGoogle(@P("search query") String query) {
    return googleSearchService.search(query);
}
@Tool("根据 URL 返回网页内容")
public String getWebPageContent(@P("URL of the page") String url) {
    Document jsoupDocument = Jsoup.connect(url).get();
    return jsoupDocument.body().text();
}
2
3
4
5
6
7
8
9
10
# 工具方法限制
标注了 @Tool 的方法:
- 可以是静态的,也可以是非静态的
- 可以具有任何可见性(public, private 等)
# 工具方法参数
标注了 @Tool 的方法可以接受任意数量的参数,支持多种类型:
- 基本类型:int、double等
- 对象类型:String、Integer、Double等
- 自定义 POJO(可以包含嵌套 POJO)
- enum
- List<T>/- Set<T>,其中- T是上述类型之一
- Map<K, V>(您需要使用- @P手动指定- K和- V的类型)
方法也支持没有参数的情况。
默认情况下,所有方法参数都被认为是必需的。这意味着 LLM 必须为这些参数提供值。如果希望某个参数为可选,可以使用 @P(required = false) 来标注。目前不支持声明 POJO 参数字段为可选。
递归参数(例如,Person 类中包含 Set<Person> children 字段)目前仅由 OpenAI 支持。
# 工具方法返回类型
标注了 @Tool 的方法可以返回任何类型,包括 void。
- 如果方法的返回类型是 void,则如果方法执行成功,将返回 "Success" 字符串给 LLM。
- 如果方法的返回类型是 String,则返回的值将直接发送给 LLM,无需转换。
- 对于其他返回类型,返回值将在发送给 LLM 之前转换为 JSON 字符串。
# 异常处理
如果标注了 @Tool 的方法抛出异常,则异常消息(e.getMessage())将作为工具执行结果发送给 LLM。这使得 LLM 能够修正错误并在必要时重试。
# @Tool
 任何标注了 @Tool 并在 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。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
当调用 ask 方法时,会发生 2 次与 LLM 的交互,如前所述。在这些交互之间,squareRoot 方法会自动调用。
@Tool 注解有 2 个可选字段:
- name:工具的名称。如果未提供,方法名将作为工具的名称。
- value:工具的描述。
根据工具的不同,即使没有描述,LLM 也可能能很好地理解它(例如,add(a, b) 很显然),但通常最好提供清晰且有意义的名称和描述。这样,LLM 可以获得更多信息来判断是否调用给定的工具,以及如何调用。
# @P
 方法参数可以选择性地使用 @P 注解。
@P 注解有 2 个字段:
- value:参数描述。必填字段。
- required:参数是否必需,默认为- true。可选字段。
# @Description
 可以使用 @Description 注解指定类和字段的描述:
@Description("要执行的查询")
class Query {
  @Description("要选择的字段")
  private List<String> select;
  @Description("筛选条件")
  private List<Condition> where;
}
@Tool
Result executeQuery(Query query) {
  ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# @ToolMemoryId
 如果您的 AI 服务方法具有标注 @MemoryId 的参数,您也可以在 @Tool 方法的参数中使用 @ToolMemoryId 注解。传递给 AI 服务方法的值将自动传递给 @Tool 方法。如果您有多个用户和/或每个用户多个聊天/记忆,并且希望在 @Tool 方法中区分它们时,这个功能非常有用。
# 访问执行的工具
如果您希望访问在调用 AI 服务期间执行的工具,可以通过将返回类型包装在 Result 类中轻松访问:
interface Assistant {
    Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("取消我的预订 123-456");
String answer = result.content();
List<ToolExecution> toolExecutions = result.toolExecutions();
2
3
4
5
6
7
8
9
在流模式下,您可以通过指定 onToolExecuted 回调来访问:
interface Assistant {
    TokenStream chat(String message);
}
TokenStream tokenStream = assistant.chat("取消我的预订");
tokenStream
    .onNext(...)
    .onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
    .onComplete(...)
    .onError(...)
    .start();
2
3
4
5
6
7
8
9
10
11
12
13
# 程序化指定工具
在使用 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();
2
3
4
5
6
7
8
9
10
11
对于每个 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();
};
2
3
4
5
6
一旦我们有了一个或多个(ToolSpecification,ToolExecutor)对,我们可以在创建 AI 服务时指定它们:
Assistant assistant = AiServices.builder(Assistant.class)
    .chatLanguageModel(chatLanguageModel)
    .tools(Map.of(toolSpecification, toolExecutor))
    .build();
2
3
4
# 动态指定工具
在使用 AI 服务时,工具也可以在每次调用时动态指定。可以配置一个 ToolProvider,每次调用 AI 服务时都会调用它,并提供当前请求中应包含的工具。ToolProvider 接受一个 ToolProviderRequest,该请求包含 UserMessage 和聊天记忆 ID,并返回一个 ToolProviderResult,该结果包含一个从 ToolSpecification 到 ToolExecutor 的工具映射。
下面是一个如何仅在用户消息中包含“预订”一词时添加 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();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
